"use client" import { dmSans125ClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { $fetch } from "@lib/api" import { hasActivePlan } from "@lib/queries" import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Check, Plus, Trash2, Zap } from "lucide-react" import { useEffect, useState } from "react" import { toast } from "sonner" import { useQueryState } from "nuqs" import type { ConnectionResponseSchema } from "@repo/validation/api" import type { z } from "zod" import { analytics } from "@/lib/analytics" import { AddDocumentModal } from "@/components/add-document" import { RemoveConnectionDialog } from "@/components/remove-connection-dialog" import { addDocumentParam } from "@/lib/search-params" import { DEFAULT_PROJECT_ID } from "@lib/constants" import type { Project } from "@lib/types" type Connection = z.infer const CONNECTORS = { "google-drive": { title: "Google Drive", icon: GoogleDrive, documentLabel: "documents", }, notion: { title: "Notion", icon: Notion, documentLabel: "pages" }, onedrive: { title: "OneDrive", icon: OneDrive, documentLabel: "documents" }, } as const type ConnectorProvider = keyof typeof CONNECTORS function ConnectionRow({ connection, onDelete, isDeleting, disabled, projects, }: { connection: Connection onDelete: () => void isDeleting: boolean disabled?: boolean projects: Project[] }) { const config = CONNECTORS[connection.provider as ConnectorProvider] if (!config) return null const Icon = config.icon const isConnected = !connection.expiresAt || new Date(connection.expiresAt) > new Date() const formatRelativeTime = (date: string | null | undefined) => { if (!date) return "Never" const d = new Date(date) const diffMs = Date.now() - d.getTime() const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) const diffDays = Math.floor(diffHours / 24) if (diffHours < 1) return "Just now" if (diffHours < 24) return `${diffHours}h ago` if (diffDays === 1) return "Yesterday" if (diffDays < 7) return `${diffDays} days ago` return d.toLocaleDateString() } const getProjectName = (tag: string): string => { if (tag === DEFAULT_PROJECT_ID) return "Default Project" return ( projects.find((p) => p.containerTag === tag)?.name ?? tag.replace(/^sm_project_/, "") ) } const documentCount = (connection.metadata?.documentCount as number) ?? 0 const containerTags = ( connection as Connection & { containerTags?: string[] } ).containerTags const projectName = containerTags?.[0] ? getProjectName(containerTags[0]) : null return (
{config.title}
{isConnected ? "Connected" : "Disconnected"}
{connection.email || "Unknown"}
{projectName && ( <> Project: {projectName}
)} Added: {formatRelativeTime(connection.createdAt)}
{documentCount} {config.documentLabel} connected
) } export function ConnectionsDetail() { const queryClient = useQueryClient() const autumn = useCustomer() const [isAddDocumentOpen, setIsAddDocumentOpen] = useState(false) const [removeDialog, setRemoveDialog] = useState<{ open: boolean connection: Connection | null }>({ open: false, connection: null }) const [, setAddDoc] = useQueryState("add", addDocumentParam) const projects = (queryClient.getQueryData(["projects"]) || []) as Project[] const hasProProduct = hasActivePlan(autumn.customer?.products, "api_pro") const connectionsFeature = autumn.customer?.features?.connections const connectionsUsed = connectionsFeature?.usage ?? 0 const connectionsLimit = connectionsFeature?.included_usage ?? 10 const canAddConnection = connectionsUsed < connectionsLimit const { data: connections = [], isLoading: isLoadingConnections, error: connectionsError, } = useQuery({ queryKey: ["connections"], queryFn: async () => { const response = await $fetch("@post/connections/list", { body: { containerTags: [] }, }) if (response.error) throw new Error(response.error?.message || "Failed to load connections") return response.data as Connection[] }, staleTime: 30 * 1000, refetchInterval: 60 * 1000, enabled: hasProProduct, }) useEffect(() => { if (connectionsError) { toast.error("Failed to load connections", { description: connectionsError instanceof Error ? connectionsError.message : "Unknown error", }) } }, [connectionsError]) const deleteConnectionMutation = useMutation({ mutationFn: async ({ connectionId, deleteDocuments, }: { connectionId: string deleteDocuments: boolean }) => { await $fetch(`@delete/connections/${connectionId}`, { query: { deleteDocuments }, }) return { deleteDocuments } }, onSuccess: (_data, variables) => { analytics.connectionDeleted() toast.success( variables.deleteDocuments ? "Connection removal has started. Documents will be permanently deleted in the next few minutes." : "Connection removed. Your memories have been kept.", ) setRemoveDialog({ open: false, connection: null }) queryClient.invalidateQueries({ queryKey: ["connections"] }) }, onError: (error) => { toast.error("Failed to remove connection", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) const handleUpgrade = async () => { try { await autumn.attach({ productId: "api_pro", successUrl: "https://app.supermemory.ai/?view=integrations", }) window.location.reload() } catch (error) { console.error(error) } } const isLoading = autumn.isLoading return (
{!hasProProduct && !isLoading && ( <>

Connect Google Drive, Notion, and OneDrive to import your knowledge

{[ "Unlimited memories", "10 connections", "Advanced search", "Priority support", ].map((text) => (
{text}
))}
)}
Connected to Supermemory {connections.length}/{connectionsLimit} connections used
{isLoadingConnections ? (
) : connections.length > 0 ? ( connections.map((connection) => ( setRemoveDialog({ open: true, connection })} isDeleting={deleteConnectionMutation.isPending} disabled={!hasProProduct} projects={projects} /> )) ) : (

No connections yet

Connect a service below to import your knowledge

)} setIsAddDocumentOpen(false)} /> { if (!open) setRemoveDialog({ open: false, connection: null }) }} provider={removeDialog.connection?.provider} documentCount={ (removeDialog.connection?.metadata?.documentCount as number) ?? 0 } onConfirm={(deleteDocuments) => { if (removeDialog.connection) { deleteConnectionMutation.mutate({ connectionId: removeDialog.connection.id, deleteDocuments, }) } }} isDeleting={deleteConnectionMutation.isPending} />
) }