feat: MetonaAI Desktop 初始项目
- 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 导入导致黑屏
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* TokenUsage — Token 统计图表
|
||||
*
|
||||
* 进度条可视化 + 数值显示。
|
||||
*/
|
||||
|
||||
import { Box, Typography, Stack, LinearProgress } from '@mui/material';
|
||||
import { Zap } from 'lucide-react';
|
||||
import { useAgentStore } from '@renderer/stores/agent-store';
|
||||
import { formatTokens } from '@renderer/lib/formatters';
|
||||
import { cn } from '@renderer/lib/cn';
|
||||
|
||||
export function TokenUsage(): React.JSX.Element {
|
||||
const tokenUsage = useAgentStore((s) => s.tokenUsage);
|
||||
const maxIterations = useAgentStore((s) => s.maxIterations);
|
||||
const currentIteration = useAgentStore((s) => s.currentIteration);
|
||||
|
||||
if (tokenUsage.totalTokens === 0) {
|
||||
return (
|
||||
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1.5 }}>
|
||||
<Zap size={14} style={{ color: '#fbbf24' }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1, color: 'text.secondary' }}>Token 用量</Typography>
|
||||
</Stack>
|
||||
<Typography variant="caption" sx={{ textAlign: 'center', py: 1, display: 'block', color: 'text.disabled' }}>等待 Agent 活动...</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const maxTokens = 128_000;
|
||||
const usagePercent = Math.min((tokenUsage.totalTokens / maxTokens) * 100, 100);
|
||||
const inputPercent = tokenUsage.totalTokens > 0 ? (tokenUsage.inputTokens / tokenUsage.totalTokens) * 100 : 0;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||
{/* 标题 */}
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1.5 }}>
|
||||
<Zap size={14} style={{ color: '#fbbf24' }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1, color: 'text.secondary' }}>Token 用量</Typography>
|
||||
</Stack>
|
||||
|
||||
{/* 进度条 */}
|
||||
<Box sx={{ width: '100%', height: 6, borderRadius: 3, overflow: 'hidden', mb: 2, bgcolor: 'action.hover', display: 'flex' }}>
|
||||
<Box sx={{ height: '100%', width: `${inputPercent}%`, bgcolor: '#22d3ee', transition: 'width 300ms', borderRadius: '3px 0 0 3px' }} />
|
||||
<Box sx={{ height: '100%', width: `${100 - inputPercent}%`, bgcolor: '#a855f7', transition: 'width 300ms', borderRadius: '0 3px 3px 0' }} />
|
||||
</Box>
|
||||
|
||||
{/* 数值网格 */}
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5, mb: 1.5 }}>
|
||||
<StatCard label="输入" value={formatTokens(tokenUsage.inputTokens)} color="#22d3ee" />
|
||||
<StatCard label="输出" value={formatTokens(tokenUsage.outputTokens)} color="#a855f7" />
|
||||
</Box>
|
||||
|
||||
{/* 底部汇总 */}
|
||||
<Stack spacing={0.5}>
|
||||
<SummaryRow label="总计" value={formatTokens(tokenUsage.totalTokens)} highlight />
|
||||
<SummaryRow label="上下文" value={`${usagePercent.toFixed(1)}%`}
|
||||
color={usagePercent > 80 ? 'error.main' : usagePercent > 60 ? 'warning.main' : 'text.secondary'} />
|
||||
<SummaryRow label="迭代" value={`${currentIteration} / ${maxIterations}`} />
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color }: { label: string; value: string; color: string }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, px: 1.5, py: 1, borderRadius: 1.5, bgcolor: 'action.hover' }}>
|
||||
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: color, flexShrink: 0 }} />
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: 11 }}>{label}</Typography>
|
||||
<Typography variant="caption" sx={{ ml: 'auto', fontFamily: 'monospace', fontWeight: 600, color: 'text.primary', fontSize: 12 }}>{value}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryRow({ label, value, highlight, color }: { label: string; value: string; highlight?: boolean; color?: string }) {
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ py: 0.25 }}>
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: 11 }}>{label}</Typography>
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: highlight ? 600 : 400, color: color ?? (highlight ? 'text.primary' : 'text.secondary'), fontSize: highlight ? 12 : 11 }}>{value}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* TraceViewer — Trace 时间轴查看器
|
||||
*/
|
||||
|
||||
import { Box, Typography, Stack } from '@mui/material';
|
||||
import { Activity } from 'lucide-react';
|
||||
import { useAgentStore } from '@renderer/stores/agent-store';
|
||||
import { TraceStep } from './TraceStep';
|
||||
import { formatTokens } from '@renderer/lib/formatters';
|
||||
|
||||
export function TraceViewer(): React.JSX.Element {
|
||||
const traceSteps = useAgentStore((s) => s.traceSteps);
|
||||
const tokenUsage = useAgentStore((s) => s.tokenUsage);
|
||||
const agentStatus = useAgentStore((s) => s.agentStatus);
|
||||
const provider = useAgentStore((s) => s.provider);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1.5 }}>
|
||||
<Activity size={14} style={{ color: '#818cf8' }} />
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1, color: 'text.secondary' }}>
|
||||
Trace Viewer
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
{traceSteps.length === 0 ? (
|
||||
<Typography variant="caption" sx={{ textAlign: 'center', py: 2, color: 'text.secondary' }}>等待 Agent 活动...</Typography>
|
||||
) : traceSteps.map((step, i) => (
|
||||
<TraceStep key={`${step.iteration}-${step.state}-${i}`} step={step} isCurrent={i === traceSteps.length - 1 && agentStatus !== 'idle'} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{tokenUsage.totalTokens > 0 && (
|
||||
<Box sx={{ mt: 1.5, pt: 1.5, borderTop: 1, borderColor: 'divider', fontSize: 11, color: 'text.secondary' }}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<span>Token:</span>
|
||||
<span>入 {formatTokens(tokenUsage.inputTokens)} | 出 {formatTokens(tokenUsage.outputTokens)} | 总 {formatTokens(tokenUsage.totalTokens)}</span>
|
||||
</Stack>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<span>Provider:</span><span>{provider}</span>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user