diff --git a/src/renderer/components/token-dashboard.ts b/src/renderer/components/token-dashboard.ts index 687b11a..8571ae9 100644 --- a/src/renderer/components/token-dashboard.ts +++ b/src/renderer/components/token-dashboard.ts @@ -239,43 +239,47 @@ function renderGlobalOverview(stats: AllSessionsTokenStats): void { `; } -/** 渲染全局柱状图(按会话) */ -function renderGlobalChart(stats: AllSessionsTokenStats): void { - const chartTitleEl = dashboardModalEl.querySelector('#tdChartTitle'); - if (chartTitleEl) chartTitleEl.textContent = '📈 Token 消耗趋势 — 按会话'; - - const chartEl = dashboardModalEl.querySelector('#tdChart')!; - const sessions = stats.sessions.filter(s => s.total_input > 0 || s.total_output > 0); - - if (sessions.length === 0) { - chartEl.innerHTML = '
暂无会话数据
'; +/** 渲染柱状图(通用,全局/会话复用) */ +function renderBarChart( + chartEl: HTMLElement, + data: Array<{ label: string; input: number; output: number; tooltip: string }>, + title: string, + note?: string, + savedScrollLeft?: number +): void { + if (data.length === 0) { + chartEl.innerHTML = '
暂无数据
'; return; } - // 最多显示 20 个会话 - const displaySessions = sessions.slice(0, 20); - const maxTokens = Math.max(...displaySessions.map(s => s.total_input + s.total_output), 1); + const maxTokens = Math.max(...data.map(d => d.input + d.output), 1); - const barsHtml = displaySessions.map(s => { - const total = s.total_input + s.total_output; - const heightPct = Math.max((total / maxTokens) * 100, 2); - const inputPct = total > 0 ? (s.total_input / total) * 100 : 0; - const title = s.title.length > 8 ? s.title.slice(0, 8) + '…' : s.title; - const tooltip = `${s.title}\n输入 ${formatTokenCount(s.total_input)} + 输出 ${formatTokenCount(s.total_output)} = ${formatTokenCount(total)} tokens\n${s.round_count} 轮 · ${formatDuration(s.total_duration)}`; + // 网格线:5 条水平线(0%, 25%, 50%, 75%, 100%) + const gridLines = [0, 0.25, 0.5, 0.75, 1].map(ratio => { + const val = Math.round(maxTokens * ratio); + return `
+ ${formatTokenCount(val)} +
`; + }).join(''); + + const barsHtml = data.map(d => { + const total = d.input + d.output; + const heightPct = Math.max((total / maxTokens) * 100, 1.5); + const inputPct = total > 0 ? (d.input / total) * 100 : 0; + const outputPct = 100 - inputPct; return ` -
-
- ${formatTokenCount(s.total_output)} - ${formatTokenCount(s.total_input)} -
-
-
-
-
-
+
+
+
+
+
-
${title}
+
+ ${formatTokenCount(d.output)} + ${formatTokenCount(d.input)} +
+
${d.label}
`; }).join(''); @@ -284,17 +288,19 @@ function renderGlobalChart(stats: AllSessionsTokenStats): void {
输出 Token 输入 Token - ${sessions.length > 20 ? `(仅显示最近 20 个会话)` : ''} + ${note ? `${note}` : ''}
-
-
- ${formatTokenCount(maxTokens)} - ${formatTokenCount(Math.round(maxTokens / 2))} - 0 -
+
+ ${gridLines}
${barsHtml}
`; + + // 恢复滚动位置 + if (savedScrollLeft && savedScrollLeft > 0) { + const newBarsEl = chartEl.querySelector('.td-chart-bars') as HTMLElement | null; + if (newBarsEl) newBarsEl.scrollLeft = savedScrollLeft; + } } /** 渲染全局明细表格 */ @@ -379,62 +385,22 @@ function renderSessionView(): void {
`; - // 柱状图标题 - const chartTitleEl = dashboardModalEl.querySelector('#tdChartTitle'); - if (chartTitleEl) chartTitleEl.textContent = '📈 Token 消耗趋势 — 按轮次'; - // 柱状图 const chartEl = dashboardModalEl.querySelector('#tdChart')!; const barsEl = chartEl.querySelector('.td-chart-bars') as HTMLElement | null; const savedScrollLeft = barsEl?.scrollLeft ?? 0; - if (rounds.length === 0) { - chartEl.innerHTML = '
暂无数据,发送消息后开始统计
'; - } else { - const maxTokens = Math.max(...rounds.map(r => r.eval_count + r.prompt_eval_count), 1); - const barsHtml = rounds.map(r => { - const total = r.eval_count + r.prompt_eval_count; - const heightPct = Math.max((total / maxTokens) * 100, 2); - const inputPct = total > 0 ? (r.prompt_eval_count / total) * 100 : 0; - const tooltip = `第${r.round}轮 · 输入 ${formatTokenCount(r.prompt_eval_count)} + 输出 ${formatTokenCount(r.eval_count)} = ${formatTokenCount(total)} tokens · ${formatDuration(r.total_duration)}`; - return ` -
-
- ${formatTokenCount(r.eval_count)} - ${formatTokenCount(r.prompt_eval_count)} -
-
-
-
-
-
-
-
-
R${r.round}
-
- `; - }).join(''); - chartEl.innerHTML = ` -
- 输出 Token - 输入 Token -
-
-
- ${formatTokenCount(maxTokens)} - ${formatTokenCount(Math.round(maxTokens / 2))} - 0 -
-
${barsHtml}
-
- `; - } + const chartData = rounds.map(r => ({ + label: `R${r.round}`, + input: r.prompt_eval_count, + output: r.eval_count, + tooltip: `第${r.round}轮 · 输入 ${formatTokenCount(r.prompt_eval_count)} + 输出 ${formatTokenCount(r.eval_count)} = ${formatTokenCount(r.eval_count + r.prompt_eval_count)} tokens · ${formatDuration(r.total_duration)}`, + })); - // 恢复滚动位置 - if (savedScrollLeft > 0) { - const newBarsEl = chartEl.querySelector('.td-chart-bars') as HTMLElement | null; - if (newBarsEl) newBarsEl.scrollLeft = savedScrollLeft; - } + const chartTitleEl = dashboardModalEl.querySelector('#tdChartTitle'); + if (chartTitleEl) chartTitleEl.textContent = '📈 Token 消耗趋势 — 按轮次'; + + renderBarChart(chartEl, chartData, '按轮次', undefined, savedScrollLeft); // 表格标题 const tableTitleEl = dashboardModalEl.querySelector('#tdTableTitle'); diff --git a/src/renderer/styles/style.css b/src/renderer/styles/style.css index aaa6250..cb54332 100644 --- a/src/renderer/styles/style.css +++ b/src/renderer/styles/style.css @@ -3615,102 +3615,115 @@ html, body { border-radius: var(--radius-md); padding: 16px; box-shadow: var(--shadow-card); - min-height: 180px; + min-height: 200px; } -.td-chart-container { - display: flex; - gap: 8px; - height: 180px; +.td-chart-grid { + position: relative; + height: 200px; + border-left: 1px solid var(--border-subtle); + border-bottom: 1px solid var(--border-subtle); + margin-left: 40px; + margin-bottom: 4px; } -.td-chart-y-axis { - display: flex; - flex-direction: column; - justify-content: space-between; - font-size: 10px; +/* 网格线 */ +.td-gridline { + position: absolute; + left: 0; + right: 0; + border-top: 1px dashed var(--border-subtle); + pointer-events: none; + opacity: 0.5; +} + +.td-gridline:first-child { + border-top: none; +} + +.td-gridline-label { + position: absolute; + right: calc(100% + 6px); + transform: translateY(-50%); + font-size: 9px; color: var(--text-tertiary); font-family: var(--font-mono); - padding: 2px 0; - min-width: 36px; - text-align: right; + white-space: nowrap; } .td-chart-bars { - flex: 1; + position: absolute; + inset: 0; display: flex; align-items: flex-end; - gap: 6px; - height: 180px; - border-left: 1px solid var(--border-subtle); - border-bottom: 1px solid var(--border-subtle); - padding: 0 8px 0 4px; + gap: 4px; + padding: 0 8px; overflow-x: auto; + scroll-behavior: smooth; } .td-bar-col { display: flex; flex-direction: column; align-items: center; - flex: 1; - min-width: 36px; - max-width: 60px; - height: 100%; + flex: 0 0 auto; + width: 40px; cursor: default; } -.td-bar-values { - display: flex; - flex-direction: column; - align-items: center; - gap: 1px; - margin-bottom: 2px; - font-size: 9px; - font-family: var(--font-mono); - white-space: nowrap; - flex-shrink: 0; -} - -.td-val-output { - color: #E8734A; -} - -.td-val-input { - color: #9B7ED8; -} - -.td-bar-wrapper { +.td-bar-area { flex: 1; width: 100%; display: flex; align-items: flex-end; } -.td-bar { +.td-bar-stack { width: 100%; - border-radius: 4px 4px 0 0; + border-radius: 3px 3px 0 0; overflow: hidden; - position: relative; - min-height: 2px; -} - -.td-bar-fill { - width: 100%; - height: 100%; - background: linear-gradient(180deg, #E8734A 0%, #F0976E 100%); - border-radius: 4px 4px 0 0; - transition: height 0.3s ease; display: flex; flex-direction: column-reverse; + transition: height 0.3s ease; } -.td-bar-input { +.td-bar-segment { width: 100%; - background: linear-gradient(180deg, #9B7ED8 0%, #B99BE5 100%); - border-radius: 4px 4px 0 0; - min-height: 0; + transition: height 0.2s ease; } +.td-bar-output { + background: linear-gradient(180deg, #E8734A 0%, #F0976E 100%); +} + +.td-bar-input-seg { + background: linear-gradient(180deg, #9B7ED8 0%, #B99BE5 100%); +} + +.td-bar-labels { + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; + margin-top: 3px; + font-size: 9px; + font-family: var(--font-mono); + white-space: nowrap; + flex-shrink: 0; +} + +.td-bar-label { + font-size: 10px; + color: var(--text-tertiary); + margin-top: 2px; + font-family: var(--font-mono); +} + +.td-bar-col:hover .td-bar-stack { + filter: brightness(1.1); +} + +/* 图例 */ .td-chart-legend { display: flex; gap: 16px; @@ -3739,15 +3752,13 @@ html, body { background: #9B7ED8; } -.td-bar-label { - font-size: 10px; - color: var(--text-tertiary); - margin-top: 4px; - font-family: var(--font-mono); +/* 柱子数值标签颜色 */ +.td-val-output { + color: #E8734A; } -.td-bar-col:hover .td-bar-fill { - background: linear-gradient(180deg, #D4623A 0%, #E8734A 100%); +.td-val-input { + color: #9B7ED8; } /* 空状态 */