Make /ai and /assistant operate as the platform admin's assistant
against arcadia-app's API:
- Add `arcadia-knowledge.ts` — domain primer (multi-tenant Phoenix
backend, tenant lifecycle, platform_admins identity, etc.) baked into
every system prompt.
- Add `admin-tools.ts` — curated tool registry exposing `list_tenants`
and `get_tenant`, callable via OpenAI-native function calling. Tools
hit arcadia through `useArcadiaClient()` and inherit the operator's
JWT + tenant header. `runLLMToolCalls()` returns `tool` role messages
ready to push back into history.
- Add `admin-context.ts` — runtime registry pages publish to so the
assistant can answer factual questions about live UI state without
scraping the DOM. Tenants page registers its summary on mount.
- Replace generic Vibespace personas (Atlas/Forge/Inkwell/Pilot/Cursor)
with arcadia-flavoured ones: Operator, Auditor, Triage, Analyst,
UI Operator. Auto-migrate stored agents from the legacy set.
- /assistant: build admin preface (role + primer + persona + ctx) and
pass it as the `useChat` system at construction. Pass `tools` on every
`send()`. Auto-loop reads `toolCalls` off the streaming assistant
message and uses `continueChat()` to push tool results.
- /ai: same wiring (this is the canonical admin chat surface; the user
prefers its look).
- MessageBody renders tool-result cards (role: "tool") and a "Called X"
pill on assistant messages with toolCalls. Strips Qwen-style
`<tool_call>` XML from prose when the tags were converted to
structured calls.
- Extend ThreadMessage with the `tool` role + tool-call metadata so
conversations round-trip through localStorage.
- Tenants page: row actions get `data-action="tenant-<slug>-{suspend,
activate,deactivate}"` (via lib-table-ui's new dataAction prop);
registers tenant summary into admin-context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.8 KiB
TypeScript
101 lines
3.8 KiB
TypeScript
// Renders an assistant message: markdown for prose, plus pills for any
|
|
// command-bus action blocks or native tool calls attached to the message.
|
|
// Tool-role messages (results from a function call) render as a JSON card.
|
|
|
|
import { useMemo } from "react"
|
|
import ReactMarkdown from "react-markdown"
|
|
import { Sparkles, Wrench } from "lucide-react"
|
|
|
|
import { extractActionBlocks } from "@crema/action-bus"
|
|
import { stripToolCallTags, type ToolCall } from "@crema/llm-ui"
|
|
|
|
const ACTION_BLOCK_RE = /```action\s*\n[\s\S]*?```/g
|
|
|
|
export type MessageBodyProps = {
|
|
content: string
|
|
/** When set, render as a tool-result card. */
|
|
isToolResult?: boolean
|
|
/** Native tool calls attached to this assistant message, if any. */
|
|
toolCalls?: ToolCall[]
|
|
}
|
|
|
|
export function MessageBody({ content, isToolResult, toolCalls }: MessageBodyProps) {
|
|
const { prose, actionCount } = useMemo(() => {
|
|
const blocks = extractActionBlocks(content)
|
|
const cleaned = stripToolCallTags(content).replace(ACTION_BLOCK_RE, "").trim()
|
|
return {
|
|
prose: cleaned,
|
|
actionCount: blocks.length,
|
|
}
|
|
}, [content])
|
|
|
|
if (isToolResult) {
|
|
return (
|
|
<div className="rounded-md border border-border/60 bg-muted/40 px-3 py-2 text-xs">
|
|
<div className="mb-1 flex items-center gap-1.5 font-medium text-muted-foreground">
|
|
<Wrench className="size-3" />
|
|
Tool result
|
|
</div>
|
|
<pre className="overflow-x-auto whitespace-pre-wrap font-mono text-[11px] leading-snug text-foreground/80">
|
|
{content}
|
|
</pre>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="prose prose-sm max-w-none dark:prose-invert">
|
|
{prose && (
|
|
<ReactMarkdown
|
|
components={{
|
|
p: ({ children }) => <p className="my-1.5 leading-relaxed">{children}</p>,
|
|
code: ({ children, className }) => {
|
|
const isBlock = className?.startsWith("language-")
|
|
if (isBlock) {
|
|
return (
|
|
<pre className="my-2 overflow-x-auto rounded-md bg-muted p-3 text-xs">
|
|
<code className="font-mono">{children}</code>
|
|
</pre>
|
|
)
|
|
}
|
|
return (
|
|
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]">
|
|
{children}
|
|
</code>
|
|
)
|
|
},
|
|
ul: ({ children }) => <ul className="my-1.5 list-disc pl-5">{children}</ul>,
|
|
ol: ({ children }) => <ol className="my-1.5 list-decimal pl-5">{children}</ol>,
|
|
li: ({ children }) => <li className="my-0.5">{children}</li>,
|
|
a: ({ children, href }) => (
|
|
<a href={href} className="text-primary underline underline-offset-2" target="_blank" rel="noreferrer">
|
|
{children}
|
|
</a>
|
|
),
|
|
}}
|
|
>
|
|
{prose}
|
|
</ReactMarkdown>
|
|
)}
|
|
{actionCount > 0 && (
|
|
<span
|
|
className="mt-1 inline-flex items-center gap-1.5 rounded-full border border-primary/40 bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary dark:border-sky-400/60 dark:bg-sky-400/15 dark:text-sky-200"
|
|
title="Action block executed by the command bus"
|
|
>
|
|
<Sparkles className="size-3" />
|
|
Ran {actionCount} action{actionCount > 1 ? "s" : ""}
|
|
</span>
|
|
)}
|
|
{toolCalls && toolCalls.length > 0 && (
|
|
<span
|
|
className="mt-1 ml-1 inline-flex items-center gap-1.5 rounded-full border border-amber-400/40 bg-amber-400/10 px-2.5 py-0.5 text-xs font-medium text-amber-700 dark:border-amber-300/60 dark:bg-amber-300/10 dark:text-amber-200"
|
|
title="Tool call dispatched"
|
|
>
|
|
<Wrench className="size-3" />
|
|
Called {toolCalls.map((c) => c.name).join(", ")}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|