import { ActionBarMorePrimitive, ActionBarPrimitive, AuiIf, BranchPickerPrimitive, ComposerPrimitive, ErrorPrimitive, MessagePrimitive, SuggestionPrimitive, ThreadPrimitive, useAui, useAuiState, } from "@assistant-ui/react"; import { ArrowDownIcon, ArrowUpIcon, BookOpenIcon, BrainIcon, CheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, DownloadIcon, FileTextIcon, ListIcon, MoreHorizontalIcon, PencilIcon, RefreshCwIcon, SquareIcon, WrenchIcon, } from "lucide-react"; import { Popover as PopoverPrimitive } from "radix-ui"; import type { ComponentProps, FC, KeyboardEvent, ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import TextareaAutosize from "react-textarea-autosize"; import { useActivityPanel } from "@/components/assistant-ui/activity-panel"; import { ComposerAddAttachment, ComposerAttachments, UserMessageAttachments, } from "@/components/assistant-ui/attachment"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { Reasoning } from "@/components/assistant-ui/reasoning"; import { fetchLatestRunStatus, toReplayRepository, } from "@/components/assistant-ui/thread-list"; import { ToolFallback } from "@/components/assistant-ui/tool-fallback"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useClawSessionReplay } from "@/lib/claw-session-replay"; import { cn } from "@/lib/utils"; type ComposerInsertEvent = CustomEvent<{ text: string }>; type SkillSummary = { name: string; description?: string; when_to_use?: string; }; type SlashCommandSummary = { names: string[]; primary: string; description?: string; webui_supported?: boolean; }; type ClawState = { model?: string; active_session_id?: string | null; }; type ClawStoredSession = { session_id?: string; model?: string; }; type ModelOption = { id: string; }; type ContextBudget = { model?: string; context_window_tokens?: number; projected_input_tokens?: number; message_tokens?: number; chat_overhead_tokens?: number; reserved_output_tokens?: number; reserved_compaction_buffer_tokens?: number; reserved_schema_tokens?: number; hard_input_limit_tokens?: number; soft_input_limit_tokens?: number; overflow_tokens?: number; soft_overflow_tokens?: number; exceeds_hard_limit?: boolean; exceeds_soft_limit?: boolean; token_counter_backend?: string; token_counter_source?: string; token_counter_accurate?: boolean; }; export const Thread: FC = () => { useRefreshReplayedRun(); return (
s.thread.isEmpty}>
{() => }
); }; function useRefreshReplayedRun() { const { replaySession } = useClawSessionReplay(); const isRunning = useAuiState((s) => s.thread.isRunning); const hasReplayedRunMessage = useAuiState((s) => s.thread.messages.some((message) => message.id.includes("-run-")), ); const latestSessionId = useAuiState((s) => findLatestMessageMetadataValue(s.thread.messages, "sessionId"), ); useEffect(() => { if (!isRunning || !hasReplayedRunMessage) return; const sessionId = normalizeSessionId( latestSessionId ?? window.localStorage.getItem("claw.activeSessionId"), ); if (!sessionId) return; const activeSessionId = sessionId; let cancelled = false; async function refreshIfFinished() { const runStatus = await fetchLatestRunStatus(activeSessionId); if (cancelled) return; if (runStatus?.status === "queued" || runStatus?.status === "running") { return; } const response = await fetch(`/api/claw/sessions/${activeSessionId}`, { cache: "no-store", }); if (!response.ok || cancelled) return; const payload = await response.json(); replaySession( activeSessionId, toReplayRepository(payload, activeSessionId, runStatus), ); window.dispatchEvent(new Event("claw-sessions-changed")); } refreshIfFinished(); const interval = window.setInterval(refreshIfFinished, 3000); return () => { cancelled = true; window.clearInterval(interval); }; }, [hasReplayedRunMessage, isRunning, latestSessionId, replaySession]); } const ChatTopActions: FC = () => { const { openFiles } = useActivityPanel(); const latestSessionId = useAuiState((s) => findLatestMessageMetadataValue(s.thread.messages, "sessionId"), ); return (
); }; const ThreadMessage: FC = () => { const role = useAuiState((s) => s.message.role); const isEditing = useAuiState((s) => s.message.composer.isEditing); if (isEditing) return ; if (role === "user") return ; return ; }; const ThreadScrollToBottom: FC = () => { return ( ); }; const ThreadWelcome: FC = () => { return (

ZK Data Agent

中控数据开发平台

); }; const ThreadSuggestions: FC = () => { return (
{() => }
); }; const ThreadSuggestionItem: FC = () => { return (
); }; const Composer: FC = () => { return (
); }; const ComposerAction: FC = () => { return (
!s.thread.isRunning}> s.thread.isRunning}>
); }; const ComposerContextStatus: FC = () => { const status = useComposerContextStatus(); const totalTokens = status.contextBudget?.projected_input_tokens ?? 0; const limit = status.contextBudget?.context_window_tokens ?? 0; const percent = limit ? Math.min(100, Math.round((totalTokens / limit) * 100)) : 0; return (
); }; function ContextWindowButton({ percent, totalTokens, limit, contextBudget, }: { percent: number; totalTokens: number; limit: number; contextBudget?: ContextBudget; }) { const ringStyle = { background: `conic-gradient(var(--primary) ${percent * 3.6}deg, var(--muted) 0deg)`, }; return (
背景信息窗口:
{percent}% 已用(剩余 {Math.max(0, 100 - percent)}%)
已用 {formatCompactTokenCount(totalTokens)} 标记,共{" "} {formatCompactTokenCount(limit)}
背景信息窗口:
{percent}% 已用(剩余 {Math.max(0, 100 - percent)}%)
已用 {formatCompactTokenCount(totalTokens)} 标记,共{" "} {formatCompactTokenCount(limit)}
软阈值: {formatCompactTokenCount( contextBudget?.soft_input_limit_tokens ?? 0, )}
硬阈值: {formatCompactTokenCount( contextBudget?.hard_input_limit_tokens ?? 0, )}
保留输出: {formatCompactTokenCount( contextBudget?.reserved_output_tokens ?? 0, )} ;压缩缓冲: {formatCompactTokenCount( contextBudget?.reserved_compaction_buffer_tokens ?? 0, )}
计数器:{contextBudget?.token_counter_backend ?? "unknown"} {contextBudget?.token_counter_accurate ? "(精确)" : "(估算)"}
); } function ModelPickerButton({ status, }: { status: { model?: string; models: ModelOption[]; setModel: (model: string) => Promise; }; }) { return (
切换模型
{status.models.length ? ( status.models.map((model) => ( )) ) : (
模型列表读取中
)}
); } function useComposerContextStatus() { const isRunning = useAuiState((s) => s.thread.isRunning); const latestSessionId = useAuiState((s) => findLatestMessageMetadataValue(s.thread.messages, "sessionId"), ); const wasRunningRef = useRef(false); const [status, setStatus] = useState<{ model?: string; contextBudget?: ContextBudget; sessionId?: string | null; models: ModelOption[]; setModel: (model: string) => Promise; }>({ models: [], setModel: async () => {}, }); useEffect(() => { const cleanLatestSessionId = normalizeSessionId(latestSessionId); if (cleanLatestSessionId) { window.localStorage.setItem("claw.activeSessionId", cleanLatestSessionId); window.setTimeout(() => { window.dispatchEvent(new Event("claw-sessions-changed")); }, 0); } }, [latestSessionId]); useEffect(() => { if (wasRunningRef.current && !isRunning) { window.dispatchEvent(new Event("claw-sessions-changed")); window.setTimeout(() => { window.dispatchEvent(new Event("claw-sessions-changed")); }, 1500); } wasRunningRef.current = isRunning; }, [isRunning]); useEffect(() => { let cancelled = false; async function refresh() { try { const stateResponse = await fetch("/api/claw/state", { cache: "no-store", }); const statePayload = stateResponse.ok ? ((await stateResponse.json()) as ClawState) : {}; let sessionId = normalizeSessionId( normalizeSessionId(latestSessionId) || normalizeSessionId( window.localStorage.getItem("claw.activeSessionId"), ) || normalizeSessionId(statePayload.active_session_id), ); if (sessionId) window.localStorage.setItem("claw.activeSessionId", sessionId); let sessionPayload: ClawStoredSession | null = null; let contextBudget: ContextBudget | undefined; if (sessionId) { const sessionResponse = await fetch( `/api/claw/sessions/${sessionId}`, { cache: "no-store", }, ); if (sessionResponse.ok) { sessionPayload = (await sessionResponse.json()) as ClawStoredSession; } else if (sessionResponse.status === 404) { window.localStorage.removeItem("claw.activeSessionId"); sessionId = null; } } const budgetUrl = new URL( "/api/claw/context-budget", window.location.origin, ); if (sessionId) budgetUrl.searchParams.set("session_id", sessionId); const budgetResponse = await fetch(budgetUrl, { cache: "no-store" }); contextBudget = budgetResponse.ok ? ((await budgetResponse.json()) as ContextBudget) : undefined; if (cancelled) return; setStatus((current) => ({ model: contextBudget?.model ?? sessionPayload?.model ?? statePayload.model, contextBudget, sessionId, models: current.models, setModel: current.setModel, })); } catch { if (!cancelled) { setStatus((current) => ({ ...current, })); } } } refresh(); const onRefresh = () => refresh(); window.addEventListener("focus", onRefresh); window.addEventListener("claw-active-session-cleared", onRefresh); const interval = window.setInterval(refresh, isRunning ? 2000 : 8000); return () => { cancelled = true; window.removeEventListener("focus", onRefresh); window.removeEventListener("claw-active-session-cleared", onRefresh); window.clearInterval(interval); }; }, [isRunning, latestSessionId]); useEffect(() => { let cancelled = false; async function refreshModels() { try { const response = await fetch("/api/claw/models", { cache: "no-store", }); const payload = (await response.json()) as { models?: ModelOption[] }; if (!cancelled && response.ok) { setStatus((current) => ({ ...current, models: Array.isArray(payload.models) ? payload.models : [], })); } } catch { if (!cancelled) { setStatus((current) => ({ ...current, models: [] })); } } } refreshModels(); return () => { cancelled = true; }; }, []); const setModel = useCallback(async (model: string) => { const response = await fetch("/api/claw/state", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ model }), }); const payload = (await response.json()) as ClawState; if (!response.ok) return; setStatus((current) => ({ ...current, model: payload.model ?? model, contextBudget: undefined, })); }, []); useEffect(() => { setStatus((current) => ({ ...current, setModel })); }, [setModel]); return status; } function findLatestMessageMetadataValue( messages: readonly unknown[], key: string, ) { for (const message of [...messages].reverse()) { if (!message || typeof message !== "object") continue; const metadata = (message as { metadata?: unknown }).metadata; const value = readMetadataValue(metadata, key); if (value !== undefined) return value; } return undefined; } function readMetadataValue(metadata: unknown, key: string): unknown { if (!metadata || typeof metadata !== "object") return undefined; const record = metadata as Record; if (record[key] !== undefined) return record[key]; const custom = record.custom; if (custom && typeof custom === "object") { return (custom as Record)[key]; } return undefined; } function normalizeSessionId(value: unknown) { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed || null; } function formatCompactTokenCount(value: number) { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`; return `${value}`; } function compactModelLabel(model?: string) { if (!model) return "模型"; const value = model.includes("/") ? model.split("/").at(-1) || model : model; if (value.toLowerCase().startsWith("mimo-v2.5")) return "MiMo 2.5"; if (value.toLowerCase().startsWith("mimo-v2")) return "MiMo 2"; if (value.toLowerCase().startsWith("deepseek")) return "DeepSeek"; if (value.toLowerCase().startsWith("qwen")) return "Qwen"; if (value.toLowerCase().startsWith("minimax")) return "MiniMax"; return value; } function modelProvider(model: string) { if (model.includes("/")) return model.split("/", 1)[0] || "unknown"; return "default"; } const ComposerAssistButtons: FC = () => { return (
} triggerLabel="" items={TOOL_PROMPTS} />
); }; const TOOL_PROMPTS = [ { title: "data_agent_profile_router_sessions", description: "查看 parquet 线上 session 数据字段、规模和样例。", prompt: "请优先使用 data_agent_profile_router_sessions 了解我指定的 router_session_parquet 数据目录,再给出可用字段和初步挖掘建议。", }, { title: "data_agent_search_router_sessions", description: "用关键词、正则、domain、device 等条件筛选候选。", prompt: "请优先使用 data_agent_search_router_sessions 按我的条件筛选线上候选,并说明筛选策略、命中数量和样例质量。", }, { title: "data_agent_normalize_dataset_draft", description: "把自然语言 draft 转换为 canonical records。", prompt: "请在我确认数据内容后,使用 data_agent_normalize_dataset_draft 把 draft 转换成 canonical records。", }, { title: "data_agent_validate_dataset_records", description: "校验 canonical records 是否符合结构约束。", prompt: "请使用 data_agent_validate_dataset_records 校验当前 canonical records,并解释每类错误应该如何修复。", }, ]; function PromptInsertDialog({ title, description, triggerIcon, triggerLabel, items, }: { title: string; description: string; triggerIcon: ReactNode; triggerLabel: string; items: { title: string; description: string; prompt: string }[]; }) { const [open, setOpen] = useState(false); return ( {title} {description}
{items.map((item) => ( ))}
); } function SlashCommandDialog() { const [open, setOpen] = useState(false); const [commands, setCommands] = useState([]); const [status, setStatus] = useState( "点击后读取当前后端 slash command 列表。", ); async function loadCommands() { setStatus("正在读取 slash command 列表..."); try { const response = await fetch("/api/claw/slash-commands", { cache: "no-store", }); const payload = (await response.json()) as | SlashCommandSummary[] | { error?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error( Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"), ); } setCommands(payload); setStatus(payload.length ? "" : "当前没有可用 slash command。"); } catch (err) { setStatus(err instanceof Error ? err.message : "读取 slash command 失败"); } } return ( { setOpen(open); if (open) loadCommands(); }} > 快捷入口 这里展示终端 `/` 命令中可在 WebUI 对话框执行的子集。 {status ? (

{status}

) : null}
{commands.map((command) => { const commandText = `/${command.primary}`; const aliases = command.names .filter((name) => name !== command.primary) .map((name) => `/${name}`) .join(" "); return ( ); })}
); } function SkillInsertDialog() { const [open, setOpen] = useState(false); const [skills, setSkills] = useState([]); const [status, setStatus] = useState("点击后读取当前后端 Skill 列表。"); async function loadSkills() { setStatus("正在读取 Skill 列表..."); try { const response = await fetch("/api/claw/skills", { cache: "no-store" }); const payload = (await response.json()) as | SkillSummary[] | { error?: string }; if (!response.ok || !Array.isArray(payload)) { throw new Error( Array.isArray(payload) ? "读取失败" : (payload.error ?? "读取失败"), ); } setSkills(payload); setStatus(payload.length ? "" : "当前没有可用 Skill。"); } catch (err) { setStatus(err instanceof Error ? err.message : "读取 Skill 失败"); } } return ( { setOpen(open); if (open) loadSkills(); }} > Skills 选择 Skill 后会插入可编辑提示词,便于调试和明确任务入口。 {status ? (

{status}

) : null}
{skills.map((skill) => ( ))}
); } function insertComposerText(text: string) { window.dispatchEvent( new CustomEvent("claw-composer-insert", { detail: { text }, }), ); } const MessageError: FC = () => { return ( ); }; const AssistantMessage: FC = () => { const ACTION_BAR_PT = "pt-1.5"; const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`; const messageId = useAuiState((s) => s.message.id); return (
{ if (part.type === "reasoning" || part.type === "tool-call") return ["group-chain"]; return null; }} > {({ part }) => { switch (part.type) { case "group-chain": return ( ); case "text": return ( ); case "reasoning": return ; case "tool-call": return part.toolUI ?? ; default: return null; } }}
); }; const ActivityChainSummary: FC<{ messageId: string; partIndex: number | undefined; }> = ({ messageId, partIndex }) => { const { openItem } = useActivityPanel(); const startedAtRef = useRef(Date.now()); const [elapsedMs, setElapsedMs] = useState(0); const label = useAuiState((s) => { const message = s.message; return summarizeActivityChainLabel( message.content, message.status?.type, elapsedMs, ); }); const active = useAuiState((s) => { const message = s.message; return message.status?.type === "running"; }); useEffect(() => { if (!active) return; setElapsedMs(Date.now() - startedAtRef.current); const timer = window.setInterval(() => { setElapsedMs(Date.now() - startedAtRef.current); }, 1000); return () => window.clearInterval(timer); }, [active]); return ( ); }; type ActivitySummaryPart = { type: string; text?: string; result?: unknown; args?: unknown; toolName?: string; }; function summarizeActivityChainLabel( content: readonly ActivitySummaryPart[], status: string | undefined, elapsedMs = 0, ) { const toolParts = content.filter((part) => part.type === "tool-call"); const runningTool = toolParts.findLast((part) => part.result === undefined); const activeTool = runningTool ?? toolParts.at(-1); const reasoningText = content.find((part) => part.type === "reasoning")?.text ?? ""; const lastReasoningLine = reasoningText .trim() .split("\n") .map((line) => line.trim()) .filter(Boolean) .at(-1); const stageNote = activeTool ? getToolStageNote(activeTool.args) : ""; const activeDetail = stageNote || (activeTool?.toolName ? `调用 ${activeTool.toolName}` : "") || (isGenericActivityLine(lastReasoningLine) ? "" : lastReasoningLine); if (status === "running") { const elapsed = `用时 ${formatActivityDuration(elapsedMs)}`; if (activeDetail) { return `${compactActivityLabel(activeDetail, 15)},${elapsed}`; } return `思考中,${elapsed}`; } const label = compactActivityLabel( activeDetail || lastReasoningLine || "思考完成", 15, ); const finalDuration = extractDurationLabel(lastReasoningLine); const elapsed = finalDuration || formatActivityDuration(elapsedMs); return `${label},用时 ${elapsed}`; } function getToolStageNote(value: unknown) { if (!value || typeof value !== "object" || Array.isArray(value)) return ""; const note = (value as Record).__claw_stage_note; return typeof note === "string" ? note.trim() : ""; } function isGenericActivityLine(value?: string) { if (!value) return true; return ( value.startsWith("思考中") || value.startsWith("思考并执行完成") || value.endsWith("输出中...") ); } function extractDurationLabel(value?: string) { if (!value) return ""; const match = value.match(/用时\s*([^。,.,\s]+)/); return match?.[1] ?? ""; } function formatActivityDuration(ms: number) { const safeMs = Math.max(0, Math.round(ms)); if (safeMs < 1000) return `${safeMs}ms`; const totalSeconds = Math.round(safeMs / 1000); if (totalSeconds < 60) return `${totalSeconds}s`; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return seconds > 0 ? `${minutes}min${seconds}s` : `${minutes}min`; } function compactActivityLabel(label: string, maxLength = 15) { const normalized = label.replace(/\s+/g, " ").trim(); if (normalized.length <= maxLength) return normalized; return `${normalized.slice(0, Math.max(1, maxLength - 1))}…`; } const AssistantActionBar: FC = () => { return ( s.message.isCopied}> !s.message.isCopied}> Export as Markdown ); }; const UserMessage: FC = () => { return (
); }; const UserActionBar: FC = () => { return ( ); }; const EditComposer: FC = () => { return (
); }; const ImeComposerInput: FC> = ({ autoFocus, onChange, onCompositionEnd, onCompositionStart, onKeyDown, disabled, ...props }) => { const aui = useAui(); const storeText = useAuiState((s) => s.composer.isEditing ? s.composer.text : "", ); const runtimeDisabled = useAuiState( (s) => s.thread.isDisabled || s.composer.dictation?.inputDisabled, ); const [localText, setLocalText] = useState(storeText); const textareaRef = useRef(null); const isComposingRef = useRef(false); const justSubmittedRef = useRef(false); const isDisabled = runtimeDisabled || disabled; useEffect(() => { if (!isComposingRef.current) { if (justSubmittedRef.current && storeText) return; if (!storeText) justSubmittedRef.current = false; setLocalText(storeText); } }, [storeText]); useEffect(() => { if ( !isComposingRef.current && localText !== storeText && aui.composer().getState().isEditing ) { aui.composer().setText(localText); } }, [aui, localText, storeText]); useEffect(() => { if (!autoFocus || isDisabled) return; const textarea = textareaRef.current; if (!textarea) return; textarea.focus({ preventScroll: true }); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }, [autoFocus, isDisabled]); useEffect(() => { const clearAfterSubmit = () => { justSubmittedRef.current = true; setLocalText(""); }; const unsubscribeComposer = aui.on("composer.send", clearAfterSubmit); const unsubscribeRun = aui.on("thread.runStart", clearAfterSubmit); return () => { unsubscribeComposer(); unsubscribeRun(); }; }, [aui]); const syncComposerText = useCallback( (value: string) => { if (!aui.composer().getState().isEditing) return; aui.composer().setText(value); }, [aui], ); useEffect(() => { const textarea = textareaRef.current; if (!textarea) return; const handleInsert = (event: Event) => { const insertText = (event as ComposerInsertEvent).detail?.text; if (!insertText) return; const nextText = [localText.trimEnd(), insertText.trim()] .filter(Boolean) .join("\n\n"); setLocalText(nextText); syncComposerText(nextText); requestAnimationFrame(() => { textarea.focus({ preventScroll: true }); textarea.setSelectionRange(nextText.length, nextText.length); }); }; window.addEventListener("claw-composer-insert", handleInsert); return () => window.removeEventListener("claw-composer-insert", handleInsert); }, [localText, syncComposerText]); const handleKeyDown = (event: KeyboardEvent) => { onKeyDown?.(event); if (event.defaultPrevented || isDisabled) return; if ( event.nativeEvent.isComposing || isComposingRef.current || event.key !== "Enter" || event.shiftKey ) { return; } const threadState = aui.thread().getState(); if (threadState.isRunning && !threadState.capabilities.queue) return; event.preventDefault(); syncComposerText(localText); aui.composer().send(); justSubmittedRef.current = true; setLocalText(""); }; return ( { onChange?.(event); const nextText = event.currentTarget.value; justSubmittedRef.current = false; setLocalText(nextText); const isNativeComposing = (event.nativeEvent as { isComposing?: boolean }).isComposing === true; if (!isNativeComposing && !isComposingRef.current) { syncComposerText(nextText); } }} onCompositionStart={(event) => { onCompositionStart?.(event); isComposingRef.current = true; }} onCompositionEnd={(event) => { onCompositionEnd?.(event); isComposingRef.current = false; const target = event.currentTarget; queueMicrotask(() => { const nextText = target.value; setLocalText(nextText); syncComposerText(nextText); }); }} onKeyDown={handleKeyDown} /> ); }; const BranchPicker: FC = ({ className, ...rest }) => { return ( / ); };