"use client" import { $fetch } from "@lib/api" import { hasActivePlan } from "@lib/queries" 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, ChevronDown, Clock, FolderOpen, Loader, 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 { RemoveConnectionDialog } from "@/components/remove-connection-dialog" 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" 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, }, } as const function formatRelativeTime(date: string | null | undefined): string { 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() } function ConnectionRow({ connection, onDelete, isDeleting, projects, }: { connection: Connection onDelete: () => void isDeleting: 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 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 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 && (
{projectName}
)}
{formatRelativeTime(connection.createdAt)}
{documentCount} {config.documentLabel}
) } interface ConnectContentProps { selectedProject: string } export function ConnectContent({ selectedProject }: ConnectContentProps) { const queryClient = useQueryClient() const autumn = useCustomer() const isProUser = hasActivePlan(autumn.customer?.products, "api_pro") const [connectingProvider, setConnectingProvider] = useState(null) const [gdriveSyncScope, setGdriveSyncScope] = useState("scoped") const [isUpgrading, setIsUpgrading] = useState(false) const [removeDialog, setRemoveDialog] = useState<{ open: boolean connection: Connection | null }>({ open: false, connection: null }) const projects = (queryClient.getQueryData(["projects"]) || []) as Project[] const handleUpgrade = async () => { setIsUpgrading(true) try { await autumn.attach({ productId: "api_pro", successUrl: window.location.href, }) } catch (error) { console.error("Upgrade error:", error) toast.error("Failed to start upgrade process") setIsUpgrading(false) } } const connectionsFeature = autumn.customer?.features?.connections const connectionsUsed = connectionsFeature?.usage ?? 0 const connectionsLimit = connectionsFeature?.included_usage ?? 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: 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 && !isProUser) { 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 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 && ( )} ))}
) : ( )}
) })}
)} {/* Connected list - rich rows with status / project / last sync / doc count */} {hasConnections && (

Connected to Supermemory

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
{connections.map((connection) => ( setRemoveDialog({ open: true, connection })} isDeleting={deleteConnectionMutation.isPending} /> ))}
)} {/* Empty state panel - only when !hasConnections */} {!hasConnections && (
{!isProUser ? ( <>

{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} />
) }