- Electron + React + TypeScript 架构 - 三栏布局: Sidebar | ChatPanel | DetailPanel - 9 个内置工具 (文件系统/网络/记忆/命令) - SQLite 持久化 (better-sqlite3) - MUI 暗色/亮色主题系统 - Agent Loop ReAct 状态机引擎 - DeepSeek / Agnes AI / Ollama Provider 适配器 - MCP 协议集成 - 系统托盘 + 全局快捷键 - Tailwind CSS v4 + Tailwind Merge - 修复: Sidebar 缺失 TextField 导入导致黑屏
49 lines
3.1 KiB
TypeScript
49 lines
3.1 KiB
TypeScript
/**
|
|
* TraceStep — 单个 Trace 步骤
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import { Box, Typography, IconButton, Collapse, Stack } from '@mui/material';
|
|
import { ChevronDown, ChevronRight, CircleDot, CheckCircle, Loader2 } from 'lucide-react';
|
|
import { TRACE_STATE_COLORS, TRACE_STATE_LABELS } from '@renderer/lib/constants';
|
|
import { formatDuration, formatTokens } from '@renderer/lib/formatters';
|
|
import type { TraceStep as TraceStepType } from '@renderer/stores/agent-store';
|
|
|
|
interface TraceStepProps { step: TraceStepType; isCurrent?: boolean; }
|
|
|
|
export function TraceStep({ step, isCurrent }: TraceStepProps): React.JSX.Element {
|
|
const [expanded, setExpanded] = useState(false);
|
|
const color = TRACE_STATE_COLORS[step.state] ?? '#8b8fa7';
|
|
const label = TRACE_STATE_LABELS[step.state] ?? step.state;
|
|
const duration = step.completedAt ? step.completedAt - step.startedAt : null;
|
|
|
|
return (
|
|
<Box sx={{ borderRadius: 1, border: '1px solid', borderColor: 'divider', bgcolor: 'secondary.main', transition: 'all 150ms', ...(isCurrent ? { boxShadow: `0 0 0 1px ${color}` } : {}) }}>
|
|
<IconButton size="small" onClick={() => setExpanded(!expanded)} sx={{ width: '100%', justifyContent: 'flex-start', gap: 1, px: 1.5, py: 1, borderRadius: '4px 4px 0 0', color: 'text.primary', fontSize: 12 }}>
|
|
{expanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
{isCurrent ? <Loader2 size={12} style={{ color, animation: 'spin 1s linear infinite' }} /> : step.completedAt ? <CheckCircle size={12} style={{ color }} /> : <CircleDot size={12} style={{ color }} />}
|
|
<Typography component="span" sx={{ fontWeight: 600, color, fontSize: 12 }}>#{step.iteration}</Typography>
|
|
<Typography component="span" sx={{ textTransform: 'uppercase', color: 'text.secondary', fontSize: 12 }}>{label}</Typography>
|
|
{duration != null && <Typography variant="caption" sx={{ ml: 'auto', color: 'text.secondary' }}>{formatDuration(duration)}</Typography>}
|
|
{step.tokenUsage && <Typography variant="caption" sx={{ color: 'text.secondary' }}>{formatTokens(step.tokenUsage.totalTokens)} tok</Typography>}
|
|
</IconButton>
|
|
<Collapse in={expanded}>
|
|
<Box sx={{ px: 1.5, pb: 1.5, borderTop: 1, borderColor: 'divider' }}>
|
|
{step.thought && (
|
|
<Box sx={{ mt: 1 }}>
|
|
<Typography variant="caption" sx={{ fontWeight: 600, color: 'warning.main', fontSize: 10 }}>💭 Thought</Typography>
|
|
<Box component="pre" sx={{ fontSize: 11, whiteSpace: 'pre-wrap', color: 'text.secondary', fontFamily: "'SF Mono',monospace", m: 0 }}>{step.thought}</Box>
|
|
</Box>
|
|
)}
|
|
{step.toolCalls?.map((tc) => (
|
|
<Box key={tc.id} sx={{ mt: 1 }}>
|
|
<Typography variant="caption" sx={{ fontWeight: 600, color: '#a855f7', fontSize: 10 }}>🔧 {tc.name}</Typography>
|
|
<Box component="pre" sx={{ fontSize: 10, overflowX: 'auto', color: 'text.secondary', fontFamily: "'SF Mono',monospace", m: 0 }}>{JSON.stringify(tc.args, null, 2)}</Box>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
</Collapse>
|
|
</Box>
|
|
);
|
|
}
|