lumencode 0.4.4 → 1.1.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/public/charts.js CHANGED
@@ -1,30 +1,59 @@
1
- import { COLORS, SCENARIO_COLORS, COMMIT_TYPE_COLORS, TEXT } from './config.js';
1
+ import { COLORS, SCENARIO_COLORS, COMMIT_TYPE_COLORS } from './config.js';
2
2
  import { esc, fmt, fmtShort, destroyChart, setChart } from './utils.js';
3
3
 
4
- // ── Doughnut ──
5
- export function renderDoughnut(canvasId, dataMap, label) {
4
+ /* ── Work Type Pie (doughnut with inner radius) ── */
5
+ export function renderWorkTypePie(canvasId, entries) {
6
6
  destroyChart(canvasId);
7
- const entries = Object.entries(dataMap).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
8
- const wrap = document.getElementById(canvasId).parentElement;
9
- wrap.style.height = '260px';
10
- const ctx = document.getElementById(canvasId).getContext('2d');
11
- const colors = entries.map(([k]) => SCENARIO_COLORS[k] || COLORS[entries.length % COLORS.length]);
12
- const instance = new Chart(ctx, {
7
+ const canvas = document.getElementById(canvasId);
8
+ if (!canvas) return;
9
+ const ctx = canvas.getContext('2d');
10
+ const isDark = document.documentElement.classList.contains('dark');
11
+ const ttBg = isDark ? '#1f222a' : '#f0eee7';
12
+ const ttFg = isDark ? '#e8e9ef' : '#15151a';
13
+
14
+ const labels = entries.map(([k]) => k);
15
+ const data = entries.map(([, v]) => v);
16
+ const colors = labels.map(k => SCENARIO_COLORS[k] || '#888');
17
+
18
+ const chart = new Chart(ctx, {
13
19
  type: 'doughnut',
14
20
  data: {
15
- labels: entries.map(([k]) => k),
16
- datasets: [{ data: entries.map(([, v]) => v), backgroundColor: colors, borderWidth: 0, hoverOffset: 4 }],
21
+ labels,
22
+ datasets: [{
23
+ data,
24
+ backgroundColor: colors,
25
+ borderWidth: 0,
26
+ hoverOffset: 4,
27
+ }],
17
28
  },
18
29
  options: {
19
- responsive: true, maintainAspectRatio: false, cutout: '65%',
30
+ responsive: true,
31
+ maintainAspectRatio: false,
32
+ cutout: '65%',
20
33
  plugins: {
21
- legend: { position: 'bottom', labels: { font: { family: 'Inter', size: 11 }, padding: 12, boxWidth: 10, usePointStyle: true, pointStyle: 'circle' } },
22
- tooltip: { callbacks: { label: (c) => { const total = c.dataset.data.reduce((s, v) => s + v, 0); return ` ${c.label}: ${c.raw} (${((c.raw / total) * 100).toFixed(1)}%)`; } } },
34
+ legend: { display: false },
35
+ tooltip: {
36
+ backgroundColor: ttBg,
37
+ titleColor: ttFg,
38
+ bodyColor: ttFg,
39
+ borderColor: isDark ? 'rgba(232,233,239,0.12)' : 'rgba(21,21,26,0.12)',
40
+ borderWidth: 1,
41
+ borderWidth: 0,
42
+ cornerRadius: 0,
43
+ padding: 8,
44
+ titleFont: { family: 'JetBrains Mono', size: 11 },
45
+ bodyFont: { family: 'JetBrains Mono', size: 11 },
46
+ callbacks: {
47
+ label: (c) => {
48
+ const total = c.dataset.data.reduce((s, v) => s + v, 0);
49
+ return ` ${c.label}: ${c.raw} (${((c.raw / total) * 100).toFixed(1)}%)`;
50
+ },
51
+ },
52
+ },
23
53
  },
24
54
  onClick: (evt, elements) => {
25
55
  if (elements.length === 0) return;
26
- const clickEntries = Object.entries(dataMap).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
27
- const label = clickEntries[elements[0].index]?.[0];
56
+ const label = entries[elements[0].index]?.[0];
28
57
  const scenarioKeyMap = { '编码': 'coding', '测试/QA': 'testing', '调试/排错': 'debugging', '文档': 'documentation', '阅读/研究': 'reading', '规划/设计': 'planning', '代码审查': 'review' };
29
58
  const key = scenarioKeyMap[label];
30
59
  if (!key) return;
@@ -32,124 +61,285 @@ export function renderDoughnut(canvasId, dataMap, label) {
32
61
  },
33
62
  },
34
63
  });
35
- setChart(canvasId, instance);
36
- return instance;
64
+ setChart(canvasId, chart);
37
65
  }
38
66
 
39
- // ── Bar ──
40
- export function renderBar(canvasId, labels, data, datasetLabel) {
67
+ /* ── Project Bars (horizontal bar, minimal style) ── */
68
+ export function renderProjectBars(canvasId, entries) {
41
69
  destroyChart(canvasId);
42
- const ctx = document.getElementById(canvasId).getContext('2d');
43
- const instance = new Chart(ctx, {
70
+ const canvas = document.getElementById(canvasId);
71
+ if (!canvas) return;
72
+ const ctx = canvas.getContext('2d');
73
+
74
+ const isDark = document.documentElement.classList.contains('dark');
75
+ const gridColor = isDark ? 'rgba(232,233,239,0.06)' : 'rgba(21,21,26,0.06)';
76
+ const tickColor = isDark ? 'rgba(232,233,239,0.55)' : 'rgba(21,21,26,0.55)';
77
+ const barColor = isDark ? '#e8e9ef' : '#15151a';
78
+ const accentColor = isDark ? '#7480e8' : '#4a52a8';
79
+ const ttBg = isDark ? '#1f222a' : '#f0eee7';
80
+ const ttFg = isDark ? '#e8e9ef' : '#15151a';
81
+
82
+ const chart = new Chart(ctx, {
44
83
  type: 'bar',
45
- data: { labels, datasets: [{ label: datasetLabel, data, backgroundColor: '#374151', borderRadius: 6, maxBarThickness: 20, barPercentage: 0.7 }] },
84
+ data: {
85
+ labels: entries.map(([k]) => k.length > 20 ? '...' + k.slice(-17) : k),
86
+ datasets: [{
87
+ data: entries.map(([, v]) => v.requests),
88
+ backgroundColor: entries.map((_, i) => i === 0 ? accentColor : barColor),
89
+ borderRadius: 4,
90
+ maxBarThickness: 14,
91
+ barPercentage: 0.7,
92
+ }],
93
+ },
46
94
  options: {
47
- responsive: true, maintainAspectRatio: false, indexAxis: 'y',
48
- scales: { x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 } } }, y: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } } },
49
- plugins: { legend: { display: false } },
95
+ responsive: true,
96
+ maintainAspectRatio: false,
97
+ indexAxis: 'y',
98
+ scales: {
99
+ x: {
100
+ grid: { color: gridColor, drawBorder: false },
101
+ ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: tickColor },
102
+ border: { display: false },
103
+ },
104
+ y: {
105
+ grid: { display: false },
106
+ ticks: { font: { family: 'JetBrains Mono', size: 11 }, color: tickColor },
107
+ border: { display: false },
108
+ },
109
+ },
110
+ plugins: {
111
+ legend: { display: false },
112
+ tooltip: {
113
+ backgroundColor: ttBg,
114
+ titleColor: ttFg,
115
+ bodyColor: ttFg,
116
+ borderColor: isDark ? 'rgba(232,233,239,0.12)' : 'rgba(21,21,26,0.12)',
117
+ borderWidth: 1,
118
+ cornerRadius: 4,
119
+ padding: 8,
120
+ titleFont: { family: 'JetBrains Mono', size: 11 },
121
+ bodyFont: { family: 'JetBrains Mono', size: 11 },
122
+ },
123
+ },
124
+ onClick: async (evt, elements) => {
125
+ if (elements.length === 0) return;
126
+ const project = entries[elements[0].index][0];
127
+ showDrill(esc(project), '<div class="drill-empty">加载中...</div>');
128
+ try {
129
+ const appEl = document.querySelector('[x-data]');
130
+ const app = appEl?._x_dataStack?.[0];
131
+ const params = { project, period: app?.period || 'daily', date: app?.currentDate || new Date().toISOString().slice(0, 10) };
132
+ if (app?.activeTool && app.activeTool !== 'all') params.tool = app.activeTool;
133
+ const { fetchSessions } = await import('./api.js');
134
+ const rows = await fetchSessions(params);
135
+ renderSessionDrill(project, rows);
136
+ } catch { showDrill(esc(project), '<div class="drill-empty">加载失败</div>'); }
137
+ },
50
138
  },
51
139
  });
52
- setChart(canvasId, instance);
53
- return instance;
140
+ setChart(canvasId, chart);
54
141
  }
55
142
 
56
- // ── Commit Type ──
57
- export function renderCommitTypeChart(typeEntries) {
58
- destroyChart('commitTypeChart');
59
- const canvas = document.getElementById('commitTypeChart');
60
- const wrap = canvas.parentElement;
61
- wrap.style.height = Math.max(180, typeEntries.length * 32 + 40) + 'px';
62
- const labels = typeEntries.map(([k]) => k);
63
- const data = typeEntries.map(([, v]) => v);
64
- const colors = labels.map(k => COMMIT_TYPE_COLORS[k] || COMMIT_TYPE_COLORS.other);
143
+ /* ── Timeline Area Chart ── */
144
+ export function renderTimelineArea(canvasId, trendData) {
145
+ destroyChart(canvasId);
146
+ const canvas = document.getElementById(canvasId);
147
+ if (!canvas) return;
65
148
  const ctx = canvas.getContext('2d');
66
- const instance = new Chart(ctx, {
67
- type: 'bar',
68
- data: { labels, datasets: [{ label: '提交数', data, backgroundColor: colors, borderRadius: 6, maxBarThickness: 20, barPercentage: 0.65, categoryPercentage: 0.85 }] },
69
- options: {
70
- responsive: true, maintainAspectRatio: false, indexAxis: 'y',
71
- scales: { x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, precision: 0 } }, y: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } } },
72
- plugins: { legend: { display: false } },
73
- },
74
- });
75
- setChart('commitTypeChart', instance);
76
- return instance;
77
- }
78
149
 
79
- // ── Trend (dual-axis line) ──
80
- export function renderTrend(trendData) {
81
- destroyChart('trendChart');
82
- const dates = Object.keys(trendData.dailyStats).sort();
83
- const requests = dates.map(d => trendData.dailyStats[d].requests);
84
- const tokens = dates.map(d => ((trendData.dailyStats[d].inputTokens || 0) + (trendData.dailyStats[d].outputTokens || 0)) / 1000);
150
+ const dates = Object.keys(trendData.dailyStats || {}).sort();
151
+ if (dates.length === 0) return;
152
+
153
+ const sessions = dates.map(d => trendData.dailyStats[d].requests || 0);
154
+ const tokens = dates.map(d => ((trendData.dailyStats[d].inputTokens || 0) + (trendData.dailyStats[d].outputTokens || 0)) / 1_000_000);
85
155
  const labels = dates.map(d => d.slice(5));
86
- const ctx = document.getElementById('trendChart');
87
- if (!ctx) return null;
88
- const instance = new Chart(ctx.getContext('2d'), {
156
+
157
+ const isDark = document.documentElement.classList.contains('dark');
158
+ const gridColor = isDark ? 'rgba(232,233,239,0.06)' : 'rgba(21,21,26,0.06)';
159
+ const tickColor = isDark ? 'rgba(232,233,239,0.55)' : 'rgba(21,21,26,0.55)';
160
+ const sessionColor = isDark ? '#e8e9ef' : '#15151a';
161
+ const ttBg = isDark ? '#1f222a' : '#f0eee7';
162
+ const ttFg = isDark ? '#e8e9ef' : '#15151a';
163
+
164
+ /* Create gradient for tokens area */
165
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height || 280);
166
+ gradient.addColorStop(0, 'rgba(116, 128, 232, 0.45)');
167
+ gradient.addColorStop(0.6, 'rgba(94, 194, 220, 0.18)');
168
+ gradient.addColorStop(1, 'rgba(94, 194, 168, 0)');
169
+
170
+ const chart = new Chart(ctx, {
89
171
  type: 'line',
90
172
  data: {
91
173
  labels,
92
174
  datasets: [
93
- { label: '请求数', data: requests, borderColor: '#111111', backgroundColor: 'rgba(17,17,17,0.08)', fill: true, tension: 0.3, pointRadius: 3, yAxisID: 'y' },
94
- { label: 'Token (K)', data: tokens, borderColor: '#8b5cf6', backgroundColor: 'rgba(139,92,246,0.08)', fill: true, tension: 0.3, pointRadius: 3, yAxisID: 'y1' },
175
+ {
176
+ label: 'Tokens (M)',
177
+ data: tokens,
178
+ borderColor: '#7480e8',
179
+ backgroundColor: gradient,
180
+ fill: true,
181
+ tension: 0.4,
182
+ pointRadius: 0,
183
+ pointHoverRadius: 4,
184
+ borderWidth: 1.4,
185
+ },
186
+ {
187
+ label: 'Sessions',
188
+ data: sessions,
189
+ borderColor: sessionColor,
190
+ backgroundColor: 'transparent',
191
+ fill: false,
192
+ tension: 0.4,
193
+ pointRadius: 0,
194
+ pointHoverRadius: 4,
195
+ borderWidth: 1.5,
196
+ },
95
197
  ],
96
198
  },
97
199
  options: {
98
- responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false },
200
+ responsive: true,
201
+ maintainAspectRatio: false,
202
+ interaction: { mode: 'index', intersect: false },
99
203
  scales: {
100
- y: { position: 'left', grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 } }, title: { display: true, text: '请求数', font: { family: 'Inter', size: 12 } } },
101
- y1: { position: 'right', grid: { display: false }, ticks: { font: { family: 'Inter', size: 11 } }, title: { display: true, text: 'Token (K)', font: { family: 'Inter', size: 12 } } },
102
- x: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 11 } } },
204
+ x: {
205
+ grid: { color: gridColor, borderDash: [2, 4], drawBorder: false },
206
+ ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: tickColor, maxTicksLimit: 12 },
207
+ border: { display: false },
208
+ },
209
+ y: {
210
+ grid: { color: gridColor, drawBorder: false },
211
+ ticks: { font: { family: 'JetBrains Mono', size: 10 }, color: tickColor },
212
+ border: { display: false },
213
+ },
214
+ },
215
+ plugins: {
216
+ legend: { display: false },
217
+ tooltip: {
218
+ backgroundColor: ttBg,
219
+ titleColor: ttFg,
220
+ bodyColor: ttFg,
221
+ borderColor: isDark ? 'rgba(232,233,239,0.12)' : 'rgba(21,21,26,0.12)',
222
+ borderWidth: 1,
223
+ cornerRadius: 4,
224
+ padding: 8,
225
+ titleFont: { family: 'JetBrains Mono', size: 11 },
226
+ bodyFont: { family: 'JetBrains Mono', size: 11 },
227
+ },
103
228
  },
104
- plugins: { legend: { position: 'top', labels: { font: { family: 'Inter', size: 12 }, padding: 16 } } },
105
229
  },
106
230
  });
107
- setChart('trendChart', instance);
108
- return instance;
231
+ setChart(canvasId, chart);
109
232
  }
110
233
 
111
- // ── Model Cost ──
112
- export function renderModelCostChart(canvasId, models, costBreakdown) {
113
- if (!costBreakdown?.models?.length) { destroyChart(canvasId); return null; }
114
-
115
- const entries = costBreakdown.models.filter(m => m.cost > 0);
116
- if (entries.length === 0) { destroyChart(canvasId); return null; }
117
-
118
- const labels = entries.map(m => m.name.length > 22 ? '...' + m.name.slice(-19) : m.name);
119
- const data = entries.map(m => m.cost);
120
- const modeColors = { actual: '#22c55e', estimated: '#3b82f6', unknown: '#9ca3af' };
121
- const colors = entries.map(m => modeColors[m.mode] || modeColors.unknown);
122
-
234
+ /* ── Cache Stack (simple bar) ── */
235
+ export function renderCacheStack(canvasId, cacheRead, cacheCreate, inputTokens) {
123
236
  destroyChart(canvasId);
124
237
  const canvas = document.getElementById(canvasId);
125
- if (!canvas) return null;
238
+ if (!canvas) return;
126
239
  const ctx = canvas.getContext('2d');
127
- const instance = new Chart(ctx, {
240
+ const isDark = document.documentElement.classList.contains('dark');
241
+ const ttBg = isDark ? '#1f222a' : '#f0eee7';
242
+ const ttFg = isDark ? '#e8e9ef' : '#15151a';
243
+ const forestColor = isDark ? '#5ec2a8' : '#3d7558';
244
+ const rustColor = isDark ? '#7480e8' : '#4a52a8';
245
+ const ochreColor = isDark ? '#c9a86b' : '#9a7836';
246
+
247
+ const chart = new Chart(ctx, {
128
248
  type: 'bar',
129
249
  data: {
130
- labels,
131
- datasets: [{ label: '费用 ($)', data, backgroundColor: colors, borderRadius: 6, maxBarThickness: 20, barPercentage: 0.7 }],
250
+ labels: ['Token 构成'],
251
+ datasets: [
252
+ { label: '缓存命中', data: [cacheRead], backgroundColor: forestColor, borderRadius: 4 },
253
+ { label: '新输入', data: [inputTokens], backgroundColor: rustColor, borderRadius: 4 },
254
+ { label: '缓存写入', data: [cacheCreate], backgroundColor: ochreColor, borderRadius: 4 },
255
+ ],
132
256
  },
133
257
  options: {
134
- responsive: true, maintainAspectRatio: false, indexAxis: 'y',
258
+ responsive: true,
259
+ maintainAspectRatio: false,
260
+ indexAxis: 'y',
135
261
  scales: {
136
- x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, callback: v => '$' + v.toFixed(2) } },
137
- y: { grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } },
262
+ x: {
263
+ stacked: true,
264
+ grid: { display: false },
265
+ ticks: { display: false },
266
+ border: { display: false },
267
+ },
268
+ y: {
269
+ stacked: true,
270
+ grid: { display: false },
271
+ ticks: { display: false },
272
+ border: { display: false },
273
+ },
138
274
  },
139
275
  plugins: {
140
276
  legend: { display: false },
141
277
  tooltip: {
142
- callbacks: {
143
- label: (c) => {
144
- const entry = entries[c.dataIndex];
145
- const modeLabel = { actual: '实际计费', estimated: '估算', unknown: '未知定价' };
146
- return ` $${c.raw.toFixed(2)} (${modeLabel[entry?.mode] || entry?.mode}, ${entry?.requests || 0} 次)`;
147
- },
148
- },
278
+ backgroundColor: ttBg,
279
+ titleColor: ttFg,
280
+ bodyColor: ttFg,
281
+ borderColor: isDark ? 'rgba(232,233,239,0.12)' : 'rgba(21,21,26,0.12)',
282
+ borderWidth: 1,
283
+ cornerRadius: 4,
284
+ padding: 8,
149
285
  },
150
286
  },
151
287
  },
152
288
  });
153
- setChart(canvasId, instance);
154
- return instance;
289
+ setChart(canvasId, chart);
290
+ }
291
+
292
+ /* ── Model Bars (rendered as HTML, not Chart.js, but kept for compatibility) ── */
293
+ export function renderModelBars(containerId, entries) {
294
+ /* This is rendered via Alpine reactive data in app.js */
295
+ }
296
+
297
+ /* ── Drill helpers ── */
298
+ function showDrill(title, html) {
299
+ const modal = document.getElementById('drillModal');
300
+ const t = document.getElementById('drillTitle');
301
+ const b = document.getElementById('drillBody');
302
+ if (t) t.textContent = title;
303
+ if (b) b.innerHTML = html;
304
+ if (modal) modal.style.display = 'flex';
305
+ }
306
+
307
+ function renderSessionDrill(project, rows) {
308
+ if (!rows.length) { showDrill(esc(project), '<div class="drill-empty">无数据</div>'); return; }
309
+ const html = '<table class="drill-table">'
310
+ + '<tr><th></th><th>会话ID</th><th>开始</th><th>时长</th><th>请求</th><th>工具</th><th>文件</th><th>提交</th></tr>'
311
+ + rows.map((r, i) => {
312
+ const start = r.startTime ? r.startTime.slice(0, 16).replace('T', ' ') : '-';
313
+ const dur = r.duration ? (r.duration >= 3600 ? (r.duration / 3600).toFixed(1) + 'h' : r.duration >= 60 ? Math.round(r.duration / 60) + 'm' : r.duration + 's') : '-';
314
+ const cn = r.commits?.length || 0;
315
+ const toggle = cn > 0 ? `<button class="commit-toggle" data-idx="${i}">▸</button>` : '';
316
+ const tools = [...new Set(r.toolSequence || [])].slice(0, 3).join(', ');
317
+ const fileCount = r.touchedFileCount || 0;
318
+ const commitRows = cn > 0
319
+ ? `<tr class="commit-subrow" data-idx="${i}" style="display:none;"><td colspan="8"><table class="commit-subtable">
320
+ <tr><th>hash</th><th>type</th><th>subject</th><th class="num">+行</th><th class="num">-行</th><th>AI</th><th>证据</th></tr>
321
+ ${r.commits.map(c => `<tr>
322
+ <td class="hash"><code>${c.hash.slice(0,7)}</code></td>
323
+ <td><span class="commit-type-tag type-${c.type}">${c.type}</span></td>
324
+ <td class="commit-subject" title="${esc(c.subject)}">${esc(c.subject)}</td>
325
+ <td class="num pos">+${fmt(c.linesAdded || 0)}</td>
326
+ <td class="num neg">-${fmt(c.linesDeleted || 0)}</td>
327
+ <td>${c.aiConfidence === 'high' ? 'H' : c.aiConfidence === 'medium' ? 'M' : c.aiConfidence === 'low' ? 'L' : ''}</td>
328
+ <td>${c.aiEvidenceDetails?.matchedFileCount ? `文件交集 ${c.aiEvidenceDetails.matchedFileCount}` : (c.attributionType || '')}</td>
329
+ </tr>`).join('')}
330
+ </table></td></tr>`
331
+ : '';
332
+ return `<tr><td>${toggle}</td><td class="drill-text" title="${esc(r.id)}">${esc(r.id)}</td><td>${start}</td><td>${dur}</td><td>${r.requests || '-'}</td><td class="drill-text">${tools || '-'}</td><td>${fileCount || '-'}</td><td>${cn || '-'}</td></tr>${commitRows}`;
333
+ }).join('')
334
+ + '</table>';
335
+ showDrill(esc(project) + ' 会话记录', html);
336
+ document.querySelectorAll('.commit-toggle').forEach(btn => {
337
+ btn.addEventListener('click', () => {
338
+ const idx = btn.dataset.idx;
339
+ const sub = document.querySelector(`.commit-subrow[data-idx="${idx}"]`);
340
+ const open = sub.style.display !== 'none';
341
+ sub.style.display = open ? 'none' : '';
342
+ btn.textContent = open ? '▸' : '▾';
343
+ });
344
+ });
155
345
  }
package/public/config.js CHANGED
@@ -21,30 +21,30 @@ export const COLORS = [
21
21
 
22
22
  // 场景色板
23
23
  export const SCENARIO_COLORS = {
24
- '编码': '#8ab8a0',
25
- '测试/QA': '#c8b880',
26
- '调试/排错': '#c49090',
27
- '文档': '#90a8c8',
28
- '阅读/研究': '#a090c0',
29
- '规划/设计': '#c8a080',
30
- '代码审查': '#80b8b8',
31
- '其他': '#a8a8a8',
24
+ '编码': '#5ec2a8',
25
+ '测试/QA': '#c9b060',
26
+ '调试/排错': '#d47878',
27
+ '文档': '#78a8d4',
28
+ '阅读/研究': '#9878d0',
29
+ '规划/设计': '#d49060',
30
+ '代码审查': '#60b8b8',
31
+ '其他': '#989898',
32
32
  };
33
33
 
34
34
  // 提交类型色板
35
35
  export const COMMIT_TYPE_COLORS = {
36
- feat: '#8ab8a0',
37
- fix: '#c49090',
38
- refactor: '#a090c0',
39
- docs: '#90a8c8',
40
- test: '#c8b880',
41
- chore: '#a8a8a8',
42
- perf: '#c890b0',
43
- style: '#80b8b8',
44
- ci: '#c8a080',
45
- build: '#a8c880',
46
- revert: '#c49090',
47
- other: '#b8b8b8',
36
+ feat: '#5ec2a8',
37
+ fix: '#d47878',
38
+ refactor: '#9878d0',
39
+ docs: '#78a8d4',
40
+ test: '#c9b060',
41
+ chore: '#989898',
42
+ perf: '#d078a8',
43
+ style: '#60b8b8',
44
+ ci: '#d49060',
45
+ build: '#88c860',
46
+ revert: '#d47878',
47
+ other: '#a8a8a8',
48
48
  };
49
49
 
50
50
  // 中文字符串
@@ -124,6 +124,7 @@ export const ID = {
124
124
  TOOL_CHART: 'toolChart',
125
125
  TREND_CHART: 'trendChart',
126
126
  COMMIT_TYPE_CHART: 'commitTypeChart',
127
+ CACHE_CHART: 'cacheChart',
127
128
  MODEL_COST_CHART: 'modelCostChart',
128
129
  CFG_CLAUDE_DIR: 'cfgClaudeDir',
129
130
  CFG_REPOS: 'cfgRepos',
@@ -135,6 +136,6 @@ export const ID = {
135
136
  // localStorage keys
136
137
  export const STORAGE = {
137
138
  CONFIG: 'ccusage-config',
138
- THEME: 'ccusage-theme',
139
+ THEME: 'lc-theme',
139
140
  SIDEBAR_COLLAPSED: 'ccusage-sidebar-collapsed',
140
141
  };