"use client" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { authClient } from "@lib/auth" import { useAuth } from "@lib/auth-context" import { hasActivePlan } from "@lib/queries" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import * as DialogPrimitive from "@radix-ui/react-dialog" import { BookOpen, Check, ChevronDown, ExternalLink, Loader, X, Zap, } from "lucide-react" import Image from "next/image" import { type ReactNode, useEffect, useMemo, useState } from "react" import { toast } from "sonner" import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog" import { Popover, PopoverContent, PopoverTrigger } from "@ui/components/popover" import { PLUGIN_CATALOG, isFreeTierPlugin, normalizePluginClientId, type InstallStep, type PluginInfo, } from "@/lib/plugin-catalog" import { INSET, InstallSteps, PillButton } from "./install-steps" import { usePromoCode } from "@/hooks/use-promo-code" interface ConnectedPlugin { id: string keyId: string pluginId: string createdAt: string lastRequest?: string | null keyStart?: string | null } type ListedApiKey = { id: string name?: string | null createdAt: string enabled?: boolean lastRequest: string | null metadata: string | Record | null start?: string | null } function SectionHeader({ children }: { children: ReactNode }) { return (

{children}

) } function PluginIconBox({ src, alt, dimmed, }: { src: string alt: string dimmed?: boolean }) { return (
{alt}
) } function ProChip() { return ( Pro ) } function DocsLink({ href }: { href: string }) { return ( {" "} Docs ) } function DisconnectButton({ onConfirm }: { onConfirm: () => void }) { const [confirming, setConfirming] = useState(false) useEffect(() => { if (!confirming) return const t = setTimeout(() => setConfirming(false), 3000) return () => clearTimeout(t) }, [confirming]) return ( ) } function toDate(value: string | Date | null | undefined): Date | null { if (!value) return null const date = value instanceof Date ? value : new Date(value) return Number.isNaN(date.getTime()) ? null : date } function formatRelativeTime(value: string | Date | null | undefined): string { const date = toDate(value) if (!date) return "Never" const absMs = Math.abs(Date.now() - date.getTime()) const minute = 60 * 1000 const hour = 60 * minute const day = 24 * hour if (absMs < minute) return "Just now" if (absMs < hour) return `${Math.round(absMs / minute)}m ago` if (absMs < day) return `${Math.round(absMs / hour)}h ago` return `${Math.round(absMs / day)}d ago` } function formatDate(value: string | Date | null | undefined): string { const date = toDate(value) if (!date) return "Unknown" return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }) } function maskKey(start: string | null | undefined): string { if (!start) return "sm_********" return `${start}********` } function keyPrefix(key: ListedApiKey): string | null { return key.start ?? (key.name?.startsWith("sm_") ? key.name : null) } function DetailStat({ label, value }: { label: string; value: string }) { return (

{label}

{value}

) } function ActivePill({ plugin, connectedKeys, onRevoke, }: { plugin: PluginInfo connectedKeys: ConnectedPlugin[] onRevoke: (keyId: string) => void }) { const primaryKey = connectedKeys[0] return (

{plugin.name}

Active

{plugin.docsUrl && ( Docs )} {plugin.githubUrl && ( GitHub )} Connected {formatDate(primaryKey?.createdAt)}

{connectedKeys.length > 1 ? `${connectedKeys.length} connections` : "Connection"}

{connectedKeys.map((k) => (
{k.keyStart ? `${k.keyStart}…` : "API key"} onRevoke(k.keyId)} />
))}
) } function PluginRow({ plugin, pluginId, connectedKeys, needsSetup, needsProUpgrade, isConnecting, actionsDisabled, onConnect, onUpgrade, onRevoke, onFinishSetup, }: { plugin: PluginInfo pluginId: string connectedKeys: ConnectedPlugin[] needsSetup: boolean needsProUpgrade: boolean isConnecting: boolean actionsDisabled: boolean onConnect: (id: string) => void onUpgrade: () => void onRevoke: (keyId: string) => void onFinishSetup: (id: string) => void }) { const isConnected = connectedKeys.length > 0 return (
{isConnected && ( )} {plugin.name} {!isConnected && needsProUpgrade && }

{plugin.tagline}

{plugin.docsUrl && } {isConnected ? ( ) : needsSetup ? ( onFinishSetup(pluginId)}> Finish setup ) : needsProUpgrade ? ( Upgrade ) : ( onConnect(pluginId)} disabled={actionsDisabled} > {isConnecting ? ( <> Connecting… ) : ( "Connect" )} )}
) } export function PluginsDetail() { const { org } = useAuth() const autumn = useCustomer() const promoCode = usePromoCode() const queryClient = useQueryClient() const [connectingPlugin, setConnectingPlugin] = useState(null) const [finishSetupPluginId, setFinishSetupPluginId] = useState( null, ) const [newKey, setNewKey] = useState<{ open: boolean key: string pluginId: string | null }>({ open: false, key: "", pluginId: null, }) const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro") const { data: pluginsData } = useQuery({ queryFn: async () => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const res = await fetch(`${API_URL}/v3/auth/plugins`, { credentials: "include", }) if (!res.ok) throw new Error("Failed to fetch plugins") return (await res.json()) as { plugins: string[] } }, queryKey: ["plugins"], }) const { data: apiKeys = [], refetch: refetchKeys } = useQuery( { enabled: !!org?.id, queryFn: async () => { if (!org?.id) return [] const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const res = await fetch(`${API_URL}/v3/auth/keys`, { credentials: "include", }) if (!res.ok) return [] const data = (await res.json()) as { keys?: ListedApiKey[] } return data.keys ?? [] }, queryKey: ["api-keys", org?.id], }, ) const setupPluginIds = useMemo(() => { const ids = new Set() for (const key of apiKeys) { if (key.enabled === false) continue if (key.lastRequest) continue if (!key.metadata) continue try { const metadata = typeof key.metadata === "string" ? (JSON.parse(key.metadata) as { sm_type?: string sm_client?: string }) : (key.metadata as { sm_type?: string; sm_client?: string }) if (metadata.sm_type === "plugin_auth" && metadata.sm_client) { ids.add(normalizePluginClientId(metadata.sm_client)) } } catch {} } return ids }, [apiKeys]) const connectedPlugins = useMemo(() => { const plugins: ConnectedPlugin[] = [] for (const key of apiKeys) { if (key.enabled === false) continue if (!key.lastRequest) continue if (!key.metadata) continue try { const metadata = typeof key.metadata === "string" ? (JSON.parse(key.metadata) as { sm_type?: string sm_client?: string }) : (key.metadata as { sm_type?: string; sm_client?: string }) if (metadata.sm_type === "plugin_auth" && metadata.sm_client) { const createdAt = toDate(key.createdAt)?.toISOString() const lastRequest = toDate(key.lastRequest)?.toISOString() plugins.push({ id: key.id, keyId: key.id, pluginId: normalizePluginClientId(metadata.sm_client), createdAt: createdAt ?? new Date().toISOString(), lastRequest: lastRequest ?? null, keyStart: keyPrefix(key), }) } } catch {} } return plugins }, [apiKeys]) const connectedPluginIds = useMemo( () => new Set(connectedPlugins.map((p) => p.pluginId)), [connectedPlugins], ) const createPluginKeyMutation = useMutation({ mutationFn: async (pluginId: string) => { const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" const params = new URLSearchParams({ client: pluginId }) const res = await fetch(`${API_URL}/v3/auth/key?${params}`, { credentials: "include", }) if (!res.ok) { const errorData = (await res.json().catch(() => ({}))) as { message?: string } throw new Error(errorData.message || "Failed to create plugin key") } return (await res.json()) as { key: string } }, onMutate: (pluginId) => setConnectingPlugin(pluginId), onError: (err) => { toast.error("Failed to connect plugin", { description: err instanceof Error ? err.message : "Unknown error", }) }, onSettled: () => { setConnectingPlugin(null) queryClient.invalidateQueries({ queryKey: ["api-keys", org?.id] }) }, onSuccess: (data, pluginId) => { setNewKey({ open: true, key: data.key, pluginId }) }, }) const handleRevoke = async (keyId: string) => { try { await authClient.apiKey.delete({ keyId }) toast.success("Plugin disconnected") refetchKeys() } catch { toast.error("Failed to disconnect plugin") } } const handleUpgrade = async () => { try { const result = await autumn.attach({ planId: "api_pro", discounts: promoCode.getDiscounts(), successUrl: `${window.location.origin}/integrations`, }) promoCode.clear() 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 const availablePlugins = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG) const catalogRows = useMemo( () => availablePlugins.filter((id) => PLUGIN_CATALOG[id]), [availablePlugins], ) const visibleRows = useMemo(() => { // Connected plugins float to the top (stable within each group). return [...catalogRows].sort( (a, b) => Number(connectedPluginIds.has(b)) - Number(connectedPluginIds.has(a)), ) }, [catalogRows, connectedPluginIds]) const dialogPlugin = newKey.pluginId ? PLUGIN_CATALOG[newKey.pluginId] : undefined const finishSetupPlugin = finishSetupPluginId ? PLUGIN_CATALOG[finishSetupPluginId] : undefined const pluginSteps = dialogPlugin?.installSteps ?? [] // If a step already embeds the key (an `export …="sm_…"` line), don't also // show the bare key in its own step — that's the repetition to avoid. // Otherwise (wizard-style installs) lead with a copy-the-key step, unless // the plugin performs browser OAuth itself. const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) const skipGeneratedKeyStep = stepsEmbedKey || !!dialogPlugin?.usesOAuth const setupSteps: InstallStep[] = skipGeneratedKeyStep ? pluginSteps : [ { title: "Copy your API key", description: "You won't be able to see it again — store it somewhere safe.", code: newKey.key, copyLabel: "API key", secret: true, }, ...pluginSteps, ] return ( <>
Plugins
{visibleRows.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) return null const needsProUpgrade = !isLoading && !hasProProduct && !isFreeTierPlugin(pluginId) return ( p.pluginId === pluginId, )} needsSetup={ !connectedPluginIds.has(pluginId) && setupPluginIds.has(pluginId) } needsProUpgrade={needsProUpgrade} isConnecting={connectingPlugin === pluginId} actionsDisabled={!!connectingPlugin} onConnect={(id) => createPluginKeyMutation.mutate(id)} onFinishSetup={(id) => setFinishSetupPluginId(id)} onUpgrade={handleUpgrade} onRevoke={handleRevoke} /> ) })} {visibleRows.length === 0 && (

No plugins in this category.

)}
setNewKey((s) => ({ open, key: open ? s.key : "", pluginId: open ? s.pluginId : null, })) } > Set up {dialogPlugin?.name ?? "your plugin"}
{dialogPlugin && ( )}

Set up {dialogPlugin?.name ?? "your plugin"}

Copy your key and run these steps to finish.

{dialogPlugin?.docsUrl && ( Docs )}
{ if (!open) setFinishSetupPluginId(null) }} > Finish setup {finishSetupPlugin?.name ?? "plugin"}
{finishSetupPlugin && ( )}

Finish setup {finishSetupPlugin?.name ?? "plugin"}

Complete install in the tool — status becomes active after the first API call.

{finishSetupPlugin?.installSteps?.length ? ( ) : (

Open {finishSetupPlugin?.name ?? "the plugin"} and finish authentication.

)}
) }