aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components/views
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-09-18 20:34:18 -0700
committerDhravya Shah <[email protected]>2025-09-18 21:03:49 -0700
commit1fcb56908920da386900abb4ce2383374a625c72 (patch)
tree0f9d7f695d4c9b1b85be3950fc869e0061dff3ed /apps/web/components/views
parentrefetching logic change (diff)
downloadsupermemory-09-18-formatting.tar.xz
supermemory-09-18-formatting.zip
Diffstat (limited to 'apps/web/components/views')
-rw-r--r--apps/web/components/views/add-memory/action-buttons.tsx118
-rw-r--r--apps/web/components/views/add-memory/fixed-mutation.tsx244
-rw-r--r--apps/web/components/views/add-memory/index.tsx417
-rw-r--r--apps/web/components/views/add-memory/memory-usage-ring.tsx23
-rw-r--r--apps/web/components/views/add-memory/project-selection.tsx164
-rw-r--r--apps/web/components/views/add-memory/tab-button.tsx42
-rw-r--r--apps/web/components/views/add-memory/text-editor.tsx22
-rw-r--r--apps/web/components/views/chat/index.tsx40
-rw-r--r--apps/web/components/views/connections-tab-content.tsx130
-rw-r--r--apps/web/components/views/integrations.tsx264
-rw-r--r--apps/web/components/views/mcp/index.tsx116
-rw-r--r--apps/web/components/views/mcp/installation-dialog-content.tsx29
-rw-r--r--apps/web/components/views/profile.tsx86
13 files changed, 735 insertions, 960 deletions
diff --git a/apps/web/components/views/add-memory/action-buttons.tsx b/apps/web/components/views/add-memory/action-buttons.tsx
index 6dc49304..596531b3 100644
--- a/apps/web/components/views/add-memory/action-buttons.tsx
+++ b/apps/web/components/views/add-memory/action-buttons.tsx
@@ -1,67 +1,67 @@
-import { Button } from '@repo/ui/components/button';
-import { Loader2, type LucideIcon } from 'lucide-react';
-import { motion } from 'motion/react';
+import { Button } from "@repo/ui/components/button";
+import { Loader2, type LucideIcon } from "lucide-react";
+import { motion } from "motion/react";
interface ActionButtonsProps {
- onCancel: () => void;
- onSubmit?: () => void;
- submitText: string;
- submitIcon?: LucideIcon;
- isSubmitting?: boolean;
- isSubmitDisabled?: boolean;
- submitType?: 'button' | 'submit';
- className?: string;
+ onCancel: () => void;
+ onSubmit?: () => void;
+ submitText: string;
+ submitIcon?: LucideIcon;
+ isSubmitting?: boolean;
+ isSubmitDisabled?: boolean;
+ submitType?: "button" | "submit";
+ className?: string;
}
export function ActionButtons({
- onCancel,
- onSubmit,
- submitText,
- submitIcon: SubmitIcon,
- isSubmitting = false,
- isSubmitDisabled = false,
- submitType = 'submit',
- className = '',
+ onCancel,
+ onSubmit,
+ submitText,
+ submitIcon: SubmitIcon,
+ isSubmitting = false,
+ isSubmitDisabled = false,
+ submitType = "submit",
+ className = "",
}: ActionButtonsProps) {
- return (
- <div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
- <Button
- className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
- onClick={onCancel}
- type="button"
- variant="ghost"
- >
- Cancel
- </Button>
+ return (
+ <div className={`flex gap-3 order-1 sm:order-2 justify-end ${className}`}>
+ <Button
+ className="hover:bg-white/10 text-white border-none flex-1 sm:flex-initial"
+ onClick={onCancel}
+ type="button"
+ variant="ghost"
+ >
+ Cancel
+ </Button>
- <motion.div
- whileHover={{ scale: 1.05 }}
- whileTap={{ scale: 0.95 }}
- className="flex-1 sm:flex-initial"
- >
- <Button
- className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
- disabled={isSubmitting || isSubmitDisabled}
- onClick={submitType === 'button' ? onSubmit : undefined}
- type={submitType}
- >
- {isSubmitting ? (
- <>
- <Loader2 className="h-4 w-4 animate-spin mr-2" />
- {submitText.includes('Add')
- ? 'Adding...'
- : submitText.includes('Upload')
- ? 'Uploading...'
- : 'Processing...'}
- </>
- ) : (
- <>
- {SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
- {submitText}
- </>
- )}
- </Button>
- </motion.div>
- </div>
- );
+ <motion.div
+ className="flex-1 sm:flex-initial"
+ whileHover={{ scale: 1.05 }}
+ whileTap={{ scale: 0.95 }}
+ >
+ <Button
+ className="bg-white/10 hover:bg-white/20 text-white border-white/20 w-full"
+ disabled={isSubmitting || isSubmitDisabled}
+ onClick={submitType === "button" ? onSubmit : undefined}
+ type={submitType}
+ >
+ {isSubmitting ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
+ {submitText.includes("Add")
+ ? "Adding..."
+ : submitText.includes("Upload")
+ ? "Uploading..."
+ : "Processing..."}
+ </>
+ ) : (
+ <>
+ {SubmitIcon && <SubmitIcon className="h-4 w-4 mr-2" />}
+ {submitText}
+ </>
+ )}
+ </Button>
+ </motion.div>
+ </div>
+ );
}
diff --git a/apps/web/components/views/add-memory/fixed-mutation.tsx b/apps/web/components/views/add-memory/fixed-mutation.tsx
deleted file mode 100644
index c71793b5..00000000
--- a/apps/web/components/views/add-memory/fixed-mutation.tsx
+++ /dev/null
@@ -1,244 +0,0 @@
-import { $fetch } from "@lib/api"
-import { useMutation } from "@tanstack/react-query"
-import { toast } from "sonner"
-
-// Simplified mutation that doesn't block UI with polling
-export const createSimpleAddContentMutation = (
- queryClient: any,
- onClose?: () => void,
-) => {
- return useMutation({
- mutationFn: async ({
- content,
- project,
- contentType,
- }: {
- content: string
- project: string
- contentType: "note" | "link"
- }) => {
- // Just create the memory, don't wait for processing
- const response = await $fetch("@post/documents", {
- body: {
- content: content,
- containerTags: [project],
- metadata: {
- sm_source: "consumer",
- },
- },
- })
-
- if (response.error) {
- throw new Error(
- response.error?.message || `Failed to add ${contentType}`,
- )
- }
-
- return { id: response.data.id, contentType }
- },
- onMutate: async ({ content, project, contentType }) => {
- // Cancel any outgoing refetches
- await queryClient.cancelQueries({
- queryKey: ["documents-with-memories", project],
- })
-
- // Snapshot the previous value
- const previousMemories = queryClient.getQueryData([
- "documents-with-memories",
- project,
- ])
-
- // Create optimistic memory
- const tempId = `temp-${Date.now()}`
- const optimisticMemory = {
- id: tempId,
- content: contentType === "link" ? "" : content,
- url: contentType === "link" ? content : null,
- title:
- contentType === "link" ? "Processing..." : content.substring(0, 100),
- description:
- contentType === "link"
- ? "Extracting content..."
- : "Processing content...",
- containerTags: [project],
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- status: "processing",
- type: contentType,
- metadata: {
- processingStage: "queued",
- processingMessage: "Added to processing queue",
- },
- memoryEntries: [],
- isOptimistic: true,
- }
-
- // Optimistically update to include the new memory
- queryClient.setQueryData(
- ["documents-with-memories", project],
- (old: any) => {
- // Handle infinite query structure
- if (old?.pages) {
- const newPages = [...old.pages]
- if (newPages.length > 0) {
- // Add to the first page
- const firstPage = { ...newPages[0] }
- firstPage.documents = [
- optimisticMemory,
- ...(firstPage.documents || []),
- ]
- newPages[0] = firstPage
- } else {
- // No pages yet, create the first page
- newPages.push({
- documents: [optimisticMemory],
- pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
- totalCount: 1,
- })
- }
-
- return {
- ...old,
- pages: newPages,
- }
- }
- // Fallback for regular query structure
- const newData = old
- ? {
- ...old,
- documents: [optimisticMemory, ...(old.documents || [])],
- totalCount: (old.totalCount || 0) + 1,
- }
- : { documents: [optimisticMemory], totalCount: 1 }
- return newData
- },
- )
-
- return { previousMemories, optimisticId: tempId }
- },
- onError: (error, variables, context) => {
- // Roll back on error
- if (context?.previousMemories) {
- queryClient.setQueryData(
- ["documents-with-memories", variables.project],
- context.previousMemories,
- )
- }
- toast.error(`Failed to add ${variables.contentType}`, {
- description: error instanceof Error ? error.message : "Unknown error",
- })
- },
- onSuccess: (data, variables, context) => {
- // Show success message
- toast.success(
- `${variables.contentType === "link" ? "Link" : "Note"} created successfully!`,
- )
-
- // Close modal
- onClose?.()
-
- // Start polling for this specific memory ID
- // The polling will happen in the background and update the optimistic memory when done
- startMemoryPolling(
- data.id,
- variables.project,
- context?.optimisticId,
- queryClient,
- )
- },
- })
-}
-
-// Background polling function
-const startMemoryPolling = (
- memoryId: string,
- project: string,
- optimisticId: string | undefined,
- queryClient: any,
-) => {
- const pollMemory = async () => {
- try {
- const memory = await $fetch(`@get/documents/${memoryId}`)
-
- if (memory.error) {
- console.error("Failed to fetch memory status:", memory.error)
- return false
- }
-
- const isComplete =
- memory.data?.status === "done" ||
- memory.data?.content ||
- memory.data?.memoryEntries?.length > 0
-
- if (isComplete) {
- // Replace optimistic memory with real data
- queryClient.setQueryData(
- ["documents-with-memories", project],
- (old: any) => {
- if (old?.pages) {
- // Handle infinite query structure
- const newPages = old.pages.map((page: any) => ({
- ...page,
- documents: page.documents.map((doc: any) => {
- if (doc.isOptimistic || doc.id === optimisticId) {
- // Replace with real memory
- return {
- ...memory.data,
- isOptimistic: false,
- }
- }
- return doc
- }),
- }))
-
- return {
- ...old,
- pages: newPages,
- }
- }
- // Handle regular query structure
- return {
- ...old,
- documents: old.documents.map((doc: any) => {
- if (doc.isOptimistic || doc.id === optimisticId) {
- return {
- ...memory.data,
- isOptimistic: false,
- }
- }
- return doc
- }),
- }
- },
- )
- return true // Stop polling
- }
-
- return false // Continue polling
- } catch (error) {
- console.error("Error polling memory:", error)
- return false // Continue polling
- }
- }
-
- // Poll every 3 seconds, max 60 attempts (3 minutes)
- let attempts = 0
- const maxAttempts = 60
-
- const poll = async () => {
- if (attempts >= maxAttempts) {
- console.log("Memory polling timed out")
- return
- }
-
- const isComplete = await pollMemory()
- attempts++
-
- if (!isComplete && attempts < maxAttempts) {
- setTimeout(poll, 3000) // Poll again in 3 seconds
- }
- }
-
- // Start polling after a short delay
- setTimeout(poll, 2000)
-}
diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx
index 5116e952..96bb0a8b 100644
--- a/apps/web/components/views/add-memory/index.tsx
+++ b/apps/web/components/views/add-memory/index.tsx
@@ -1,9 +1,9 @@
-import { $fetch } from "@lib/api";
+import { $fetch } from "@lib/api"
import {
fetchConsumerProProduct,
fetchMemoriesFeature,
-} from "@repo/lib/queries";
-import { Button } from "@repo/ui/components/button";
+} from "@repo/lib/queries"
+import { Button } from "@repo/ui/components/button"
import {
Dialog,
DialogContent,
@@ -11,18 +11,19 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
-} from "@repo/ui/components/dialog";
-import { Input } from "@repo/ui/components/input";
-import { Label } from "@repo/ui/components/label";
-import { Textarea } from "@repo/ui/components/textarea";
-import { useForm } from "@tanstack/react-form";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+} from "@repo/ui/components/dialog"
+import { Input } from "@repo/ui/components/input"
+import { Label } from "@repo/ui/components/label"
+import { Textarea } from "@repo/ui/components/textarea"
+import type { GetMemoryResponseSchema } from "@repo/validation/api"
+import { useForm } from "@tanstack/react-form"
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
-} from "@ui/components/shadcn-io/dropzone";
-import { useCustomer } from "autumn-js/react";
+} from "@ui/components/shadcn-io/dropzone"
+import { useCustomer } from "autumn-js/react"
import {
Brain,
FileIcon,
@@ -31,19 +32,19 @@ import {
PlugIcon,
Plus,
UploadIcon,
-} from "lucide-react";
-import { AnimatePresence, motion } from "motion/react";
-import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
-import { toast } from "sonner";
-import { z } from "zod";
-import { analytics } from "@/lib/analytics";
-import { useProject } from "@/stores";
-import { ConnectionsTabContent } from "../connections-tab-content";
-import { ActionButtons } from "./action-buttons";
-import { MemoryUsageRing } from "./memory-usage-ring";
-import { ProjectSelection } from "./project-selection";
-import { TabButton } from "./tab-button";
+} from "lucide-react"
+import { AnimatePresence, motion } from "motion/react"
+import dynamic from "next/dynamic"
+import { useEffect, useState } from "react"
+import { toast } from "sonner"
+import { z } from "zod"
+import { analytics } from "@/lib/analytics"
+import { useProject } from "@/stores"
+import { ConnectionsTabContent } from "../connections-tab-content"
+import { ActionButtons } from "./action-buttons"
+import { MemoryUsageRing } from "./memory-usage-ring"
+import { ProjectSelection } from "./project-selection"
+import { TabButton } from "./tab-button"
const TextEditor = dynamic(
() => import("./text-editor").then((mod) => ({ default: mod.TextEditor })),
@@ -64,91 +65,100 @@ const TextEditor = dynamic(
),
ssr: false,
},
-);
+)
// Simple function to extract plain text title from HTML content
const getPlainTextTitle = (htmlContent: string) => {
- const temp = document.createElement("div");
- temp.innerHTML = htmlContent;
- const plainText = temp.textContent || temp.innerText || htmlContent;
- const firstLine = plainText.split("\n")[0].trim();
+ const temp = document.createElement("div")
+ temp.innerHTML = htmlContent
+ const plainText = temp.textContent || temp.innerText || htmlContent
+
+ if (!plainText) {
+ return "Untitled"
+ }
+ const firstLine = plainText.split("\n")[0]?.trim()
+
+ if (!firstLine) {
+ return plainText.substring(0, 100)
+ }
+
return firstLine.length > 0
? firstLine.substring(0, 100)
- : plainText.substring(0, 100);
-};
+ : plainText.substring(0, 100)
+}
export function AddMemoryView({
onClose,
initialTab = "note",
}: {
- onClose?: () => void;
- initialTab?: "note" | "link" | "file" | "connect";
+ onClose?: () => void
+ initialTab?: "note" | "link" | "file" | "connect"
}) {
- const queryClient = useQueryClient();
- const { selectedProject, setSelectedProject } = useProject();
- const [showAddDialog, setShowAddDialog] = useState(true);
- const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
+ const queryClient = useQueryClient()
+ const { selectedProject, setSelectedProject } = useProject()
+ const [showAddDialog, setShowAddDialog] = useState(true)
+ const [selectedFiles, setSelectedFiles] = useState<File[]>([])
const [activeTab, setActiveTab] = useState<
"note" | "link" | "file" | "connect"
- >(initialTab);
- const autumn = useCustomer();
- const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false);
- const [newProjectName, setNewProjectName] = useState("");
+ >(initialTab)
+ const autumn = useCustomer()
+ const [showCreateProjectDialog, setShowCreateProjectDialog] = useState(false)
+ const [newProjectName, setNewProjectName] = useState("")
// Check memory limits
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn)
- const memoriesUsed = memoriesCheck?.usage ?? 0;
- const memoriesLimit = memoriesCheck?.included_usage ?? 0;
+ const memoriesUsed = memoriesCheck?.usage ?? 0
+ const memoriesLimit = memoriesCheck?.included_usage ?? 0
// Fetch projects for the dropdown
const { data: projects = [], isLoading: isLoadingProjects } = useQuery({
queryKey: ["projects"],
queryFn: async () => {
- const response = await $fetch("@get/projects");
+ const response = await $fetch("@get/projects")
if (response.error) {
- throw new Error(response.error?.message || "Failed to load projects");
+ throw new Error(response.error?.message || "Failed to load projects")
}
- return response.data?.projects || [];
+ return response.data?.projects || []
},
staleTime: 30 * 1000,
- });
+ })
// Create project mutation
const createProjectMutation = useMutation({
mutationFn: async (name: string) => {
const response = await $fetch("@post/projects", {
body: { name },
- });
+ })
if (response.error) {
- throw new Error(response.error?.message || "Failed to create project");
+ throw new Error(response.error?.message || "Failed to create project")
}
- return response.data;
+ return response.data
},
onSuccess: (data) => {
- analytics.projectCreated();
- toast.success("Project created successfully!");
- setShowCreateProjectDialog(false);
- setNewProjectName("");
- queryClient.invalidateQueries({ queryKey: ["projects"] });
+ analytics.projectCreated()
+ toast.success("Project created successfully!")
+ setShowCreateProjectDialog(false)
+ setNewProjectName("")
+ queryClient.invalidateQueries({ queryKey: ["projects"] })
// Set the newly created project as selected
if (data?.containerTag) {
- setSelectedProject(data.containerTag);
+ setSelectedProject(data.containerTag)
// Update form values
- addContentForm.setFieldValue("project", data.containerTag);
- fileUploadForm.setFieldValue("project", data.containerTag);
+ addContentForm.setFieldValue("project", data.containerTag)
+ fileUploadForm.setFieldValue("project", data.containerTag)
}
},
onError: (error) => {
toast.error("Failed to create project", {
description: error instanceof Error ? error.message : "Unknown error",
- });
+ })
},
- });
+ })
const addContentForm = useForm({
defaultValues: {
@@ -160,8 +170,8 @@ export function AddMemoryView({
content: value.content,
project: value.project,
contentType: activeTab as "note" | "link",
- });
- formApi.reset();
+ })
+ formApi.reset()
},
validators: {
onChange: z.object({
@@ -169,19 +179,19 @@ export function AddMemoryView({
project: z.string(),
}),
},
- });
+ })
// Re-validate content field when tab changes between note/link
// biome-ignore lint/correctness/useExhaustiveDependencies: It is what it is
useEffect(() => {
// Trigger validation of the content field when switching between note/link
if (activeTab === "note" || activeTab === "link") {
- const currentValue = addContentForm.getFieldValue("content");
+ const currentValue = addContentForm.getFieldValue("content")
if (currentValue) {
- addContentForm.validateField("content", "change");
+ addContentForm.validateField("content", "change")
}
}
- }, [activeTab]);
+ }, [activeTab])
// Form for file upload metadata
const fileUploadForm = useForm({
@@ -192,8 +202,8 @@ export function AddMemoryView({
},
onSubmit: async ({ value, formApi }) => {
if (selectedFiles.length === 0) {
- toast.error("Please select a file to upload");
- return;
+ toast.error("Please select a file to upload")
+ return
}
for (const file of selectedFiles) {
@@ -202,13 +212,13 @@ export function AddMemoryView({
title: value.title || undefined,
description: value.description || undefined,
project: value.project,
- });
+ })
}
- formApi.reset();
- setSelectedFiles([]);
+ formApi.reset()
+ setSelectedFiles([])
},
- });
+ })
const addContentMutation = useMutation({
mutationFn: async ({
@@ -216,11 +226,11 @@ export function AddMemoryView({
project,
contentType,
}: {
- content: string;
- project: string;
- contentType: "note" | "link";
+ content: string
+ project: string
+ contentType: "note" | "link"
}) => {
- console.log("📤 Creating memory...");
+ console.log("📤 Creating memory...")
const response = await $fetch("@post/documents", {
body: {
@@ -230,34 +240,34 @@ export function AddMemoryView({
sm_source: "consumer",
},
},
- });
+ })
if (response.error) {
throw new Error(
response.error?.message || `Failed to add ${contentType}`,
- );
+ )
}
- console.log("✅ Memory created:", response.data);
- return response.data;
+ console.log("✅ Memory created:", response.data)
+ return response.data
},
onMutate: async ({ content, project, contentType }) => {
- console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType);
+ console.log("🚀 OPTIMISTIC UPDATE: Starting for", contentType)
// Cancel queries to prevent conflicts
await queryClient.cancelQueries({
queryKey: ["documents-with-memories", project],
- });
- console.log("📞 QUERIES CANCELLED for project:", project);
+ })
+ console.log("📞 QUERIES CANCELLED for project:", project)
// Get previous data for rollback
const previousMemories = queryClient.getQueryData([
"documents-with-memories",
project,
- ]);
+ ])
// Create optimistic memory with proper title
- const tempId = `temp-${Date.now()}`;
+ const tempId = `temp-${Date.now()}`
const optimisticMemory = {
id: tempId,
content: contentType === "link" ? "" : content,
@@ -270,9 +280,9 @@ export function AddMemoryView({
updatedAt: new Date().toISOString(),
memoryEntries: [],
isOptimistic: true,
- };
+ }
- console.log("🎯 Adding optimistic memory:", optimisticMemory);
+ console.log("🎯 Adding optimistic memory:", optimisticMemory)
// Add to cache optimistically
queryClient.setQueryData(
@@ -280,19 +290,19 @@ export function AddMemoryView({
(old: any) => {
if (old?.pages) {
// Infinite query structure
- const newPages = [...old.pages];
+ const newPages = [...old.pages]
if (newPages.length > 0) {
newPages[0] = {
...newPages[0],
documents: [optimisticMemory, ...(newPages[0].documents || [])],
- };
+ }
} else {
newPages.push({
documents: [optimisticMemory],
pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
- });
+ })
}
- return { ...old, pages: newPages };
+ return { ...old, pages: newPages }
}
// Regular query structure
@@ -302,44 +312,46 @@ export function AddMemoryView({
documents: [optimisticMemory, ...(old.documents || [])],
totalCount: (old.totalCount || 0) + 1,
}
- : { documents: [optimisticMemory], totalCount: 1 };
+ : { documents: [optimisticMemory], totalCount: 1 }
},
- );
+ )
- return { previousMemories, optimisticId: tempId };
+ return { previousMemories, optimisticId: tempId }
},
onError: (error, variables, context) => {
- console.log("❌ Mutation failed, rolling back");
+ console.log("❌ Mutation failed, rolling back")
if (context?.previousMemories) {
queryClient.setQueryData(
["documents-with-memories", variables.project],
context.previousMemories,
- );
+ )
}
- toast.error(`Failed to add ${variables.contentType}`);
+ toast.error(`Failed to add ${variables.contentType}`)
},
onSuccess: (data, variables, context) => {
console.log(
"✅ Mutation succeeded, starting simple polling for memory:",
data.id,
- );
+ )
analytics.memoryAdded({
type: variables.contentType === "link" ? "link" : "note",
project_id: variables.project,
content_length: variables.content.length,
- });
+ })
toast.success(
`${variables.contentType === "link" ? "Link" : "Note"} added successfully!`,
- );
- setShowAddDialog(false);
- onClose?.();
+ )
+ setShowAddDialog(false)
+ onClose?.()
// Simple polling to replace optimistic update when ready
const pollMemory = async () => {
try {
- const memory = await $fetch(`@get/documents/${data.id}`);
- console.log("🔍 Polling memory:", memory.data);
+ const memory = await $fetch<z.infer<typeof GetMemoryResponseSchema>>(
+ `@get/documents/${data.id}`,
+ )
+ console.log("🔍 Polling memory:", memory.data)
if (memory.data && !memory.error) {
// Check if memory has been processed (has memory entries, substantial content, and NOT untitled/processing)
@@ -347,17 +359,16 @@ export function AddMemoryView({
memory.data.title &&
!memory.data.title.toLowerCase().includes("untitled") &&
!memory.data.title.toLowerCase().includes("processing") &&
- memory.data.title.length > 3;
+ memory.data.title.length > 3
const isReady =
- memory.data.memoryEntries?.length > 0 ||
- (memory.data.content &&
- memory.data.content.length > 10 &&
- hasRealTitle);
+ memory.data.content &&
+ memory.data.content.length > 10 &&
+ hasRealTitle
console.log("📊 Memory ready check:", {
isReady,
- hasMemoryEntries: memory.data.memoryEntries?.length,
+ hasMemoryEntries: 0,
hasContent: memory.data.content?.length,
title: memory.data.title,
hasRealTitle,
@@ -367,10 +378,10 @@ export function AddMemoryView({
titleNotProcessing:
memory.data.title &&
!memory.data.title.toLowerCase().includes("processing"),
- });
+ })
if (isReady) {
- console.log("✅ Memory ready, replacing optimistic update");
+ console.log("✅ Memory ready, replacing optimistic update")
// Replace optimistic memory with real data
queryClient.setQueryData(
["documents-with-memories", variables.project],
@@ -380,48 +391,48 @@ export function AddMemoryView({
...page,
documents: page.documents.map((doc: any) => {
if (doc.isOptimistic && doc.id.startsWith("temp-")) {
- return { ...memory.data, isOptimistic: false };
+ return { ...memory.data, isOptimistic: false }
}
- return doc;
+ return doc
}),
- }));
- return { ...old, pages: newPages };
+ }))
+ return { ...old, pages: newPages }
}
- return old;
+ return old
},
- );
- return true; // Stop polling
+ )
+ return true // Stop polling
}
}
- return false; // Continue polling
+ return false // Continue polling
} catch (error) {
- console.error("❌ Error polling memory:", error);
- return false;
+ console.error("❌ Error polling memory:", error)
+ return false
}
- };
+ }
// Poll every 3 seconds for up to 30 attempts (90 seconds)
- let attempts = 0;
- const maxAttempts = 30;
+ let attempts = 0
+ const maxAttempts = 30
const poll = async () => {
if (attempts >= maxAttempts) {
- console.log("⚠️ Polling stopped after max attempts");
- return;
+ console.log("⚠️ Polling stopped after max attempts")
+ return
}
- const isDone = await pollMemory();
- attempts++;
+ const isDone = await pollMemory()
+ attempts++
if (!isDone && attempts < maxAttempts) {
- setTimeout(poll, 3000);
+ setTimeout(poll, 3000)
}
- };
+ }
// Start polling after 2 seconds
- setTimeout(poll, 2000);
+ setTimeout(poll, 2000)
},
- });
+ })
const fileUploadMutation = useMutation({
mutationFn: async ({
@@ -430,10 +441,10 @@ export function AddMemoryView({
description,
project,
}: {
- file: File;
- title?: string;
- description?: string;
- project: string;
+ file: File
+ title?: string
+ description?: string
+ project: string
}) => {
// TEMPORARILY DISABLED: Limit check disabled
// Check if user can add more memories
@@ -443,9 +454,9 @@ export function AddMemoryView({
// );
// }
- const formData = new FormData();
- formData.append("file", file);
- formData.append("containerTags", JSON.stringify([project]));
+ const formData = new FormData()
+ formData.append("file", file)
+ formData.append("containerTags", JSON.stringify([project]))
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/v3/documents/file`,
@@ -454,14 +465,14 @@ export function AddMemoryView({
body: formData,
credentials: "include",
},
- );
+ )
if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || "Failed to upload file");
+ const error = await response.json()
+ throw new Error(error.error || "Failed to upload file")
}
- const data = await response.json();
+ const data = await response.json()
// If we have metadata, we can update the document after creation
if (title || description) {
@@ -473,23 +484,23 @@ export function AddMemoryView({
sm_source: "consumer", // Use "consumer" source to bypass limits
},
},
- });
+ })
}
- return data;
+ return data
},
// Optimistic update
onMutate: async ({ file, title, description, project }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({
queryKey: ["documents-with-memories", project],
- });
+ })
// Snapshot the previous value
const previousMemories = queryClient.getQueryData([
"documents-with-memories",
project,
- ]);
+ ])
// Create optimistic memory for the file
const optimisticMemory = {
@@ -509,7 +520,7 @@ export function AddMemoryView({
mimeType: file.type,
},
memoryEntries: [],
- };
+ }
// Optimistically update to include the new memory
queryClient.setQueryData(
@@ -518,41 +529,41 @@ export function AddMemoryView({
// Handle infinite query structure
if (old?.pages) {
// This is an infinite query - add to the first page
- const newPages = [...old.pages];
+ const newPages = [...old.pages]
if (newPages.length > 0) {
// Add to the first page
- const firstPage = { ...newPages[0] };
+ const firstPage = { ...newPages[0] }
firstPage.documents = [
optimisticMemory,
...(firstPage.documents || []),
- ];
- newPages[0] = firstPage;
+ ]
+ newPages[0] = firstPage
} else {
// No pages yet, create the first page
newPages.push({
documents: [optimisticMemory],
pagination: { currentPage: 1, totalPages: 1, totalCount: 1 },
totalCount: 1,
- });
+ })
}
return {
...old,
pages: newPages,
- };
+ }
}
// Fallback for regular query structure
- if (!old) return { documents: [optimisticMemory], totalCount: 1 };
+ if (!old) return { documents: [optimisticMemory], totalCount: 1 }
return {
...old,
documents: [optimisticMemory, ...(old.documents || [])],
totalCount: (old.totalCount || 0) + 1,
- };
+ }
},
- );
+ )
// Return a context object with the snapshotted value
- return { previousMemories };
+ return { previousMemories }
},
// If the mutation fails, roll back to the previous value
onError: (error, variables, context) => {
@@ -560,11 +571,11 @@ export function AddMemoryView({
queryClient.setQueryData(
["documents-with-memories", variables.project],
context.previousMemories,
- );
+ )
}
toast.error("Failed to upload file", {
description: error instanceof Error ? error.message : "Unknown error",
- });
+ })
},
onSuccess: (_data, variables) => {
analytics.memoryAdded({
@@ -572,18 +583,18 @@ export function AddMemoryView({
project_id: variables.project,
file_size: variables.file.size,
file_type: variables.file.type,
- });
+ })
toast.success("File uploaded successfully!", {
description: "Your file is being processed",
- });
- setShowAddDialog(false);
- onClose?.();
+ })
+ setShowAddDialog(false)
+ onClose?.()
},
// Don't invalidate queries immediately - let optimistic updates work
// onSettled: () => {
// queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] })
// },
- });
+ })
return (
<AnimatePresence mode="wait">
@@ -591,8 +602,8 @@ export function AddMemoryView({
<Dialog
key="add-memory-dialog"
onOpenChange={(open) => {
- setShowAddDialog(open);
- if (!open) onClose?.();
+ setShowAddDialog(open)
+ if (!open) onClose?.()
}}
open={showAddDialog}
>
@@ -651,9 +662,9 @@ export function AddMemoryView({
<div className="space-y-4">
<form
onSubmit={(e) => {
- e.preventDefault();
- e.stopPropagation();
- addContentForm.handleSubmit();
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
}}
>
<div className="grid gap-4">
@@ -669,9 +680,9 @@ export function AddMemoryView({
validators={{
onChange: ({ value }) => {
if (!value || value.trim() === "") {
- return "Note is required";
+ return "Note is required"
}
- return undefined;
+ return undefined
},
}}
>
@@ -753,9 +764,9 @@ export function AddMemoryView({
isSubmitDisabled={!addContentForm.state.canSubmit}
isSubmitting={addContentMutation.isPending}
onCancel={() => {
- setShowAddDialog(false);
- onClose?.();
- addContentForm.reset();
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
}}
submitIcon={Plus}
submitText="Add Note"
@@ -769,9 +780,9 @@ export function AddMemoryView({
<div className="space-y-4">
<form
onSubmit={(e) => {
- e.preventDefault();
- e.stopPropagation();
- addContentForm.handleSubmit();
+ e.preventDefault()
+ e.stopPropagation()
+ addContentForm.handleSubmit()
}}
>
<div className="grid gap-4">
@@ -793,13 +804,13 @@ export function AddMemoryView({
validators={{
onChange: ({ value }) => {
if (!value || value.trim() === "") {
- return "Link is required";
+ return "Link is required"
}
try {
- new URL(value);
- return undefined;
+ new URL(value)
+ return undefined
} catch {
- return "Please enter a valid link";
+ return "Please enter a valid link"
}
},
}}
@@ -879,9 +890,9 @@ export function AddMemoryView({
isSubmitDisabled={!addContentForm.state.canSubmit}
isSubmitting={addContentMutation.isPending}
onCancel={() => {
- setShowAddDialog(false);
- onClose?.();
- addContentForm.reset();
+ setShowAddDialog(false)
+ onClose?.()
+ addContentForm.reset()
}}
submitIcon={Plus}
submitText="Add Link"
@@ -895,9 +906,9 @@ export function AddMemoryView({
<div className="space-y-4">
<form
onSubmit={(e) => {
- e.preventDefault();
- e.stopPropagation();
- fileUploadForm.handleSubmit();
+ e.preventDefault()
+ e.stopPropagation()
+ fileUploadForm.handleSubmit()
}}
>
<div className="grid gap-4">
@@ -1031,10 +1042,10 @@ export function AddMemoryView({
isSubmitDisabled={selectedFiles.length === 0}
isSubmitting={fileUploadMutation.isPending}
onCancel={() => {
- setShowAddDialog(false);
- onClose?.();
- fileUploadForm.reset();
- setSelectedFiles([]);
+ setShowAddDialog(false)
+ onClose?.()
+ fileUploadForm.reset()
+ setSelectedFiles([])
}}
submitIcon={UploadIcon}
submitText="Upload File"
@@ -1102,8 +1113,8 @@ export function AddMemoryView({
<Button
className="bg-white/5 hover:bg-white/10 border-white/10 text-white w-full sm:w-auto"
onClick={() => {
- setShowCreateProjectDialog(false);
- setNewProjectName("");
+ setShowCreateProjectDialog(false)
+ setNewProjectName("")
}}
type="button"
variant="outline"
@@ -1140,19 +1151,19 @@ export function AddMemoryView({
</Dialog>
)}
</AnimatePresence>
- );
+ )
}
export function AddMemoryExpandedView() {
- const [showDialog, setShowDialog] = useState(false);
+ const [showDialog, setShowDialog] = useState(false)
const [selectedTab, setSelectedTab] = useState<
"note" | "link" | "file" | "connect"
- >("note");
+ >("note")
const handleOpenDialog = (tab: "note" | "link" | "file" | "connect") => {
- setSelectedTab(tab);
- setShowDialog(true);
- };
+ setSelectedTab(tab)
+ setShowDialog(true)
+ }
return (
<>
@@ -1223,5 +1234,5 @@ export function AddMemoryExpandedView() {
/>
)}
</>
- );
+ )
}
diff --git a/apps/web/components/views/add-memory/memory-usage-ring.tsx b/apps/web/components/views/add-memory/memory-usage-ring.tsx
index f6fb9836..7dbf8563 100644
--- a/apps/web/components/views/add-memory/memory-usage-ring.tsx
+++ b/apps/web/components/views/add-memory/memory-usage-ring.tsx
@@ -1,7 +1,7 @@
interface MemoryUsageRingProps {
- memoriesUsed: number;
- memoriesLimit: number;
- className?: string;
+ memoriesUsed: number
+ memoriesLimit: number
+ className?: string
}
export function MemoryUsageRing({
@@ -9,10 +9,10 @@ export function MemoryUsageRing({
memoriesLimit,
className = "",
}: MemoryUsageRingProps) {
- const usagePercentage = memoriesUsed / memoriesLimit;
+ const usagePercentage = memoriesUsed / memoriesLimit
const strokeColor =
- memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)";
- const circumference = 2 * Math.PI * 10;
+ memoriesUsed >= memoriesLimit * 0.8 ? "rgb(251 191 36)" : "rgb(34 197 94)"
+ const circumference = 2 * Math.PI * 10
return (
<div
@@ -20,27 +20,28 @@ export function MemoryUsageRing({
title={`${memoriesUsed} of ${memoriesLimit} memories used`}
>
<svg className="w-6 h-6 transform -rotate-90" viewBox="0 0 24 24">
+ <title>Memory Usage</title>
{/* Background circle */}
<circle
cx="12"
cy="12"
+ fill="none"
r="10"
stroke="rgb(255 255 255 / 0.1)"
strokeWidth="2"
- fill="none"
/>
{/* Progress circle */}
<circle
+ className="transition-all duration-300"
cx="12"
cy="12"
+ fill="none"
r="10"
stroke={strokeColor}
- strokeWidth="2"
- fill="none"
strokeDasharray={`${circumference}`}
strokeDashoffset={`${circumference * (1 - usagePercentage)}`}
- className="transition-all duration-300"
strokeLinecap="round"
+ strokeWidth="2"
/>
</svg>
@@ -49,5 +50,5 @@ export function MemoryUsageRing({
{memoriesUsed} / {memoriesLimit}
</div>
</div>
- );
+ )
}
diff --git a/apps/web/components/views/add-memory/project-selection.tsx b/apps/web/components/views/add-memory/project-selection.tsx
index f23768a3..b0f79792 100644
--- a/apps/web/components/views/add-memory/project-selection.tsx
+++ b/apps/web/components/views/add-memory/project-selection.tsx
@@ -1,94 +1,94 @@
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@repo/ui/components/select';
-import { Plus } from 'lucide-react';
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@repo/ui/components/select";
+import { Plus } from "lucide-react";
interface Project {
- id?: string;
- containerTag: string;
- name: string;
+ id?: string;
+ containerTag: string;
+ name: string;
}
interface ProjectSelectionProps {
- projects: Project[];
- selectedProject: string;
- onProjectChange: (value: string) => void;
- onCreateProject: () => void;
- disabled?: boolean;
- isLoading?: boolean;
- className?: string;
- id?: string;
+ projects: Project[];
+ selectedProject: string;
+ onProjectChange: (value: string) => void;
+ onCreateProject: () => void;
+ disabled?: boolean;
+ isLoading?: boolean;
+ className?: string;
+ id?: string;
}
export function ProjectSelection({
- projects,
- selectedProject,
- onProjectChange,
- onCreateProject,
- disabled = false,
- isLoading = false,
- className = '',
- id = 'project-select',
+ projects,
+ selectedProject,
+ onProjectChange,
+ onCreateProject,
+ disabled = false,
+ isLoading = false,
+ className = "",
+ id = "project-select",
}: ProjectSelectionProps) {
- const handleValueChange = (value: string) => {
- if (value === 'create-new-project') {
- onCreateProject();
- } else {
- onProjectChange(value);
- }
- };
+ const handleValueChange = (value: string) => {
+ if (value === "create-new-project") {
+ onCreateProject();
+ } else {
+ onProjectChange(value);
+ }
+ };
- return (
- <Select
- key={`${id}-${selectedProject}`}
- disabled={isLoading || disabled}
- onValueChange={handleValueChange}
- value={selectedProject}
- >
- <SelectTrigger
- className={`bg-white/5 border-white/10 text-white ${className}`}
- id={id}
- >
- <SelectValue placeholder="Select a project" />
- </SelectTrigger>
- <SelectContent
- className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
- position="popper"
- sideOffset={5}
- >
- <SelectItem
- className="text-white hover:bg-white/10"
- key="default"
- value="sm_project_default"
- >
- Default Project
- </SelectItem>
- {projects
- .filter((p) => p.containerTag !== 'sm_project_default' && p.id)
- .map((project) => (
- <SelectItem
- className="text-white hover:bg-white/10"
- key={project.id || project.containerTag}
- value={project.containerTag}
- >
- {project.name}
- </SelectItem>
- ))}
- <SelectItem
- className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
- key="create-new"
- value="create-new-project"
- >
- <div className="flex items-center gap-2">
- <Plus className="h-4 w-4" />
- <span>Create new project</span>
- </div>
- </SelectItem>
- </SelectContent>
- </Select>
- );
+ return (
+ <Select
+ disabled={isLoading || disabled}
+ key={`${id}-${selectedProject}`}
+ onValueChange={handleValueChange}
+ value={selectedProject}
+ >
+ <SelectTrigger
+ className={`bg-white/5 border-white/10 text-white ${className}`}
+ id={id}
+ >
+ <SelectValue placeholder="Select a project" />
+ </SelectTrigger>
+ <SelectContent
+ className="bg-black/90 backdrop-blur-xl border-white/10 z-[90]"
+ position="popper"
+ sideOffset={5}
+ >
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key="default"
+ value="sm_project_default"
+ >
+ Default Project
+ </SelectItem>
+ {projects
+ .filter((p) => p.containerTag !== "sm_project_default" && p.id)
+ .map((project) => (
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ key={project.id || project.containerTag}
+ value={project.containerTag}
+ >
+ {project.name}
+ </SelectItem>
+ ))}
+ <SelectItem
+ className="text-white hover:bg-white/10 border-t border-white/10 mt-1"
+ key="create-new"
+ value="create-new-project"
+ >
+ <div className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ <span>Create new project</span>
+ </div>
+ </SelectItem>
+ </SelectContent>
+ </Select>
+ );
}
diff --git a/apps/web/components/views/add-memory/tab-button.tsx b/apps/web/components/views/add-memory/tab-button.tsx
index 72dfbbd7..3afbdedf 100644
--- a/apps/web/components/views/add-memory/tab-button.tsx
+++ b/apps/web/components/views/add-memory/tab-button.tsx
@@ -1,28 +1,28 @@
-import type { LucideIcon } from 'lucide-react';
+import type { LucideIcon } from "lucide-react";
interface TabButtonProps {
- icon: LucideIcon;
- label: string;
- isActive: boolean;
- onClick: () => void;
+ icon: LucideIcon;
+ label: string;
+ isActive: boolean;
+ onClick: () => void;
}
export function TabButton({
- icon: Icon,
- label,
- isActive,
- onClick,
+ icon: Icon,
+ label,
+ isActive,
+ onClick,
}: TabButtonProps) {
- return (
- <button
- className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
- isActive ? 'bg-white/10' : 'hover:bg-white/5'
- }`}
- onClick={onClick}
- type="button"
- >
- <Icon className="h-4 w-4 sm:h-3 sm:w-3" />
- {label}
- </button>
- );
+ return (
+ <button
+ className={`flex items-center gap-1.5 text-xs sm:text-xs px-4 sm:px-3 py-2 sm:py-1 h-8 sm:h-6 rounded-sm transition-colors whitespace-nowrap min-w-0 ${
+ isActive ? "bg-white/10" : "hover:bg-white/5"
+ }`}
+ onClick={onClick}
+ type="button"
+ >
+ <Icon className="h-4 w-4 sm:h-3 sm:w-3" />
+ {label}
+ </button>
+ );
}
diff --git a/apps/web/components/views/add-memory/text-editor.tsx b/apps/web/components/views/add-memory/text-editor.tsx
index 5a07b8f7..79b42aa8 100644
--- a/apps/web/components/views/add-memory/text-editor.tsx
+++ b/apps/web/components/views/add-memory/text-editor.tsx
@@ -399,8 +399,6 @@ export function TextEditor({
title: string;
}) => (
<Button
- variant="ghost"
- size="sm"
className={cn(
"h-8 w-8 !p-0 text-white/70 transition-all duration-200 rounded-sm",
"hover:bg-white/15 hover:text-white hover:scale-105",
@@ -408,8 +406,10 @@ export function TextEditor({
isActive && "bg-white/20 text-white",
)}
onMouseDown={onMouseDown}
+ size="sm"
title={title}
type="button"
+ variant="ghost"
>
<Icon
className={cn(
@@ -426,13 +426,20 @@ export function TextEditor({
<Slate
editor={editor}
initialValue={editorValue}
- onValueChange={handleSlateChange}
onSelectionChange={() => setSelection(editor.selection)}
+ onValueChange={handleSlateChange}
>
<Editable
+ className={cn(
+ "outline-none w-full h-full text-white placeholder:text-white/50",
+ disabled && "opacity-50 cursor-not-allowed",
+ )}
+ onBlur={onBlur}
+ onKeyDown={handleKeyDown}
+ placeholder={placeholder}
+ readOnly={disabled}
renderElement={renderElement}
renderLeaf={renderLeaf}
- placeholder={placeholder}
renderPlaceholder={({ children, attributes }) => {
return (
<div {...attributes} className="mt-2">
@@ -440,13 +447,6 @@ export function TextEditor({
</div>
);
}}
- onKeyDown={handleKeyDown}
- onBlur={onBlur}
- readOnly={disabled}
- className={cn(
- "outline-none w-full h-full text-white placeholder:text-white/50",
- disabled && "opacity-50 cursor-not-allowed",
- )}
style={{
minHeight: "11rem",
maxHeight: "15rem",
diff --git a/apps/web/components/views/chat/index.tsx b/apps/web/components/views/chat/index.tsx
index 2b7befa1..c9fdf9f1 100644
--- a/apps/web/components/views/chat/index.tsx
+++ b/apps/web/components/views/chat/index.tsx
@@ -50,12 +50,12 @@ export function ChatRewrite() {
{getCurrentChat()?.title ?? "New Chat"}
</h3>
<div className="flex items-center gap-2">
- <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
+ <Dialog onOpenChange={setIsDialogOpen} open={isDialogOpen}>
<DialogTrigger asChild>
<Button
- variant="outline"
- size="icon"
onClick={() => analytics.chatHistoryViewed()}
+ size="icon"
+ variant="outline"
>
<HistoryIcon className="size-4 text-muted-foreground" />
</Button>
@@ -77,19 +77,19 @@ export function ChatRewrite() {
const isActive = c.id === currentChatId;
return (
<div
- key={c.id}
- role="button"
- tabIndex={0}
- onClick={() => {
- setCurrentChatId(c.id);
- setIsDialogOpen(false);
- }}
+ aria-current={isActive ? "true" : undefined}
className={cn(
"flex items-center justify-between rounded-md px-3 py-2 outline-none",
"transition-colors",
isActive ? "bg-primary/10" : "hover:bg-muted",
)}
- aria-current={isActive ? "true" : undefined}
+ key={c.id}
+ onClick={() => {
+ setCurrentChatId(c.id);
+ setIsDialogOpen(false);
+ }}
+ role="button"
+ tabIndex={0}
>
<div className="min-w-0">
<div className="flex items-center gap-2">
@@ -107,14 +107,14 @@ export function ChatRewrite() {
</div>
</div>
<Button
- type="button"
- variant="ghost"
- size="icon"
+ aria-label="Delete conversation"
onClick={(e) => {
e.stopPropagation();
analytics.chatDeleted();
}}
- aria-label="Delete conversation"
+ size="icon"
+ type="button"
+ variant="ghost"
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
@@ -129,22 +129,22 @@ export function ChatRewrite() {
</div>
</ScrollArea>
<Button
- variant="outline"
- size="lg"
className="w-full border-dashed"
onClick={handleNewChat}
+ size="lg"
+ variant="outline"
>
<Plus className="size-4 mr-1" /> New Conversation
</Button>
</DialogContent>
</Dialog>
- <Button variant="outline" size="icon" onClick={handleNewChat}>
+ <Button onClick={handleNewChat} size="icon" variant="outline">
<Plus className="size-4 text-muted-foreground" />
</Button>
<Button
- variant="outline"
- size="icon"
onClick={() => setIsOpen(false)}
+ size="icon"
+ variant="outline"
>
<X className="size-4 text-muted-foreground" />
</Button>
diff --git a/apps/web/components/views/connections-tab-content.tsx b/apps/web/components/views/connections-tab-content.tsx
index 14a63e8f..cb25a9e9 100644
--- a/apps/web/components/views/connections-tab-content.tsx
+++ b/apps/web/components/views/connections-tab-content.tsx
@@ -1,22 +1,22 @@
-"use client"
+"use client";
-import { $fetch } from "@lib/api"
-import { Button } from "@repo/ui/components/button"
-import { Skeleton } from "@repo/ui/components/skeleton"
-import type { ConnectionResponseSchema } from "@repo/validation/api"
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
-import { useCustomer } from "autumn-js/react"
-import { Trash2 } from "lucide-react"
-import { AnimatePresence, motion } from "motion/react"
-import { useEffect, useState } from "react"
-import { toast } from "sonner"
-import type { z } from "zod"
-import { analytics } from "@/lib/analytics"
-import { useProject } from "@/stores"
+import { $fetch } from "@lib/api";
+import { Button } from "@repo/ui/components/button";
+import { Skeleton } from "@repo/ui/components/skeleton";
+import type { ConnectionResponseSchema } from "@repo/validation/api";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons";
+import { useCustomer } from "autumn-js/react";
+import { Trash2 } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import type { z } from "zod";
+import { analytics } from "@/lib/analytics";
+import { useProject } from "@/stores";
// Define types
-type Connection = z.infer<typeof ConnectionResponseSchema>
+type Connection = z.infer<typeof ConnectionResponseSchema>;
// Connector configurations
const CONNECTORS = {
@@ -35,27 +35,27 @@ const CONNECTORS = {
description: "Access your Microsoft Office documents",
icon: OneDrive,
},
-} as const
+} as const;
-type ConnectorProvider = keyof typeof CONNECTORS
+type ConnectorProvider = keyof typeof CONNECTORS;
export function ConnectionsTabContent() {
- const queryClient = useQueryClient()
- const { selectedProject } = useProject()
- const autumn = useCustomer()
- const [isProUser, setIsProUser] = useState(false)
+ const queryClient = useQueryClient();
+ const { selectedProject } = useProject();
+ const autumn = useCustomer();
+ const [isProUser, setIsProUser] = useState(false);
const handleUpgrade = async () => {
try {
await autumn.attach({
productId: "consumer_pro",
successUrl: "https://app.supermemory.ai/",
- })
- window.location.reload()
+ });
+ window.location.reload();
} catch (error) {
- console.error(error)
+ console.error(error);
}
- }
+ };
// Set pro user status when autumn data loads
useEffect(() => {
@@ -64,16 +64,16 @@ export function ConnectionsTabContent() {
autumn.customer?.products.some(
(product) => product.id === "consumer_pro",
) ?? false,
- )
+ );
}
- }, [autumn.isLoading, autumn.customer])
+ }, [autumn.isLoading, autumn.customer]);
// Get connections data directly from autumn customer
- const connectionsFeature = autumn.customer?.features?.connections
- const connectionsUsed = connectionsFeature?.usage ?? 0
- const connectionsLimit = connectionsFeature?.included_usage ?? 0
+ const connectionsFeature = autumn.customer?.features?.connections;
+ const connectionsUsed = connectionsFeature?.usage ?? 0;
+ const connectionsLimit = connectionsFeature?.included_usage ?? 0;
- const canAddConnection = connectionsUsed < connectionsLimit
+ const canAddConnection = connectionsUsed < connectionsLimit;
// Fetch connections
const {
@@ -87,26 +87,28 @@ export function ConnectionsTabContent() {
body: {
containerTags: [],
},
- })
+ });
if (response.error) {
- throw new Error(response.error?.message || "Failed to load connections")
+ throw new Error(
+ response.error?.message || "Failed to load connections",
+ );
}
- return response.data as Connection[]
+ return response.data as Connection[];
},
staleTime: 30 * 1000,
refetchInterval: 60 * 1000,
- })
+ });
// Show error toast if connections fail to load
useEffect(() => {
if (error) {
toast.error("Failed to load connections", {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
}
- }, [error])
+ }, [error]);
// Add connection mutation
const addConnectionMutation = useMutation({
@@ -115,7 +117,7 @@ export function ConnectionsTabContent() {
if (!canAddConnection && !isProUser) {
throw new Error(
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
- )
+ );
}
const response = await $fetch("@post/connections/:provider", {
@@ -124,61 +126,61 @@ export function ConnectionsTabContent() {
redirectUrl: window.location.href,
containerTags: [selectedProject],
},
- })
+ });
// biome-ignore lint/style/noNonNullAssertion: its fine
if ("data" in response && !("error" in response.data!)) {
- return response.data
+ return response.data;
}
- throw new Error(response.error?.message || "Failed to connect")
+ throw new Error(response.error?.message || "Failed to connect");
},
onSuccess: (data, provider) => {
- analytics.connectionAdded(provider)
- analytics.connectionAuthStarted()
+ analytics.connectionAdded(provider);
+ analytics.connectionAuthStarted();
autumn.track({
featureId: "connections",
value: 1,
- })
+ });
if (data?.authLink) {
- window.location.href = data.authLink
+ window.location.href = data.authLink;
}
},
onError: (error, provider) => {
- analytics.connectionAuthFailed()
+ analytics.connectionAuthFailed();
toast.error(`Failed to connect ${provider}`, {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
- })
+ });
// Delete connection mutation
const deleteConnectionMutation = useMutation({
mutationFn: async (connectionId: string) => {
- await $fetch(`@delete/connections/${connectionId}`)
+ await $fetch(`@delete/connections/${connectionId}`);
},
onSuccess: () => {
- analytics.connectionDeleted()
+ analytics.connectionDeleted();
toast.success(
"Connection removal has started. supermemory will permanently delete the documents in the next few minutes.",
- )
- queryClient.invalidateQueries({ queryKey: ["connections"] })
+ );
+ queryClient.invalidateQueries({ queryKey: ["connections"] });
},
onError: (error) => {
toast.error("Failed to remove connection", {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
- })
+ });
const getProviderIcon = (provider: string) => {
- const connector = CONNECTORS[provider as ConnectorProvider]
+ const connector = CONNECTORS[provider as ConnectorProvider];
if (connector) {
- const Icon = connector.icon
- return <Icon className="h-10 w-10" />
+ const Icon = connector.icon;
+ return <Icon className="h-10 w-10" />;
}
- return <span className="text-2xl">📎</span>
- }
+ return <span className="text-2xl">📎</span>;
+ };
return (
<div className="space-y-4">
@@ -310,7 +312,7 @@ export function ConnectionsTabContent() {
</h3>
<div className="grid gap-3">
{Object.entries(CONNECTORS).map(([provider, config], index) => {
- const Icon = config.icon
+ const Icon = config.icon;
return (
<motion.div
animate={{ opacity: 1, y: 0 }}
@@ -324,7 +326,7 @@ export function ConnectionsTabContent() {
className="justify-start h-auto p-4 bg-white/5 hover:bg-white/10 border-white/10 text-white w-full"
disabled={addConnectionMutation.isPending}
onClick={() => {
- addConnectionMutation.mutate(provider as ConnectorProvider)
+ addConnectionMutation.mutate(provider as ConnectorProvider);
}}
variant="outline"
>
@@ -337,10 +339,10 @@ export function ConnectionsTabContent() {
</div>
</Button>
</motion.div>
- )
+ );
})}
</div>
</div>
</div>
- )
+ );
}
diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx
index b3e3c92d..1724231f 100644
--- a/apps/web/components/views/integrations.tsx
+++ b/apps/web/components/views/integrations.tsx
@@ -1,35 +1,35 @@
-import { $fetch } from "@lib/api"
-import { authClient } from "@lib/auth"
-import { useAuth } from "@lib/auth-context"
-import { generateId } from "@lib/generate-id"
+import { $fetch } from "@lib/api";
+import { authClient } from "@lib/auth";
+import { useAuth } from "@lib/auth-context";
+import { generateId } from "@lib/generate-id";
import {
ADD_MEMORY_SHORTCUT_URL,
SEARCH_MEMORY_SHORTCUT_URL,
-} from "@repo/lib/constants"
-import { fetchConnectionsFeature } from "@repo/lib/queries"
-import { Button } from "@repo/ui/components/button"
+} from "@repo/lib/constants";
+import { fetchConnectionsFeature } from "@repo/lib/queries";
+import { Button } from "@repo/ui/components/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogPortal,
DialogTitle,
-} from "@repo/ui/components/dialog"
-import { Skeleton } from "@repo/ui/components/skeleton"
-import type { ConnectionResponseSchema } from "@repo/validation/api"
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
-import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
-import { useCustomer } from "autumn-js/react"
-import { Check, Copy, Smartphone, Trash2 } from "lucide-react"
-import { motion } from "motion/react"
-import Image from "next/image"
-import { useEffect, useId, useState } from "react"
-import { toast } from "sonner"
-import type { z } from "zod"
-import { analytics } from "@/lib/analytics"
-import { useProject } from "@/stores"
-
-type Connection = z.infer<typeof ConnectionResponseSchema>
+} from "@repo/ui/components/dialog";
+import { Skeleton } from "@repo/ui/components/skeleton";
+import type { ConnectionResponseSchema } from "@repo/validation/api";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons";
+import { useCustomer } from "autumn-js/react";
+import { Check, Copy, Smartphone, Trash2 } from "lucide-react";
+import { motion } from "motion/react";
+import Image from "next/image";
+import { useEffect, useId, useState } from "react";
+import { toast } from "sonner";
+import type { z } from "zod";
+import { analytics } from "@/lib/analytics";
+import { useProject } from "@/stores";
+
+type Connection = z.infer<typeof ConnectionResponseSchema>;
const CONNECTORS = {
"google-drive": {
@@ -47,66 +47,66 @@ const CONNECTORS = {
description: "Access your Microsoft Office documents",
icon: OneDrive,
},
-} as const
+} as const;
-type ConnectorProvider = keyof typeof CONNECTORS
+type ConnectorProvider = keyof typeof CONNECTORS;
const ChromeIcon = ({ className }: { className?: string }) => (
<svg
- xmlns="http://www.w3.org/2000/svg"
+ className={className}
preserveAspectRatio="xMidYMid"
viewBox="0 0 190.5 190.5"
- className={className}
+ xmlns="http://www.w3.org/2000/svg"
>
<title>Google Chrome Icon</title>
<path
- fill="#fff"
d="M95.252 142.873c26.304 0 47.627-21.324 47.627-47.628s-21.323-47.628-47.627-47.628-47.627 21.324-47.627 47.628 21.323 47.628 47.627 47.628z"
+ fill="#fff"
/>
<path
- fill="#229342"
d="m54.005 119.07-41.24-71.43a95.227 95.227 0 0 0-.003 95.25 95.234 95.234 0 0 0 82.496 47.61l41.24-71.43v-.011a47.613 47.613 0 0 1-17.428 17.443 47.62 47.62 0 0 1-47.632.007 47.62 47.62 0 0 1-17.433-17.437z"
+ fill="#229342"
/>
<path
- fill="#fbc116"
d="m136.495 119.067-41.239 71.43a95.229 95.229 0 0 0 82.489-47.622A95.24 95.24 0 0 0 190.5 95.248a95.237 95.237 0 0 0-12.772-47.623H95.249l-.01.007a47.62 47.62 0 0 1 23.819 6.372 47.618 47.618 0 0 1 17.439 17.431 47.62 47.62 0 0 1-.001 47.633z"
+ fill="#fbc116"
/>
<path
- fill="#1a73e8"
d="M95.252 132.961c20.824 0 37.705-16.881 37.705-37.706S116.076 57.55 95.252 57.55 57.547 74.431 57.547 95.255s16.881 37.706 37.705 37.706z"
+ fill="#1a73e8"
/>
<path
- fill="#e33b2e"
d="M95.252 47.628h82.479A95.237 95.237 0 0 0 142.87 12.76 95.23 95.23 0 0 0 95.245 0a95.222 95.222 0 0 0-47.623 12.767 95.23 95.23 0 0 0-34.856 34.872l41.24 71.43.011.006a47.62 47.62 0 0 1-.015-47.633 47.61 47.61 0 0 1 41.252-23.815z"
+ fill="#e33b2e"
/>
</svg>
-)
+);
export function IntegrationsView() {
- const { org } = useAuth()
- const queryClient = useQueryClient()
- const { selectedProject } = useProject()
- const autumn = useCustomer()
- const [showApiKeyModal, setShowApiKeyModal] = useState(false)
- const [apiKey, setApiKey] = useState<string>("")
- const [copied, setCopied] = useState(false)
- const [isProUser, setIsProUser] = useState(false)
+ const { org } = useAuth();
+ const queryClient = useQueryClient();
+ const { selectedProject } = useProject();
+ const autumn = useCustomer();
+ const [showApiKeyModal, setShowApiKeyModal] = useState(false);
+ const [apiKey, setApiKey] = useState<string>("");
+ const [copied, setCopied] = useState(false);
+ const [isProUser, setIsProUser] = useState(false);
const [selectedShortcutType, setSelectedShortcutType] = useState<
"add" | "search" | null
- >(null)
- const apiKeyId = useId()
+ >(null);
+ const apiKeyId = useId();
const handleUpgrade = async () => {
try {
await autumn.attach({
productId: "consumer_pro",
successUrl: "https://app.supermemory.ai/",
- })
- window.location.reload()
+ });
+ window.location.reload();
} catch (error) {
- console.error(error)
+ console.error(error);
}
- }
+ };
useEffect(() => {
if (!autumn.isLoading) {
@@ -114,15 +114,15 @@ export function IntegrationsView() {
autumn.customer?.products.some(
(product) => product.id === "consumer_pro",
) ?? false,
- )
+ );
}
- }, [autumn.isLoading, autumn.customer])
+ }, [autumn.isLoading, autumn.customer]);
- const { data: connectionsCheck } = fetchConnectionsFeature(autumn)
- const connectionsUsed = connectionsCheck?.balance ?? 0
- const connectionsLimit = connectionsCheck?.included_usage ?? 0
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn);
+ const connectionsUsed = connectionsCheck?.balance ?? 0;
+ const connectionsLimit = connectionsCheck?.included_usage ?? 0;
- const canAddConnection = connectionsUsed < connectionsLimit
+ const canAddConnection = connectionsUsed < connectionsLimit;
const {
data: connections = [],
@@ -135,17 +135,19 @@ export function IntegrationsView() {
body: {
containerTags: [],
},
- })
+ });
if (response.error) {
- throw new Error(response.error?.message || "Failed to load connections")
+ throw new Error(
+ response.error?.message || "Failed to load connections",
+ );
}
- return response.data as Connection[]
+ return response.data as Connection[];
},
staleTime: 30 * 1000,
refetchInterval: 60 * 1000,
- })
+ });
useEffect(() => {
if (connectionsError) {
@@ -154,16 +156,16 @@ export function IntegrationsView() {
connectionsError instanceof Error
? connectionsError.message
: "Unknown error",
- })
+ });
}
- }, [connectionsError])
+ }, [connectionsError]);
const addConnectionMutation = useMutation({
mutationFn: async (provider: ConnectorProvider) => {
if (!canAddConnection && !isProUser) {
throw new Error(
"Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
- )
+ );
}
const response = await $fetch("@post/connections/:provider", {
@@ -172,47 +174,47 @@ export function IntegrationsView() {
redirectUrl: window.location.href,
containerTags: [selectedProject],
},
- })
+ });
// biome-ignore lint/style/noNonNullAssertion: its fine
if ("data" in response && !("error" in response.data!)) {
- return response.data
+ return response.data;
}
- throw new Error(response.error?.message || "Failed to connect")
+ throw new Error(response.error?.message || "Failed to connect");
},
onSuccess: (data, provider) => {
- analytics.connectionAdded(provider)
- analytics.connectionAuthStarted()
+ analytics.connectionAdded(provider);
+ analytics.connectionAuthStarted();
if (data?.authLink) {
- window.location.href = data.authLink
+ window.location.href = data.authLink;
}
},
onError: (error, provider) => {
- analytics.connectionAuthFailed()
+ analytics.connectionAuthFailed();
toast.error(`Failed to connect ${provider}`, {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
- })
+ });
const deleteConnectionMutation = useMutation({
mutationFn: async (connectionId: string) => {
- await $fetch(`@delete/connections/${connectionId}`)
+ await $fetch(`@delete/connections/${connectionId}`);
},
onSuccess: () => {
- analytics.connectionDeleted()
+ analytics.connectionDeleted();
toast.success(
"Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.",
- )
- queryClient.invalidateQueries({ queryKey: ["connections"] })
+ );
+ queryClient.invalidateQueries({ queryKey: ["connections"] });
},
onError: (error) => {
toast.error("Failed to remove connection", {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
- })
+ });
const createApiKeyMutation = useMutation({
mutationFn: async () => {
@@ -223,60 +225,60 @@ export function IntegrationsView() {
},
name: `ios-${generateId().slice(0, 8)}`,
prefix: `sm_${org?.id}_`,
- })
- return res.key
+ });
+ return res.key;
},
onSuccess: (apiKey) => {
- setApiKey(apiKey)
- setShowApiKeyModal(true)
- setCopied(false)
- handleCopyApiKey()
+ setApiKey(apiKey);
+ setShowApiKeyModal(true);
+ setCopied(false);
+ handleCopyApiKey();
},
onError: (error) => {
toast.error("Failed to create API key", {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
- })
+ });
const handleShortcutClick = (shortcutType: "add" | "search") => {
- setSelectedShortcutType(shortcutType)
- createApiKeyMutation.mutate()
- }
+ setSelectedShortcutType(shortcutType);
+ createApiKeyMutation.mutate();
+ };
const handleCopyApiKey = async () => {
try {
- await navigator.clipboard.writeText(apiKey)
- setCopied(true)
- toast.success("API key copied to clipboard!")
- setTimeout(() => setCopied(false), 2000)
+ await navigator.clipboard.writeText(apiKey);
+ setCopied(true);
+ toast.success("API key copied to clipboard!");
+ setTimeout(() => setCopied(false), 2000);
} catch {
- toast.error("Failed to copy API key")
+ toast.error("Failed to copy API key");
}
- }
+ };
const handleOpenShortcut = () => {
if (!selectedShortcutType) {
- toast.error("No shortcut type selected")
- return
+ toast.error("No shortcut type selected");
+ return;
}
if (selectedShortcutType === "add") {
- window.open(ADD_MEMORY_SHORTCUT_URL, "_blank")
+ window.open(ADD_MEMORY_SHORTCUT_URL, "_blank");
} else if (selectedShortcutType === "search") {
- window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank")
+ window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank");
}
- }
+ };
const handleDialogClose = (open: boolean) => {
- setShowApiKeyModal(open)
+ setShowApiKeyModal(open);
if (!open) {
// Reset state when dialog closes
- setSelectedShortcutType(null)
- setApiKey("")
- setCopied(false)
+ setSelectedShortcutType(null);
+ setApiKey("");
+ setCopied(false);
}
- }
+ };
return (
<div className="space-y-4 sm:space-y-4 custom-scrollbar">
@@ -298,32 +300,32 @@ export function IntegrationsView() {
</div>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
<Button
- variant="ghost"
className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75 "
- onClick={() => handleShortcutClick("add")}
disabled={createApiKeyMutation.isPending}
+ onClick={() => handleShortcutClick("add")}
+ variant="ghost"
>
<Image
- src="/images/ios-shortcuts.png"
alt="iOS Shortcuts"
- width={20}
height={20}
+ src="/images/ios-shortcuts.png"
+ width={20}
/>
{createApiKeyMutation.isPending
? "Creating..."
: "Add Memory Shortcut"}
</Button>
<Button
- variant="ghost"
className="flex-1 text-white hover:bg-blue-500/10 bg-[#171F59]/75"
- onClick={() => handleShortcutClick("search")}
disabled={createApiKeyMutation.isPending}
+ onClick={() => handleShortcutClick("search")}
+ variant="ghost"
>
<Image
- src="/images/ios-shortcuts.png"
alt="iOS Shortcuts"
- width={20}
height={20}
+ src="/images/ios-shortcuts.png"
+ width={20}
/>
{createApiKeyMutation.isPending
? "Creating..."
@@ -352,8 +354,8 @@ export function IntegrationsView() {
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc",
"_blank",
"noopener,noreferrer",
- )
- analytics.extensionInstallClicked()
+ );
+ analytics.extensionInstallClicked();
}}
size="sm"
variant="ghost"
@@ -465,11 +467,11 @@ export function IntegrationsView() {
) : (
<div className="space-y-2">
{Object.entries(CONNECTORS).map(([provider, config], index) => {
- const Icon = config.icon
+ const Icon = config.icon;
const connection = connections.find(
(conn) => conn.provider === provider,
- )
- const isConnected = !!connection
+ );
+ const isConnected = !!connection;
return (
<motion.div
@@ -548,9 +550,9 @@ export function IntegrationsView() {
</span>
</div>
<motion.div
+ className="flex-shrink-0"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
- className="flex-shrink-0"
>
<Button
className="bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 border-blue-600/30 min-w-[80px] disabled:cursor-not-allowed"
@@ -560,7 +562,7 @@ export function IntegrationsView() {
onClick={() => {
addConnectionMutation.mutate(
provider as ConnectorProvider,
- )
+ );
}}
size="sm"
variant="outline"
@@ -575,7 +577,7 @@ export function IntegrationsView() {
)}
</div>
</motion.div>
- )
+ );
})}
</div>
)}
@@ -587,10 +589,10 @@ export function IntegrationsView() {
More integrations are coming soon! Have a suggestion? Share it with us
on{" "}
<a
+ className="text-orange-500 hover:text-orange-400 underline"
href="https://x.com/supermemoryai"
- target="_blank"
rel="noopener noreferrer"
- className="text-orange-500 hover:text-orange-400 underline"
+ target="_blank"
>
X
</a>
@@ -599,7 +601,7 @@ export function IntegrationsView() {
</div>
{/* API Key Modal */}
- <Dialog open={showApiKeyModal} onOpenChange={handleDialogClose}>
+ <Dialog onOpenChange={handleDialogClose} open={showApiKeyModal}>
<DialogPortal>
<DialogContent className="bg-[#0f1419] border-white/10 text-white md:max-w-md z-[100]">
<DialogHeader>
@@ -618,24 +620,24 @@ export function IntegrationsView() {
{/* API Key Section */}
<div className="space-y-2">
<label
- htmlFor={apiKeyId}
className="text-sm font-medium text-white/80"
+ htmlFor={apiKeyId}
>
Your API Key
</label>
<div className="flex items-center gap-2">
<input
+ className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono"
id={apiKeyId}
+ readOnly
type="text"
value={apiKey}
- readOnly
- className="flex-1 bg-white/5 border border-white/20 rounded-lg px-3 py-2 text-sm text-white font-mono"
/>
<Button
+ className="text-white/70 hover:text-white hover:bg-white/10"
+ onClick={handleCopyApiKey}
size="sm"
variant="ghost"
- onClick={handleCopyApiKey}
- className="text-white/70 hover:text-white hover:bg-white/10"
>
{copied ? (
<Check className="h-4 w-4 text-green-400" />
@@ -681,16 +683,16 @@ export function IntegrationsView() {
<div className="flex gap-2 pt-2">
<Button
- onClick={handleOpenShortcut}
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white"
disabled={!selectedShortcutType}
+ onClick={handleOpenShortcut}
>
<Image
- src="/images/ios-shortcuts.png"
alt="iOS Shortcuts"
- width={16}
- height={16}
className="mr-2"
+ height={16}
+ src="/images/ios-shortcuts.png"
+ width={16}
/>
Add to Shortcuts
</Button>
@@ -700,5 +702,5 @@ export function IntegrationsView() {
</DialogPortal>
</Dialog>
</div>
- )
+ );
}
diff --git a/apps/web/components/views/mcp/index.tsx b/apps/web/components/views/mcp/index.tsx
index e79c2716..c2cc1d5c 100644
--- a/apps/web/components/views/mcp/index.tsx
+++ b/apps/web/components/views/mcp/index.tsx
@@ -1,9 +1,9 @@
-import { $fetch } from "@lib/api"
-import { authClient } from "@lib/auth"
-import { useAuth } from "@lib/auth-context"
-import { useForm } from "@tanstack/react-form"
-import { useMutation } from "@tanstack/react-query"
-import { Button } from "@ui/components/button"
+import { $fetch } from "@lib/api";
+import { authClient } from "@lib/auth";
+import { useAuth } from "@lib/auth-context";
+import { useForm } from "@tanstack/react-form";
+import { useMutation } from "@tanstack/react-query";
+import { Button } from "@ui/components/button";
import {
Dialog,
DialogContent,
@@ -11,18 +11,18 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
-} from "@ui/components/dialog"
-import { Input } from "@ui/components/input"
-import { CopyableCell } from "@ui/copyable-cell"
-import { Loader2 } from "lucide-react"
-import { AnimatePresence, motion } from "motion/react"
-import Image from "next/image"
-import { generateSlug } from "random-word-slugs"
-import { useEffect, useState } from "react"
-import { toast } from "sonner"
-import { z } from "zod/v4"
-import { analytics } from "@/lib/analytics"
-import { InstallationDialogContent } from "./installation-dialog-content"
+} from "@ui/components/dialog";
+import { Input } from "@ui/components/input";
+import { CopyableCell } from "@ui/copyable-cell";
+import { Loader2 } from "lucide-react";
+import { AnimatePresence, motion } from "motion/react";
+import Image from "next/image";
+import { generateSlug } from "random-word-slugs";
+import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { z } from "zod/v4";
+import { analytics } from "@/lib/analytics";
+import { InstallationDialogContent } from "./installation-dialog-content";
// Validation schemas
const mcpMigrationSchema = z.object({
@@ -33,56 +33,56 @@ const mcpMigrationSchema = z.object({
/^https:\/\/mcp\.supermemory\.ai\/[^/]+\/sse$/,
"Link must be in format: https://mcp.supermemory.ai/userId/sse",
),
-})
+});
export function MCPView() {
- const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false)
- const projectId = localStorage.getItem("selectedProject") ?? "default"
- const { org } = useAuth()
- const [apiKey, setApiKey] = useState<string>()
- const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false)
+ const [isMigrateDialogOpen, setIsMigrateDialogOpen] = useState(false);
+ const projectId = localStorage.getItem("selectedProject") ?? "default";
+ const { org } = useAuth();
+ const [apiKey, setApiKey] = useState<string>();
+ const [isInstallDialogOpen, setIsInstallDialogOpen] = useState(false);
useEffect(() => {
- analytics.mcpViewOpened()
- }, [])
+ analytics.mcpViewOpened();
+ }, []);
const apiKeyMutation = useMutation({
mutationFn: async () => {
- if (apiKey) return apiKey
+ if (apiKey) return apiKey;
const res = await authClient.apiKey.create({
metadata: {
organizationId: org?.id,
},
name: generateSlug(),
prefix: `sm_${org?.id}_`,
- })
- return res.key
+ });
+ return res.key;
},
onSuccess: (data) => {
- setApiKey(data)
- setIsInstallDialogOpen(true)
+ setApiKey(data);
+ setIsInstallDialogOpen(true);
},
- })
+ });
// Form for MCP migration
const mcpMigrationForm = useForm({
defaultValues: { url: "" },
onSubmit: async ({ value, formApi }) => {
- const userId = extractUserIdFromMCPUrl(value.url)
+ const userId = extractUserIdFromMCPUrl(value.url);
if (userId) {
- migrateMCPMutation.mutate({ userId, projectId })
- formApi.reset()
+ migrateMCPMutation.mutate({ userId, projectId });
+ formApi.reset();
}
},
validators: {
onChange: mcpMigrationSchema,
},
- })
+ });
const extractUserIdFromMCPUrl = (url: string): string | null => {
- const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/
- const match = url.trim().match(regex)
- return match?.[1] || null
- }
+ const regex = /^https:\/\/mcp\.supermemory\.ai\/([^/]+)\/sse$/;
+ const match = url.trim().match(regex);
+ return match?.[1] || null;
+ };
// Migrate MCP mutation
const migrateMCPMutation = useMutation({
@@ -90,33 +90,33 @@ export function MCPView() {
userId,
projectId,
}: {
- userId: string
- projectId: string
+ userId: string;
+ projectId: string;
}) => {
const response = await $fetch("@post/documents/migrate-mcp", {
body: { userId, projectId },
- })
+ });
if (response.error) {
throw new Error(
response.error?.message || "Failed to migrate documents",
- )
+ );
}
- return response.data
+ return response.data;
},
onSuccess: (data) => {
toast.success("Migration completed!", {
description: `Successfully migrated ${data?.migratedCount} documents`,
- })
- setIsMigrateDialogOpen(false)
+ });
+ setIsMigrateDialogOpen(false);
},
onError: (error) => {
toast.error("Migration failed", {
description: error instanceof Error ? error.message : "Unknown error",
- })
+ });
},
- })
+ });
return (
<div className="space-y-6">
@@ -155,9 +155,9 @@ export function MCPView() {
<Button
disabled={apiKeyMutation.isPending}
onClick={(e) => {
- e.preventDefault()
- e.stopPropagation()
- apiKeyMutation.mutate()
+ e.preventDefault();
+ e.stopPropagation();
+ apiKeyMutation.mutate();
}}
>
Install Now
@@ -213,9 +213,9 @@ export function MCPView() {
</DialogHeader>
<form
onSubmit={(e) => {
- e.preventDefault()
- e.stopPropagation()
- mcpMigrationForm.handleSubmit()
+ e.preventDefault();
+ e.stopPropagation();
+ mcpMigrationForm.handleSubmit();
}}
>
<div className="grid gap-4">
@@ -268,8 +268,8 @@ export function MCPView() {
<Button
className="bg-white/5 hover:bg-white/10 border-white/10 text-white"
onClick={() => {
- setIsMigrateDialogOpen(false)
- mcpMigrationForm.reset()
+ setIsMigrateDialogOpen(false);
+ mcpMigrationForm.reset();
}}
type="button"
variant="outline"
@@ -307,5 +307,5 @@ export function MCPView() {
)}
</AnimatePresence>
</div>
- )
+ );
}
diff --git a/apps/web/components/views/mcp/installation-dialog-content.tsx b/apps/web/components/views/mcp/installation-dialog-content.tsx
index 4c04c6ac..3150c098 100644
--- a/apps/web/components/views/mcp/installation-dialog-content.tsx
+++ b/apps/web/components/views/mcp/installation-dialog-content.tsx
@@ -1,3 +1,5 @@
+import { $fetch } from "@repo/lib/api";
+import { useQuery } from "@tanstack/react-query";
import { Button } from "@ui/components/button";
import {
DialogContent,
@@ -6,6 +8,7 @@ import {
DialogTitle,
} from "@ui/components/dialog";
import { Input } from "@ui/components/input";
+import { Label } from "@ui/components/label";
import {
Select,
SelectContent,
@@ -13,13 +16,10 @@ import {
SelectTrigger,
SelectValue,
} from "@ui/components/select";
-import { Label } from "@ui/components/label";
import { CopyIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { analytics } from "@/lib/analytics";
-import { $fetch } from "@repo/lib/api";
-import { useQuery } from "@tanstack/react-query";
const clients = {
cursor: "Cursor",
@@ -67,7 +67,7 @@ export function InstallationDialogContent() {
if (selectedProject && selectedProject !== "none") {
// Remove the "sm_project_" prefix from the containerTag
- const projectId = selectedProject.replace(/^sm_project_/, '');
+ const projectId = selectedProject.replace(/^sm_project_/, "");
command += ` --project ${projectId}`;
}
@@ -79,8 +79,8 @@ export function InstallationDialogContent() {
<DialogHeader>
<DialogTitle>Install the supermemory MCP Server</DialogTitle>
<DialogDescription>
- Select the app and project you want to install supermemory MCP to, then run the
- following command:
+ Select the app and project you want to install supermemory MCP to,
+ then run the following command:
</DialogDescription>
</DialogHeader>
@@ -91,7 +91,7 @@ export function InstallationDialogContent() {
onValueChange={(value) => setClient(value as keyof typeof clients)}
value={client}
>
- <SelectTrigger id="client-select" className="w-full">
+ <SelectTrigger className="w-full" id="client-select">
<SelectValue placeholder="Select client" />
</SelectTrigger>
<SelectContent>
@@ -107,27 +107,30 @@ export function InstallationDialogContent() {
<div className="space-y-2">
<Label htmlFor="project-select">Target Project (Optional)</Label>
<Select
+ disabled={isLoadingProjects}
onValueChange={setSelectedProject}
value={selectedProject || "none"}
- disabled={isLoadingProjects}
>
- <SelectTrigger id="project-select" className="w-full">
+ <SelectTrigger className="w-full" id="project-select">
<SelectValue placeholder="Select project" />
</SelectTrigger>
<SelectContent className="bg-black/90 backdrop-blur-xl border-white/10">
- <SelectItem value="none" className="text-white hover:bg-white/10">
+ <SelectItem className="text-white hover:bg-white/10" value="none">
Auto-select project
</SelectItem>
- <SelectItem value="sm_project_default" className="text-white hover:bg-white/10">
+ <SelectItem
+ className="text-white hover:bg-white/10"
+ value="sm_project_default"
+ >
Default Project
</SelectItem>
{projects
.filter((p: Project) => p.containerTag !== "sm_project_default")
.map((project: Project) => (
<SelectItem
+ className="text-white hover:bg-white/10"
key={project.id}
value={project.containerTag}
- className="text-white hover:bg-white/10"
>
{project.name}
</SelectItem>
@@ -139,8 +142,8 @@ export function InstallationDialogContent() {
<div className="space-y-2">
<Label htmlFor="command-input">Installation Command</Label>
<Input
- id="command-input"
className="font-mono text-xs!"
+ id="command-input"
readOnly
value={generateInstallCommand()}
/>
diff --git a/apps/web/components/views/profile.tsx b/apps/web/components/views/profile.tsx
index 9fa086ec..9b3dc387 100644
--- a/apps/web/components/views/profile.tsx
+++ b/apps/web/components/views/profile.tsx
@@ -1,10 +1,11 @@
-"use client"
+"use client";
-import { authClient } from "@lib/auth"
-import { useAuth } from "@lib/auth-context"
-import { Button } from "@repo/ui/components/button"
-import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold"
-import { useCustomer } from "autumn-js/react"
+import { $fetch } from "@lib/api";
+import { authClient } from "@lib/auth";
+import { useAuth } from "@lib/auth-context";
+import { Button } from "@repo/ui/components/button";
+import { HeadingH3Bold } from "@repo/ui/text/heading/heading-h3-bold";
+import { useCustomer } from "autumn-js/react";
import {
CheckCircle,
CreditCard,
@@ -12,48 +13,47 @@ import {
LogOut,
User,
X,
-} from "lucide-react"
-import { motion } from "motion/react"
-import Link from "next/link"
-import { useRouter } from "next/navigation"
-import { useEffect, useState } from "react"
-import { analytics } from "@/lib/analytics"
-import { $fetch } from "@lib/api"
+} from "lucide-react";
+import { motion } from "motion/react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { analytics } from "@/lib/analytics";
export function ProfileView() {
- const router = useRouter()
- const { user: session } = useAuth()
+ const router = useRouter();
+ const { user: session } = useAuth();
const {
customer,
isLoading: isCustomerLoading,
openBillingPortal,
attach,
- } = useCustomer()
- const [isLoading, setIsLoading] = useState(false)
+ } = useCustomer();
+ const [isLoading, setIsLoading] = useState(false);
const [billingData, setBillingData] = useState<{
- isPro: boolean
- memoriesUsed: number
- memoriesLimit: number
- connectionsUsed: number
- connectionsLimit: number
+ isPro: boolean;
+ memoriesUsed: number;
+ memoriesLimit: number;
+ connectionsUsed: number;
+ connectionsLimit: number;
}>({
isPro: false,
memoriesUsed: 0,
memoriesLimit: 0,
connectionsUsed: 0,
connectionsLimit: 0,
- })
+ });
useEffect(() => {
if (!isCustomerLoading) {
const memoriesFeature = customer?.features?.memories ?? {
usage: 0,
included_usage: 0,
- }
+ };
const connectionsFeature = customer?.features?.connections ?? {
usage: 0,
included_usage: 0,
- }
+ };
setBillingData({
isPro:
@@ -64,30 +64,30 @@ export function ProfileView() {
memoriesLimit: memoriesFeature?.included_usage ?? 0,
connectionsUsed: connectionsFeature?.usage ?? 0,
connectionsLimit: connectionsFeature?.included_usage ?? 0,
- })
+ });
}
- }, [isCustomerLoading, customer])
+ }, [isCustomerLoading, customer]);
const handleLogout = () => {
- analytics.userSignedOut()
- authClient.signOut()
- router.push("/login")
- }
+ analytics.userSignedOut();
+ authClient.signOut();
+ router.push("/login");
+ };
const handleUpgrade = async () => {
- setIsLoading(true)
+ setIsLoading(true);
try {
const upgradeResult = await attach({
productId: "consumer_pro",
successUrl: "https://app.supermemory.ai/",
- })
+ });
if (
upgradeResult.statusCode === 200 &&
upgradeResult.data &&
"code" in upgradeResult.data
) {
const isProPlanActivated =
- upgradeResult.data.code === "new_product_attached"
+ upgradeResult.data.code === "new_product_attached";
if (isProPlanActivated && session?.name && session?.email) {
try {
await $fetch("@post/emails/welcome/pro", {
@@ -95,24 +95,24 @@ export function ProfileView() {
email: session?.email,
firstName: session?.name,
},
- })
+ });
} catch (error) {
- console.error(error)
+ console.error(error);
}
}
}
} catch (error) {
- console.error(error)
- setIsLoading(false)
+ console.error(error);
+ setIsLoading(false);
}
- }
+ };
// Handle manage billing
const handleManageBilling = async () => {
await openBillingPortal({
returnUrl: "https://app.supermemory.ai",
- })
- }
+ });
+ };
if (session?.isAnonymous) {
return (
@@ -137,7 +137,7 @@ export function ProfileView() {
</motion.div>
</motion.div>
</div>
- )
+ );
}
return (
@@ -337,5 +337,5 @@ export function ProfileView() {
Sign Out
</Button>
</div>
- )
+ );
}