refactor: 详情面板布局优化 — 固定高度 + 空值占位

- DetailPanel: TraceViewer flex:1 撑满, TokenUsage/AgentMonitor flexShrink:0 固定
- TraceViewer: 空内容时「等待 Agent 活动」居中撑满剩余空间
- TokenUsage: 始终展示完整结构(无会话时显示 -/0 而非隐藏)
- AgentMonitor: 固定 flexShrink:0 防止被挤压
This commit is contained in:
thzxx
2026-06-27 22:36:21 +08:00
parent 8a385b8e1a
commit a45b4c321b
4 changed files with 47 additions and 37 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ export function AgentMonitor(): React.JSX.Element {
);
return (
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider', flexShrink: 0 }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1.5 }}>
<Cpu size={14} style={{ color: '#818cf8' }} />
<Typography variant="caption" sx={{ fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1, color: 'text.secondary' }}>Agent </Typography>
+9 -3
View File
@@ -1,7 +1,7 @@
/**
* DetailPanel — 右侧详情面板
*
* 始终显示,包含 TraceViewerTokenUsageAgentMonitor。
* 布局策略:TraceViewer flex:1 撑满,TokenUsage/AgentMonitor flexShrink:0 固定
*/
import { Box } from '@mui/material';
@@ -12,8 +12,14 @@ import { LAYOUT } from '@renderer/lib/constants';
export function DetailPanel(): React.JSX.Element {
return (
<Box component="aside" sx={{ flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden', bgcolor: 'background.paper', borderLeft: 1, borderColor: 'divider', width: LAYOUT.DETAIL_WIDTH }}>
<Box sx={{ p: 1.5, flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Box
component="aside"
sx={{
flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden',
bgcolor: 'background.paper', borderLeft: 1, borderColor: 'divider', width: LAYOUT.DETAIL_WIDTH,
}}
>
<Box sx={{ p: 1.5, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0 }}>
<TraceViewer />
<TokenUsage />
<AgentMonitor />
+19 -25
View File
@@ -1,51 +1,45 @@
/**
* TokenUsage — Token 统计图表
* TokenUsage — Token 统计面板
*
* 进度条可视化 + 数值显示
* 始终展示完整结构,无会话时显示零值
* 固定在详情面板中部,高度由内容自然决定。
*/
import { Box, Typography, Stack, LinearProgress } from '@mui/material';
import { Box, Typography, Stack } 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;
const usagePercent = tokenUsage.totalTokens > 0
? Math.min((tokenUsage.totalTokens / maxTokens) * 100, 100)
: 0;
const inputPercent = tokenUsage.totalTokens > 0
? (tokenUsage.inputTokens / tokenUsage.totalTokens) * 100
: 0;
return (
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider' }}>
<Box sx={{ mt: 2, pt: 2, borderTop: 1, borderColor: 'divider', flexShrink: 0 }}>
{/* 标题 */}
<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>
<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 sx={{ height: '100%', width: `${Math.max(100 - inputPercent, 0)}%`, 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" />
@@ -53,9 +47,9 @@ export function TokenUsage(): React.JSX.Element {
{/* 底部汇总 */}
<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={tokenUsage.totalTokens > 0 ? formatTokens(tokenUsage.totalTokens) : '-'} highlight />
<SummaryRow label="上下文" value={tokenUsage.totalTokens > 0 ? `${usagePercent.toFixed(1)}%` : '-'}
color={usagePercent > 80 ? 'error.main' : usagePercent > 60 ? 'warning.main' : undefined} />
<SummaryRow label="迭代" value={`${currentIteration} / ${maxIterations}`} />
</Stack>
</Box>
+18 -8
View File
@@ -1,5 +1,7 @@
/**
* TraceViewer — Trace 时间轴查看器
* TraceViewer — ReAct 迭代追踪时间轴
*
* flex:1 撑满详情面板剩余空间,内容超出时内部滚动。
*/
import { Box, Typography, Stack } from '@mui/material';
@@ -12,20 +14,28 @@ export function TraceViewer(): React.JSX.Element {
const agentStatus = useAgentStore((s) => s.agentStatus);
return (
<Box sx={{ flexShrink: 0, display: 'flex', flexDirection: 'column' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1.5 }}>
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mb: 1.5, flexShrink: 0 }}>
<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={{ overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 0.5, maxHeight: 200 }}>
<Box sx={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: 0.5, minHeight: 0 }}>
{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'} />
))}
<Typography variant="caption" sx={{ textAlign: 'center', py: 2, color: 'text.disabled', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
Agent ...
</Typography>
) : (
traceSteps.map((step, i) => (
<TraceStep
key={`${step.iteration}-${step.state}-${i}`}
step={step}
isCurrent={i === traceSteps.length - 1 && agentStatus !== 'idle'}
/>
))
)}
</Box>
</Box>
);