diff --git a/apps/web/components/chat/chat-empty-state.tsx b/apps/web/components/chat/chat-empty-state.tsx index 1fe230bf..bef9894a 100644 --- a/apps/web/components/chat/chat-empty-state.tsx +++ b/apps/web/components/chat/chat-empty-state.tsx @@ -7,8 +7,8 @@ import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts" export const DEFAULT_CHAT_PROMPTS = [ "What do you know about me?", - "What have I been working on lately?", - "What themes keep showing up in my memories?", + "Set up Cursor", + "Show my active plugins", ] as const const SUGGESTION_PILL_CLASS = cn( diff --git a/apps/web/components/chat/message/agent-message.tsx b/apps/web/components/chat/message/agent-message.tsx index 1b7f69a8..cf047c68 100644 --- a/apps/web/components/chat/message/agent-message.tsx +++ b/apps/web/components/chat/message/agent-message.tsx @@ -5,10 +5,14 @@ import type { UIMessage } from "@ai-sdk/react" import { Streamdown } from "streamdown" import { BookOpenIcon, + CheckIcon, ChevronDownIcon, ChevronRightIcon, ClockIcon, + CopyIcon, + ExternalLinkIcon, GlobeIcon, + KeyRoundIcon, ListIcon, Loader2, PlusIcon, @@ -16,6 +20,7 @@ import { TerminalIcon, WrenchIcon, XCircleIcon, + ZapIcon, } from "lucide-react" import { cn } from "@lib/utils" import { isWebSearchToolName } from "@/lib/chat-web-search-tools" @@ -64,6 +69,386 @@ function faviconUrl(host: string): string { return `https://www.google.com/s2/favicons?sz=64&domain=${host}` } +type NovaConnectorStatus = + | "active" + | "setup_pending" + | "not_connected" + | "upgrade_required" + | "setup_available" + +type NovaConnectorStep = { + title?: string + description?: string + code?: string + link?: { url: string; label: string } + createPluginKey?: boolean +} + +type NovaConnectorCardData = { + kind?: "plugin" | "mcp" + id?: string + name?: string + icon?: string + description?: string + features?: string[] + docsUrl?: string + repoUrl?: string + installSteps?: NovaConnectorStep[] + status?: NovaConnectorStatus + requiresPro?: boolean + canGenerateKey?: boolean + keyPluginId?: string +} + +type NovaConnectorToolOutput = { + success?: boolean + error?: string + kind?: string + connectors?: NovaConnectorCardData[] + connector?: NovaConnectorCardData + keyReveal?: { pluginId: string; label?: string } | null + available?: Array<{ kind: "plugin" | "mcp"; id: string; name: string }> +} + +const NOVA_CONNECTOR_TOOLS = new Set([ + "listNovaConnectors", + "getNovaConnectorSetup", + "prepareNovaPluginSetup", +]) + +const STATUS_COPY: Record< + NovaConnectorStatus, + { label: string; className: string } +> = { + active: { + label: "Active", + className: "border-emerald-400/20 bg-emerald-400/10 text-emerald-300", + }, + setup_pending: { + label: "Finish setup", + className: "border-amber-400/20 bg-amber-400/10 text-amber-300", + }, + not_connected: { + label: "Not connected", + className: "border-white/10 bg-white/[0.05] text-white/55", + }, + upgrade_required: { + label: "Pro required", + className: "border-[#4BA0FA]/25 bg-[#4BA0FA]/10 text-[#4BA0FA]", + }, + setup_available: { + label: "Setup available", + className: "border-white/10 bg-white/[0.05] text-white/65", + }, +} + +function unwrapToolOutput(output: unknown): NovaConnectorToolOutput | null { + if (typeof output === "string") { + try { + return JSON.parse(output) as NovaConnectorToolOutput + } catch { + return null + } + } + if (!output || typeof output !== "object") return null + const record = output as Record + if ( + record.type === "json" && + record.value && + typeof record.value === "object" + ) { + return record.value as NovaConnectorToolOutput + } + if (record.type === "text" && typeof record.value === "string") { + try { + return JSON.parse(record.value) as NovaConnectorToolOutput + } catch { + return null + } + } + return record as NovaConnectorToolOutput +} + +function StatusPill({ status }: { status?: NovaConnectorStatus }) { + const copy = STATUS_COPY[status ?? "not_connected"] + return ( + + {copy.label} + + ) +} + +function MiniCopyButton({ text, label }: { text: string; label?: string }) { + const [copied, setCopied] = useState(false) + return ( + + ) +} + +function ConnectorCodeBlock({ + code, + apiKey, +}: { + code: string + apiKey?: string +}) { + const rendered = apiKey ? code.replaceAll("sm_...", apiKey) : code + return ( +
+
+				{rendered}
+			
+ +
+ ) +} + +function RevealPluginKeyButton({ + pluginId, + onReveal, +}: { + pluginId: string + onReveal: (key: string) => void +}) { + const [state, setState] = useState<"idle" | "loading" | "copied" | "error">( + "idle", + ) + return ( + + ) +} + +function NovaConnectorCard({ + connector, +}: { + connector: NovaConnectorCardData +}) { + const [revealedKey, setRevealedKey] = useState() + const needsKey = Boolean(connector.canGenerateKey && connector.keyPluginId) + const isUpgrade = connector.status === "upgrade_required" + return ( +
+
+
+ {connector.icon ? ( + + ) : ( + + )} +
+
+
+

+ {connector.name ?? connector.id ?? "Connector"} +

+ +
+ {connector.description ? ( +

+ {connector.description} +

+ ) : null} +
+
+ + {connector.installSteps?.length ? ( +
    + {connector.installSteps.map((step, index) => ( +
  1. + + {index + 1} + +
    +

    + {step.title} +

    + {step.description ? ( +

    + {step.description} +

    + ) : null} + {step.code ? ( + + ) : null} + {step.link ? ( + + {step.link.label} + + + ) : null} +
    +
  2. + ))} +
+ ) : null} + +
+ {needsKey && connector.keyPluginId && !isUpgrade ? ( + + ) : null} + {isUpgrade ? ( + + + Upgrade to connect + + ) : null} + {connector.docsUrl ? ( + + + Docs + + ) : null} +
+
+ ) +} + +function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) { + const toolName = part.type.replace("tool-", "") + const output = unwrapToolOutput(part.output) + const isLoading = + part.state === "input-streaming" || part.state === "input-available" + const isError = part.state === "error" || part.state === "output-error" + if (isLoading) { + return ( +
+ + Checking Supermemory setup… +
+ ) + } + if (isError || !output) { + return ( +
+ Couldn't load connector setup. +
+ ) + } + if (output.success === false) { + return ( +
+

+ {output.error ?? "Connector not found"} +

+ {output.available?.length ? ( +

+ Try one of: {output.available.map((item) => item.name).join(", ")} +

+ ) : null} +
+ ) + } + const connectors = output.connector + ? [output.connector] + : (output.connectors ?? []) + return ( +
+ {toolName === "listNovaConnectors" && connectors.length > 1 ? ( +

+ Supermemory setup options +

+ ) : null} + {connectors.map((connector) => ( + + ))} +
+ ) +} + function isWebSearchPart(part: { type: string; toolName?: string }): boolean { if (part.type === "dynamic-tool") { return isWebSearchToolName(part.toolName ?? "") @@ -364,6 +749,9 @@ function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) { function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) { const [expanded, setExpanded] = useState(false) const toolName = part.type.replace("tool-", "") + if (NOVA_CONNECTOR_TOOLS.has(toolName)) { + return + } if (toolName === "bash") { return }