codebuddy-stats 1.2.11 → 1.3.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/dist/index.js +104 -712
- package/dist/lib/pricing.js +1 -0
- package/dist/tui/keybindings.js +293 -0
- package/dist/tui/state.js +24 -0
- package/dist/views/by-model.js +46 -0
- package/dist/views/by-project.js +46 -0
- package/dist/views/daily-detail.js +109 -0
- package/dist/views/daily.js +75 -0
- package/dist/views/monthly-detail.js +97 -0
- package/dist/views/monthly.js +101 -0
- package/dist/views/overview.js +216 -0
- package/package.json +1 -1
package/dist/lib/pricing.js
CHANGED
|
@@ -25,6 +25,7 @@ export const MODEL_PRICING = {
|
|
|
25
25
|
"gpt-5-nano": createPricing(0.05, 0.005, 0.4),
|
|
26
26
|
"gpt-5.1-chat-latest": createPricing(1.25, 0.125, 10.0),
|
|
27
27
|
"gpt-5-chat-latest": createPricing(1.25, 0.125, 10.0),
|
|
28
|
+
"gpt-5.2-codex": createPricing(1.75, 0.175, 14.0),
|
|
28
29
|
"gpt-5.1-codex": createPricing(1.25, 0.125, 10.0),
|
|
29
30
|
"gpt-5.1-codex-max": createPricing(1.25, 0.125, 10.0),
|
|
30
31
|
"gpt-5.1-codex-mini": createPricing(0.25, 0.025, 2.0),
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
export function registerKeybindings(screen, handlers) {
|
|
2
|
+
screen.key(['tab'], () => handlers.onTab());
|
|
3
|
+
screen.key(['S-tab'], () => handlers.onShiftTab());
|
|
4
|
+
screen.key(['up', 'k'], () => handlers.onUp());
|
|
5
|
+
screen.key(['down', 'j'], () => handlers.onDown());
|
|
6
|
+
screen.key(['enter'], () => handlers.onEnter());
|
|
7
|
+
screen.key(['escape', 'backspace'], () => handlers.onEscape());
|
|
8
|
+
screen.key(['q', 'C-c'], () => handlers.onQuit());
|
|
9
|
+
screen.key(['r'], () => void handlers.onReload());
|
|
10
|
+
screen.key(['s'], () => void handlers.onSwitchSource());
|
|
11
|
+
}
|
|
12
|
+
function getSortedMonthsFromDailyData(dailyData) {
|
|
13
|
+
const months = new Set();
|
|
14
|
+
for (const date of Object.keys(dailyData)) {
|
|
15
|
+
const month = date.slice(0, 7);
|
|
16
|
+
if (month)
|
|
17
|
+
months.add(month);
|
|
18
|
+
}
|
|
19
|
+
return [...months].sort().reverse();
|
|
20
|
+
}
|
|
21
|
+
function countProjectsInMonth(dailyData, month) {
|
|
22
|
+
const projects = new Set();
|
|
23
|
+
for (const [date, dayData] of Object.entries(dailyData)) {
|
|
24
|
+
if (!date.startsWith(month))
|
|
25
|
+
continue;
|
|
26
|
+
for (const projectName of Object.keys(dayData ?? {}))
|
|
27
|
+
projects.add(projectName);
|
|
28
|
+
}
|
|
29
|
+
return projects.size;
|
|
30
|
+
}
|
|
31
|
+
export function registerAppKeybindings(ctx) {
|
|
32
|
+
const { screen, contentBox, statusBar, state } = ctx;
|
|
33
|
+
registerKeybindings(screen, {
|
|
34
|
+
onTab: () => {
|
|
35
|
+
if (state.dailyDetailDate || state.monthlyDetailMonth)
|
|
36
|
+
return; // 在 detail 视图时禁用 tab 切换
|
|
37
|
+
state.currentTab = (state.currentTab + 1) % state.tabs.length;
|
|
38
|
+
state.modelScrollOffset = 0;
|
|
39
|
+
state.projectScrollOffset = 0;
|
|
40
|
+
state.dailyScrollOffset = 0;
|
|
41
|
+
state.dailySelectedIndex = 0;
|
|
42
|
+
state.monthlyScrollOffset = 0;
|
|
43
|
+
state.monthlySelectedIndex = 0;
|
|
44
|
+
state.monthlyDetailScrollOffset = 0;
|
|
45
|
+
contentBox.scrollTo(0);
|
|
46
|
+
ctx.updateTabBar();
|
|
47
|
+
ctx.updateContent();
|
|
48
|
+
screen.render();
|
|
49
|
+
},
|
|
50
|
+
onShiftTab: () => {
|
|
51
|
+
if (state.dailyDetailDate || state.monthlyDetailMonth)
|
|
52
|
+
return; // 在 detail 视图时禁用 tab 切换
|
|
53
|
+
state.currentTab = (state.currentTab - 1 + state.tabs.length) % state.tabs.length;
|
|
54
|
+
state.modelScrollOffset = 0;
|
|
55
|
+
state.projectScrollOffset = 0;
|
|
56
|
+
state.dailyScrollOffset = 0;
|
|
57
|
+
state.dailySelectedIndex = 0;
|
|
58
|
+
state.monthlyScrollOffset = 0;
|
|
59
|
+
state.monthlySelectedIndex = 0;
|
|
60
|
+
state.monthlyDetailScrollOffset = 0;
|
|
61
|
+
contentBox.scrollTo(0);
|
|
62
|
+
ctx.updateTabBar();
|
|
63
|
+
ctx.updateContent();
|
|
64
|
+
screen.render();
|
|
65
|
+
},
|
|
66
|
+
onUp: () => {
|
|
67
|
+
if (state.currentTab === 1) {
|
|
68
|
+
state.modelScrollOffset = Math.max(0, state.modelScrollOffset - 1);
|
|
69
|
+
ctx.updateContent();
|
|
70
|
+
screen.render();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (state.currentTab === 2) {
|
|
74
|
+
state.projectScrollOffset = Math.max(0, state.projectScrollOffset - 1);
|
|
75
|
+
ctx.updateContent();
|
|
76
|
+
screen.render();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (state.currentTab === 3) {
|
|
80
|
+
if (state.dailyDetailDate) {
|
|
81
|
+
// 在 detail 视图中滚动
|
|
82
|
+
state.dailyDetailScrollOffset = Math.max(0, state.dailyDetailScrollOffset - 1);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// 在列表视图中移动选中项
|
|
86
|
+
if (state.dailySelectedIndex > 0) {
|
|
87
|
+
state.dailySelectedIndex--;
|
|
88
|
+
// 如果选中项在当前页之上,滚动页面
|
|
89
|
+
if (state.dailySelectedIndex < state.dailyScrollOffset) {
|
|
90
|
+
state.dailyScrollOffset = state.dailySelectedIndex;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
ctx.updateContent();
|
|
95
|
+
screen.render();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (state.currentTab === 4) {
|
|
99
|
+
if (state.monthlyDetailMonth) {
|
|
100
|
+
state.monthlyDetailScrollOffset = Math.max(0, state.monthlyDetailScrollOffset - 1);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if (state.monthlySelectedIndex > 0) {
|
|
104
|
+
state.monthlySelectedIndex--;
|
|
105
|
+
if (state.monthlySelectedIndex < state.monthlyScrollOffset) {
|
|
106
|
+
state.monthlyScrollOffset = state.monthlySelectedIndex;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
ctx.updateContent();
|
|
111
|
+
screen.render();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
contentBox.scroll(-1);
|
|
115
|
+
screen.render();
|
|
116
|
+
},
|
|
117
|
+
onDown: () => {
|
|
118
|
+
if (state.currentTab === 1) {
|
|
119
|
+
const maxOffset = Math.max(0, Object.keys(state.data.modelTotals).length - state.modelPageSize);
|
|
120
|
+
state.modelScrollOffset = Math.min(maxOffset, state.modelScrollOffset + 1);
|
|
121
|
+
ctx.updateContent();
|
|
122
|
+
screen.render();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (state.currentTab === 2) {
|
|
126
|
+
const maxOffset = Math.max(0, Object.keys(state.data.projectTotals).length - state.projectPageSize);
|
|
127
|
+
state.projectScrollOffset = Math.min(maxOffset, state.projectScrollOffset + 1);
|
|
128
|
+
ctx.updateContent();
|
|
129
|
+
screen.render();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (state.currentTab === 3) {
|
|
133
|
+
if (state.dailyDetailDate) {
|
|
134
|
+
// 在 detail 视图中滚动(计算总行数:project 数 + 每个 project 下的 model 数)
|
|
135
|
+
const dayData = state.data.dailyData[state.dailyDetailDate];
|
|
136
|
+
if (dayData) {
|
|
137
|
+
let totalLines = 0;
|
|
138
|
+
for (const models of Object.values(dayData)) {
|
|
139
|
+
totalLines += 1 + Object.keys(models).length; // 1 for project header + model count
|
|
140
|
+
}
|
|
141
|
+
const maxOffset = Math.max(0, totalLines - state.dailyDetailPageSize);
|
|
142
|
+
state.dailyDetailScrollOffset = Math.min(maxOffset, state.dailyDetailScrollOffset + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// 在列表视图中移动选中项
|
|
147
|
+
const totalDays = Object.keys(state.data.dailySummary).length;
|
|
148
|
+
if (state.dailySelectedIndex < totalDays - 1) {
|
|
149
|
+
state.dailySelectedIndex++;
|
|
150
|
+
// 如果选中项超出当前页,滚动页面
|
|
151
|
+
if (state.dailySelectedIndex >= state.dailyScrollOffset + state.dailyPageSize) {
|
|
152
|
+
state.dailyScrollOffset = state.dailySelectedIndex - state.dailyPageSize + 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
ctx.updateContent();
|
|
157
|
+
screen.render();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (state.currentTab === 4) {
|
|
161
|
+
if (state.monthlyDetailMonth) {
|
|
162
|
+
const totalProjects = countProjectsInMonth(state.data.dailyData, state.monthlyDetailMonth);
|
|
163
|
+
const maxOffset = Math.max(0, totalProjects - state.monthlyDetailPageSize);
|
|
164
|
+
state.monthlyDetailScrollOffset = Math.min(maxOffset, state.monthlyDetailScrollOffset + 1);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const months = getSortedMonthsFromDailyData(state.data.dailyData);
|
|
168
|
+
if (state.monthlySelectedIndex < months.length - 1) {
|
|
169
|
+
state.monthlySelectedIndex++;
|
|
170
|
+
if (state.monthlySelectedIndex >= state.monthlyScrollOffset + state.monthlyPageSize) {
|
|
171
|
+
state.monthlyScrollOffset = state.monthlySelectedIndex - state.monthlyPageSize + 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
ctx.updateContent();
|
|
176
|
+
screen.render();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
contentBox.scroll(1);
|
|
180
|
+
screen.render();
|
|
181
|
+
},
|
|
182
|
+
onEnter: () => {
|
|
183
|
+
if (state.currentTab === 3 && !state.dailyDetailDate) {
|
|
184
|
+
// 进入 detail 视图
|
|
185
|
+
const sortedDates = Object.keys(state.data.dailySummary).sort().reverse();
|
|
186
|
+
if (sortedDates[state.dailySelectedIndex]) {
|
|
187
|
+
state.dailyDetailDate = sortedDates[state.dailySelectedIndex];
|
|
188
|
+
state.dailyDetailScrollOffset = 0;
|
|
189
|
+
ctx.updateContent();
|
|
190
|
+
screen.render();
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (state.currentTab === 4 && !state.monthlyDetailMonth) {
|
|
195
|
+
// 进入 monthly detail 视图
|
|
196
|
+
const months = getSortedMonthsFromDailyData(state.data.dailyData);
|
|
197
|
+
if (months[state.monthlySelectedIndex]) {
|
|
198
|
+
state.monthlyDetailMonth = months[state.monthlySelectedIndex];
|
|
199
|
+
state.monthlyDetailScrollOffset = 0;
|
|
200
|
+
ctx.updateContent();
|
|
201
|
+
screen.render();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
onEscape: () => {
|
|
206
|
+
if (state.currentTab === 3 && state.dailyDetailDate) {
|
|
207
|
+
// 返回列表视图
|
|
208
|
+
state.dailyDetailDate = null;
|
|
209
|
+
state.dailyDetailScrollOffset = 0;
|
|
210
|
+
ctx.updateContent();
|
|
211
|
+
screen.render();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (state.currentTab === 4 && state.monthlyDetailMonth) {
|
|
215
|
+
// 返回 monthly 列表视图
|
|
216
|
+
state.monthlyDetailMonth = null;
|
|
217
|
+
state.monthlyDetailScrollOffset = 0;
|
|
218
|
+
ctx.updateContent();
|
|
219
|
+
screen.render();
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
onQuit: () => {
|
|
223
|
+
screen.destroy();
|
|
224
|
+
process.exit(0);
|
|
225
|
+
},
|
|
226
|
+
onReload: async () => {
|
|
227
|
+
statusBar.setContent(' {yellow-fg}Reloading...{/yellow-fg}');
|
|
228
|
+
screen.render();
|
|
229
|
+
try {
|
|
230
|
+
const prevDetailDate = state.dailyDetailDate;
|
|
231
|
+
const prevDetailMonth = state.monthlyDetailMonth;
|
|
232
|
+
state.data = await ctx.loadUsageData({ days: ctx.options.days, source: state.currentSource });
|
|
233
|
+
state.modelScrollOffset = 0;
|
|
234
|
+
state.projectScrollOffset = 0;
|
|
235
|
+
state.dailyScrollOffset = 0;
|
|
236
|
+
state.dailySelectedIndex = 0;
|
|
237
|
+
state.dailyDetailScrollOffset = 0;
|
|
238
|
+
state.monthlyScrollOffset = 0;
|
|
239
|
+
state.monthlySelectedIndex = 0;
|
|
240
|
+
state.monthlyDetailScrollOffset = 0;
|
|
241
|
+
// 如果之前在详情视图且该日期仍存在,保持在详情视图
|
|
242
|
+
if (prevDetailDate && state.data.dailySummary[prevDetailDate]) {
|
|
243
|
+
state.dailyDetailDate = prevDetailDate;
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
state.dailyDetailDate = null;
|
|
247
|
+
}
|
|
248
|
+
// 如果之前在 monthly detail 且该月份仍存在,保持在详情视图
|
|
249
|
+
const months = getSortedMonthsFromDailyData(state.data.dailyData);
|
|
250
|
+
if (prevDetailMonth && months.includes(prevDetailMonth)) {
|
|
251
|
+
state.monthlyDetailMonth = prevDetailMonth;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
state.monthlyDetailMonth = null;
|
|
255
|
+
}
|
|
256
|
+
contentBox.scrollTo(0);
|
|
257
|
+
ctx.updateTabBar();
|
|
258
|
+
ctx.updateContent();
|
|
259
|
+
ctx.updateStatusBar();
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
statusBar.setContent(` {red-fg}Reload failed: ${String(err)}{/red-fg}`);
|
|
263
|
+
}
|
|
264
|
+
screen.render();
|
|
265
|
+
},
|
|
266
|
+
onSwitchSource: async () => {
|
|
267
|
+
statusBar.setContent(' {yellow-fg}Switching source...{/yellow-fg}');
|
|
268
|
+
screen.render();
|
|
269
|
+
try {
|
|
270
|
+
state.currentSource = state.currentSource === 'code' ? 'ide' : 'code';
|
|
271
|
+
state.data = await ctx.loadUsageData({ days: ctx.options.days, source: state.currentSource });
|
|
272
|
+
state.modelScrollOffset = 0;
|
|
273
|
+
state.projectScrollOffset = 0;
|
|
274
|
+
state.dailyScrollOffset = 0;
|
|
275
|
+
state.dailySelectedIndex = 0;
|
|
276
|
+
state.dailyDetailDate = null;
|
|
277
|
+
state.dailyDetailScrollOffset = 0;
|
|
278
|
+
state.monthlyScrollOffset = 0;
|
|
279
|
+
state.monthlySelectedIndex = 0;
|
|
280
|
+
state.monthlyDetailMonth = null;
|
|
281
|
+
state.monthlyDetailScrollOffset = 0;
|
|
282
|
+
contentBox.scrollTo(0);
|
|
283
|
+
ctx.updateTabBar();
|
|
284
|
+
ctx.updateContent();
|
|
285
|
+
ctx.updateStatusBar();
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
statusBar.setContent(` {red-fg}Switch source failed: ${String(err)}{/red-fg}`);
|
|
289
|
+
}
|
|
290
|
+
screen.render();
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function createInitialState(params) {
|
|
2
|
+
return {
|
|
3
|
+
tabs: params.tabs,
|
|
4
|
+
currentTab: 0,
|
|
5
|
+
currentSource: params.source,
|
|
6
|
+
data: params.data,
|
|
7
|
+
modelScrollOffset: 0,
|
|
8
|
+
projectScrollOffset: 0,
|
|
9
|
+
dailyScrollOffset: 0,
|
|
10
|
+
dailySelectedIndex: 0,
|
|
11
|
+
dailyDetailDate: null,
|
|
12
|
+
dailyDetailScrollOffset: 0,
|
|
13
|
+
monthlyScrollOffset: 0,
|
|
14
|
+
monthlySelectedIndex: 0,
|
|
15
|
+
monthlyDetailMonth: null,
|
|
16
|
+
monthlyDetailScrollOffset: 0,
|
|
17
|
+
modelPageSize: 10,
|
|
18
|
+
projectPageSize: 10,
|
|
19
|
+
dailyPageSize: 20,
|
|
20
|
+
dailyDetailPageSize: 10,
|
|
21
|
+
monthlyPageSize: 20,
|
|
22
|
+
monthlyDetailPageSize: 10,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { compareByCostThenTokens, formatCost, formatNumber, formatTokens, truncate } from '../lib/utils.js';
|
|
2
|
+
export function renderByModel(box, data, scrollOffset = 0, width, note, pageSize) {
|
|
3
|
+
const { modelTotals, grandTotal } = data;
|
|
4
|
+
const sorted = Object.entries(modelTotals).sort(compareByCostThenTokens);
|
|
5
|
+
// 根据宽度计算列宽
|
|
6
|
+
const availableWidth = width - 6; // padding
|
|
7
|
+
const fixedCols = 12 + 12 + 12 + 10; // Cost + Requests + Tokens + Avg/Req
|
|
8
|
+
const modelCol = Math.max(20, Math.min(40, availableWidth - fixedCols));
|
|
9
|
+
const totalWidth = modelCol + fixedCols;
|
|
10
|
+
let content = '{bold}Cost by Model{/bold}\n\n';
|
|
11
|
+
content +=
|
|
12
|
+
'{underline}' +
|
|
13
|
+
'Model'.padEnd(modelCol) +
|
|
14
|
+
'~Cost'.padStart(12) +
|
|
15
|
+
'Requests'.padStart(12) +
|
|
16
|
+
'Tokens'.padStart(12) +
|
|
17
|
+
'Avg/Req'.padStart(10) +
|
|
18
|
+
'{/underline}\n';
|
|
19
|
+
const safePageSize = Math.max(1, Math.floor(pageSize || 1));
|
|
20
|
+
const visibleModels = sorted.slice(scrollOffset, scrollOffset + safePageSize);
|
|
21
|
+
for (const [modelId, stats] of visibleModels) {
|
|
22
|
+
const avgPerReq = stats.requests > 0 ? stats.cost / stats.requests : 0;
|
|
23
|
+
content +=
|
|
24
|
+
truncate(modelId, modelCol - 1).padEnd(modelCol) +
|
|
25
|
+
formatCost(stats.cost).padStart(12) +
|
|
26
|
+
formatNumber(stats.requests).padStart(12) +
|
|
27
|
+
formatTokens(stats.tokens).padStart(12) +
|
|
28
|
+
formatCost(avgPerReq).padStart(10) +
|
|
29
|
+
'\n';
|
|
30
|
+
}
|
|
31
|
+
content += '─'.repeat(totalWidth) + '\n';
|
|
32
|
+
content +=
|
|
33
|
+
'{bold}' +
|
|
34
|
+
'Total'.padEnd(modelCol) +
|
|
35
|
+
formatCost(grandTotal.cost).padStart(12) +
|
|
36
|
+
formatNumber(grandTotal.requests).padStart(12) +
|
|
37
|
+
formatTokens(grandTotal.tokens).padStart(12) +
|
|
38
|
+
'{/bold}\n';
|
|
39
|
+
if (sorted.length > safePageSize) {
|
|
40
|
+
content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + safePageSize, sorted.length)} of ${sorted.length} models (↑↓ to scroll){/gray-fg}`;
|
|
41
|
+
}
|
|
42
|
+
if (note) {
|
|
43
|
+
content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
44
|
+
}
|
|
45
|
+
box.setContent(content);
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveProjectName } from '../lib/workspace-resolver.js';
|
|
2
|
+
import { formatCost, formatNumber, formatTokens, truncate } from '../lib/utils.js';
|
|
3
|
+
export function renderByProject(box, data, scrollOffset = 0, width, note, pageSize) {
|
|
4
|
+
const { projectTotals, grandTotal } = data;
|
|
5
|
+
const sorted = Object.entries(projectTotals).sort((a, b) => b[1].cost - a[1].cost);
|
|
6
|
+
// 根据宽度计算列宽
|
|
7
|
+
const availableWidth = width - 6; // padding
|
|
8
|
+
const fixedCols = 12 + 12 + 12; // Cost + Requests + Tokens
|
|
9
|
+
const projectCol = Math.max(25, availableWidth - fixedCols);
|
|
10
|
+
const totalWidth = projectCol + fixedCols;
|
|
11
|
+
let content = '{bold}Cost by Project{/bold}\n\n';
|
|
12
|
+
content +=
|
|
13
|
+
'{underline}' +
|
|
14
|
+
'Project'.padEnd(projectCol) +
|
|
15
|
+
'~Cost'.padStart(12) +
|
|
16
|
+
'Requests'.padStart(12) +
|
|
17
|
+
'Tokens'.padStart(12) +
|
|
18
|
+
'{/underline}\n';
|
|
19
|
+
const safePageSize = Math.max(1, Math.floor(pageSize || 1));
|
|
20
|
+
const visibleProjects = sorted.slice(scrollOffset, scrollOffset + safePageSize);
|
|
21
|
+
for (const [projectName, stats] of visibleProjects) {
|
|
22
|
+
// 简化项目名
|
|
23
|
+
const shortName = resolveProjectName(projectName, data.workspaceMappings);
|
|
24
|
+
content +=
|
|
25
|
+
truncate(shortName, projectCol - 1).padEnd(projectCol) +
|
|
26
|
+
formatCost(stats.cost).padStart(12) +
|
|
27
|
+
formatNumber(stats.requests).padStart(12) +
|
|
28
|
+
formatTokens(stats.tokens).padStart(12) +
|
|
29
|
+
'\n';
|
|
30
|
+
}
|
|
31
|
+
content += '─'.repeat(totalWidth) + '\n';
|
|
32
|
+
content +=
|
|
33
|
+
'{bold}' +
|
|
34
|
+
`Total (${sorted.length} projects)`.padEnd(projectCol) +
|
|
35
|
+
formatCost(grandTotal.cost).padStart(12) +
|
|
36
|
+
formatNumber(grandTotal.requests).padStart(12) +
|
|
37
|
+
formatTokens(grandTotal.tokens).padStart(12) +
|
|
38
|
+
'{/bold}\n';
|
|
39
|
+
if (sorted.length > safePageSize) {
|
|
40
|
+
content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + safePageSize, sorted.length)} of ${sorted.length} projects (↑↓ to scroll){/gray-fg}`;
|
|
41
|
+
}
|
|
42
|
+
if (note) {
|
|
43
|
+
content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
44
|
+
}
|
|
45
|
+
box.setContent(content);
|
|
46
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { resolveProjectName } from '../lib/workspace-resolver.js';
|
|
2
|
+
import { formatCost, formatNumber, formatTokens, truncate } from '../lib/utils.js';
|
|
3
|
+
export function renderDailyDetail(box, data, date, scrollOffset = 0, width, pageSize) {
|
|
4
|
+
const { dailySummary, dailyData } = data;
|
|
5
|
+
const daySummary = dailySummary[date];
|
|
6
|
+
const dayData = dailyData[date];
|
|
7
|
+
if (!daySummary || !dayData) {
|
|
8
|
+
box.setContent(`{bold}${date}{/bold}\n\nNo data available for this date.`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const projectDetails = [];
|
|
12
|
+
for (const [projectName, models] of Object.entries(dayData)) {
|
|
13
|
+
const shortName = resolveProjectName(projectName, data.workspaceMappings);
|
|
14
|
+
const modelList = [];
|
|
15
|
+
let totalCost = 0;
|
|
16
|
+
let totalTokens = 0;
|
|
17
|
+
let totalRequests = 0;
|
|
18
|
+
for (const [modelId, stats] of Object.entries(models)) {
|
|
19
|
+
const s = stats;
|
|
20
|
+
const cost = Number(s.cost ?? 0);
|
|
21
|
+
const tokens = Number(s.totalTokens ?? 0);
|
|
22
|
+
const requests = Number(s.requests ?? 0);
|
|
23
|
+
modelList.push({ id: modelId, cost, tokens, requests });
|
|
24
|
+
totalCost += cost;
|
|
25
|
+
totalTokens += tokens;
|
|
26
|
+
totalRequests += requests;
|
|
27
|
+
}
|
|
28
|
+
// 按 cost 降序排序 models,cost 相同时按 tokens 降序
|
|
29
|
+
modelList.sort((a, b) => {
|
|
30
|
+
const costDiff = b.cost - a.cost;
|
|
31
|
+
if (Math.abs(costDiff) > 0.001)
|
|
32
|
+
return costDiff;
|
|
33
|
+
return b.tokens - a.tokens;
|
|
34
|
+
});
|
|
35
|
+
projectDetails.push({
|
|
36
|
+
name: projectName,
|
|
37
|
+
shortName,
|
|
38
|
+
totalCost,
|
|
39
|
+
totalTokens,
|
|
40
|
+
totalRequests,
|
|
41
|
+
models: modelList,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// 按 project 总 cost 降序排序
|
|
45
|
+
projectDetails.sort((a, b) => b.totalCost - a.totalCost);
|
|
46
|
+
const displayLines = [];
|
|
47
|
+
for (const project of projectDetails) {
|
|
48
|
+
displayLines.push({ type: 'project', project });
|
|
49
|
+
for (const model of project.models) {
|
|
50
|
+
displayLines.push({ type: 'model', model });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// 根据宽度计算列宽
|
|
54
|
+
const availableWidth = width - 6; // padding
|
|
55
|
+
const fixedCols = 12 + 12 + 12; // Cost + Requests + Tokens
|
|
56
|
+
const nameCol = Math.max(25, availableWidth - fixedCols);
|
|
57
|
+
const totalWidth = nameCol + fixedCols;
|
|
58
|
+
let content = `{bold}${date} - Project & Model Usage Details{/bold}\n\n`;
|
|
59
|
+
// 当天汇总
|
|
60
|
+
content += `{green-fg}Total cost:{/green-fg} ${formatCost(daySummary.cost)} `;
|
|
61
|
+
content += `{green-fg}Tokens:{/green-fg} ${formatTokens(daySummary.tokens)} `;
|
|
62
|
+
content += `{green-fg}Requests:{/green-fg} ${formatNumber(daySummary.requests)} `;
|
|
63
|
+
content += `{green-fg}Projects:{/green-fg} ${projectDetails.length}\n\n`;
|
|
64
|
+
content +=
|
|
65
|
+
'{underline}' +
|
|
66
|
+
'Project / Model'.padEnd(nameCol) +
|
|
67
|
+
'~Cost'.padStart(12) +
|
|
68
|
+
'Requests'.padStart(12) +
|
|
69
|
+
'Tokens'.padStart(12) +
|
|
70
|
+
'{/underline}\n';
|
|
71
|
+
const safePageSize = Math.max(1, Math.floor(pageSize || 1));
|
|
72
|
+
const visibleLines = displayLines.slice(scrollOffset, scrollOffset + safePageSize);
|
|
73
|
+
for (const line of visibleLines) {
|
|
74
|
+
if (line.type === 'project') {
|
|
75
|
+
const p = line.project;
|
|
76
|
+
content +=
|
|
77
|
+
'{cyan-fg}' +
|
|
78
|
+
truncate(p.shortName, nameCol - 1).padEnd(nameCol) +
|
|
79
|
+
formatCost(p.totalCost).padStart(12) +
|
|
80
|
+
formatNumber(p.totalRequests).padStart(12) +
|
|
81
|
+
formatTokens(p.totalTokens).padStart(12) +
|
|
82
|
+
'{/cyan-fg}\n';
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const m = line.model;
|
|
86
|
+
content +=
|
|
87
|
+
(' ' + truncate(m.id, nameCol - 3)).padEnd(nameCol) +
|
|
88
|
+
formatCost(m.cost).padStart(12) +
|
|
89
|
+
formatNumber(m.requests).padStart(12) +
|
|
90
|
+
formatTokens(m.tokens).padStart(12) +
|
|
91
|
+
'\n';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
content += '─'.repeat(totalWidth) + '\n';
|
|
95
|
+
content +=
|
|
96
|
+
'{bold}' +
|
|
97
|
+
`Total (${projectDetails.length} projects)`.padEnd(nameCol) +
|
|
98
|
+
formatCost(daySummary.cost).padStart(12) +
|
|
99
|
+
formatNumber(daySummary.requests).padStart(12) +
|
|
100
|
+
formatTokens(daySummary.tokens).padStart(12) +
|
|
101
|
+
'{/bold}\n';
|
|
102
|
+
if (displayLines.length > safePageSize) {
|
|
103
|
+
content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + safePageSize, displayLines.length)} of ${displayLines.length} rows (↑↓ scroll, Esc back){/gray-fg}`;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
content += `\n{gray-fg}(Esc back to Daily list){/gray-fg}`;
|
|
107
|
+
}
|
|
108
|
+
box.setContent(content);
|
|
109
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { resolveProjectName } from '../lib/workspace-resolver.js';
|
|
2
|
+
import { formatCost, formatNumber, formatTokens, truncate } from '../lib/utils.js';
|
|
3
|
+
export function renderDaily(box, data, scrollOffset = 0, selectedIndex = 0, width, note, pageSize) {
|
|
4
|
+
const { dailySummary, dailyData } = data;
|
|
5
|
+
const sortedDates = Object.keys(dailySummary).sort().reverse();
|
|
6
|
+
// 根据宽度计算列宽
|
|
7
|
+
const availableWidth = width - 6; // padding
|
|
8
|
+
const dateCol = 12;
|
|
9
|
+
const costCol = 12;
|
|
10
|
+
const tokensCol = 10;
|
|
11
|
+
const reqCol = 10;
|
|
12
|
+
const fixedCols = dateCol + costCol + tokensCol + reqCol;
|
|
13
|
+
const remainingWidth = availableWidth - fixedCols;
|
|
14
|
+
const modelCol = Math.max(15, Math.min(25, Math.floor(remainingWidth * 0.4)));
|
|
15
|
+
const projectCol = Math.max(20, remainingWidth - modelCol);
|
|
16
|
+
let content = '{bold}Daily Cost Details{/bold}\n\n';
|
|
17
|
+
content +=
|
|
18
|
+
'{underline}' +
|
|
19
|
+
'Date'.padEnd(dateCol) +
|
|
20
|
+
'~Cost'.padStart(costCol) +
|
|
21
|
+
'Tokens'.padStart(tokensCol) +
|
|
22
|
+
'Requests'.padStart(reqCol) +
|
|
23
|
+
'Top Model'.padStart(modelCol) +
|
|
24
|
+
'Top Project'.padStart(projectCol) +
|
|
25
|
+
'{/underline}\n';
|
|
26
|
+
const safePageSize = Math.max(1, Math.floor(pageSize || 1));
|
|
27
|
+
const visibleDates = sortedDates.slice(scrollOffset, scrollOffset + safePageSize);
|
|
28
|
+
for (let i = 0; i < visibleDates.length; i++) {
|
|
29
|
+
const date = visibleDates[i];
|
|
30
|
+
const daySummary = dailySummary[date];
|
|
31
|
+
const dayData = dailyData[date];
|
|
32
|
+
if (!daySummary || !dayData)
|
|
33
|
+
continue;
|
|
34
|
+
// 找出当天 top model 和 project
|
|
35
|
+
let topModel = { id: '-', cost: 0 };
|
|
36
|
+
let topProject = { name: '-', cost: 0 };
|
|
37
|
+
for (const [project, models] of Object.entries(dayData)) {
|
|
38
|
+
let projectCost = 0;
|
|
39
|
+
for (const [model, stats] of Object.entries(models)) {
|
|
40
|
+
const modelStats = stats;
|
|
41
|
+
projectCost += Number(modelStats.cost ?? 0);
|
|
42
|
+
if (Number(modelStats.cost ?? 0) > topModel.cost) {
|
|
43
|
+
topModel = { id: model, cost: Number(modelStats.cost ?? 0) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (projectCost > topProject.cost) {
|
|
47
|
+
topProject = { name: project, cost: projectCost };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const shortProject = resolveProjectName(topProject.name, data.workspaceMappings);
|
|
51
|
+
const isSelected = scrollOffset + i === selectedIndex;
|
|
52
|
+
const rowContent = date.padEnd(dateCol) +
|
|
53
|
+
formatCost(daySummary.cost).padStart(costCol) +
|
|
54
|
+
formatTokens(daySummary.tokens).padStart(tokensCol) +
|
|
55
|
+
formatNumber(daySummary.requests).padStart(reqCol) +
|
|
56
|
+
truncate(topModel.id, modelCol - 1).padStart(modelCol) +
|
|
57
|
+
truncate(shortProject, projectCol - 1).padStart(projectCol);
|
|
58
|
+
if (isSelected) {
|
|
59
|
+
content += `{black-fg}{green-bg}${rowContent}{/green-bg}{/black-fg}\n`;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
content += rowContent + '\n';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (sortedDates.length > safePageSize) {
|
|
66
|
+
content += `\n{gray-fg}Showing ${scrollOffset + 1}-${Math.min(scrollOffset + safePageSize, sortedDates.length)} of ${sortedDates.length} days (↑↓ scroll, Enter detail){/gray-fg}`;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
content += `\n{gray-fg}(↑↓ select, Enter detail){/gray-fg}`;
|
|
70
|
+
}
|
|
71
|
+
if (note) {
|
|
72
|
+
content += `\n\n{gray-fg}备注:${note}{/gray-fg}\n`;
|
|
73
|
+
}
|
|
74
|
+
box.setContent(content);
|
|
75
|
+
}
|