"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, 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, type InstallStep, type PluginInfo, } from "@/lib/plugin-catalog" import { INSET, InstallSteps, PillButton } from "./install-steps" interface ConnectedPlugin { id: string keyId: string pluginId: string createdAt: string keyStart?: 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 ConnectedPill({ connectedKeys, onRevoke, }: { connectedKeys: ConnectedPlugin[] onRevoke: (keyId: string) => void }) { return (

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

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

{plugin.tagline}

{plugin.docsUrl && } {isConnected ? ( ) : needsProUpgrade ? ( Upgrade ) : ( onConnect(pluginId)} disabled={actionsDisabled} > {isConnecting ? ( <> Connecting… ) : ( "Connect" )} )}
) } type TierFilter = "all" | "pro" | "free" const TIER_FILTERS: { value: TierFilter; label: string }[] = [ { value: "all", label: "All" }, { value: "pro", label: "Pro" }, { value: "free", label: "Free" }, ] function TierFilterToggle({ value, onChange, }: { value: TierFilter onChange: (value: TierFilter) => void }) { return (
{TIER_FILTERS.map((filter) => ( ))}
) } export function PluginsDetail() { const { org } = useAuth() const autumn = useCustomer() const queryClient = useQueryClient() const [tierFilter, setTierFilter] = useState("all") const [connectingPlugin, setConnectingPlugin] = 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 data = await authClient.apiKey.list({ fetchOptions: { query: { metadata: { organizationId: org.id } } }, }) return data.filter((key) => key.metadata?.organizationId === org.id) }, queryKey: ["api-keys", org?.id], }) const connectedPlugins = useMemo(() => { const plugins: ConnectedPlugin[] = [] for (const key of apiKeys) { 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) { plugins.push({ id: key.id, keyId: key.id, pluginId: metadata.sm_client, createdAt: key.createdAt.toISOString(), keyStart: key.start ?? null, }) } } 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) { if (res.status === 403) { throw new Error( "This plugin requires a Pro plan. Hermes is available on the Free plan.", ) } 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", successUrl: `${window.location.origin}/?view=integrations`, }) 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(() => { const filtered = catalogRows.filter((id) => { if (tierFilter === "free") return isFreeTierPlugin(id) if (tierFilter === "pro") return !isFreeTierPlugin(id) return true }) // Connected plugins float to the top (stable within each group). return [...filtered].sort( (a, b) => Number(connectedPluginIds.has(b)) - Number(connectedPluginIds.has(a)), ) }, [catalogRows, tierFilter, connectedPluginIds]) const dialogPlugin = newKey.pluginId ? PLUGIN_CATALOG[newKey.pluginId] : 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. const stepsEmbedKey = pluginSteps.some((s) => s.code?.includes("sm_...")) const setupSteps: InstallStep[] = stepsEmbedKey ? 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 {catalogRows.length > 0 && ( )}
{visibleRows.map((pluginId) => { const plugin = PLUGIN_CATALOG[pluginId] if (!plugin) return null const needsProUpgrade = !isLoading && !hasProProduct && !isFreeTierPlugin(pluginId) return ( p.pluginId === pluginId, )} needsProUpgrade={needsProUpgrade} isConnecting={connectingPlugin === pluginId} actionsDisabled={!!connectingPlugin} onConnect={(id) => createPluginKeyMutation.mutate(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 )}
) }