diff options
| author | Dhravya Shah <[email protected]> | 2025-09-18 20:34:18 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2025-09-18 21:03:49 -0700 |
| commit | 1fcb56908920da386900abb4ce2383374a625c72 (patch) | |
| tree | 0f9d7f695d4c9b1b85be3950fc869e0061dff3ed /apps/web/components/views | |
| parent | refetching logic change (diff) | |
| download | supermemory-09-18-formatting.tar.xz supermemory-09-18-formatting.zip | |
formatting09-18-formatting
Diffstat (limited to 'apps/web/components/views')
| -rw-r--r-- | apps/web/components/views/add-memory/action-buttons.tsx | 118 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/fixed-mutation.tsx | 244 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/index.tsx | 417 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/memory-usage-ring.tsx | 23 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/project-selection.tsx | 164 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/tab-button.tsx | 42 | ||||
| -rw-r--r-- | apps/web/components/views/add-memory/text-editor.tsx | 22 | ||||
| -rw-r--r-- | apps/web/components/views/chat/index.tsx | 40 | ||||
| -rw-r--r-- | apps/web/components/views/connections-tab-content.tsx | 130 | ||||
| -rw-r--r-- | apps/web/components/views/integrations.tsx | 264 | ||||
| -rw-r--r-- | apps/web/components/views/mcp/index.tsx | 116 | ||||
| -rw-r--r-- | apps/web/components/views/mcp/installation-dialog-content.tsx | 29 | ||||
| -rw-r--r-- | apps/web/components/views/profile.tsx | 86 |
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> - ) + ); } |