token-studio 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nvmrc +1 -0
- package/CHANGELOG.md +89 -0
- package/Dockerfile +17 -0
- package/LICENSE +22 -0
- package/NOTICE.md +21 -0
- package/PRIVACY.md +68 -0
- package/README.en.md +220 -0
- package/README.md +220 -0
- package/config/collectors.json +54 -0
- package/data/.gitkeep +1 -0
- package/docker-compose.yml +17 -0
- package/docs/assets/.gitkeep +1 -0
- package/docs/assets/token-studio-v44-dashboard.png +0 -0
- package/docs/assets/token-studio-v44-live.png +0 -0
- package/docs/assets/token-studio-v44-review-mobile.png +0 -0
- package/docs/assets/token-studio-v44-review.png +0 -0
- package/docs/assets/token-studio-v45-dashboard.png +0 -0
- package/docs/assets/token-studio-v45-live.png +0 -0
- package/docs/assets/token-studio-v45-review-mobile.png +0 -0
- package/docs/assets/token-studio-v45-review.png +0 -0
- package/docs/blog-case-study.md +34 -0
- package/docs/collector-support-matrix.md +65 -0
- package/docs/competitive-notes.md +87 -0
- package/docs/demo-data/README.md +12 -0
- package/docs/demo-data/token-studio-v2-demo.json +146 -0
- package/docs/demo-flow.md +39 -0
- package/docs/first-run.md +95 -0
- package/docs/local-collectors.md +49 -0
- package/docs/public-launch-checklist.md +45 -0
- package/docs/resume-bullets.md +7 -0
- package/docs/statusline.md +52 -0
- package/index.html +16 -0
- package/package.json +36 -0
- package/render.yaml +17 -0
- package/src/auto-attribution.mjs +396 -0
- package/src/ccusage-bridge.mjs +74 -0
- package/src/ccusage-import.mjs +415 -0
- package/src/cli.mjs +643 -0
- package/src/client/dashboard/App.jsx +1734 -0
- package/src/client/dashboard/annotation-presets.js +138 -0
- package/src/client/dashboard/attribution.js +328 -0
- package/src/client/dashboard/components-charts.jsx +622 -0
- package/src/client/dashboard/components-tables.jsx +1531 -0
- package/src/client/dashboard/components-top.jsx +307 -0
- package/src/client/dashboard/import-budget.js +41 -0
- package/src/client/dashboard/model-usage.js +108 -0
- package/src/client/dashboard/onboarding.js +80 -0
- package/src/client/dashboard/styles.css +2606 -0
- package/src/client/live/LiveApp.jsx +226 -0
- package/src/client/live/styles.css +446 -0
- package/src/client/main.jsx +20 -0
- package/src/client/review/ReviewApp.jsx +507 -0
- package/src/client/review/closure-progress.js +165 -0
- package/src/client/review/markdown-report.js +401 -0
- package/src/client/review/model-strategy.js +273 -0
- package/src/client/review/roi-advisor.js +255 -0
- package/src/client/review/roi-evidence.js +78 -0
- package/src/client/review/savings-simulator.js +252 -0
- package/src/client/review/sections-1.jsx +277 -0
- package/src/client/review/sections-2.jsx +927 -0
- package/src/client/review/styles.css +2321 -0
- package/src/client/review/utils.js +345 -0
- package/src/client/shared/utils.js +236 -0
- package/src/closure-check.mjs +537 -0
- package/src/closure-import.mjs +646 -0
- package/src/collect.mjs +247 -0
- package/src/collector-config.mjs +82 -0
- package/src/collector-registry.mjs +333 -0
- package/src/collectors/claude-code.mjs +355 -0
- package/src/collectors/codex.mjs +418 -0
- package/src/collectors/copilot.mjs +19 -0
- package/src/collectors/cursor.mjs +23 -0
- package/src/collectors/gemini.mjs +530 -0
- package/src/collectors/goose.mjs +15 -0
- package/src/collectors/hermes.mjs +206 -0
- package/src/collectors/kimi.mjs +15 -0
- package/src/collectors/openclaw.mjs +400 -0
- package/src/collectors/opencode.mjs +349 -0
- package/src/collectors/qwen.mjs +15 -0
- package/src/collectors/structured-usage.mjs +437 -0
- package/src/collectors/utils.mjs +93 -0
- package/src/db.mjs +1397 -0
- package/src/demo-seed.mjs +39 -0
- package/src/dev.mjs +43 -0
- package/src/live.mjs +428 -0
- package/src/model-policy.mjs +147 -0
- package/src/pricing.mjs +434 -0
- package/src/privacy-check.mjs +126 -0
- package/src/server.mjs +1240 -0
- package/src/source-health.mjs +195 -0
- package/src/statusline.mjs +156 -0
- package/src/terminal-report.mjs +245 -0
- package/src/update-pricing.mjs +8 -0
- package/test/annotation-presets.test.mjs +137 -0
- package/test/api-annotations.test.mjs +202 -0
- package/test/api-auto-attribution.test.mjs +169 -0
- package/test/api-source-health.test.mjs +109 -0
- package/test/api-v2.test.mjs +278 -0
- package/test/api-v43.test.mjs +151 -0
- package/test/api-v44.test.mjs +128 -0
- package/test/attribution-summary.test.mjs +164 -0
- package/test/auto-attribution.test.mjs +116 -0
- package/test/ccusage-bridge.test.mjs +36 -0
- package/test/ccusage-import.test.mjs +93 -0
- package/test/cli-v43.test.mjs +64 -0
- package/test/cli-v45.test.mjs +34 -0
- package/test/cli-v46.test.mjs +129 -0
- package/test/cli-v47.test.mjs +98 -0
- package/test/closure-check.test.mjs +202 -0
- package/test/closure-import.test.mjs +263 -0
- package/test/collector-config.test.mjs +25 -0
- package/test/collector-registry.test.mjs +56 -0
- package/test/csv.test.mjs +19 -0
- package/test/db-annotations.test.mjs +186 -0
- package/test/db-v2.test.mjs +200 -0
- package/test/db-v4.test.mjs +178 -0
- package/test/experimental-collectors.test.mjs +103 -0
- package/test/fixtures/collectors/copilot/usage.jsonl +2 -0
- package/test/fixtures/collectors/cursor/usage.jsonl +2 -0
- package/test/fixtures/collectors/goose/usage.jsonl +2 -0
- package/test/fixtures/collectors/kimi/usage.jsonl +2 -0
- package/test/fixtures/collectors/qwen/usage.jsonl +2 -0
- package/test/import-budget.test.mjs +40 -0
- package/test/live.test.mjs +256 -0
- package/test/markdown-report.test.mjs +193 -0
- package/test/model-policy.test.mjs +34 -0
- package/test/model-strategy.test.mjs +116 -0
- package/test/model-usage.test.mjs +99 -0
- package/test/official-pricing.test.mjs +70 -0
- package/test/onboarding.test.mjs +55 -0
- package/test/privacy-check.test.mjs +33 -0
- package/test/review-closure-progress.test.mjs +99 -0
- package/test/roi-advisor.test.mjs +188 -0
- package/test/roi-evidence.test.mjs +48 -0
- package/test/roi-summary.test.mjs +101 -0
- package/test/savings-simulator.test.mjs +141 -0
- package/test/source-health.test.mjs +62 -0
- package/test/statusline.test.mjs +148 -0
- package/vite.config.js +23 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/* =============================================================
|
|
2
|
+
Review-page sections — Hero, Projects, Calendar
|
|
3
|
+
============================================================= */
|
|
4
|
+
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
|
+
import { U } from '../shared/utils.js';
|
|
7
|
+
import { RU } from './utils.js';
|
|
8
|
+
|
|
9
|
+
// ───────────────────────────────────────────────────────────────
|
|
10
|
+
// Hero / opening
|
|
11
|
+
// ───────────────────────────────────────────────────────────────
|
|
12
|
+
function HeroSection({ period, totals, prevTotals, stats }) {
|
|
13
|
+
const delta = prevTotals && prevTotals.total > 0
|
|
14
|
+
? ((totals.total - prevTotals.total) / prevTotals.total) * 100
|
|
15
|
+
: null;
|
|
16
|
+
const deltaUp = delta != null && delta > 0;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<section className="hero">
|
|
20
|
+
<div className="hero-eyebrow">
|
|
21
|
+
<span>AI TOKEN 复盘</span>
|
|
22
|
+
<span className="sep"/>
|
|
23
|
+
<span>{period.pretty}</span>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<h1 className="hero-headline">
|
|
27
|
+
{period.label},你用了 <span className="num">{U.compactCN(totals.total)}</span> tokens
|
|
28
|
+
</h1>
|
|
29
|
+
|
|
30
|
+
<p className="hero-sub">
|
|
31
|
+
官方价换算 <b style={{color: 'var(--ink)', fontVariantNumeric: 'tabular-nums'}}>{U.fmtUS.format(totals.cost)}</b>
|
|
32
|
+
{delta != null && (
|
|
33
|
+
<>
|
|
34
|
+
,比上一周期
|
|
35
|
+
<span className={`delta ${deltaUp ? 'up' : 'down'}`} style={{marginLeft: 6}}>
|
|
36
|
+
{deltaUp ? '↑' : '↓'} {Math.abs(delta).toFixed(0)}%
|
|
37
|
+
</span>
|
|
38
|
+
</>
|
|
39
|
+
)}
|
|
40
|
+
</p>
|
|
41
|
+
|
|
42
|
+
<div className="hero-meta">
|
|
43
|
+
花在了
|
|
44
|
+
<b>{stats.projectCount}</b> 个项目、
|
|
45
|
+
<b>{stats.sourceCount}</b> 种工具、
|
|
46
|
+
<b>{stats.activeDays}</b> 个活跃天上
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="stat-strip">
|
|
50
|
+
<div className="stat-cell">
|
|
51
|
+
<div className="l">最高单日</div>
|
|
52
|
+
<div className="v">{stats.peakDay ? U.compactCN(stats.peakDay.total) : '—'}</div>
|
|
53
|
+
<div className="s">{stats.peakDay ? stats.peakDay.date : '—'}</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div className="stat-cell">
|
|
56
|
+
<div className="l">最常用工具</div>
|
|
57
|
+
<div className="v" style={{color: 'var(--ink)'}}>
|
|
58
|
+
{stats.topTool ? stats.topTool.short : '—'}
|
|
59
|
+
</div>
|
|
60
|
+
<div className="s">
|
|
61
|
+
<span style={{display: 'inline-flex', alignItems: 'center', gap: 5}}>
|
|
62
|
+
<span style={{width:6, height:6, borderRadius:'50%', background: U.getSourceColor(stats.topTool?.key)}}/>
|
|
63
|
+
{stats.topTool ? `${stats.topTool.share.toFixed(0)}% · ${U.compactCN(stats.topTool.totalTokens)}` : '—'}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div className="stat-cell">
|
|
68
|
+
<div className="l">缓存命中率</div>
|
|
69
|
+
<div className="v">{totals.cacheHitRate.toFixed(0)}<span style={{fontSize: 18, color: 'var(--ink-3)'}}>%</span></div>
|
|
70
|
+
<div className="s">节省约 {Math.round(totals.cacheHitRate / 5)}× 重复计算</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="stat-cell">
|
|
73
|
+
<div className="l">日均官方价</div>
|
|
74
|
+
<div className="v">{U.fmtUS.format(stats.avgDailyCost)}</div>
|
|
75
|
+
<div className="s">{stats.activeDays} 天 / {U.fmtUS.format(totals.cost)}</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ───────────────────────────────────────────────────────────────
|
|
83
|
+
// Project bars
|
|
84
|
+
// ───────────────────────────────────────────────────────────────
|
|
85
|
+
function ProjectSection({ daily, totalTokens }) {
|
|
86
|
+
const agg = useMemo(() => {
|
|
87
|
+
const list = RU.aggregateBy(daily, 'projectPath').sort((a, b) => b.totalTokens - a.totalTokens);
|
|
88
|
+
if (list.length <= 8) return list;
|
|
89
|
+
const top = list.slice(0, 7);
|
|
90
|
+
const rest = list.slice(7);
|
|
91
|
+
const restTotal = rest.reduce((s, x) => s + x.totalTokens, 0);
|
|
92
|
+
const restCost = rest.reduce((s, x) => s + x.costUSD, 0);
|
|
93
|
+
return [...top, {
|
|
94
|
+
key: '(其他 ' + rest.length + ' 个项目)',
|
|
95
|
+
totalTokens: restTotal,
|
|
96
|
+
costUSD: restCost,
|
|
97
|
+
dayCount: 0,
|
|
98
|
+
isRest: true
|
|
99
|
+
}];
|
|
100
|
+
}, [daily]);
|
|
101
|
+
|
|
102
|
+
const max = agg[0]?.totalTokens || 1;
|
|
103
|
+
const maxCost = Math.max(...agg.map(a => a.costUSD), 1);
|
|
104
|
+
|
|
105
|
+
const narrative = useMemo(() => RU.narrativeForProjects(agg, totalTokens, daily), [agg, totalTokens, daily]);
|
|
106
|
+
|
|
107
|
+
if (!agg.length) return null;
|
|
108
|
+
|
|
109
|
+
const colorFor = (cost) => {
|
|
110
|
+
// shade: more cost = deeper indigo
|
|
111
|
+
const t = Math.min(1, cost / maxCost);
|
|
112
|
+
const L = 0.78 - t * 0.30;
|
|
113
|
+
const C = 0.05 + t * 0.13;
|
|
114
|
+
return `oklch(${L} ${C} 265)`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<section className="story">
|
|
119
|
+
<div className="section-label">02 · 项目</div>
|
|
120
|
+
<h2 className="section-title">按项目看,钱主要花在这里</h2>
|
|
121
|
+
<p className="section-sub">条形长度代表 token 占比,颜色深浅代表官方价大小。Token 与官方价并不总是同步——重读 cache 的项目消耗大但成本低。</p>
|
|
122
|
+
|
|
123
|
+
<div className="proj-list">
|
|
124
|
+
{agg.map((p, i) => {
|
|
125
|
+
const pct = (p.totalTokens / (totalTokens || 1)) * 100;
|
|
126
|
+
return (
|
|
127
|
+
<div key={p.key} className="proj-row">
|
|
128
|
+
<div className="proj-rank">{String(i + 1).padStart(2, '0')}</div>
|
|
129
|
+
<div className="proj-body">
|
|
130
|
+
<div className="proj-head">
|
|
131
|
+
<span className="proj-name" title={p.key}>{p.key}</span>
|
|
132
|
+
<span className="proj-meta">
|
|
133
|
+
{!p.isRest && p.dayCount + ' 活跃天 · '}
|
|
134
|
+
{pct.toFixed(1)}%
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div className="proj-bar">
|
|
138
|
+
<div className="proj-bar-fill"
|
|
139
|
+
style={{
|
|
140
|
+
width: `${(p.totalTokens / max) * 100}%`,
|
|
141
|
+
background: colorFor(p.costUSD)
|
|
142
|
+
}}/>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="proj-val">
|
|
146
|
+
<div className="big">{U.compactCN(p.totalTokens)}</div>
|
|
147
|
+
<div className="small">{p.costUSD > 0 ? U.fmtUS.format(p.costUSD) : '—'}</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{narrative && (
|
|
155
|
+
<div className="pullquote">
|
|
156
|
+
<code>{narrative.top.key}</code> 是这个周期消耗最大的项目,占总量的 <b>{narrative.share.toFixed(0)}%</b>,
|
|
157
|
+
主要使用 <code>{narrative.topModel}</code> 模型,cache 命中率 <b>{narrative.cacheRate}%</b>
|
|
158
|
+
{narrative.cacheRate > 60 ? '——说明你在这个项目里反复在同一上下文工作。'
|
|
159
|
+
: narrative.cacheRate < 30 ? '——上下文切换频繁,每次都重新喂入大量信息。'
|
|
160
|
+
: ',上下文复用程度中等。'}
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</section>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ───────────────────────────────────────────────────────────────
|
|
168
|
+
// Calendar heatmap
|
|
169
|
+
// ───────────────────────────────────────────────────────────────
|
|
170
|
+
function CalendarSection({ daily, period }) {
|
|
171
|
+
const days = useMemo(() => RU.dailyTotals(daily, period), [daily, period]);
|
|
172
|
+
const dayMap = useMemo(() => new Map(days.map(d => [d.date, d])), [days]);
|
|
173
|
+
const months = useMemo(() => RU.monthsInPeriod(period), [period]);
|
|
174
|
+
const max = useMemo(() => Math.max(...days.map(d => d.total), 1), [days]);
|
|
175
|
+
|
|
176
|
+
const peaks = useMemo(() => RU.findPeaks(days, 3), [days]);
|
|
177
|
+
|
|
178
|
+
const [tip, setTip] = useState(null);
|
|
179
|
+
const onEnter = (e, cell) => {
|
|
180
|
+
if (!cell || cell.total === 0) return;
|
|
181
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
182
|
+
const topTool = Object.entries(cell.byTool).sort((a, b) => b[1] - a[1])[0];
|
|
183
|
+
setTip({
|
|
184
|
+
x: rect.left + rect.width / 2,
|
|
185
|
+
y: rect.top,
|
|
186
|
+
date: cell.date,
|
|
187
|
+
total: cell.total,
|
|
188
|
+
tool: topTool ? topTool[0] : null,
|
|
189
|
+
toolShare: topTool ? (topTool[1] / cell.total) * 100 : 0
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
const onLeave = () => setTip(null);
|
|
193
|
+
|
|
194
|
+
const DOW = ['日', '一', '二', '三', '四', '五', '六'];
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<section className="story">
|
|
198
|
+
<div className="section-label">03 · 时间线</div>
|
|
199
|
+
<h2 className="section-title">用量高峰出现在这几天</h2>
|
|
200
|
+
<p className="section-sub">日历视图按天展示 token 消耗,颜色越深消耗越大。鼠标悬停可以看当天明细。</p>
|
|
201
|
+
|
|
202
|
+
<div className="calendar">
|
|
203
|
+
<div className="cal-months">
|
|
204
|
+
{months.map(({ year, month }) => {
|
|
205
|
+
const cells = RU.buildMonthGrid(year, month, dayMap);
|
|
206
|
+
const weekCount = cells.length / 7;
|
|
207
|
+
return (
|
|
208
|
+
<div className="cal-month" key={`${year}-${month}`}>
|
|
209
|
+
<div className="cal-month-label">{year} · {String(month + 1).padStart(2, '0')}</div>
|
|
210
|
+
<div className="cal-weeks" style={{
|
|
211
|
+
gridTemplateColumns: 'repeat(7, 14px)',
|
|
212
|
+
gridTemplateRows: `12px repeat(${weekCount}, 14px)`
|
|
213
|
+
}}>
|
|
214
|
+
{DOW.map((d, i) => <div key={i} className="cal-dow">{d}</div>)}
|
|
215
|
+
{cells.map((c, i) => (
|
|
216
|
+
<div key={i} className="cal-cell"
|
|
217
|
+
style={{
|
|
218
|
+
background: c ? RU.heatColor(c.total / max) : 'transparent',
|
|
219
|
+
visibility: c ? 'visible' : 'hidden'
|
|
220
|
+
}}
|
|
221
|
+
onMouseEnter={c ? (e) => onEnter(e, c) : undefined}
|
|
222
|
+
onMouseLeave={onLeave}/>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
<div className="cal-scale">
|
|
230
|
+
少
|
|
231
|
+
<span className="cal-scale-cells">
|
|
232
|
+
{[0, 0.15, 0.3, 0.5, 0.75, 1].map((t, i) => (
|
|
233
|
+
<span key={i} style={{background: RU.heatColor(t)}}/>
|
|
234
|
+
))}
|
|
235
|
+
</span>
|
|
236
|
+
多
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{tip && (
|
|
241
|
+
<div className="cal-tip" style={{left: tip.x, top: tip.y}}>
|
|
242
|
+
<b>{tip.date}</b>
|
|
243
|
+
<span style={{margin: '0 6px'}}>·</span>
|
|
244
|
+
{U.compactCN(tip.total)} tokens
|
|
245
|
+
{tip.tool && (
|
|
246
|
+
<span className="dim">主要 · {tip.tool} ({tip.toolShare.toFixed(0)}%)</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{peaks.length > 0 && (
|
|
252
|
+
<div className="peaks">
|
|
253
|
+
{peaks.map((p, i) => {
|
|
254
|
+
const topTool = Object.entries(p.byTool).sort((a, b) => b[1] - a[1])[0];
|
|
255
|
+
const topProject = RU.aggregateBy(daily.filter(r => r.usageDate === p.date), 'projectPath')
|
|
256
|
+
.sort((a, b) => b.totalTokens - a.totalTokens)[0];
|
|
257
|
+
return (
|
|
258
|
+
<div className="peak-row" key={p.date}>
|
|
259
|
+
<div className="peak-rank">{String(i + 1).padStart(2, '0')}</div>
|
|
260
|
+
<div className="peak-date">{p.date}</div>
|
|
261
|
+
<div className="peak-detail">
|
|
262
|
+
{topProject && (
|
|
263
|
+
<>主要项目 <b>{topProject.key}</b><span className="arrow">→</span></>
|
|
264
|
+
)}
|
|
265
|
+
{topTool && <>工具 <b>{topTool[0]}</b></>}
|
|
266
|
+
</div>
|
|
267
|
+
<div className="peak-total">{U.compactCN(p.total)}</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
})}
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</section>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { HeroSection, ProjectSection, CalendarSection };
|