Files
deerflow-code/frontend-web/src/roundtable-planning/lib/step-display.tsx
T

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 };
}