187 lines
6.8 KiB
TypeScript
187 lines
6.8 KiB
TypeScript
/**
|
|
* Step 1 / Step 2 对话气泡里 ChainOfThought 折叠卡的渲染辅助。
|
|
*
|
|
* 把后端流式帧里的 `phase:toolName` 翻译成可点的图标 + 中文文案 + 子节点,
|
|
* 与主聊天 `chats/new` 的 `message-group.tsx` 里 `ToolCall` 视觉语义保持一致。
|
|
*
|
|
* 工具图标 + 文案映射严格对齐 `src/core/i18n/locales/zh-CN.ts` 的 `toolCalls` 字段,
|
|
* 改文案时**两边都要改**(主聊天走 i18n,圆桌规划写死字符串)。
|
|
*/
|
|
import React from "react";
|
|
import {
|
|
BookOpenTextIcon,
|
|
Dot as DotIcon,
|
|
FolderOpenIcon,
|
|
GlobeIcon,
|
|
LightbulbIcon,
|
|
ListTodoIcon,
|
|
MessageCircleQuestionMarkIcon,
|
|
NotebookPenIcon,
|
|
SearchIcon,
|
|
SquareTerminalIcon,
|
|
Wrench as WrenchIcon,
|
|
} from "lucide-react";
|
|
|
|
import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-thought";
|
|
|
|
/**
|
|
* 根据工具名 + args 渲染 ChainOfThoughtStep 的子节点(路径 chip / 命令代码块等)。
|
|
* 数据来自 streamMultiAgent 解析 LangGraph messages-tuple 帧的 tool_calls[0].args。
|
|
*
|
|
* 主聊天的 `ToolCall` 还会消费 tool result(走 `findToolCallResult`),我们这边
|
|
* 拿不到 result(只走 tool_calls 增量帧),所以只渲染 args。
|
|
*/
|
|
export function renderStepArgs(
|
|
key: string,
|
|
args?: Record<string, unknown>,
|
|
): React.ReactNode {
|
|
if (!args) return null;
|
|
const [, toolName] = key.split(":");
|
|
if (!toolName) return null;
|
|
switch (toolName) {
|
|
case "agent_orchestration": {
|
|
const agentName = typeof args.agent_name === "string" ? args.agent_name : "";
|
|
const task = typeof args.task === "string" ? args.task : "";
|
|
if (!agentName && !task) return null;
|
|
return (
|
|
<>
|
|
{agentName && (
|
|
<ChainOfThoughtSearchResult>{agentName}</ChainOfThoughtSearchResult>
|
|
)}
|
|
{task && (
|
|
<span className="block text-[11px] text-slate-600 line-clamp-3 mt-0.5">
|
|
{task}
|
|
</span>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
case "web_search":
|
|
case "image_search": {
|
|
const query = typeof args.query === "string" ? args.query : "";
|
|
return query ? <ChainOfThoughtSearchResult>{query}</ChainOfThoughtSearchResult> : null;
|
|
}
|
|
case "web_fetch": {
|
|
const url = typeof args.url === "string" ? args.url : "";
|
|
return url ? <ChainOfThoughtSearchResult>{url}</ChainOfThoughtSearchResult> : null;
|
|
}
|
|
case "read_file":
|
|
case "ls":
|
|
case "write_file":
|
|
case "str_replace": {
|
|
const path = typeof args.path === "string" ? args.path : "";
|
|
return path ? <ChainOfThoughtSearchResult>{path}</ChainOfThoughtSearchResult> : null;
|
|
}
|
|
case "bash": {
|
|
const command = typeof args.command === "string" ? args.command : "";
|
|
return command ? (
|
|
<code className="block bg-slate-100 rounded px-2 py-1 text-[11px] font-mono text-slate-800 mt-0.5 line-clamp-2 break-all">
|
|
{command}
|
|
</code>
|
|
) : null;
|
|
}
|
|
case "write_todos": {
|
|
// 主聊天里 write_todos 不显示子节点,只渲染标签;这里也保持一致。
|
|
return null;
|
|
}
|
|
case "ask_clarification": {
|
|
const question = typeof args.question === "string" ? args.question : "";
|
|
return question ? (
|
|
<span className="block text-[11px] text-slate-600 line-clamp-2 mt-0.5">
|
|
{question}
|
|
</span>
|
|
) : null;
|
|
}
|
|
default: {
|
|
const entries = Object.entries(args).filter(
|
|
([, v]) => v !== null && v !== undefined && v !== "",
|
|
);
|
|
if (entries.length === 0) return null;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{entries.slice(0, 4).map(([k, v]) => (
|
|
<ChainOfThoughtSearchResult key={k}>
|
|
<span className="opacity-60">{k}:</span>{" "}
|
|
{typeof v === "string" ? v : JSON.stringify(v)}
|
|
</ChainOfThoughtSearchResult>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 根据 step.key(格式 `phase:toolName`)给出 ChainOfThoughtStep 的图标 + 文案。
|
|
*
|
|
* 工具调用文案严格对齐主聊天 `toolCalls.*` 字段。args 可选:有 args 且工具支持
|
|
* query / path 等关键参数时,会把它拼入 label(「在网络上搜索 'X'」/「读取文件」等)。
|
|
*
|
|
* 非工具阶段(connecting / connected / streaming / tool_result)走 fallback label + 小点 / 灯泡图标。
|
|
*/
|
|
export function getStepDisplay(
|
|
key: string,
|
|
fallbackLabel: string,
|
|
args?: Record<string, unknown>,
|
|
): { icon: typeof DotIcon; label: string } {
|
|
const [phase, toolName] = key.split(":");
|
|
if (phase === "tool_calling" && toolName) {
|
|
switch (toolName) {
|
|
case "web_search": {
|
|
const query = typeof args?.query === "string" ? args.query.trim() : "";
|
|
return {
|
|
icon: SearchIcon,
|
|
label: query ? `在网络上搜索 “${query}”` : "搜索相关信息",
|
|
};
|
|
}
|
|
case "image_search": {
|
|
const query = typeof args?.query === "string" ? args.query.trim() : "";
|
|
return {
|
|
icon: SearchIcon,
|
|
label: query ? `搜索相关图片 “${query}”` : "搜索相关图片",
|
|
};
|
|
}
|
|
case "web_fetch":
|
|
return { icon: GlobeIcon, label: "查看网页" };
|
|
case "ls": {
|
|
const desc =
|
|
typeof args?.description === "string" ? args.description.trim() : "";
|
|
return { icon: FolderOpenIcon, label: desc || "列出文件夹" };
|
|
}
|
|
case "read_file": {
|
|
const desc =
|
|
typeof args?.description === "string" ? args.description.trim() : "";
|
|
return { icon: BookOpenTextIcon, label: desc || "读取文件" };
|
|
}
|
|
case "write_file":
|
|
case "str_replace": {
|
|
const desc =
|
|
typeof args?.description === "string" ? args.description.trim() : "";
|
|
return { icon: NotebookPenIcon, label: desc || "写入文件" };
|
|
}
|
|
case "bash": {
|
|
const desc =
|
|
typeof args?.description === "string" ? args.description.trim() : "";
|
|
return { icon: SquareTerminalIcon, label: desc || "执行命令" };
|
|
}
|
|
case "write_todos":
|
|
return { icon: ListTodoIcon, label: "更新 To-do 列表" };
|
|
case "ask_clarification":
|
|
return { icon: MessageCircleQuestionMarkIcon, label: "需要你的协助" };
|
|
case "present_files":
|
|
return { icon: FolderOpenIcon, label: "展示文件" };
|
|
case "agent_orchestration":
|
|
// 圆桌特有的「派活」工具,主聊天没有;用扳手图标承载。
|
|
return { icon: WrenchIcon, label: "派活给子智能体" };
|
|
default:
|
|
return { icon: WrenchIcon, label: `使用 “${toolName}” 工具` };
|
|
}
|
|
}
|
|
if (phase === "streaming") {
|
|
// 主聊天用 LightbulbIcon 表示「正在思考」;我们沿用同一图标。
|
|
return { icon: LightbulbIcon, label: fallbackLabel };
|
|
}
|
|
// connecting / connected / tool_result / 其它 — 一律用小点 + fallback 文案
|
|
return { icon: DotIcon, label: fallbackLabel };
|
|
}
|