"use client" import { dmSans125ClassName } from "@/lib/fonts" import { OAUTH_PLUGINS } from "@/lib/oauth-plugins" import { cn } from "@lib/utils" import { Building2, ExternalLink, LoaderIcon, Plug, Trash2 } from "lucide-react" import Image from "next/image" import { useCallback, useEffect, useState } from "react" const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" interface Connection { clientId: string name: string icon: string | null isFirstParty: boolean workspaceId: string | null workspaceName: string | null scopes: string[] connectedAt: string | null lastUsedAt: string | null } function relativeTime(iso: string | null): string | null { if (!iso) return null const then = new Date(iso).getTime() if (Number.isNaN(then)) return null const diff = Date.now() - then const mins = Math.round(diff / 60000) if (mins < 1) return "just now" if (mins < 60) return `${mins}m ago` const hrs = Math.round(mins / 60) if (hrs < 24) return `${hrs}h ago` const days = Math.round(hrs / 24) if (days < 30) return `${days}d ago` const months = Math.round(days / 30) if (months < 12) return `${months}mo ago` return `${Math.round(months / 12)}y ago` } const cardClass = cn( "rounded-[14px] bg-[#14161A] p-5", "shadow-[inset_2.42px_2.42px_4.263px_rgba(11,15,21,0.7)]", ) function PluginIcon({ src, alt }: { src: string | null; alt: string }) { const [failed, setFailed] = useState(false) if (!src || failed) { return (
) } return (
{alt} setFailed(true)} src={src} width={20} />
) } export default function ConnectPage() { const [connections, setConnections] = useState(null) const [error, setError] = useState(null) const [revoking, setRevoking] = useState(null) const load = useCallback(async () => { try { const res = await fetch(`${API_URL}/v3/oauth/grants`, { credentials: "include", }) if (!res.ok) throw new Error(`Failed to load connections (${res.status})`) const data = (await res.json()) as { grants: Connection[] } setConnections(data.grants) setError(null) } catch (err) { console.error("Failed to load connections:", err) setError( err instanceof Error ? err.message : "Failed to load connections", ) setConnections([]) } }, []) useEffect(() => { load() }, [load]) async function revoke(clientId: string) { setRevoking(clientId) try { const res = await fetch( `${API_URL}/v3/oauth/grants/${encodeURIComponent(clientId)}`, { method: "DELETE", credentials: "include" }, ) if (!res.ok && res.status !== 204) throw new Error(`Failed to revoke (${res.status})`) setConnections((prev) => prev ? prev.filter((c) => c.clientId !== clientId) : prev, ) } catch (err) { console.error("Failed to revoke connection:", err) setError(err instanceof Error ? err.message : "Failed to revoke") } finally { setRevoking(null) } } const connectedClientIds = new Set(connections?.map((c) => c.clientId) ?? []) return (

Connections

Apps and plugins you've connected to your Supermemory account.

Connected apps

{connections === null ? (
Loading…
) : connections.length === 0 ? (

No apps connected yet

Connect a plugin below — anything you authorize will show up here.

) : (
{connections.map((c) => { const connectedRel = relativeTime(c.connectedAt) const usedRel = relativeTime(c.lastUsedAt) return (

{c.name}

{!c.isFirstParty && ( external )}
{c.workspaceName && ( {c.workspaceName} )} {connectedRel && Connected {connectedRel}} {usedRel && Last used {usedRel}}
) })}
)} {error &&

{error}

}

Available plugins

{OAUTH_PLUGINS.map((p) => { const isConnected = p.oauthClientId != null && connectedClientIds.has(p.oauthClientId) return (

{p.name}

{isConnected && ( Connected )}

{p.description}

Setup guide
) })}
) }