diff --git a/src/components/agent/chat/components/AgentThreadTimeline.tsx b/src/components/agent/chat/components/AgentThreadTimeline.tsx index 9d947f9d3..9d8fdf0f9 100644 --- a/src/components/agent/chat/components/AgentThreadTimeline.tsx +++ b/src/components/agent/chat/components/AgentThreadTimeline.tsx @@ -58,6 +58,7 @@ import { resolveTimelineArtifactNavigation, type ArtifactTimelineOpenTarget, } from "../utils/artifactTimelineNavigation"; +import { TimelineInlineItem } from "./TimelineInlineItem"; interface AgentThreadTimelineProps { turn: AgentThreadTurn; diff --git a/src/components/agent/chat/components/TimelineFlowDemo.tsx b/src/components/agent/chat/components/TimelineFlowDemo.tsx new file mode 100644 index 000000000..116917dbb --- /dev/null +++ b/src/components/agent/chat/components/TimelineFlowDemo.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { TimelineInlineItem } from "./TimelineInlineItem"; +import type { AgentThreadItem } from "@/lib/api/agentProtocol"; + +interface TimelineFlowDemoProps { + items: AgentThreadItem[]; +} + +/** + * 时间线流式展示演示组件 + * + * 这是一个简化的演示,展示新的时间线 + 内联操作设计 + */ +export function TimelineFlowDemo({ items }: TimelineFlowDemoProps) { + // 过滤出需要在时间线上显示的项目 + const timelineItems = items.filter( + (item) => + item.type === "tool_call" || + item.type === "command_execution" || + item.type === "web_search" + ); + + // 分组:将 Agent 消息和工具调用交错显示 + const groupedItems: Array<{ type: "text" | "timeline"; content: any }> = []; + + items.forEach((item, index) => { + if (item.type === "agent_message") { + groupedItems.push({ + type: "text", + content: item, + }); + } else if ( + item.type === "tool_call" || + item.type === "command_execution" || + item.type === "web_search" + ) { + // 检查是否已经有一个 timeline 组 + const lastGroup = groupedItems[groupedItems.length - 1]; + if (lastGroup && lastGroup.type === "timeline") { + lastGroup.content.push(item); + } else { + groupedItems.push({ + type: "timeline", + content: [item], + }); + } + } + }); + + return ( +
+ {groupedItems.map((group, groupIndex) => { + if (group.type === "text") { + // Agent 文本消息 + const item = group.content; + return ( +
+ {item.text} +
+ ); + } else { + // 时间线组 + const timelineItems = group.content as AgentThreadItem[]; + return ( +
+ {timelineItems.map((item, itemIndex) => ( + + ))} +
+ ); + } + })} +
+ ); +} diff --git a/src/components/agent/chat/components/TimelineInlineItem.tsx b/src/components/agent/chat/components/TimelineInlineItem.tsx new file mode 100644 index 000000000..cebf0e508 --- /dev/null +++ b/src/components/agent/chat/components/TimelineInlineItem.tsx @@ -0,0 +1,287 @@ +import React, { useState } from "react"; +import { + ChevronDown, + ChevronUp, + FileText, + Globe, + Loader2, + Search, + TerminalSquare, + Edit3, + FileEdit, + AlertTriangle, + CheckCircle2, + XCircle, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; +import type { + AgentThreadItem, + AgentThreadToolCallItem, + AgentThreadCommandExecutionItem, + AgentThreadWebSearchItem, +} from "@/lib/api/agentProtocol"; + +interface TimelineInlineItemProps { + item: AgentThreadItem; + isLast?: boolean; +} + +/** + * 类型守卫:检查是否为工具调用类型 + */ +function isToolCallItem(item: AgentThreadItem): item is AgentThreadToolCallItem { + return item.type === "tool_call"; +} + +function isCommandExecutionItem(item: AgentThreadItem): item is AgentThreadCommandExecutionItem { + return item.type === "command_execution"; +} + +function isWebSearchItem(item: AgentThreadItem): item is AgentThreadWebSearchItem { + return item.type === "web_search"; +} + +/** + * 获取工具调用的图标 + */ +function getToolIcon(toolName: string) { + const normalized = toolName.toLowerCase(); + + if (normalized.includes("read") || normalized.includes("file")) { + return FileText; + } + if (normalized.includes("bash") || normalized.includes("command")) { + return TerminalSquare; + } + if (normalized.includes("web") || normalized.includes("fetch")) { + return Globe; + } + if (normalized.includes("search") || normalized.includes("grep")) { + return Search; + } + if (normalized.includes("edit")) { + return Edit3; + } + if (normalized.includes("write")) { + return FileEdit; + } + + return TerminalSquare; +} + +/** + * 获取状态图标 + */ +function getStatusIcon(status: string) { + if (status === "running" || status === "pending") { + return Loader2; + } + if (status === "completed" || status === "success") { + return CheckCircle2; + } + if (status === "failed" || status === "error") { + return XCircle; + } + return CheckCircle2; +} + +/** + * 格式化工具调用的标题 + */ +function formatToolCallTitle(item: AgentThreadItem): string { + // 命令执行 + if (isCommandExecutionItem(item)) { + const cmd = item.command.trim(); + const shortCmd = cmd.length > 50 ? cmd.slice(0, 50) + "..." : cmd; + return `执行命令 ${shortCmd}`; + } + + // Web 搜索 + if (isWebSearchItem(item)) { + return item.query ? `搜索 ${item.query}` : "Web 搜索"; + } + + // 工具调用 + if (isToolCallItem(item)) { + const toolName = item.tool_name || "操作"; + const args = item.arguments; + + // 尝试提取有意义的参数 + if (args && typeof args === "object" && args !== null) { + // Read 工具:显示文件路径 + if ("file_path" in args && typeof args.file_path === "string") { + const fileName = args.file_path.split("/").pop() || args.file_path; + return `查看 ${fileName}`; + } + + // Bash 工具:显示命令 + if ("command" in args && typeof args.command === "string") { + const cmd = args.command.trim(); + const shortCmd = cmd.length > 50 ? cmd.slice(0, 50) + "..." : cmd; + return `执行命令 ${shortCmd}`; + } + + // WebFetch 工具:显示 URL + if ("url" in args && typeof args.url === "string") { + return `访问 ${args.url}`; + } + + // Grep 工具:显示搜索模式 + if ("pattern" in args && typeof args.pattern === "string") { + return `搜索 ${args.pattern}`; + } + + // Edit 工具:显示文件路径 + if ("file_path" in args && typeof args.file_path === "string") { + const fileName = args.file_path.split("/").pop() || args.file_path; + return `编辑 ${fileName}`; + } + + // Write 工具:显示文件路径 + if ("file_path" in args && typeof args.file_path === "string") { + const fileName = args.file_path.split("/").pop() || args.file_path; + return `写入 ${fileName}`; + } + } + + return toolName; + } + + return "操作"; +} + +/** + * 截断长文本 + */ +function truncateText(text: string, maxLines: number = 3): { preview: string; hasMore: boolean } { + const lines = text.split("\n"); + + if (lines.length <= maxLines) { + return { preview: text, hasMore: false }; + } + + const preview = lines.slice(0, maxLines).join("\n"); + return { preview, hasMore: true }; +} + +export function TimelineInlineItem({ item, isLast }: TimelineInlineItemProps) { + const [isExpanded, setIsExpanded] = useState(false); + + // 获取工具名称 + let toolName = ""; + if (isToolCallItem(item)) { + toolName = item.tool_name; + } else if (isCommandExecutionItem(item)) { + toolName = "Bash"; + } else if (isWebSearchItem(item)) { + toolName = "WebSearch"; + } + + const ToolIcon = getToolIcon(toolName); + const StatusIcon = getStatusIcon(item.status); + const title = formatToolCallTitle(item); + + const isRunning = item.status === "in_progress"; + const isFailed = item.status === "failed"; + + // 获取输出内容 + let output = ""; + if (isToolCallItem(item)) { + output = item.output || item.error || ""; + } else if (isCommandExecutionItem(item)) { + output = item.aggregated_output || item.error || ""; + } else if (isWebSearchItem(item)) { + output = item.output || ""; + } + + const { preview, hasMore } = truncateText(output, 3); + + // 默认展开失败的工具调用 + const shouldDefaultExpand = isFailed; + + return ( +
+ {/* 左侧时间线 */} +
+ {/* 状态图标 */} +
+ +
+ + {/* 连接线 */} + {!isLast && ( +
+ )} +
+ + {/* 右侧内容 */} +
+ + {/* 标题行 */} +
+ + {title} + + {isRunning && ( + 正在执行... + )} +
+ + {/* 输出内容 */} + {output && ( +
+ {/* 预览 */} +
+
+                  {shouldDefaultExpand || isExpanded ? output : preview}
+                
+
+ + {/* 展开/收起按钮 */} + {hasMore && !shouldDefaultExpand && ( + + + + )} +
+ )} +
+
+
+ ); +}