mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
Render Nova connector setup cards (#1071)
## Summary - Add custom assistant-message rendering for Nova connector tool results - Show integration-style setup/status cards with icon, status pill, setup steps, docs links, copy buttons, and key reveal/generate action - Keep plugin API keys client-side only by calling the existing `/v3/auth/key?client=...` endpoint from the UI - Add Nova empty-state suggestions for Cursor setup and active plugins
This commit is contained in:
parent
db7f5c3f64
commit
5fa0535a64
3 changed files with 643 additions and 3 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ import { useQuery } from "@tanstack/react-query"
|
|||
import { Streamdown } from "streamdown"
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
ExternalLinkIcon,
|
||||
GlobeIcon,
|
||||
ListIcon,
|
||||
Loader2,
|
||||
|
|
@ -17,6 +20,7 @@ import {
|
|||
TerminalIcon,
|
||||
WrenchIcon,
|
||||
XCircleIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react"
|
||||
import { cn } from "@lib/utils"
|
||||
import { isWebSearchToolName } from "@/lib/chat-web-search-tools"
|
||||
|
|
@ -84,6 +88,107 @@ 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 CONNECTOR_ICON_FALLBACKS: Record<string, string> = {
|
||||
codex: "/images/plugins/codex.png",
|
||||
cursor: "/images/plugins/cursor.png",
|
||||
mcp_cursor: "/mcp-supported-tools/cursor.png",
|
||||
}
|
||||
|
||||
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 connectorToolName(part: ToolCallDisplayPart): string {
|
||||
return part.type.startsWith("tool-")
|
||||
? part.type.slice("tool-".length)
|
||||
: part.type
|
||||
}
|
||||
|
||||
function connectorToolNameFromPart(part: unknown): string | null {
|
||||
if (!part || typeof part !== "object") return null
|
||||
const record = part as { type?: string; toolName?: string }
|
||||
if (record.type === "dynamic-tool") return record.toolName ?? null
|
||||
if (record.type?.startsWith("tool-")) return record.type.slice("tool-".length)
|
||||
return null
|
||||
}
|
||||
|
||||
function parseConnectorOutput(value: string): NovaConnectorToolOutput | null {
|
||||
try {
|
||||
return JSON.parse(value) as NovaConnectorToolOutput
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function safeExternalUrl(url: string | null | undefined): string | null {
|
||||
if (!url) return null
|
||||
if (url.startsWith("/") && !url.startsWith("//")) return url
|
||||
|
|
@ -98,6 +203,491 @@ function safeExternalUrl(url: string | null | undefined): string | null {
|
|||
}
|
||||
}
|
||||
|
||||
function unwrapToolOutput(output: unknown): NovaConnectorToolOutput | null {
|
||||
if (typeof output === "string") {
|
||||
return parseConnectorOutput(output)
|
||||
}
|
||||
if (!output || typeof output !== "object") return null
|
||||
const record = output as Record<string, unknown>
|
||||
for (const key of ["value", "result", "data", "output"]) {
|
||||
const nested = record[key]
|
||||
if (nested && nested !== output) {
|
||||
const parsed = unwrapToolOutput(nested)
|
||||
if (parsed) return parsed
|
||||
}
|
||||
}
|
||||
if (
|
||||
record.type === "json" &&
|
||||
record.value &&
|
||||
typeof record.value === "object"
|
||||
) {
|
||||
return record.value as NovaConnectorToolOutput
|
||||
}
|
||||
if (record.type === "text" && typeof record.value === "string") {
|
||||
return parseConnectorOutput(record.value)
|
||||
}
|
||||
if (typeof record.text === "string") {
|
||||
return parseConnectorOutput(record.text)
|
||||
}
|
||||
return record as NovaConnectorToolOutput
|
||||
}
|
||||
|
||||
function connectorIconSrc(
|
||||
connector: NovaConnectorCardData,
|
||||
): string | undefined {
|
||||
if (connector.id && CONNECTOR_ICON_FALLBACKS[connector.id]) {
|
||||
return CONNECTOR_ICON_FALLBACKS[connector.id]
|
||||
}
|
||||
if (connector.icon?.endsWith("/codex.svg"))
|
||||
return CONNECTOR_ICON_FALLBACKS.codex
|
||||
if (connector.icon?.endsWith("/cursor.svg"))
|
||||
return CONNECTOR_ICON_FALLBACKS.cursor
|
||||
return connector.icon
|
||||
}
|
||||
|
||||
function connectorCardKey(connector: NovaConnectorCardData): string {
|
||||
return `${connector.kind ?? "connector"}-${connector.id ?? connector.name ?? "unknown"}`
|
||||
}
|
||||
|
||||
function connectorIdentity(
|
||||
output: NovaConnectorToolOutput | null,
|
||||
): string | null {
|
||||
if (!output) return null
|
||||
if (output.connectors && output.connectors.length !== 1) return null
|
||||
const connector = output.connector ?? output.connectors?.[0]
|
||||
if (!connector) return null
|
||||
return `${connector.kind ?? "connector"}:${connector.id ?? connector.name ?? ""}`
|
||||
}
|
||||
|
||||
function connectorOutputFromPart(
|
||||
part: unknown,
|
||||
): NovaConnectorToolOutput | null {
|
||||
if (!part || typeof part !== "object") return null
|
||||
const record = part as {
|
||||
type?: string
|
||||
toolName?: string
|
||||
output?: unknown
|
||||
}
|
||||
const toolName = connectorToolNameFromPart(record)
|
||||
if (!toolName || !NOVA_CONNECTOR_TOOLS.has(toolName)) return null
|
||||
return unwrapToolOutput(record.output)
|
||||
}
|
||||
|
||||
function connectorToolPriority(toolName: string | null): number {
|
||||
if (toolName === "prepareNovaPluginSetup") return 2
|
||||
if (toolName === "getNovaConnectorSetup") return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
function shouldSkipNovaConnectorPart(parts: unknown[], index: number): boolean {
|
||||
const part = parts[index]
|
||||
const toolName = connectorToolNameFromPart(part)
|
||||
if (!toolName || !NOVA_CONNECTOR_TOOLS.has(toolName)) return false
|
||||
const identity = connectorIdentity(connectorOutputFromPart(part))
|
||||
if (!identity) return false
|
||||
const priority = connectorToolPriority(toolName)
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i === index) continue
|
||||
const otherTool = connectorToolNameFromPart(parts[i])
|
||||
if (!otherTool || !NOVA_CONNECTOR_TOOLS.has(otherTool)) continue
|
||||
const otherIdentity = connectorIdentity(connectorOutputFromPart(parts[i]))
|
||||
if (otherIdentity !== identity) continue
|
||||
const otherPriority = connectorToolPriority(otherTool)
|
||||
if (i < index && otherPriority >= priority) return true
|
||||
if (i > index && otherPriority > priority) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function StatusPill({ status }: { status?: NovaConnectorStatus }) {
|
||||
const copy =
|
||||
STATUS_COPY[status ?? "not_connected"] ?? STATUS_COPY.not_connected
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-6 shrink-0 items-center rounded-full border px-2 text-[11px] font-medium",
|
||||
copy.className,
|
||||
)}
|
||||
>
|
||||
{copy.label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function MiniCopyButton({ text, label }: { text: string; label?: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`Copy ${label ?? "value"}`}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1800)
|
||||
} catch {
|
||||
setCopied(false)
|
||||
}
|
||||
}}
|
||||
className="flex size-7 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-white/45 transition-colors hover:text-white/80"
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="size-3.5 text-emerald-300" />
|
||||
) : (
|
||||
<CopyIcon className="size-3.5" />
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function ConnectorCodeBlock({
|
||||
code,
|
||||
apiKey,
|
||||
}: {
|
||||
code: string
|
||||
apiKey?: string
|
||||
}) {
|
||||
const rendered = apiKey ? code.replaceAll("sm_...", apiKey) : code
|
||||
return (
|
||||
<div className="group flex min-w-0 items-center gap-2 rounded-[10px] border border-white/[0.07] bg-[#080B10] px-3 py-2.5">
|
||||
<pre
|
||||
className={cn(
|
||||
"scrollbar-none min-w-0 flex-1 overflow-x-auto whitespace-pre font-mono text-[12px] leading-[1.6] text-[#E4E4E7]",
|
||||
code.includes("sm_...") &&
|
||||
!apiKey &&
|
||||
"select-none blur-[4px] transition-[filter] group-hover:blur-none",
|
||||
)}
|
||||
>
|
||||
{rendered}
|
||||
</pre>
|
||||
<MiniCopyButton text={rendered} label="setup step" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RevealPluginKeyButton({
|
||||
pluginId,
|
||||
onReveal,
|
||||
}: {
|
||||
pluginId: string
|
||||
onReveal: (key: string) => void
|
||||
}) {
|
||||
const [state, setState] = useState<"idle" | "loading" | "copied" | "error">(
|
||||
"idle",
|
||||
)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={state === "loading"}
|
||||
onClick={async () => {
|
||||
setState("loading")
|
||||
try {
|
||||
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) throw new Error("Failed to create plugin key")
|
||||
const data = (await res.json()) as { key?: string }
|
||||
if (!data.key) throw new Error("Plugin key missing")
|
||||
onReveal(data.key)
|
||||
await navigator.clipboard.writeText(data.key).catch(() => undefined)
|
||||
setState("copied")
|
||||
setTimeout(() => setState("idle"), 2200)
|
||||
} catch {
|
||||
setState("error")
|
||||
setTimeout(() => setState("idle"), 2200)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"inline-flex h-8 items-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-medium text-[#FAFAFA]",
|
||||
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)] transition-opacity hover:opacity-80 disabled:opacity-60",
|
||||
)}
|
||||
>
|
||||
{state === "loading" ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : state === "copied" ? (
|
||||
<CheckIcon className="size-3.5 text-emerald-300" />
|
||||
) : state === "error" ? (
|
||||
<XCircleIcon className="size-3.5 text-red-300" />
|
||||
) : null}
|
||||
{state === "copied"
|
||||
? "Key copied"
|
||||
: state === "error"
|
||||
? "Try again"
|
||||
: state === "loading"
|
||||
? "Generating"
|
||||
: "Generate key"}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function NovaConnectorCard({
|
||||
connector,
|
||||
}: {
|
||||
connector: NovaConnectorCardData
|
||||
}) {
|
||||
const [revealedKey, setRevealedKey] = useState<string | undefined>()
|
||||
const needsKey = Boolean(connector.canGenerateKey && connector.keyPluginId)
|
||||
const isUpgrade = connector.status === "upgrade_required"
|
||||
const iconSrc = connectorIconSrc(connector)
|
||||
return (
|
||||
<div className="rounded-xl border border-white/[0.08] bg-[#0D121A] p-3 text-sm text-white/90 shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.55)]">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-[#080B0F]">
|
||||
{iconSrc ? (
|
||||
<img
|
||||
src={iconSrc}
|
||||
alt=""
|
||||
className="size-6 rounded object-contain"
|
||||
onError={(event) => {
|
||||
const img = event.currentTarget
|
||||
const fallback = connector.id
|
||||
? CONNECTOR_ICON_FALLBACKS[connector.id]
|
||||
: undefined
|
||||
if (fallback && img.dataset.fallbackApplied !== "true") {
|
||||
img.dataset.fallbackApplied = "true"
|
||||
img.src = fallback
|
||||
} else {
|
||||
img.style.display = "none"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<WrenchIcon className="size-5 text-white/50" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<p className="truncate text-[14px] font-semibold text-[#FAFAFA]">
|
||||
{connector.name ?? connector.id ?? "Connector"}
|
||||
</p>
|
||||
<StatusPill status={connector.status} />
|
||||
</div>
|
||||
{connector.description ? (
|
||||
<p className="mt-1 text-[12px] leading-snug text-[#A1A1AA]">
|
||||
{connector.description}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{connector.installSteps?.length ? (
|
||||
<ol className="mt-3 space-y-3">
|
||||
{connector.installSteps.map((step, index) => (
|
||||
<li key={`${step.title}-${index}`} className="flex gap-2.5">
|
||||
<span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-[#080B10] text-[10px] font-semibold text-[#4BA0FA]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<p className="text-[12px] font-medium text-[#FAFAFA]">
|
||||
{step.title}
|
||||
</p>
|
||||
{step.description ? (
|
||||
<p className="text-[12px] leading-snug text-[#A1A1AA]">
|
||||
{step.description}
|
||||
</p>
|
||||
) : null}
|
||||
{step.code ? (
|
||||
<ConnectorCodeBlock code={step.code} apiKey={revealedKey} />
|
||||
) : null}
|
||||
{step.link ? (
|
||||
<a
|
||||
href={step.link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-[12px] text-[#4BA0FA] hover:text-[#86C5FF]"
|
||||
>
|
||||
{step.link.label}
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : null}
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{needsKey && connector.keyPluginId && !isUpgrade ? (
|
||||
<RevealPluginKeyButton
|
||||
pluginId={connector.keyPluginId}
|
||||
onReveal={setRevealedKey}
|
||||
/>
|
||||
) : null}
|
||||
{isUpgrade ? (
|
||||
<span className="inline-flex h-8 items-center gap-1.5 rounded-full bg-[#0D121A] px-3 text-[12px] font-medium text-[#4BA0FA]">
|
||||
<ZapIcon className="size-3.5" />
|
||||
Upgrade to connect
|
||||
</span>
|
||||
) : null}
|
||||
{connector.docsUrl ? (
|
||||
<a
|
||||
href={connector.docsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex h-8 items-center gap-1.5 rounded-full px-3 text-[12px] text-[#A1A1AA] transition-colors hover:text-white"
|
||||
>
|
||||
<BookOpenIcon className="size-3.5" />
|
||||
Docs
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NovaConnectorCompactCard({
|
||||
connector,
|
||||
expanded,
|
||||
onToggle,
|
||||
}: {
|
||||
connector: NovaConnectorCardData
|
||||
expanded: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
const iconSrc = connectorIconSrc(connector)
|
||||
return (
|
||||
<div className="min-w-0 rounded-xl">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={expanded}
|
||||
onClick={onToggle}
|
||||
className={cn(
|
||||
"flex min-h-14 w-full cursor-pointer items-center gap-2 rounded-xl border px-2.5 py-2 text-left transition-colors focus:outline-none focus-visible:border-[#4BA0FA]/50",
|
||||
expanded
|
||||
? "border-[#4BA0FA]/30 bg-[#111820]"
|
||||
: "border-white/[0.08] bg-[#0D121A] hover:border-white/[0.14] hover:bg-[#111820]",
|
||||
)}
|
||||
>
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-[8px] bg-[#080B0F]">
|
||||
{iconSrc ? (
|
||||
<img
|
||||
src={iconSrc}
|
||||
alt=""
|
||||
className="size-5 rounded object-contain"
|
||||
onError={(event) => {
|
||||
event.currentTarget.style.display = "none"
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<WrenchIcon className="size-4 text-white/50" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[12px] font-semibold text-[#FAFAFA]">
|
||||
{connector.name ?? connector.id ?? "Connector"}
|
||||
</p>
|
||||
<p className="mt-0.5 truncate text-[11px] text-[#737373]">
|
||||
{connector.kind === "mcp" ? "MCP" : "Plugin"}
|
||||
</p>
|
||||
</div>
|
||||
<StatusPill status={connector.status} />
|
||||
{expanded ? (
|
||||
<ChevronDownIcon className="size-3.5 shrink-0 text-[#737373]" />
|
||||
) : (
|
||||
<ChevronRightIcon className="size-3.5 shrink-0 text-[#737373]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NovaConnectorToolDisplay({ part }: { part: ToolCallDisplayPart }) {
|
||||
const [expandedConnectorKey, setExpandedConnectorKey] = useState<
|
||||
string | null
|
||||
>(null)
|
||||
const toolName = connectorToolName(part)
|
||||
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 (
|
||||
<div className="my-2 flex items-center gap-2 rounded-xl border border-white/[0.08] bg-[#0D121A] px-3 py-2 text-xs text-white/55">
|
||||
<Loader2 className="size-3.5 animate-spin text-[#4BA0FA]" />
|
||||
<span>Checking Supermemory setup…</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="my-2 rounded-xl border border-red-400/15 bg-red-400/10 px-3 py-2 text-xs text-red-200">
|
||||
Couldn't load connector setup.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (!output) return null
|
||||
if (output.success === false) {
|
||||
return (
|
||||
<div className="my-2 rounded-xl border border-white/[0.08] bg-[#0D121A] p-3 text-sm text-white/80">
|
||||
<p className="font-medium text-[#FAFAFA]">
|
||||
{output.error ?? "Connector not found"}
|
||||
</p>
|
||||
{output.available?.length ? (
|
||||
<p className="mt-1 text-xs text-[#A1A1AA]">
|
||||
Try one of: {output.available.map((item) => item.name).join(", ")}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const connectors = output.connector
|
||||
? [output.connector]
|
||||
: (output.connectors ?? [])
|
||||
const isConnectorList =
|
||||
toolName === "listNovaConnectors" && connectors.length > 1
|
||||
const expandedConnector =
|
||||
isConnectorList && expandedConnectorKey
|
||||
? connectors.find(
|
||||
(connector) => connectorCardKey(connector) === expandedConnectorKey,
|
||||
)
|
||||
: null
|
||||
return (
|
||||
<div className="my-2 space-y-2">
|
||||
{isConnectorList ? (
|
||||
<p className="px-1 text-[11px] font-medium uppercase tracking-[0.08em] text-[#737373]">
|
||||
Supermemory setup options
|
||||
</p>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
isConnectorList && "grid grid-cols-1 gap-2 sm:grid-cols-2",
|
||||
!isConnectorList && "space-y-2",
|
||||
)}
|
||||
>
|
||||
{connectors.map((connector) =>
|
||||
isConnectorList ? (
|
||||
<NovaConnectorCompactCard
|
||||
key={connectorCardKey(connector)}
|
||||
connector={connector}
|
||||
expanded={connectorCardKey(connector) === expandedConnectorKey}
|
||||
onToggle={() => {
|
||||
const nextKey = connectorCardKey(connector)
|
||||
setExpandedConnectorKey((current) =>
|
||||
current === nextKey ? null : nextKey,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<NovaConnectorCard
|
||||
key={connectorCardKey(connector)}
|
||||
connector={connector}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
{expandedConnector ? (
|
||||
<div className="pt-1">
|
||||
<NovaConnectorCard connector={expandedConnector} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function isWebSearchPart(part: { type: string; toolName?: string }): boolean {
|
||||
if (part.type === "dynamic-tool") {
|
||||
return isWebSearchToolName(part.toolName ?? "")
|
||||
|
|
@ -548,7 +1138,10 @@ function BashToolDisplay({ part }: { part: ToolCallDisplayPart }) {
|
|||
|
||||
function ToolCallDisplay({ part }: { part: ToolCallDisplayPart }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const toolName = part.type.replace("tool-", "")
|
||||
const toolName = connectorToolName(part)
|
||||
if (NOVA_CONNECTOR_TOOLS.has(toolName)) {
|
||||
return <NovaConnectorToolDisplay part={part} />
|
||||
}
|
||||
if (toolName === "bash") {
|
||||
return <BashToolDisplay part={part} />
|
||||
}
|
||||
|
|
@ -829,6 +1422,9 @@ export function AgentMessage({
|
|||
)
|
||||
}
|
||||
if (part.type === "dynamic-tool") {
|
||||
if (shouldSkipNovaConnectorPart(message.parts, partIndex)) {
|
||||
return null
|
||||
}
|
||||
const dt = part as {
|
||||
type: "dynamic-tool"
|
||||
toolName: string
|
||||
|
|
@ -856,6 +1452,9 @@ export function AgentMessage({
|
|||
)
|
||||
}
|
||||
if (part.type.startsWith("tool-")) {
|
||||
if (shouldSkipNovaConnectorPart(message.parts, partIndex)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ToolCallDisplay
|
||||
key={`${message.id}-${partIndex}`}
|
||||
|
|
|
|||
|
|
@ -195,6 +195,47 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.chat-markdown-content pre {
|
||||
margin: 0.75rem 0;
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0.75rem;
|
||||
background: #080b10 !important;
|
||||
padding: 0.875rem 1rem;
|
||||
color: #f4f4f5 !important;
|
||||
}
|
||||
|
||||
.chat-markdown-content pre code {
|
||||
display: block;
|
||||
background: transparent !important;
|
||||
padding: 0;
|
||||
color: #f4f4f5 !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.65;
|
||||
text-shadow: none;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.chat-markdown-content pre code *,
|
||||
.chat-markdown-content pre [style] {
|
||||
background: transparent !important;
|
||||
color: #f4f4f5 !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.chat-markdown-content pre code .line {
|
||||
min-height: 1.45em;
|
||||
}
|
||||
|
||||
.chat-markdown-content :not(pre) > code {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
padding: 0.125rem 0.3125rem;
|
||||
color: #fafafa;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
/* Model spams `---` between every section; headings already separate them */
|
||||
.chat-markdown-content hr {
|
||||
display: none;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue