lumencode 0.4.3 → 1.0.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,166 +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, {
44
- type: 'bar',
45
- data: { labels, datasets: [{ label: datasetLabel, data, backgroundColor: '#374151', borderRadius: 6, maxBarThickness: 20, barPercentage: 0.7 }] },
46
- 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 } },
50
- },
51
- });
52
- setChart(canvasId, instance);
53
- return instance;
54
- }
55
-
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);
70
+ const canvas = document.getElementById(canvasId);
71
+ if (!canvas) return;
65
72
  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
73
 
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);
85
- 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'), {
89
- type: 'line',
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, {
83
+ type: 'bar',
90
84
  data: {
91
- labels,
92
- 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' },
95
- ],
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
+ }],
96
93
  },
97
94
  options: {
98
- responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false },
95
+ responsive: true,
96
+ maintainAspectRatio: false,
97
+ indexAxis: 'y',
99
98
  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 } } },
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>'); }
103
137
  },
104
- plugins: { legend: { position: 'top', labels: { font: { family: 'Inter', size: 12 }, padding: 16 } } },
105
138
  },
106
139
  });
107
- setChart('trendChart', instance);
108
- return instance;
140
+ setChart(canvasId, chart);
109
141
  }
110
142
 
111
- // ── Cache Efficiency ──
112
- export function renderCacheEfficiency(canvasId, cacheRead, cacheCreate, inputTokens, costBreakdown) {
113
- const total = cacheRead + inputTokens + cacheCreate;
114
- if (total === 0) { destroyChart(canvasId); return null; }
115
-
143
+ /* ── Timeline Area Chart ── */
144
+ export function renderTimelineArea(canvasId, trendData) {
116
145
  destroyChart(canvasId);
117
146
  const canvas = document.getElementById(canvasId);
118
- if (!canvas) return null;
147
+ if (!canvas) return;
119
148
  const ctx = canvas.getContext('2d');
120
- const instance = new Chart(ctx, {
121
- type: 'bar',
149
+
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);
155
+ const labels = dates.map(d => d.slice(5));
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, {
171
+ type: 'line',
122
172
  data: {
123
- labels: ['Token 构成'],
173
+ labels,
124
174
  datasets: [
125
- { label: '缓存命中', data: [cacheRead], backgroundColor: '#22c55e', borderRadius: 4 },
126
- { label: '新输入', data: [inputTokens], backgroundColor: '#3b82f6', borderRadius: 4 },
127
- { label: '缓存写入', data: [cacheCreate], backgroundColor: '#f59e0b', borderRadius: 4 },
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
+ },
128
197
  ],
129
198
  },
130
199
  options: {
131
- responsive: true, maintainAspectRatio: false, indexAxis: 'y',
200
+ responsive: true,
201
+ maintainAspectRatio: false,
202
+ interaction: { mode: 'index', intersect: false },
132
203
  scales: {
133
- x: { stacked: true, grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, callback: v => fmtShort(v) } },
134
- y: { stacked: true, grid: { display: false }, ticks: { font: { family: 'Inter', size: 12 } } },
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
+ },
135
214
  },
136
215
  plugins: {
137
- legend: { position: 'bottom', labels: { font: { family: 'Inter', size: 11 }, padding: 12, boxWidth: 10, usePointStyle: true, pointStyle: 'circle' } },
216
+ legend: { display: false },
138
217
  tooltip: {
139
- callbacks: {
140
- label: (c) => {
141
- const pct = total > 0 ? ((c.raw / total) * 100).toFixed(1) : 0;
142
- return ` ${c.dataset.label}: ${fmtShort(c.raw)} (${pct}%)`;
143
- },
144
- },
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 },
145
227
  },
146
228
  },
147
229
  },
148
230
  });
149
- setChart(canvasId, instance);
150
- return instance;
231
+ setChart(canvasId, chart);
151
232
  }
152
233
 
153
- // ── Model Cost ──
154
- export function renderModelCostChart(canvasId, models, costBreakdown) {
155
- if (!costBreakdown?.models?.length) { destroyChart(canvasId); return null; }
156
-
157
- const entries = costBreakdown.models.filter(m => m.cost > 0);
158
- if (entries.length === 0) { destroyChart(canvasId); return null; }
159
-
160
- const labels = entries.map(m => m.name.length > 22 ? '...' + m.name.slice(-19) : m.name);
161
- const data = entries.map(m => m.cost);
162
- const modeColors = { actual: '#22c55e', estimated: '#3b82f6', unknown: '#9ca3af' };
163
- const colors = entries.map(m => modeColors[m.mode] || modeColors.unknown);
164
-
234
+ /* ── Cache Stack (simple bar) ── */
235
+ export function renderCacheStack(canvasId, cacheRead, cacheCreate, inputTokens) {
165
236
  destroyChart(canvasId);
166
237
  const canvas = document.getElementById(canvasId);
167
- if (!canvas) return null;
238
+ if (!canvas) return;
168
239
  const ctx = canvas.getContext('2d');
169
- 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, {
170
248
  type: 'bar',
171
249
  data: {
172
- labels,
173
- 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
+ ],
174
256
  },
175
257
  options: {
176
- responsive: true, maintainAspectRatio: false, indexAxis: 'y',
258
+ responsive: true,
259
+ maintainAspectRatio: false,
260
+ indexAxis: 'y',
177
261
  scales: {
178
- x: { grid: { color: '#f3f4f6' }, ticks: { font: { family: 'Inter', size: 11 }, callback: v => '$' + v.toFixed(2) } },
179
- 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
+ },
180
274
  },
181
275
  plugins: {
182
276
  legend: { display: false },
183
277
  tooltip: {
184
- callbacks: {
185
- label: (c) => {
186
- const entry = entries[c.dataIndex];
187
- const modeLabel = { actual: '实际计费', estimated: '估算', unknown: '未知定价' };
188
- return ` $${c.raw.toFixed(2)} (${modeLabel[entry?.mode] || entry?.mode}, ${entry?.requests || 0} 次)`;
189
- },
190
- },
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,
191
285
  },
192
286
  },
193
287
  },
194
288
  });
195
- setChart(canvasId, instance);
196
- 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
+ });
197
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
  // 中文字符串
@@ -136,6 +136,6 @@ export const ID = {
136
136
  // localStorage keys
137
137
  export const STORAGE = {
138
138
  CONFIG: 'ccusage-config',
139
- THEME: 'ccusage-theme',
139
+ THEME: 'lc-theme',
140
140
  SIDEBAR_COLLAPSED: 'ccusage-sidebar-collapsed',
141
141
  };