"use client" import { dmSans125ClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { $fetch } from "@lib/api" import { hasActivePlan } from "@lib/queries" import { GoogleDrive, Granola, Notion, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Check, ChevronDown, History, Loader2, RefreshCw, 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 { ConnectAIModal } from "@/components/connect-ai-modal" 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" import { SyncStatusBadge } from "@/components/settings/sync-status-badge" import { SyncHistoryPanel } from "@/components/settings/sync-history-panel" import { useConnectionHealth } from "@/hooks/use-connection-health" import { useTriggerSync } from "@/hooks/use-trigger-sync" import { formatRelativeTime, getConnectionSubtitle, } from "@/components/settings/sync-utils" import type { ImportProvider } from "@/components/settings/sync-utils" type Connection = z.infer /** Extract typed metadata from a connection, with runtime validation. */ function getConnectionMeta(connection: Connection) { const m = connection.metadata as Record | undefined return { syncInProgress: m?.syncInProgress === true, lastSyncedAt: typeof m?.lastSyncedAt === "number" ? m.lastSyncedAt : undefined, documentCount: typeof m?.documentCount === "number" ? m.documentCount : 0, } } const CONNECTORS = { "google-drive": { title: "Google Drive", description: "Connect your Google Docs, Sheets, and Slides", icon: GoogleDrive, documentLabel: "documents", }, notion: { title: "Notion", description: "Import your Notion pages and databases", icon: Notion, documentLabel: "pages", }, onedrive: { title: "OneDrive", description: "Access your Microsoft Office documents", icon: OneDrive, documentLabel: "documents", }, granola: { title: "Granola", description: "Sync AI meeting notes and transcripts", icon: Granola, documentLabel: "notes", }, } as const type ConnectorProvider = keyof typeof CONNECTORS function SectionTitle({ children, badge, }: { children: React.ReactNode badge?: React.ReactNode }) { return (

{children}

{badge}
) } function ProBadge() { return ( PRO ) } function ConnectionsCard({ children, className, }: { children: React.ReactNode className?: string }) { return (
{children}
) } function PillButton({ children, onClick, disabled, className, }: { children: React.ReactNode onClick?: () => void disabled?: boolean className?: string }) { return ( ) } function ConnectionRow({ connection, onDelete, isDeleting, disabled, projects, onTriggerSync, isSyncing, onReconnect, isReconnecting, }: { connection: Connection onDelete: () => void isDeleting: boolean disabled?: boolean projects: Project[] onTriggerSync: () => void isSyncing: boolean onReconnect: () => void isReconnecting: boolean }) { const [historyOpen, setHistoryOpen] = useState(false) const config = CONNECTORS[connection.provider as ConnectorProvider] const { needsReauth } = useConnectionHealth(connection.id) if (!config) return null const Icon = config.icon const meta = getConnectionMeta(connection) const expired = needsReauth const getProjectDisplayName = (containerTag: string): string => { if (containerTag === DEFAULT_PROJECT_ID) return "Default Project" const found = projects.find((p) => p.containerTag === containerTag) if (found) return found.name return containerTag.replace(/^sm_project_/, "") // if cached project is not found, remove the prefix } const projectName = connection.containerTags && connection.containerTags.length > 0 && connection.containerTags[0] ? getProjectDisplayName(connection.containerTags[0]) : null return (
{/* Main row */}
{config.title}
{getConnectionSubtitle(connection)}
{expired ? ( ) : ( )}
{/* Meta row */}
{projectName && ( <> Project: {projectName}
)} Last synced: {formatRelativeTime(meta.lastSyncedAt)}
{meta.documentCount} {config.documentLabel} connected
{historyOpen && (
)}
) } function UpgradeOverlay({ onUpgrade }: { onUpgrade: () => void }) { return (

{" "} to get Supermemory Connections

) } function FeatureItem({ text }: { text: string }) { return (
{text}
) } export default function ConnectionsMCP() { const queryClient = useQueryClient() const autumn = useCustomer() const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam) const [mcpModalOpen, setMcpModalOpen] = useState(false) const [removeDialog, setRemoveDialog] = useState<{ open: boolean connection: Connection | null }>({ open: false, connection: null }) const triggerSync = useTriggerSync() const projects = (queryClient.getQueryData(["projects"]) || []) as Project[] const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") const connectionsBalance = autumn.data?.balances?.connections const connectionsUsed = connectionsBalance?.usage ?? 0 const connectionsLimit = connectionsBalance?.granted ?? 10 const canAddConnection = connectionsUsed < connectionsLimit // Fetch connections 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: (query) => { const conns = query.state.data as Connection[] | undefined if (conns?.some((c) => getConnectionMeta(c).syncInProgress)) { return 5000 } return 60 * 1000 }, enabled: hasProProduct, }) useEffect(() => { if (connectionsError) { toast.error("Failed to load connections", { description: connectionsError instanceof Error ? connectionsError.message : "Unknown error", }) } }, [connectionsError]) const reconnectMutation = useMutation({ mutationFn: async ({ connectionId: _connectionId, provider, containerTags, }: { connectionId: string provider: ConnectorProvider containerTags: string[] | undefined }) => { const response = await $fetch("@post/connections/:provider", { params: { provider }, body: { redirectUrl: window.location.href, containerTags: containerTags ?? [], }, }) if ("data" in response && response.data && !("error" in response.data)) { return response.data } throw new Error(response.error?.message || "Failed to reconnect") }, onSuccess: (data) => { if (data?.authLink) { window.location.href = data.authLink return } toast.error("Reconnect link missing — try again.") }, onError: (error) => { toast.error("Failed to reconnect", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) 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. Supermemory will permanently delete the documents 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", }) }, }) // Upgrade handler const handleUpgrade = async () => { try { const result = await autumn.attach({ planId: "api_pro", successUrl: `${window.location.origin}/settings#connections`, }) if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return } autumn.refetch?.() } catch (error) { console.error(error) toast.error("Failed to start checkout. Please try again.") } } const isLoading = autumn.isLoading return (
{/* Supermemory Connections Section */}
}> Supermemory Connections {/* Blur overlay for free users */} {!hasProProduct && !isLoading && ( <>
)}
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} onTriggerSync={() => triggerSync.mutate({ connectionId: connection.id, provider: connection.provider as ImportProvider, containerTags: connection.containerTags, }) } isSyncing={ (triggerSync.isPending && triggerSync.variables?.connectionId === connection.id) || getConnectionMeta(connection).syncInProgress } onReconnect={() => { reconnectMutation.mutate({ connectionId: connection.id, provider: connection.provider as ConnectorProvider, containerTags: connection.containerTags, }) }} isReconnecting={ reconnectMutation.isPending && reconnectMutation.variables?.connectionId === connection.id } /> )) ) : (

No connections yet

Connect a service below to import your knowledge

)}
setAddDoc("connect")} disabled={!hasProProduct || !canAddConnection} > Connect knowledge bases
{/* Supermemory MCP Section */}
Supermemory MCP

Connect your AI to create and use your memories via MCP.{" "} Learn more

setMcpModalOpen(true)}> Connect your AI to Supermemory
{/* Add Document Modal */} setAddDoc(null)} /> { if (!open) setRemoveDialog({ open: false, connection: null }) }} provider={removeDialog.connection?.provider} documentCount={ removeDialog.connection ? getConnectionMeta(removeDialog.connection).documentCount : 0 } onConfirm={(deleteDocuments) => { if (removeDialog.connection) { deleteConnectionMutation.mutate({ connectionId: removeDialog.connection.id, deleteDocuments, }) } }} isDeleting={deleteConnectionMutation.isPending} />
) }