"use client"; import { useState, useEffect } from "react"; import { ChevronDown, ChevronRight, Filter, Copy, Check, Loader2 } from "lucide-react"; import type { RawTrace } from "@/lib/store"; import { getRawTraces } from "@/lib/api"; interface Props { // 会话级模式:传 sessionId,组件内部自行拉取全部 traces sessionId?: string; // 消息级模式:直接传 rawTraces(向后兼容) rawTraces?: RawTrace[]; // 默认是否展开(固定入口用) defaultExpanded?: boolean; } type FilterType = "all" | "stop" | "length" | "tool_calls" | "unknown"; export default function RawTraceExplorer({ sessionId, rawTraces: propRawTraces, defaultExpanded = false }: Props) { const [expanded, setExpanded] = useState(defaultExpanded); const [filter, setFilter] = useState("all"); const [selectedTraceId, setSelectedTraceId] = useState(null); const [expandedSections, setExpandedSections] = useState>({}); const [copiedId, setCopiedId] = useState(null); // 会话级模式:自行拉取 traces const [fetchedTraces, setFetchedTraces] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 当传入 sessionId 时,自动拉取该会话的全部 traces useEffect(() => { if (!sessionId) return; setLoading(true); setError(null); getRawTraces(sessionId, 50) // 拉取最近 50 条 .then((data) => { setFetchedTraces(data.traces || []); }) .catch((err) => { setError(err.message || "拉取报文失败"); setFetchedTraces([]); }) .finally(() => { setLoading(false); }); }, [sessionId]); // 优先使用传入的 rawTraces,否则用拉取的 const rawTraces = propRawTraces || fetchedTraces; // 如果没有 sessionId 也没有 rawTraces,不显示(向后兼容) if (!sessionId && (!propRawTraces || propRawTraces.length === 0)) return null; // 获取 finish_reason,向后兼容 stop_reason const getFinishReason = (trace: RawTrace): string => { return trace.finish_reason || trace.stop_reason || "unknown"; }; // 按 finish_reason 分类统计 const counts = { all: rawTraces.length, stop: rawTraces.filter((t) => getFinishReason(t) === "stop").length, length: rawTraces.filter((t) => getFinishReason(t) === "length").length, tool_calls: rawTraces.filter((t) => getFinishReason(t) === "tool_calls").length, unknown: rawTraces.filter((t) => getFinishReason(t) === "unknown").length, }; // 过滤后的 traces const filteredTraces = filter === "all" ? rawTraces : rawTraces.filter((t) => getFinishReason(t) === filter); const handleCopy = async (text: string, id: string) => { try { await navigator.clipboard.writeText(text); setCopiedId(id); setTimeout(() => setCopiedId(null), 2000); } catch (err) { console.error("复制失败:", err); } }; return (
{/* 主标题 */} {expanded && (
{/* 加载中 / 错误状态 */} {loading ? (
正在拉取报文...
) : error ? (
❌ {error}
) : rawTraces.length === 0 ? (
暂无报文记录
) : ( <> {/* 筛选标签 */}
筛选: setFilter("all")} /> setFilter("stop")} /> setFilter("length")} /> setFilter("tool_calls")} /> setFilter("unknown")} />
{/* Trace 列表 */}
{filteredTraces.length === 0 ? (
无匹配的报文记录
) : ( filteredTraces.map((trace, idx) => { const finishReason = getFinishReason(trace); const isSelected = selectedTraceId === trace.trace_id; const hasError = !!trace.error; // 提取响应文本摘要 let responsePreview = ""; if (trace.response?.generations?.[0]?.text) { responsePreview = trace.response.generations[0].text.slice(0, 50); } // 提取工具调用信息 let toolNames: string[] = []; if (trace.response?.generations?.[0]?.message?.tool_calls) { toolNames = trace.response.generations[0].message.tool_calls.map( (tc: any) => tc.name || tc.function?.name || "unknown" ); } return (
{/* Trace 摘要行 */} {/* 完整 Trajectory 展开 */} {isSelected && ( )}
); }) )}
)}
)}
); } /** 筛选按钮 */ function FilterButton({ label, count, active, color, onClick, }: { label: string; count: number; active: boolean; color: "gray" | "green" | "orange" | "blue"; onClick: () => void; }) { const colorClasses = { gray: active ? "bg-gray-200 text-gray-800 border-gray-300" : "bg-gray-50 text-gray-600 border-gray-200", green: active ? "bg-green-100 text-green-800 border-green-300" : "bg-green-50 text-green-600 border-green-200", orange: active ? "bg-orange-100 text-orange-800 border-orange-300" : "bg-orange-50 text-orange-600 border-orange-200", blue: active ? "bg-blue-100 text-blue-800 border-blue-300" : "bg-blue-50 text-blue-600 border-blue-200", }; return ( ); } /** finish_reason 彩色小标签 */ function FinishReasonBadge({ reason }: { reason: string }) { const colorClasses = { stop: "bg-green-100 text-green-700 border-green-200", length: "bg-orange-100 text-orange-700 border-orange-200", tool_calls: "bg-blue-100 text-blue-700 border-blue-200", unknown: "bg-gray-100 text-gray-600 border-gray-200", }; const colorClass = colorClasses[reason as keyof typeof colorClasses] || colorClasses.unknown; return ( {reason} ); } /** 完整 Trajectory 视图(请求→响应→工具调用→usage) */ function TrajectoryView({ trace, expandedSections, setExpandedSections, onCopy, copiedId, }: { trace: RawTrace; expandedSections: Record; setExpandedSections: React.Dispatch>>; onCopy: (text: string, id: string) => void; copiedId: string | null; }) { // 提取响应文本 let responseText = ""; let reasoningContent = ""; if (trace.response?.generations?.[0]) { responseText = trace.response.generations[0].text || ""; // 尝试提取 reasoning_content const msg = trace.response.generations[0].message; if (msg?.additional_kwargs?.reasoning_content) { reasoningContent = msg.additional_kwargs.reasoning_content; } } // 提取工具调用 let toolCalls: any[] = []; if (trace.response?.generations?.[0]?.message?.tool_calls) { toolCalls = trace.response.generations[0].message.tool_calls; } return (
{/* 时间线可视化 */}
完整路径: 用户输入 → LLM 请求 {toolCalls.length > 0 && ( <> → [工具调用] )} → LLM 响应
{/* 错误信息 */} {trace.error && (
❌ 错误
            {trace.error.type}: {trace.error.message}
          
)} {/* 1. 请求 messages */} onCopy( JSON.stringify(trace.request.messages, null, 2), `req-messages-${trace.trace_id}` ) } copied={copiedId === `req-messages-${trace.trace_id}`} >
{trace.request.messages.map((msg: any, idx: number) => (
{msg.type || msg.role || "message"} {idx + 1}
                {typeof msg.content === "string"
                  ? msg.content
                  : JSON.stringify(msg.content, null, 2)}
              
))}
{/* 2. 模型响应文本 */} {(responseText || reasoningContent) && ( onCopy( reasoningContent ? `[Reasoning]\n${reasoningContent}\n\n[Response]\n${responseText}` : responseText, `response-text-${trace.trace_id}` ) } copied={copiedId === `response-text-${trace.trace_id}`} >
{reasoningContent && (
🧠 Reasoning
                  {reasoningContent}
                
)}
📝 最终文本
                {responseText}
              
)} {/* 3. 工具调用 */} {toolCalls.length > 0 && ( onCopy( JSON.stringify(toolCalls, null, 2), `tool-calls-${trace.trace_id}` ) } copied={copiedId === `tool-calls-${trace.trace_id}`} >
{toolCalls.map((tc: any, idx: number) => (
🔧 {tc.name || tc.function?.name || `Tool ${idx + 1}`}
参数:
                  {JSON.stringify(
                    tc.args || tc.function?.arguments || tc,
                    null,
                    2
                  )}
                
))}
)} {/* 4. Usage 摘要 */} {trace.usage && (
📊 Token 使用情况
输入: {trace.usage.input_tokens} 输出: {trace.usage.output_tokens} 总计: {trace.usage.total_tokens}
)} {/* 完整原始 JSON(可选) */} onCopy(JSON.stringify(trace, null, 2), `full-json-${trace.trace_id}`) } copied={copiedId === `full-json-${trace.trace_id}`} defaultClosed >
          {JSON.stringify(trace, null, 2)}
        
); } /** 可折叠区域 */ function CollapsibleSection({ title, id, children, expanded, setExpanded, onCopy, copied, defaultClosed = false, }: { title: string; id: string; children: React.ReactNode; expanded: Record; setExpanded: React.Dispatch>>; onCopy: () => void; copied: boolean; defaultClosed?: boolean; }) { const isOpen = expanded[id] ?? !defaultClosed; return (
{isOpen &&
{children}
}
); }