Wire AI assistant to arcadia: domain primer, tool calling, admin context
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>
This commit is contained in:
@@ -1,30 +1,53 @@
|
||||
// Renders an assistant message: markdown for prose, and a small "Ran N
|
||||
// actions" pill in place of any ```action``` fenced blocks (which are the
|
||||
// machine-readable instructions the bus has already executed).
|
||||
// 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 } from "lucide-react"
|
||||
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 function MessageBody({ content }: { content: string }) {
|
||||
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: content.replace(ACTION_BLOCK_RE, "").trim(),
|
||||
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={{
|
||||
// Tight overrides — keep paragraphs compact in chat bubbles
|
||||
p: ({ children }) => <p className="my-1.5 leading-relaxed">{children}</p>,
|
||||
code: ({ children, className }) => {
|
||||
const isBlock = className?.startsWith("language-")
|
||||
@@ -63,6 +86,15 @@ export function MessageBody({ content }: { content: string }) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user