"use client" import { $fetch } from "@lib/api" import { useConnectorAccess } from "@/hooks/use-connector-access" import type { ConnectionResponseSchema } from "@repo/validation/api" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { GoogleDrive, Granola, Notion, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { Check, ChevronDown, FolderOpen, History, Loader, Loader2, Play, Trash2, Zap, } from "lucide-react" import { useEffect, useState } from "react" import { toast } from "sonner" import type { z } from "zod" import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { DEFAULT_PROJECT_ID } from "@lib/constants" import type { Project } from "@lib/types" import { Button } from "@ui/components/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@ui/components/dropdown-menu" import { GranolaConnectModal } from "@/components/granola-connect-modal" import { RemoveConnectionDialog } from "@/components/remove-connection-dialog" 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 GDriveSyncScope = "scoped" | "full" const GDRIVE_SCOPE_LABELS: Record = { scoped: "Files & Folders", full: "Whole Drive", } type Connection = z.infer type ConnectorProvider = "google-drive" | "notion" | "onedrive" | "granola" const CONNECTORS: Record< ConnectorProvider, { title: string description: string documentLabel: string icon: React.ComponentType<{ className?: string }> } > = { "google-drive": { title: "Google Drive", description: "Connect your Google docs, sheets and slides", documentLabel: "documents", icon: GoogleDrive, }, notion: { title: "Notion", description: "Import your Notion pages and databases", documentLabel: "pages", icon: Notion, }, onedrive: { title: "OneDrive", description: "Access your Microsoft Office documents", documentLabel: "documents", icon: OneDrive, }, granola: { title: "Granola", description: "Sync AI meeting notes and transcripts", documentLabel: "notes", icon: Granola, }, } as const /** 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, } } function ConnectionRow({ connection, onDelete, isDeleting, projects, onTriggerSync, isSyncing, onReconnect, isReconnecting, }: { connection: Connection onDelete: () => void isDeleting: 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 getProjectName = (tag: string): string => { if (tag === DEFAULT_PROJECT_ID) return "Default" return ( projects.find((p) => p.containerTag === tag)?.name ?? tag.replace(/^sm_project_/, "").replace(/_/g, " ") ) } const projectName = connection.containerTags?.[0] ? getProjectName(connection.containerTags[0]) : null return (
{config.title}
{getConnectionSubtitle(connection)}
{expired ? ( ) : ( )}
{projectName && (
{projectName}
)} Last synced: {formatRelativeTime(meta.lastSyncedAt)}
{meta.documentCount} {config.documentLabel}
{historyOpen && (
)}
) } interface ConnectContentProps { selectedProject: string } export function ConnectContent({ selectedProject }: ConnectContentProps) { const queryClient = useQueryClient() const autumn = useCustomer() const { connectorAccess } = useConnectorAccess() const [connectingProvider, setConnectingProvider] = useState(null) const [granolaModalOpen, setGranolaModalOpen] = useState(false) const [gdriveSyncScope, setGdriveSyncScope] = useState("scoped") const [isUpgrading, setIsUpgrading] = 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 handleUpgrade = async (planId: "api_pro" | "api_max" = "api_pro") => { setIsUpgrading(true) try { const result = await autumn.attach({ planId, successUrl: window.location.href, }) if (result?.paymentUrl) { window.open(result.paymentUrl, "_self") return } autumn.refetch?.() } catch (error) { console.error("Upgrade error:", error) toast.error("Failed to start upgrade process") } finally { setIsUpgrading(false) } } 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 = [], 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 }, refetchIntervalInBackground: true, }) // Handle connection errors useEffect(() => { if (connectionsError) { toast.error("Failed to load connections", { description: connectionsError instanceof Error ? connectionsError.message : "Unknown error", }) } }, [connectionsError]) // Connect mutation const addConnectionMutation = useMutation({ mutationFn: async ({ provider, syncScope, }: { provider: ConnectorProvider syncScope?: GDriveSyncScope }) => { if (!canAddConnection && !connectorAccess) { throw new Error( "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.", ) } const response = await $fetch("@post/connections/:provider", { params: { provider }, body: { redirectUrl: window.location.href, containerTags: [selectedProject], metadata: provider === "google-drive" && syncScope === "full" ? { syncScope: "full" } : undefined, }, }) // biome-ignore lint/style/noNonNullAssertion: its fine if ("data" in response && !("error" in response.data!)) { return response.data } throw new Error(response.error?.message || "Failed to connect") }, onSuccess: (data) => { if (data?.authLink) { window.location.href = data.authLink } }, onError: (error) => { setConnectingProvider(null) toast.error("Failed to connect", { description: error instanceof Error ? error.message : "Unknown error", }) }, }) 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 ?? [selectedProject], }, }) 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) => { 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 handleConnect = (provider: ConnectorProvider) => { setConnectingProvider(provider) addConnectionMutation.mutate({ provider, syncScope: provider === "google-drive" ? gdriveSyncScope : undefined, }) } const hasConnections = connections.length > 0 const isAnyConnecting = connectingProvider !== null || addConnectionMutation.isPending return (
{/* Top header — only when empty; once connected, the Add CTA moves into the list header below */} {!hasConnections && (

Add a connection

PRO
)} {/* Provider rows — only on empty state. Each is a labelled, descriptive CTA. */} {!hasConnections && (
{Object.entries(CONNECTORS).map(([provider, config]) => { const Icon = config.icon const isConnecting = connectingProvider === provider || (addConnectionMutation.isPending && addConnectionMutation.variables?.provider === provider) return (

{config.title}

{config.description}

{provider === "google-drive" ? (
{( Object.entries(GDRIVE_SCOPE_LABELS) as [ GDriveSyncScope, string, ][] ).map(([scope, label]) => ( { e.stopPropagation() setGdriveSyncScope(scope) }} className="flex items-center justify-between" > {label} {gdriveSyncScope === scope && ( )} ))}
) : provider === "granola" ? ( <> {!connectorAccess && ( Pro )} ) : ( )}
) })}
)} {/* Connected list - rich rows with status / project / last sync / doc count */} {hasConnections && (

Connected to Supermemory Connections

PRO
{connectionsLimit > 0 && (

{connections.length}/{connectionsLimit} connections used

)}
Choose a service
{ setConnectingProvider("google-drive") addConnectionMutation.mutate({ provider: "google-drive", syncScope: "scoped", }) }} className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100" >
Google Drive Pick specific files & folders
{ setConnectingProvider("google-drive") addConnectionMutation.mutate({ provider: "google-drive", syncScope: "full", }) }} className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100" >
Google Drive Sync entire drive
{ setConnectingProvider("notion") addConnectionMutation.mutate({ provider: "notion" }) }} className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100" >
Notion Pages and databases
{ setConnectingProvider("onedrive") addConnectionMutation.mutate({ provider: "onedrive" }) }} className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100" >
OneDrive Office documents
{ if (!connectorAccess) { handleUpgrade("api_pro") return } setGranolaModalOpen(true) }} className="flex items-start gap-2.5 px-3 py-2.5 rounded-md cursor-pointer text-white opacity-60 hover:opacity-100 hover:bg-[#293952]/40 focus:bg-[#293952]/40 focus:opacity-100 data-disabled:opacity-40 data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent" >
Granola {!connectorAccess && ( Pro )} {connectorAccess ? "Meeting notes & transcripts" : "Upgrade to Pro"}
{connections.map((connection) => ( setRemoveDialog({ open: true, connection })} isDeleting={deleteConnectionMutation.isPending} 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 } /> ))}
)} {/* Empty state panel - only when !hasConnections */} {!hasConnections && (
{!connectorAccess ? ( <>

{isUpgrading || autumn.isLoading ? ( Upgrading… ) : ( <> {" "} to get
Supermemory Connections )}

Unlimited memories
10 connections
Advanced search
Priority support
) : (

No connections yet

Choose a service above to import your knowledge

)}
)} { 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} /> setGranolaModalOpen(open && connectorAccess)} containerTags={[selectedProject]} />
) }