diff options
| -rw-r--r-- | apps/web/components/views/add-memory/index.tsx | 419 | ||||
| -rw-r--r-- | apps/web/components/views/connections-tab-content.tsx | 162 |
2 files changed, 285 insertions, 296 deletions
diff --git a/apps/web/components/views/add-memory/index.tsx b/apps/web/components/views/add-memory/index.tsx index ff1fa077..74469f3b 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,18 @@ 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 { 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,37 +31,40 @@ import { PlugIcon, Plus, UploadIcon, -} from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -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"; -import dynamic from "next/dynamic"; - -const TextEditor = dynamic(() => import("./text-editor").then(mod => ({ default: mod.TextEditor })), { - loading: () => ( - <div className="bg-white/5 border border-white/10 rounded-md"> - <div className="flex-1 min-h-48 max-h-64 overflow-y-auto flex items-center justify-center text-white/70"> - Loading editor... - </div> - <div className="p-1 flex items-center gap-2 bg-white/5 backdrop-blur-sm rounded-b-md"> - <div className="flex items-center gap-1 opacity-50"> - <div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" /> - <div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" /> - <div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" /> +} from "lucide-react" +import { AnimatePresence, motion } from "motion/react" +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" +import dynamic from "next/dynamic" + +const TextEditor = dynamic( + () => import("./text-editor").then((mod) => ({ default: mod.TextEditor })), + { + loading: () => ( + <div className="bg-white/5 border border-white/10 rounded-md"> + <div className="flex-1 min-h-48 max-h-64 overflow-y-auto flex items-center justify-center text-white/70"> + Loading editor... + </div> + <div className="p-1 flex items-center gap-2 bg-white/5 backdrop-blur-sm rounded-b-md"> + <div className="flex items-center gap-1 opacity-50"> + <div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" /> + <div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" /> + <div className="h-8 w-8 bg-white/10 rounded-sm animate-pulse" /> + </div> </div> </div> - </div> - ), - ssr: false, -}); + ), + ssr: false, + }, +) // // Processing status component // function ProcessingStatus({ status }: { status: string }) { @@ -89,80 +92,74 @@ 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 as any); - - const memoriesUsed = memoriesCheck?.usage ?? 0; - const memoriesLimit = memoriesCheck?.included_usage ?? 0; + const { data: memoriesCheck } = fetchMemoriesFeature(autumn) - // Check if user is pro - const { data: proCheck } = fetchConsumerProProduct(autumn as any); - const isProUser = proCheck?.allowed ?? false; - - const canAddMemory = memoriesUsed < memoriesLimit; + 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: { @@ -174,8 +171,8 @@ export function AddMemoryView({ content: value.content, project: value.project, contentType: activeTab as "note" | "link", - }); - formApi.reset(); + }) + formApi.reset() }, validators: { onChange: z.object({ @@ -183,19 +180,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({ @@ -206,8 +203,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) { @@ -216,25 +213,13 @@ export function AddMemoryView({ title: value.title || undefined, description: value.description || undefined, project: value.project, - }); + }) } - formApi.reset(); - setSelectedFiles([]); + formApi.reset() + setSelectedFiles([]) }, - }); - - const handleUpgrade = async () => { - try { - await autumn.attach({ - productId: "consumer_pro", - successUrl: "https://app.supermemory.ai/", - }); - window.location.reload(); - } catch (error) { - console.error(error); - } - }; + }) const addContentMutation = useMutation({ mutationFn: async ({ @@ -242,12 +227,12 @@ export function AddMemoryView({ project, contentType, }: { - content: string; - project: string; - contentType: "note" | "link"; + content: string + project: string + contentType: "note" | "link" }) => { // close the modal - onClose?.(); + onClose?.() const processingPromise = (async () => { // First, create the memory @@ -259,31 +244,31 @@ export function AddMemoryView({ sm_source: "consumer", // Use "consumer" source to bypass limits }, }, - }); + }) if (response.error) { throw new Error( response.error?.message || `Failed to add ${contentType}`, - ); + ) } - const memoryId = response.data.id; + const memoryId = response.data.id // Polling function to check status const pollForCompletion = async (): Promise<any> => { - let attempts = 0; - const maxAttempts = 60; // Maximum 5 minutes (60 attempts * 5 seconds) + let attempts = 0 + const maxAttempts = 60 // Maximum 5 minutes (60 attempts * 5 seconds) while (attempts < maxAttempts) { try { const memory = await $fetch<{ status: string; content: string }>( "@get/memories/" + memoryId, - ); + ) if (memory.error) { throw new Error( memory.error?.message || "Failed to fetch memory status", - ); + ) } // Check if processing is complete @@ -293,58 +278,58 @@ export function AddMemoryView({ // Sometimes the memory might be ready when it has content and no processing status memory.data?.content ) { - return memory.data; + return memory.data } // If still processing, wait and try again - await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds - attempts++; + await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait 5 seconds + attempts++ } catch (error) { - console.error("Error polling memory status:", error); + console.error("Error polling memory status:", error) // Don't throw immediately, retry a few times if (attempts >= 3) { - throw new Error("Failed to check processing status"); + throw new Error("Failed to check processing status") } - await new Promise((resolve) => setTimeout(resolve, 5000)); - attempts++; + await new Promise((resolve) => setTimeout(resolve, 5000)) + attempts++ } } // If we've exceeded max attempts, throw an error throw new Error( "Memory processing timed out. Please check back later.", - ); - }; + ) + } // Wait for completion - const completedMemory = await pollForCompletion(); - return completedMemory; - })(); + const completedMemory = await pollForCompletion() + return completedMemory + })() toast.promise(processingPromise, { loading: "Processing...", success: `${contentType === "link" ? "Link" : "Note"} created successfully!`, error: (err) => `Failed to add ${contentType}: ${err instanceof Error ? err.message : "Unknown error"}`, - }); + }) - return processingPromise; + return processingPromise }, onMutate: async ({ content, project, contentType }) => { - console.log("🚀 onMutate starting..."); + console.log("🚀 onMutate starting...") // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: ["documents-with-memories", project], - }); - console.log("✅ Cancelled queries"); + }) + console.log("✅ Cancelled queries") // Snapshot the previous value const previousMemories = queryClient.getQueryData([ "documents-with-memories", project, - ]); - console.log("📸 Previous memories:", previousMemories); + ]) + console.log("📸 Previous memories:", previousMemories) // Create optimistic memory const optimisticMemory = { @@ -368,28 +353,28 @@ export function AddMemoryView({ }, memoryEntries: [], isOptimistic: true, - }; - console.log("🎯 Created optimistic memory:", optimisticMemory); + } + console.log("🎯 Created optimistic memory:", optimisticMemory) // Optimistically update to include the new memory queryClient.setQueryData( ["documents-with-memories", project], (old: any) => { - console.log("🔄 Old data:", old); + console.log("🔄 Old data:", old) const newData = old ? { ...old, documents: [optimisticMemory, ...(old.documents || [])], totalCount: (old.totalCount || 0) + 1, } - : { documents: [optimisticMemory], totalCount: 1 }; - console.log("✨ New data:", newData); - return newData; + : { documents: [optimisticMemory], totalCount: 1 } + console.log("✨ New data:", newData) + return newData }, - ); + ) - console.log("✅ onMutate completed"); - return { previousMemories, optimisticId: optimisticMemory.id }; + console.log("✅ onMutate completed") + return { previousMemories, optimisticId: optimisticMemory.id } }, // If the mutation fails, roll back to the previous value onError: (error, variables, context) => { @@ -397,7 +382,7 @@ export function AddMemoryView({ queryClient.setQueryData( ["documents-with-memories", variables.project], context.previousMemories, - ); + ) } }, onSuccess: (_data, variables) => { @@ -405,28 +390,28 @@ export function AddMemoryView({ type: variables.contentType === "link" ? "link" : "note", project_id: variables.project, content_length: variables.content.length, - }); + }) queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project], - }); + }) setTimeout(() => { queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project], - }); - }, 30000); // 30 seconds + }) + }, 30000) // 30 seconds setTimeout(() => { queryClient.invalidateQueries({ queryKey: ["documents-with-memories", variables.project], - }); - }, 120000); // 2 minutes + }) + }, 120000) // 2 minutes - setShowAddDialog(false); - onClose?.(); + setShowAddDialog(false) + onClose?.() }, - }); + }) const fileUploadMutation = useMutation({ mutationFn: async ({ @@ -435,10 +420,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 @@ -448,9 +433,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/memories/file`, @@ -459,14 +444,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) { @@ -478,23 +463,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 = { @@ -514,23 +499,23 @@ export function AddMemoryView({ mimeType: file.type, }, memoryEntries: [], - }; + } // Optimistically update to include the new memory queryClient.setQueryData( ["documents-with-memories", project], (old: any) => { - 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) => { @@ -538,11 +523,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({ @@ -550,18 +535,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?.() }, // Always refetch after error or success onSettled: () => { - queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] }); + queryClient.invalidateQueries({ queryKey: ["documents-with-memories"] }) }, - }); + }) return ( <AnimatePresence mode="wait"> @@ -569,8 +554,8 @@ export function AddMemoryView({ <Dialog key="add-memory-dialog" onOpenChange={(open) => { - setShowAddDialog(open); - if (!open) onClose?.(); + setShowAddDialog(open) + if (!open) onClose?.() }} open={showAddDialog} > @@ -597,26 +582,26 @@ export function AddMemoryView({ <div className="bg-white/5 p-1 h-10 sm:h-8 rounded-md flex overflow-x-auto"> <TabButton icon={Brain} - label="Note" isActive={activeTab === "note"} + label="Note" onClick={() => setActiveTab("note")} /> <TabButton icon={LinkIcon} - label="Link" isActive={activeTab === "link"} + label="Link" onClick={() => setActiveTab("link")} /> <TabButton icon={FileIcon} - label="File" isActive={activeTab === "file"} + label="File" onClick={() => setActiveTab("file")} /> <TabButton icon={PlugIcon} - label="Connect" isActive={activeTab === "connect"} + label="Connect" onClick={() => setActiveTab("connect")} /> </div> @@ -629,9 +614,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"> @@ -647,9 +632,9 @@ export function AddMemoryView({ validators={{ onChange: ({ value }) => { if (!value || value.trim() === "") { - return "Note is required"; + return "Note is required" } - return undefined; + return undefined }, }} > @@ -729,9 +714,9 @@ export function AddMemoryView({ <ActionButtons onCancel={() => { - setShowAddDialog(false); - onClose?.(); - addContentForm.reset(); + setShowAddDialog(false) + onClose?.() + addContentForm.reset() }} submitText="Add Note" submitIcon={Plus} @@ -747,9 +732,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"> @@ -771,13 +756,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" } }, }} @@ -855,9 +840,9 @@ export function AddMemoryView({ <ActionButtons onCancel={() => { - setShowAddDialog(false); - onClose?.(); - addContentForm.reset(); + setShowAddDialog(false) + onClose?.() + addContentForm.reset() }} submitText="Add Link" submitIcon={Plus} @@ -873,9 +858,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"> @@ -1007,10 +992,10 @@ export function AddMemoryView({ <ActionButtons onCancel={() => { - setShowAddDialog(false); - onClose?.(); - fileUploadForm.reset(); - setSelectedFiles([]); + setShowAddDialog(false) + onClose?.() + fileUploadForm.reset() + setSelectedFiles([]) }} submitText="Upload File" submitIcon={UploadIcon} @@ -1080,8 +1065,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" @@ -1118,19 +1103,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 ( <> @@ -1201,5 +1186,5 @@ export function AddMemoryExpandedView() { /> )} </> - ); + ) } diff --git a/apps/web/components/views/connections-tab-content.tsx b/apps/web/components/views/connections-tab-content.tsx index 64aa35ef..14a63e8f 100644 --- a/apps/web/components/views/connections-tab-content.tsx +++ b/apps/web/components/views/connections-tab-content.tsx @@ -1,34 +1,22 @@ -"use client"; +"use client" -import { $fetch } from "@lib/api"; -import { - fetchConnectionsFeature, - fetchConsumerProProduct, -} from "@repo/lib/queries"; -import { Button } from "@repo/ui/components/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - 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 { Trash2 } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import Link from "next/link"; -import { useEffect } 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 = { @@ -47,35 +35,45 @@ 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 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) } - }; + } - const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any); - const connectionsUsed = connectionsCheck?.balance ?? 0; - const connectionsLimit = connectionsCheck?.included_usage ?? 0; + // Set pro user status when autumn data loads + useEffect(() => { + if (!autumn.isLoading) { + setIsProUser( + autumn.customer?.products.some( + (product) => product.id === "consumer_pro", + ) ?? false, + ) + } + }, [autumn.isLoading, autumn.customer]) - const { data: proCheck } = fetchConsumerProProduct(autumn as any); - const isProUser = proCheck?.allowed ?? false; + // 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 canAddConnection = connectionsUsed < connectionsLimit; + const canAddConnection = connectionsUsed < connectionsLimit // Fetch connections const { @@ -89,28 +87,26 @@ 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({ @@ -119,7 +115,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", { @@ -128,57 +124,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"> @@ -186,7 +186,12 @@ export function ConnectionsTabContent() { <p className="text-sm text-white/70"> Connect your favorite services to import documents </p> - {!isProUser && ( + {isProUser && !autumn.isLoading && ( + <p className="text-xs text-white/50 mt-1"> + {connectionsUsed} of {connectionsLimit} connections used + </p> + )} + {!isProUser && !autumn.isLoading && ( <p className="text-xs text-white/50 mt-1"> Connections require a Pro subscription </p> @@ -194,7 +199,7 @@ export function ConnectionsTabContent() { </div> {/* Show upgrade prompt for free users */} - {!isProUser && ( + {!autumn.isLoading && !isProUser && ( <motion.div animate={{ opacity: 1, y: 0 }} className="p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg" @@ -208,7 +213,6 @@ export function ConnectionsTabContent() { sync your documents. </p> <Button - asChild className="bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-400 border-yellow-500/30" onClick={handleUpgrade} size="sm" @@ -306,7 +310,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 }} @@ -320,7 +324,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" > @@ -333,10 +337,10 @@ export function ConnectionsTabContent() { </div> </Button> </motion.div> - ); + ) })} </div> </div> </div> - ); + ) } |