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/README.md +54 -38
- package/index.js +128 -48
- package/lib/aggregate.js +40 -6
- package/lib/cache.js +8 -0
- package/lib/git.js +75 -20
- package/lib/parsers/claude.js +321 -316
- package/lib/parsers/codex.js +360 -316
- package/lib/parsers/opencode.js +236 -216
- package/lib/record-utils.js +36 -35
- package/lib/report.js +53 -16
- package/lib/server.js +191 -30
- package/package.json +1 -1
- package/public/api.js +6 -0
- package/public/app.js +827 -636
- package/public/charts.js +285 -95
- package/public/config.js +22 -21
- package/public/git-insights.js +39 -113
- package/public/index.html +728 -341
- package/public/style.css +829 -1701
- package/public/ui-state.js +8 -67
- package/public/utils.js +10 -0
- package/public/work-report.js +1 -22
package/public/charts.js
CHANGED
|
@@ -1,30 +1,59 @@
|
|
|
1
|
-
import { COLORS, SCENARIO_COLORS, COMMIT_TYPE_COLORS
|
|
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
|
-
|
|
5
|
-
export function
|
|
4
|
+
/* ── Work Type Pie (doughnut with inner radius) ── */
|
|
5
|
+
export function renderWorkTypePie(canvasId, entries) {
|
|
6
6
|
destroyChart(canvasId);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
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
|
|
16
|
-
datasets: [{
|
|
21
|
+
labels,
|
|
22
|
+
datasets: [{
|
|
23
|
+
data,
|
|
24
|
+
backgroundColor: colors,
|
|
25
|
+
borderWidth: 0,
|
|
26
|
+
hoverOffset: 4,
|
|
27
|
+
}],
|
|
17
28
|
},
|
|
18
29
|
options: {
|
|
19
|
-
responsive: true,
|
|
30
|
+
responsive: true,
|
|
31
|
+
maintainAspectRatio: false,
|
|
32
|
+
cutout: '65%',
|
|
20
33
|
plugins: {
|
|
21
|
-
legend: {
|
|
22
|
-
tooltip: {
|
|
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
|
|
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,
|
|
36
|
-
return instance;
|
|
64
|
+
setChart(canvasId, chart);
|
|
37
65
|
}
|
|
38
66
|
|
|
39
|
-
|
|
40
|
-
export function
|
|
67
|
+
/* ── Project Bars (horizontal bar, minimal style) ── */
|
|
68
|
+
export function renderProjectBars(canvasId, entries) {
|
|
41
69
|
destroyChart(canvasId);
|
|
42
|
-
const
|
|
43
|
-
|
|
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: {
|
|
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,
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
53
|
-
return instance;
|
|
140
|
+
setChart(canvasId, chart);
|
|
54
141
|
}
|
|
55
142
|
|
|
56
|
-
|
|
57
|
-
export function
|
|
58
|
-
destroyChart(
|
|
59
|
-
const canvas = document.getElementById(
|
|
60
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
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
|
-
{
|
|
94
|
-
|
|
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,
|
|
200
|
+
responsive: true,
|
|
201
|
+
maintainAspectRatio: false,
|
|
202
|
+
interaction: { mode: 'index', intersect: false },
|
|
99
203
|
scales: {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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(
|
|
108
|
-
return instance;
|
|
231
|
+
setChart(canvasId, chart);
|
|
109
232
|
}
|
|
110
233
|
|
|
111
|
-
|
|
112
|
-
export function
|
|
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
|
|
238
|
+
if (!canvas) return;
|
|
126
239
|
const ctx = canvas.getContext('2d');
|
|
127
|
-
const
|
|
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: [
|
|
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,
|
|
258
|
+
responsive: true,
|
|
259
|
+
maintainAspectRatio: false,
|
|
260
|
+
indexAxis: 'y',
|
|
135
261
|
scales: {
|
|
136
|
-
x: {
|
|
137
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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,
|
|
154
|
-
|
|
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
|
-
'编码': '#
|
|
25
|
-
'测试/QA': '#
|
|
26
|
-
'调试/排错': '#
|
|
27
|
-
'文档': '#
|
|
28
|
-
'阅读/研究': '#
|
|
29
|
-
'规划/设计': '#
|
|
30
|
-
'代码审查': '#
|
|
31
|
-
'其他': '#
|
|
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: '#
|
|
37
|
-
fix: '#
|
|
38
|
-
refactor: '#
|
|
39
|
-
docs: '#
|
|
40
|
-
test: '#
|
|
41
|
-
chore: '#
|
|
42
|
-
perf: '#
|
|
43
|
-
style: '#
|
|
44
|
-
ci: '#
|
|
45
|
-
build: '#
|
|
46
|
-
revert: '#
|
|
47
|
-
other: '#
|
|
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: '
|
|
139
|
+
THEME: 'lc-theme',
|
|
139
140
|
SIDEBAR_COLLAPSED: 'ccusage-sidebar-collapsed',
|
|
140
141
|
};
|