fix(desktop): 优化用量历史图表配色、交互反馈与文字层级 - #4076
Conversation
Signed-off-by: kirozeng <kirozeng@gmail.com>
Signed-off-by: kirozeng <kirozeng@gmail.com>
Signed-off-by: kirozeng <kirozeng@gmail.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/components/settings/usage/UsageHistorySection.tsx | 汇总日期选择、稳定模型颜色顺序和两类图表的共享筛选路径,未发现行为缺陷。 |
| apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx | 恢复 12px 密集网格并加入独立图元放大与焦点指示,保留非交互分支和横向滚动。 |
| apps/desktop/src/renderer/components/settings/usage/UsageTokenBars.tsx | 将 30 根柱自适应到可用宽度,并统一模型配色、选中状态和键盘语义。 |
| apps/desktop/src/renderer/components/settings/usage/UsageDateFilter.tsx | 新增受 todayKey 约束、显式提交且与图表筛选同步的原生日期入口。 |
| apps/desktop/src/renderer/components/settings/usage/usageCharts.css | 集中实现悬停、焦点、选中淡化及减少动态效果规则,状态范围与设计规范一致。 |
| apps/desktop/src/renderer/themes/colors.ts | 注册用量热力图和五个模型类别的 Light/Dark 语义 Token,现有主题注册机制可正确解析。 |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
H[热力图日期格] --> S[handleDayClick]
B[每日 Token 柱] --> S
D[日期输入与提交] --> S
S --> R["day:YYYY-MM-DD 范围"]
R --> O[概览与明细重聚合]
R --> A[图表 aria-pressed 与视觉反馈]
F[固定图表历史窗口] --> H
F --> B
Reviews (1): Last reviewed commit: "fix(desktop): 按参考图微调用量配色并取消选中外框" | Re-trigger Greptile
本 PR 给 colors.ts 与冻结快照新增了 usage-heatmap-high 和 usage-model-1..5 共 6 个 token,但没跑 design-tokens 生成器,classification.json 仍停在 535 条,「磁盘上的影子层与内存生成字节一致」守卫在 Linux/Windows unit tests 的同一断言上红灯。 执行 pnpm --filter @cindy/design-tokens generate 再生成影子层: - snapshotCount 535 → 541,literal +5(usage-model-1..5)、alias +1 (usage-heatmap-high) - 不改任何颜色取值;reference/semantic/component 三层与现状字节一致 Signed-off-by: Kirozeng <kirozeng@gmail.com>
|
本轮修复:CI 红灯是 design-tokens 影子层没跟着新 token 再生成,已补上 失败在哪:Linux unit tests (1/2) 和 Windows unit tests (1/2) 挂在同一个断言——packages/design-tokens 的 classification.test.ts「磁盘上的影子层与内存生成字节一致」。verify 和那个 4 秒的 Windows unit tests 只是分片的聚合门,不是独立故障。两个平台红在同一处,不是 Windows 特有问题。 为什么挂:本 PR 给 colors.ts 和冻结快照 desktop-color-defaults.json 新增了 usage-heatmap-high 和 usage-model-1..5 共 6 个 token,但没有跑 design-tokens 生成器,classification.json 还停在 535 条,跟不上快照的 541 条。 修复(779f9f47b):跑 pnpm --filter @cindy/design-tokens generate 再生成影子层。snapshotCount 535→541,literal +5(usage-model-1..5)、alias +1(usage-heatmap-high)。不改任何颜色取值;reference/semantic/component 三层与之前字节一致。 证据:
CI 正在新 head 上重跑,等结果。 另外两条评论无需处理:Codex 那条是账号用量限额通知;Greptile 摘要结论 5/5、没有待办项。 dispatch:5259cbde02e1fa05 |
|
命中 UI 路径(apps/desktop/src/renderer/components/new-chat/UsageHeatmap.tsx / apps/desktop/src/renderer/components/settings/usage/UsageBreakdownTables.tsx / apps/desktop/src/renderer/components/settings/usage/UsageDateFilter.tsx 等)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范 |
|
本轮核实:无需代码改动,CI 已全绿 这条「Windows unit tests 缺席」的信号来自上轮推送后 CI 刚启动的瞬间——当时聚合门还没在新 head 上登记,不是故障。现在 run 34158598361 已出终局:
dispatch:24087dfa4bb0b004 |
|
本轮针对 UI 证据提醒:补一份可直接打开的改动后界面预览(HTML) 为什么用 HTML 预览:真实 macOS Electron 的 Light/Dark 截图与录屏此前已采集,但一直存放在本地忽略目录、尚未上传公开附件(PR 描述与 这份预览的构成(值全部来自 779f9f4 的源码,不是设计稿):
已验证(headless Chrome 实测计算样式,无脚本错误):
怎么用:把下面代码块存成 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PR #4076 · 用量历史图表改动后界面预览(Light / Dark)</title>
<style>
/* ============================================================
PR #4076 @ 779f9f47b 用量历史图表 · 界面效果预览
- token 取自 apps/desktop/src/renderer/themes/colors.ts(真实值;
usage-heatmap-high 按 colors.ts 的注册方式别名为 process-agent-task-icon)
- 图表 CSS 为 usageCharts.css 逐字嵌入(真实规则)
- 组件几何取自 UsageHeatmap / UsageTokenBars / UsageDateFilter /
UsageBreakdownTables(12px 热力格、3px 间隙、96px 柱高、2px 圆角等)
- 数据为合成 fixture(六组模型、150 天、约 13% 零值日),非真实用量
- 页面级控件(输入框/按钮圆角、概览卡)为近似还原,非像素级复刻
============================================================ */
/* motion tokens — apps/desktop/src/renderer/styles/globals.css 真实值 */
:root { --motion-fast: 150ms; --motion-base: 200ms; --motion-ease-out: cubic-bezier(0.16, 1, 0.3, 1); }
@media (prefers-reduced-motion: reduce) { :root { --motion-fast: 0ms; --motion-base: 0ms; } }
/* 主题 token — colors.ts 真实值(Light / Dark) */
.theme-light {
color-scheme: light;
--surface: #f8f8f6; --surface-elevated: #ffffff; --surface-chip: #e5e5e5;
--border-default: #d7d7d4; --text-primary: #262626; --text-secondary: #737373;
--text-tertiary: #a3a3a3; --focus-ring: #417CDD; --accent-emphasis: #262626;
--process-agent-task-icon: #2563EB;
--usage-heatmap-high: var(--process-agent-task-icon);
--usage-model-1: #14B8A6; --usage-model-2: #F43F5E; --usage-model-3: #8B5CF6;
--usage-model-4: #6366F1; --usage-model-5: #F59E0B;
}
.theme-dark {
color-scheme: dark;
--surface: #1f1f1e; --surface-elevated: #2c2c2a; --surface-chip: #3c3c3a;
--border-default: #3c3c3a; --text-primary: #d4d4d4; --text-secondary: #a3a3a3;
--text-tertiary: #737373; --focus-ring: #417CDD; --accent-emphasis: #d4d4d4;
--process-agent-task-icon: #60A5FA;
--usage-heatmap-high: var(--process-agent-task-icon);
--usage-model-1: #2DD4BF; --usage-model-2: #FB7185; --usage-model-3: #A78BFA;
--usage-model-4: #818CF8; --usage-model-5: #FBBF24;
}
/* ===== usageCharts.css @ 779f9f47b — 逐字嵌入 ===== */
.usage-history-charts {
--usage-heatmap-accent: var(--usage-heatmap-high);
--usage-mark-ease: cubic-bezier(0.2, 0.8, 0.25, 1.18);
}
.usage-history-charts .usage-chart-target { --usage-mark-grow: 0px; }
.usage-history-charts .usage-chart-target .usage-chart-mark { pointer-events: none; }
.usage-history-charts .usage-chart-mark {
transition-property: width, height, opacity;
transition-duration: var(--motion-fast);
transition-timing-function: var(--motion-ease-out);
}
.usage-history-charts .usage-chart-target:is(:focus-visible, [aria-pressed='true']) {
--usage-mark-grow: 2px; z-index: 1;
}
.usage-history-charts .usage-chart-target:is(:focus-visible, [aria-pressed='true']) .usage-chart-mark {
transition-duration: var(--motion-base);
transition-timing-function: var(--usage-mark-ease);
}
@media (hover: hover) {
.usage-history-charts .usage-chart-target:enabled:hover { --usage-mark-grow: 2px; z-index: 1; }
.usage-history-charts .usage-chart-target:enabled:hover .usage-chart-mark {
transition-duration: var(--motion-base);
transition-timing-function: var(--usage-mark-ease);
}
}
.usage-history-charts .usage-token-plot:has(.usage-chart-target[aria-pressed='true'])
.usage-chart-target:not([aria-pressed='true']):not(:focus-visible) .usage-chart-mark { opacity: 0.24; }
@media (hover: hover) {
.usage-history-charts .usage-token-plot:has(.usage-chart-target[aria-pressed='true'])
.usage-chart-target:enabled:hover .usage-chart-mark { opacity: 1; }
}
.usage-history-charts .usage-chart-target:focus-visible .usage-chart-indicator {
outline: 2px solid var(--focus-ring);
outline-offset: 1px;
}
@media (prefers-reduced-motion: reduce) {
.usage-history-charts .usage-chart-mark { transition: none; }
}
/* ===== usageCharts.css 嵌入结束 ===== */
/* ===== 页面还原样式(对应组件 Tailwind 类的几何/排版结果) ===== */
body { margin: 0; background: #494947; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; }
.doc { max-width: 1340px; margin: 0 auto; padding: 20px 16px 8px; color: #f0f0ee; font-size: 12px; line-height: 1.8; }
.doc h1 { font-size: 15px; margin: 0 0 6px; }
.doc code { background: rgb(255 255 255 / 0.12); padding: 0 4px; border-radius: 3px; }
.panels { display: flex; flex-wrap: wrap; gap: 16px; max-width: 1340px; margin: 0 auto; padding: 8px 16px 32px; align-items: flex-start; }
.panel { flex: 1 1 600px; max-width: 660px; background: var(--surface); color: var(--text-primary);
border-radius: 8px; padding: 20px 24px 24px; box-shadow: 0 4px 16px rgb(0 0 0 / 0.25); }
.panel-tag { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; color: var(--text-tertiary); margin-bottom: 10px; }
.page-h { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
.page-desc { font-size: 12px; line-height: 1.7; color: var(--text-tertiary); margin: 0 0 16px; max-width: 560px; }
/* 日期筛选(UsageDateFilter:label 12px medium,输入 190×36,按钮 36px) */
.date-filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 16px; }
.date-filter label { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
.date-filter input[type="date"] {
width: 190px; height: 36px; font-size: 13px; font-family: inherit; color: var(--text-primary);
background: var(--surface-elevated); border: 1px solid var(--border-default); border-radius: 9999px;
padding: 0 16px; outline: none;
}
.date-filter input[type="date"]:focus { border-color: var(--text-tertiary); box-shadow: 0 0 0 2px var(--focus-ring); }
.date-filter .apply {
height: 36px; padding: 0 24px; font-size: 13px; font-weight: 500; font-family: inherit; cursor: pointer;
color: var(--text-primary); background: var(--surface-elevated); border: 1px solid var(--border-default);
border-radius: 9999px;
}
.date-filter .apply:hover { background: color-mix(in srgb, var(--surface-elevated) 92%, var(--text-primary)); }
.date-filter .apply:disabled { opacity: 0.5; cursor: default; }
.date-filter .preview-only { font-size: 11px; color: var(--text-tertiary); background: none; border: none;
cursor: pointer; text-decoration: underline dotted; font-family: inherit; }
.summary { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 18px; padding: 10px 14px;
background: var(--surface-elevated); border: 1px solid var(--border-default); border-radius: 8px; }
.summary .stat { min-width: 90px; }
.summary .k { font-size: 11px; color: var(--text-tertiary); margin-bottom: 3px; }
.summary .v { font-size: 16px; font-weight: 500; font-variant-numeric: tabular-nums; }
.section-h { display: flex; align-items: baseline; gap: 8px; margin: 18px 0 10px; }
.section-h .t { font-size: 12px; font-weight: 500; color: var(--text-secondary); }
.section-h .s { font-size: 11px; color: var(--text-tertiary); }
/* 热力图(UsageHeatmap:12px 格、3px 间隙、月份标签行 14px 高) */
.hm-wrap { width: 100%; overflow-x: auto; }
.hm-inner { display: flex; flex-direction: column; gap: 6px; padding: 3px; width: max-content; }
.hm-months { position: relative; height: 14px; }
.hm-months span { position: absolute; top: 0; white-space: nowrap; font-size: 10px; line-height: 1.4; color: var(--text-tertiary); }
.hm-grid { display: flex; gap: 3px; }
.hm-col { display: flex; flex-direction: column; gap: 3px; }
.usage-chart-target { display: flex; align-items: center; justify-content: center; background: transparent;
border: 0; padding: 0; cursor: pointer; outline: none; position: relative; font-family: inherit; }
.hm-cell-mark { width: calc(12px + var(--usage-mark-grow, 0px)); height: calc(12px + var(--usage-mark-grow, 0px)); border-radius: 2px; flex-shrink: 0; }
.usage-chart-indicator { position: absolute; inset: 0; border-radius: 2px; pointer-events: none; }
/* 每日 token 柱图(UsageTokenBars:96px 高、3px 间隙、Y 轴 30px、参考线半透明) */
.bars-row { display: flex; gap: 6px; height: 96px; }
.y-axis { position: relative; width: 30px; flex-shrink: 0; }
.y-axis span { position: absolute; right: 0; transform: translateY(50%); font-size: 10px; line-height: 1;
color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
.bars-plot-wrap { position: relative; min-width: 0; flex: 1; }
.gridline { position: absolute; left: 0; right: 0; border-top: 1px solid var(--border-default); opacity: 0.5; }
.usage-token-plot { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: 3px; }
.usage-token-plot .usage-chart-target { flex: 1 1 0; min-width: 0; align-items: flex-end; justify-content: center; }
.bar-mark { display: flex; flex-direction: column; overflow: hidden; border-radius: 2px; flex-shrink: 0;
width: calc(100% + var(--usage-mark-grow, 0px)); }
.bar-mark span { display: block; }
/* 明细表(UsageBreakdownTables:th 11px medium tertiary、td 12px tabular、边框分割) */
table { width: 100%; border-collapse: collapse; }
th { white-space: nowrap; border-bottom: 1px solid var(--border-default); padding: 0 0 8px 12px;
text-align: right; font-size: 11px; font-weight: 500; color: var(--text-tertiary); }
th:first-child, td:first-child { padding-left: 0; text-align: left; width: 100%; max-width: 0; }
td { white-space: nowrap; border-bottom: 1px solid var(--border-default); padding: 8px 0 8px 12px;
text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; }
td.muted, .share-cell { color: var(--text-tertiary); }
.cell-main { display: flex; min-width: 0; align-items: center; gap: 8px; }
.cell-main .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.swatch { width: 8px; height: 8px; flex-shrink: 0; border-radius: 2px; }
.agent-tag { flex-shrink: 0; border: 1px solid var(--border-default); border-radius: 4px; padding: 1px 4px;
font-size: 10px; line-height: 1.2; color: var(--text-tertiary); }
.share-cell { display: flex; align-items: center; justify-content: flex-end; }
.share-bar { display: inline-block; height: 4px; border-radius: 2px; margin-right: 6px; }
.agent-sharebar { display: flex; height: 8px; overflow: hidden; border-radius: 2px; margin-bottom: 12px; }
.hint { font-size: 11px; color: var(--text-tertiary); margin-top: 8px; }
</style>
</head>
<body>
<header class="doc">
<h1>PR #4076 · 用量历史图表改动后界面预览(Light / Dark 同数据同布局)</h1>
颜色 token 取自 <code>apps/desktop/src/renderer/themes/colors.ts</code>(commit <code>779f9f47b</code>);图表交互 CSS 为
<code>usageCharts.css</code> 逐字嵌入;热力格 12px、柱图 96px 高 / 2px 圆角 / 悬停与选中 +2px、非选中柱 24% 不透明度等几何与
<code>UsageHeatmap</code> / <code>UsageTokenBars</code> / <code>UsageDateFilter</code> / <code>UsageBreakdownTables</code> 一致。
数据为合成 fixture(六组模型、150 天、含零值日)。悬停、点击、Tab 焦点、日期提交均可试;两个面板共享同一条日期选择。
输入框 / 按钮等页面级控件为近似还原。
</header>
<main class="panels">
<section class="panel theme-light usage-history-charts" data-panel="light"></section>
<section class="panel theme-dark usage-history-charts" data-panel="dark"></section>
</main>
<script>
'use strict';
/* ---------- 与生产同源的常量/算法(拷贝自组件源码) ---------- */
var CELL_PX = 12, GAP_PX = 3, LEVEL_MIX = [0.22, 0.42, 0.68, 1], CHART_HEIGHT_PX = 96;
var RANK_MIX = [1, 0.75, 0.55, 0.4, 0.3, 0.18];
var TODAY = '2026-09-07'; /* todayKey 锚点(renderer 不自取系统日期) */
var HEATMAP_WEEKS = 23; /* resolveHeatmapWeeks(windowDays=150) 的结果 */
function fmtTokens(n) { /* maker-shared usageFormat.ts 同口径 */
if (n < 1000) return String(n);
var units = [[1e3, 'k'], [1e6, 'M'], [1e9, 'B']];
for (var i = 0; i < units.length; i++) { var v = n / units[i][0]; if (v < 999.95) return v.toFixed(1) + units[i][1]; }
return (n / 1e9).toFixed(1) + 'B';
}
function niceTicks(max) { /* UsageTokenBars 同算法 */
if (!(max > 0)) return [];
var rawStep = max / 4, pow = Math.pow(10, Math.floor(Math.log10(rawStep)));
var step = [1, 2, 2.5, 5, 10].map(function (m) { return m * pow; }).find(function (s) { return s >= rawStep; }) || 10 * pow;
var ticks = [];
for (var v = step; v <= max * 0.95 && ticks.length < 3; v += step) ticks.push(v);
return ticks;
}
function modelColor(rank, colorCount) { /* usageHistoryColors.ts 同规则 */
return rank >= 0 && rank < colorCount && rank < 5 ? 'var(--usage-model-' + (rank + 1) + ')' : 'var(--text-tertiary)';
}
function rankColor(rank) { /* usagePalette.ts 同规则(Agent 表中性阶梯) */
return 'color-mix(in srgb, var(--accent-emphasis) ' + RANK_MIX[Math.min(rank, 5)] * 100 + '%, var(--surface-chip))';
}
function parseDay(k) { var p = k.split('-'); return new Date(+p[0], +p[1] - 1, +p[2]); }
function toDayKey(d) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
function shiftDay(k, n) { var d = parseDay(k); d.setDate(d.getDate() + n); return toDayKey(d); }
/* ---------- 合成 fixture(确定性 PRNG,六组模型 × 150 天,含零值日) ---------- */
function mulberry32(a) { return function () { a |= 0; a = (a + 0x6D2B79F5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
var rand = mulberry32(20260907);
var MODELS = [
{ key: 'claude-code claude-opus-4-5', name: 'claude-opus-4-5', agent: 'claude-code', w: 0.34 },
{ key: 'codex gpt-5.5', name: 'gpt-5.5', agent: 'codex', w: 0.25 },
{ key: 'claude-code glm-5.3', name: 'glm-5.3', agent: 'claude-code', w: 0.16 },
{ key: 'codex gemini-3-flash', name: 'gemini-3-flash', agent: 'codex', w: 0.11 },
{ key: 'pi kimi-k2.5', name: 'kimi-k2.5', agent: 'pi', w: 0.08 },
{ key: '__others__', name: '其它', agent: '', w: 0.06 }
];
var byDay = {}; /* day -> { total, per: [tokens×6] } */
(function genData() {
for (var back = 149; back >= 0; back--) {
var day = shiftDay(TODAY, -back), d = parseDay(day);
var zero = rand() < 0.13; /* 150 天中约 130 天非零,与 fixture 口径一致 */
var weekday = d.getDay(), weekend = weekday === 0 || weekday === 6 ? 0.5 : 1;
var trend = 0.55 + 0.45 * (1 - back / 149); /* 温和增长趋势 */
var total = zero ? 0 : Math.round((18000 + rand() * 92000) * weekend * trend);
var per = [], sum = 0, i;
for (i = 0; i < MODELS.length; i++) { var w = MODELS[i].w * (0.55 + rand() * 0.9); per.push(w); sum += w; }
for (i = 0; i < MODELS.length; i++) per[i] = total > 0 ? Math.round(total * per[i] / sum) : 0;
byDay[day] = { total: total, per: per };
}
})();
var totalsAll = MODELS.map(function (m, i) { return Object.values(byDay).reduce(function (s, d) { return s + d.per[i]; }, 0); });
var colorOrder = MODELS.map(function (m, i) { return i; }).sort(function (a, b) { return totalsAll[b] - totalsAll[a]; }).slice(0, 5);
function rankOf(idx) { var p = colorOrder.indexOf(idx); return p >= 0 ? p : 5; }
/* ---------- 全局共享的日期选择(两面板同一条状态,便于并排对比) ---------- */
var selectedDay = null;
var panelUpdaters = [];
function selectDay(day) { selectedDay = day; panelUpdaters.forEach(function (u) { u(); }); }
/* ---------- 渲染 ---------- */
function el(tag, cls, style, text) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (style) for (var k in style) e.style[k] = style[k];
if (text != null) e.textContent = text;
return e;
}
function sectionH(title, sub) {
var h = el('div', 'section-h');
h.appendChild(el('span', 't', null, title));
if (sub) h.appendChild(el('span', 's', null, sub));
return h;
}
function renderPanel(root, mode) {
root.appendChild(el('div', 'panel-tag', null, mode === 'light' ? 'LIGHT 模式' : 'DARK 模式'));
root.appendChild(el('h2', 'page-h', null, '用量'));
root.appendChild(el('p', 'page-desc', null, '这台设备上 Cindy 内部产生的 token 用量。记录从该功能上线起积累,不回填更早的历史。'));
/* 日期筛选(UsageDateFilter) */
var form = el('form', 'date-filter');
var label = el('label', null, null, '选择日期');
var input = el('input'); input.type = 'date'; input.max = TODAY; input.required = true; input.lang = 'zh-CN';
var apply = el('button', 'apply', null, '查看当天'); apply.type = 'submit';
var reset = el('button', 'preview-only', null, '重置选择(预览辅助,非产品 UI)'); reset.type = 'button';
form.appendChild(label); form.appendChild(input); form.appendChild(apply); form.appendChild(reset);
root.appendChild(form);
/* 概览 */
var summary = el('div', 'summary');
root.appendChild(summary);
/* 活跃热力图 */
root.appendChild(sectionH('活跃热力图', '近 ' + HEATMAP_WEEKS + ' 周'));
var hmWrap = el('div', 'hm-wrap'), hmInner = el('div', 'hm-inner'),
months = el('div', 'hm-months'), grid = el('div', 'hm-grid');
hmInner.appendChild(months); hmInner.appendChild(grid); hmWrap.appendChild(hmInner); root.appendChild(hmWrap);
var today = parseDay(TODAY);
var start = new Date(today); start.setHours(12, 0, 0, 0); start.setDate(start.getDate() - start.getDay() - (HEATMAP_WEEKS - 1) * 7);
var keys = [], cursor = new Date(start);
for (var i = 0; i < HEATMAP_WEEKS * 7; i++) { keys.push(toDayKey(cursor)); cursor.setDate(cursor.getDate() + 1); }
var nonZero = keys.filter(function (k) { return k <= TODAY && byDay[k] && byDay[k].total > 0; })
.map(function (k) { return byDay[k].total; }).sort(function (a, b) { return a - b; });
function q(p) { return nonZero.length ? nonZero[Math.min(nonZero.length - 1, Math.floor(p * nonZero.length))] : 0; }
var thresholds = [q(0.25), q(0.5), q(0.75)];
function levelFor(v) { return v <= 0 ? 0 : v <= thresholds[0] ? 1 : v <= thresholds[1] ? 2 : v <= thresholds[2] ? 3 : 4; }
var cols = [];
for (var ci = 0; ci < keys.length; ci += 7) cols.push(keys.slice(ci, ci + 7));
var lastLabelCol = -9;
cols.forEach(function (col, idx) {
var colEl = el('div', 'hm-col');
col.forEach(function (k) {
if (k > TODAY) { colEl.appendChild(el('div', null, { width: CELL_PX + 'px', height: CELL_PX + 'px' })); return; }
var day = byDay[k] || { total: 0 };
var level = levelFor(day.total);
var bg = level === 0 ? 'var(--surface-chip)'
: 'color-mix(in srgb, var(--usage-heatmap-accent) ' + LEVEL_MIX[level - 1] * 100 + '%, var(--surface-chip))';
var btn = el('button', 'usage-chart-target', { width: CELL_PX + 'px', height: CELL_PX + 'px' });
btn.type = 'button'; btn.dataset.day = k; btn.setAttribute('aria-pressed', 'false');
btn.title = k + ' · ' + (day.total > 0 ? fmtTokens(day.total) + ' token' : '当天没有用量记录');
btn.setAttribute('aria-label', btn.title);
var mark = el('div', 'usage-chart-mark hm-cell-mark', { backgroundColor: bg });
mark.setAttribute('data-usage-mark', 'usage-heatmap-day'); mark.title = btn.title;
btn.appendChild(mark);
btn.appendChild(el('span', 'usage-chart-indicator'));
colEl.appendChild(btn);
});
grid.appendChild(colEl);
var firstOfMonth = col.find(function (k) { return k <= TODAY && k.endsWith('-01'); });
if (firstOfMonth && idx - lastLabelCol >= 2) {
lastLabelCol = idx;
var m = parseDay(firstOfMonth);
months.appendChild(el('span', null, { left: idx * (CELL_PX + GAP_PX) + 'px' }, (m.getMonth() + 1) + '月'));
}
});
months.style.width = cols.length * (CELL_PX + GAP_PX) - GAP_PX + 'px';
/* 近 30 天每日 token 柱图 */
root.appendChild(sectionH('近 30 天每日 token'));
var barsRow = el('div', 'bars-row'), yAxis = el('div', 'y-axis'), plotWrap = el('div', 'bars-plot-wrap'),
plot = el('div', 'usage-token-plot');
plotWrap.appendChild(plot); barsRow.appendChild(yAxis); barsRow.appendChild(plotWrap); root.appendChild(barsRow);
var barDays = []; for (var b = 29; b >= 0; b--) barDays.push(shiftDay(TODAY, -b));
var barMax = Math.max.apply(null, barDays.map(function (k) { return (byDay[k] || { total: 0 }).total; }));
var ticks = niceTicks(barMax);
ticks.forEach(function (v) {
yAxis.appendChild(el('span', null, { bottom: (v / barMax) * CHART_HEIGHT_PX + 'px' }, fmtTokens(v)));
plotWrap.appendChild(el('div', 'gridline', { bottom: (v / barMax) * CHART_HEIGHT_PX + 'px' }));
});
barDays.forEach(function (k) {
var day = byDay[k] || { total: 0, per: [] };
var ratio = barMax > 0 ? day.total / barMax : 0;
var visual = day.total > 0 ? Math.max(3, Math.round(ratio * CHART_HEIGHT_PX)) : 2;
var hit = Math.max(24, visual);
var btn = el('button', 'usage-chart-target', { height: hit + 'px' });
btn.type = 'button'; btn.dataset.day = k; btn.setAttribute('aria-pressed', 'false');
btn.title = k + ' · ' + (day.total > 0 ? fmtTokens(day.total) + ' token' : '当天没有用量记录');
btn.setAttribute('aria-label', btn.title);
var mark = el('span', 'usage-chart-mark bar-mark', { height: visual + 'px' });
mark.setAttribute('data-usage-mark', 'usage-token-bar');
if (day.total === 0) mark.style.backgroundColor = 'var(--surface-chip)';
var segs = [];
MODELS.forEach(function (m, idx) { if (day.per[idx] > 0) segs.push({ rank: rankOf(idx), tokens: day.per[idx] }); });
segs.sort(function (a, b) { return a.rank - b.rank; });
segs.slice().reverse().forEach(function (s) { /* rank 0(最大头)渲染在柱底,与组件一致 */
mark.appendChild(el('span', null, { height: (s.tokens / day.total) * 100 + '%', backgroundColor: modelColor(s.rank, 5) }));
});
btn.appendChild(mark);
btn.appendChild(el('span', 'usage-chart-indicator'));
plot.appendChild(btn);
});
/* 按 Agent / harness 表 + 按模型表(宿主节点先建好,update 时原地重聚合) */
root.appendChild(sectionH('按 Agent / harness', '近 30 天'));
var agentHost = el('div'); agentHost.dataset.table = 'agent'; root.appendChild(agentHost);
root.appendChild(sectionH('按模型', '近 30 天'));
var modelHost = el('div'); modelHost.dataset.table = 'model'; root.appendChild(modelHost);
root.appendChild(el('p', 'hint', null, '三个入口等价:点击热力格、点击柱体、填写日期后提交,都会选中同一天(DESIGN §5)。'));
/* ---------- 交互与重聚合(handleDayClick 同语义) ---------- */
function fmtRangeLabel() {
if (!selectedDay) return '近 30 天';
var d = parseDay(selectedDay);
return (d.getMonth() + 1) + '月' + d.getDate() + '日';
}
function rangeDays() {
var from = selectedDay || shiftDay(TODAY, -29);
var to = selectedDay || TODAY;
return Object.keys(byDay).filter(function (k) { return k >= from && k <= to; });
}
function renderAgentTable(rangeLabel) {
agentHost.innerHTML = '';
var days = rangeDays(), agents = ['claude-code', 'codex', 'pi'], rows = [], total = 0;
agents.forEach(function (a) {
var t = 0, mc = new Set();
days.forEach(function (k) { byDay[k].per.forEach(function (v, i) { if (v > 0 && MODELS[i].agent === a) { t += v; mc.add(i); } }); });
rows.push({ agent: a, tokens: t, models: mc.size }); total += t;
});
var bar = el('div', 'agent-sharebar');
rows.forEach(function (r, ai) { bar.appendChild(el('div', null, { width: (total > 0 ? r.tokens / total * 100 : 0) + '%', backgroundColor: rankColor(ai) })); });
agentHost.appendChild(bar);
var tb = el('table'), thead = el('thead'), trh = el('tr');
['Agent / harness', rangeLabel + ' token', '占比', '今日', '缓存命中率', '用到的模型'].forEach(function (h) { trh.appendChild(el('th', null, null, h)); });
thead.appendChild(trh); tb.appendChild(thead);
var tbody = el('tbody');
rows.forEach(function (r, ai) {
var tr = el('tr'), main = el('div', 'cell-main');
main.appendChild(el('span', 'swatch', { backgroundColor: rankColor(ai) }));
main.appendChild(el('span', 'name', null, r.agent));
var tdMain = el('td'); tdMain.appendChild(main); tr.appendChild(tdMain);
tr.appendChild(el('td', null, null, fmtTokens(r.tokens)));
var share = total > 0 ? r.tokens / total : 0;
var shareTd = el('td', 'share-cell');
shareTd.appendChild(el('span', 'share-bar', { width: Math.max(2, share * 46) + 'px', backgroundColor: rankColor(ai) }));
shareTd.appendChild(document.createTextNode(Math.round(share * 100) + '%'));
tr.appendChild(shareTd);
var todayTokens = byDay[TODAY] ? byDay[TODAY].per.reduce(function (s, v, i) { return s + (MODELS[i].agent === r.agent ? v : 0); }, 0) : 0;
tr.appendChild(el('td', 'muted', null, todayTokens > 0 ? fmtTokens(todayTokens) : '—'));
tr.appendChild(el('td', 'muted', null, Math.round((48 + ai * 11)) + '%'));
tr.appendChild(el('td', 'muted', null, String(r.models)));
tbody.appendChild(tr);
});
tb.appendChild(tbody); agentHost.appendChild(tb);
}
function renderModelTable(rangeLabel) {
modelHost.innerHTML = '';
var days = rangeDays();
var mTotals = MODELS.map(function (m, i) {
var t = 0, input2 = 0, output = 0, cr = 0, cc = 0;
days.forEach(function (k) {
var v = byDay[k].per[i]; t += v;
input2 += Math.round(v * 0.22); output += Math.round(v * 0.14); cr += Math.round(v * 0.58); cc += Math.round(v * 0.06);
});
return { m: m, tokens: t, input: input2, output: output, cr: cr, cc: cc };
});
var grand = mTotals.reduce(function (s, r) { return s + r.tokens; }, 0);
mTotals.sort(function (a, b) { return b.tokens - a.tokens; });
var tb = el('table'), thead = el('thead'), trh = el('tr');
['模型', rangeLabel + ' token', '占比', '输入', '输出', '缓存读取', '缓存写入', '缓存命中率'].forEach(function (h) { trh.appendChild(el('th', null, null, h)); });
thead.appendChild(trh); tb.appendChild(thead);
var tbody = el('tbody');
mTotals.forEach(function (r) {
if (r.tokens <= 0) return;
var rank = r.m.key === '__others__' ? 5 : rankOf(MODELS.indexOf(r.m));
var color = modelColor(rank, 5);
var tr = el('tr'), main = el('div', 'cell-main');
main.appendChild(el('span', 'swatch', { backgroundColor: color }));
main.appendChild(el('span', 'name', null, r.m.name));
if (r.m.agent) main.appendChild(el('span', 'agent-tag', null, r.m.agent));
var tdMain = el('td'); tdMain.appendChild(main); tr.appendChild(tdMain);
tr.appendChild(el('td', null, null, fmtTokens(r.tokens)));
var share = grand > 0 ? r.tokens / grand : 0;
var shareTd = el('td', 'share-cell');
shareTd.appendChild(el('span', 'share-bar', { width: Math.max(2, share * 46) + 'px', backgroundColor: color }));
shareTd.appendChild(document.createTextNode(Math.round(share * 100) + '%'));
tr.appendChild(shareTd);
[r.input, r.output, r.cr, r.cc].forEach(function (v) { tr.appendChild(el('td', 'muted', null, fmtTokens(v))); });
tr.appendChild(el('td', 'muted', null, (38 + (5 - rank) * 9) + '%'));
tbody.appendChild(tr);
});
tb.appendChild(tbody); modelHost.appendChild(tb);
}
function update() {
root.querySelectorAll('.usage-chart-target').forEach(function (btn) {
btn.setAttribute('aria-pressed', selectedDay != null && btn.dataset.day === selectedDay ? 'true' : 'false');
});
input.value = selectedDay || '';
apply.disabled = !selectedDay;
/* 概览与两张表按当前范围重聚合(选中单日 = day:YYYY-MM-DD 范围) */
var days = rangeDays();
var total = days.reduce(function (s, k) { return s + byDay[k].total; }, 0);
var streak = 0; for (var k2 = TODAY; byDay[k2] && byDay[k2].total > 0; k2 = shiftDay(k2, -1)) streak++;
var modelsUsed = new Set(); days.forEach(function (k) { byDay[k].per.forEach(function (t, i) { if (t > 0) modelsUsed.add(i); }); });
summary.innerHTML = '';
var rangeLabel = fmtRangeLabel();
var stats = [
['今日 token', (byDay[TODAY] || { total: 0 }).total, true],
[rangeLabel + ' token', total, true],
['连续活跃天数', streak, false],
['缓存命中率(' + rangeLabel + ')', 62, false],
['用到的模型', modelsUsed.size, false]
];
stats.forEach(function (p) {
var stat = el('div', 'stat');
stat.appendChild(el('div', 'k', null, p[0]));
stat.appendChild(el('div', 'v', null, p[2] ? fmtTokens(p[1]) : (p[0].indexOf('命中率') >= 0 ? p[1] + '%' : String(p[1]))));
summary.appendChild(stat);
});
renderAgentTable(rangeLabel);
renderModelTable(rangeLabel);
}
form.addEventListener('submit', function (e) {
e.preventDefault();
if (input.value && input.value <= TODAY) selectDay(input.value);
});
root.addEventListener('click', function (e) {
var btn = e.target.closest('.usage-chart-target');
if (btn && btn.dataset.day) selectDay(btn.dataset.day);
});
reset.addEventListener('click', function () { selectDay(null); });
panelUpdaters.push(update);
update();
}
document.querySelectorAll('[data-panel]').forEach(function (root) {
renderPanel(root, root.dataset.panel);
});
</script>
</body>
</html>dispatch:a55ee9dffdda23dd |
Signed-off-by: kirozeng <kirozeng@gmail.com>
|
本轮核实:无需代码改动,新 head 上 CI 已全绿 这条「Windows unit tests 缺席」的信号来自 fea5c7b 推送后 CI 刚启动的瞬间——聚合门当时还没登记,不是故障。现在 run 34178086110 已出终局:
即所有者同步的「交互反馈与排版优化」(移除日期表单入口、范围强调、描边与排版层级)在新 head 上一次通过全部必需检查,无需任何代码改动。 dispatch:5bc25e7f411b8ede |
MagicLizi
left a comment
There was a problem hiding this comment.
独立审查未通过(P1×2)。用量图改动把未落地的规则写进 DESIGN.md 正本,并且会覆盖已合入的 #4064。
-
[P1]
docs/design-rules/DESIGN.md:309— §4 写用量图已有 “equivalent single-day selection control”,圆角走 §5 已登记成员 ID。本 PR 最终提交已删掉新增日期入口(没有UsageDateFilter,热力格仍是 12×12);同 PR 的usage-history-charts.md也写明 Equivalent 未满足、不批准 Essential 豁免。权威正本不能给没落地的等价控件背书。请删掉该表述,或把合规日期入口做回来再写进去。 -
[P1]
docs/design-rules/DESIGN.md:334— §5 把usage-heatmap-day/usage-token-bar当已登记成员,并写交互放大 supersede 尺寸禁令。相对当前main(#4064),本分支会删掉 Settings segmented controls、Usage data graphics,以及 usage data marks 的 2px 例外,并把 pill 口径退回「every button is a pill — the only exemption is registered bare text buttons」。这是用未合入的 #4072 口径改写正本,给本 PR 的放大/彩色实现开脱。请先 rebasemain,保留 #4064 的分段控件与 usage-mark 例外;成员 ID 要么等 #4072 合入再引用,要么在本 PR 完整登记。
按 review 5137957030 的两条 P1 同步 main(取到 makecindy#4064 为止,不含其后 的 CI 工作流改动):保留 makecindy#4064 已合入的 §4 Settings segmented controls / Usage data graphics 与 §5 usage data marks 2px 例外及 pill 措辞,不再以 未合入的规范 PR 框架改写正本。 - §4 Usage History Charts 指针节去掉对已移除等价日期表单的背书,改为 锚定 §5 已登记例外与 §2 颜色登记;Usage data graphics 的灰度条款让位 §2 已登记的用量图表类别色 - §5 交互几何登记改写为锚定 usage data marks 例外、在本 PR 内完整登记 usage-heatmap-day / usage-token-bar,去掉 supersede 框架 - usage-history-charts.md 的登记引用改为本 PR 自身,不再指向未合入 PR - 组件与测试冲突取本分支终态(无框选中、独立指示层与 grow 结构); usageChartInteraction.test.tsx 两处断言对齐合并后实现 - design-inventory 由 pnpm design:inventory 从合并后代码再生成 Signed-off-by: Kirozeng <kirozeng@gmail.com>
|
本轮修复:按 review 的两条 P1 同步 #4064,并把登记收进本 PR(a87a13fac) P1-1(§4 给未落地的等价控件背书):已删。§4 的 Usage History Charts 指针节不再提「equivalent single-day selection control」,改为明确说明草稿日期表单不属于本条目、命中尺寸裁决仍在组件规范里单独记录;几何锚定回 §5 已登记的 usage data marks 例外,颜色锚定 §2 的图表类别色登记。 P1-2(覆盖 #4064、借用未合入规范 PR 的口径):已合并 main 到 #4064(merge 701c1bf)。#4064 的登记全部保留:
交互几何段从「supersedes + member IDs」改写为锚定该例外、在本 PR 内完整登记 为什么 merge 而不是 rebase:本分支禁 force-push,merge 到 701c1bf 内容上等效且不重写历史;没有取 #4064 之后的 main,是因为其后进了 CI 工作流改动,避免本 PR 的 diff 触碰 CI 路径。 验证:
CI 在新 head 上重跑中。 dispatch:6ef142a592dccb54 |
这些问题已在当前 head 的后续 commit 中修复(格式门/安全门已通过),自动 dismiss 旧的 CHANGES_REQUESTED 以解除合并阻塞。
同步 main(至 043eaec,makecindy#4072 已合入),解决与 makecindy#4076 的设计文档冲突: - §5 采用 makecindy#4072 重写的「可见层 + 登记驱动」框架(Step 1 已登记形状 / Step 2 控件三档);makecindy#4064 的 usage data marks 例外已由框架内成员表吸收 - 本 PR 的交互几何登记改写为 Interaction constraints 内的 「Registered interaction emphasis」条目,锚定 data mark 成员 usage-heatmap-day / usage-token-bar;§4 指针与 cindy-design-system 版本记录同步改引用 - §14.4 Press 行取 main 新措辞,Done 行保留对下方图表响应登记的指引 - design-inventory 人工区融合两边跟进记录,生成区由 pnpm design:inventory 再生成并通过 check:design-inventory Signed-off-by: Kirozeng <kirozeng@gmail.com>
|
分支冲突已解决(96ab9ebba):合并 main 至 #4072(圆角登记框架) #4072 合入后与本 PR 改到同一批设计文档产生冲突。本次合并对齐:
验证:干净环境全仓 pnpm test:unit:related 通过(desktop 171s、maker-core 143s 等 30 个 workspace 全 PASS)、desktop typecheck 通过、DCO 通过。GitHub 现在报告 mergeable。 |
这次改了什么
摘要
优化用量历史的图表配色、交互反馈和文字层级。本地最后一轮试用修改已同步至本 PR,保留此前颜色分类登记的 CI 修复。
暂不合并: 新增日期入口按所有者要求取消后,密集目标的点击尺寸方案仍未解决。本次同步供评审,不代表已满足 Equivalent 条件或获准 Essential 豁免;同时仍依赖 #4072 的圆角登记规范。此前对旧提交的批准与预览不能代替本轮验证。
变更类型
feat新功能fix缺陷修复refactor/perf重构或性能优化docs/test/chore文档、测试或工程维护范围
UI 变化
docs/design-rules/DESIGN.md§2/§10 语义配色、§3 字号字重、§5 登记圆角、§14.4 局部反馈;docs/design-rules/usage-history-charts.md;design-governance.md§6/§7。docs/design-evidence/2026-09-08/usage-chart-shapes.md的“当前同步版本”。真实截图及实测 JSON 以sync-开头,保存在本地忽略目录;公开附件待上传。评论中的旧 HTML 近似预览不代表本轮 UI。fea5c7b7890a2fb99bc3338060a2e8acef63abec。怎么验证的
自动验证
按所有者取消新增日期入口的要求,将旧表单存在性断言更新为保留两张图表的日期选择路径,并验证页面不再显示日期输入。仍覆盖相同日期筛选结果、默认状态、重复与空白点击、零值/历史日、固定图表数据,以及七日范围跨月、单日优先和图外日期。行为测试通过不代表点击尺寸合规。
手工验证
真实隔离 Electron 的 Light/Dark 均验证:默认无选中、七个日历日强调、单日联动、图外日期无柱选中、60% 不透明度、热力格描边与间隙、2px 键盘焦点,以及选择前后柱高不变。
两种模式均确认概览 16px/500;临时按 UI 字号 14→18 放大本页 Token,概览计算为 21px且无水平溢出,测试后恢复。数据为六组模型、150 天合成用量,未修改正式数据。GPT-6 已自审最终差异,按所有者授权未运行双审。
未执行的验证
风险
风险分类
影响与回滚
提交前检查