/** * Chat Message Component */ import { User, Bot, Copy, Check, Wrench, ChevronDown, ChevronRight, Clock, AlertCircle, CheckCircle2, Loader2, GitCompare, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useState, forwardRef } from 'react'; import { cn } from '../utils/cn'; import { fadeInUp, smoothTransition } from '../utils/animations'; import { getAgentDisplayName } from '../utils/agent'; import { Markdown } from './Markdown'; import { FileMentionText } from './FileMentionTag'; import type { Message, ToolCallInfo, ToolStatus, ToolMessagePart, QuestionMessagePart, FileDiffInfo } from '../api/types.js'; import { AskUserQuestion } from './AskUserQuestion.js'; interface ChatMessageProps { message: Message; /** 是否为流式输出中(显示打字光标) */ isStreaming?: boolean; /** 回答问题的回调(用于 ask_user_question 工具) */ onAnswerQuestion?: (questionPartId: string, answers: string[]) => void; /** 查看文件 Diff 的回调 */ onViewDiff?: (diff: FileDiffInfo) => void; } export const ChatMessage = forwardRef( ({ message, isStreaming = false, onAnswerQuestion, onViewDiff }, ref) => { const isUser = message.role === 'user'; const [copied, setCopied] = useState(false); const handleCopy = async () => { await navigator.clipboard.writeText(message.content ?? ''); setCopied(true); setTimeout(() => setCopied(false), 2000); }; // 渲染消息内容 - 使用 parts 保持原始顺序 const renderContent = () => { // 优先使用 parts 数组(保持原始顺序) if (message.parts && message.parts.length > 0) { // 查找最后一个文本 part 的索引(用于显示打字光标) let lastTextPartIndex = -1; if (isStreaming) { for (let i = message.parts.length - 1; i >= 0; i--) { if (message.parts[i].type === 'text') { lastTextPartIndex = i; break; } } } return (
{message.parts.map((part, index) => { switch (part.type) { case 'text': if (!part.text && index !== lastTextPartIndex) return null; return isUser ? (
) : (
{/* 流式输出时在最后一个文本末尾显示打字光标 */} {isStreaming && index === lastTextPartIndex && ( )}
); case 'tool': return ; case 'question': { // 问题组件:即使在流式输出时也允许用户回答(除非已回答) const questionPart = part as QuestionMessagePart; return ( onAnswerQuestion(part.id, answers) : undefined } disabled={questionPart.answered} /> ); } case 'reasoning': return (
{part.text}
); default: return null; } })}
); } // 回退:使用旧的 content + toolCalls 字段 return ( <> {!isUser && message.toolCalls && message.toolCalls.length > 0 && ( )}
{isUser ? (
) : ( )}
); }; return (
{isUser ? : }
{isUser ? 'You' : getAgentDisplayName(message.metadata?.agentName)}
{renderContent()}
); } ); interface StreamingMessageProps { content: string; /** 当前 Agent 名称 */ agentName?: string; } export function StreamingMessage({ content, agentName }: StreamingMessageProps) { return (
{getAgentDisplayName(agentName)}
); } interface TypingIndicatorProps { /** 当前 Agent 名称 */ agentName?: string; } export function TypingIndicator({ agentName }: TypingIndicatorProps) { return (
{getAgentDisplayName(agentName)}
{[0, 1, 2].map((i) => ( ))}
); } // ============ 工具调用显示组件 ============ /** * 单个工具 Part 项(用于 parts 数组渲染) */ interface ToolPartItemProps { part: ToolMessagePart; /** 查看文件 Diff 的回调 */ onViewDiff?: (diff: FileDiffInfo) => void; } function ToolPartItem({ part, onViewDiff }: ToolPartItemProps) { const [expanded, setExpanded] = useState(false); const hasDetails = Object.keys(part.arguments).length > 0 || part.result !== undefined || part.error !== undefined; // 判断是否为文件操作工具且有 diff 数据 const isFileOperation = part.toolName === 'write_file' || part.toolName === 'edit_file'; const hasDiff = isFileOperation && part.fileDiff && part.status === 'completed'; return (
{/* 头部:工具名称、状态、时长 */} {/* 展开的详情 */} {expanded && hasDetails && (
{/* 参数 */} {Object.keys(part.arguments).length > 0 && (
Arguments:
                    {JSON.stringify(part.arguments, null, 2)}
                  
)} {/* 结果 */} {part.result !== undefined && (
Result:
                    {typeof part.result === 'string'
                      ? part.result
                      : JSON.stringify(part.result, null, 2)}
                  
)} {/* 错误 */} {part.error && (
Error:
                    {part.error}
                  
)}
)}
); } /** * 获取工具状态图标 */ function getStatusIcon(status: ToolStatus) { switch (status) { case 'pending': return ; case 'running': return ; case 'completed': return ; case 'error': return ; } } /** * 格式化执行时长 */ function formatDuration(ms?: number): string { if (!ms) return ''; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; } /** * 工具调用列表容器 */ interface ToolCallsDisplayProps { toolCalls: ToolCallInfo[]; } function ToolCallsDisplay({ toolCalls }: ToolCallsDisplayProps) { return (
{toolCalls.map((toolCall) => ( ))}
); } /** * 单个工具调用项 */ interface ToolCallItemProps { toolCall: ToolCallInfo; } function ToolCallItem({ toolCall }: ToolCallItemProps) { const [expanded, setExpanded] = useState(false); const hasDetails = Object.keys(toolCall.arguments).length > 0 || toolCall.result !== undefined || toolCall.error !== undefined; return (
{/* 头部:工具名称、状态、时长 */} {/* 展开的详情 */} {expanded && hasDetails && (
{/* 参数 */} {Object.keys(toolCall.arguments).length > 0 && (
Arguments:
                    {JSON.stringify(toolCall.arguments, null, 2)}
                  
)} {/* 结果 */} {toolCall.result !== undefined && (
Result:
                    {typeof toolCall.result === 'string'
                      ? toolCall.result
                      : JSON.stringify(toolCall.result, null, 2)}
                  
)} {/* 错误 */} {toolCall.error && (
Error:
                    {toolCall.error}
                  
)}
)}
); }