openclacky 1.2.5 → 1.2.7
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +34 -0
- data/README_CN.md +34 -0
- data/lib/clacky/agent/cost_tracker.rb +24 -10
- data/lib/clacky/agent/llm_caller.rb +25 -3
- data/lib/clacky/agent/message_compressor.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +6 -2
- data/lib/clacky/agent/session_serializer.rb +23 -4
- data/lib/clacky/agent/tool_executor.rb +14 -0
- data/lib/clacky/agent/tool_registry.rb +0 -7
- data/lib/clacky/agent.rb +43 -10
- data/lib/clacky/agent_config.rb +54 -6
- data/lib/clacky/billing/billing_store.rb +62 -4
- data/lib/clacky/brand_config.rb +5 -0
- data/lib/clacky/cli.rb +76 -24
- data/lib/clacky/client.rb +59 -4
- data/lib/clacky/default_parsers/wps_parser.rb +82 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +5 -2
- data/lib/clacky/message_format/anthropic.rb +13 -3
- data/lib/clacky/message_format/bedrock.rb +2 -2
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/platform_http_client.rb +28 -1
- data/lib/clacky/providers.rb +11 -29
- data/lib/clacky/server/channel/channel_manager.rb +148 -12
- data/lib/clacky/server/channel/channel_ui_controller.rb +4 -2
- data/lib/clacky/server/http_server.rb +133 -13
- data/lib/clacky/server/session_registry.rb +30 -4
- data/lib/clacky/server/web_ui_controller.rb +6 -3
- data/lib/clacky/tools/browser.rb +4 -13
- data/lib/clacky/tools/terminal.rb +23 -27
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +659 -75
- data/lib/clacky/web/app.js +0 -1
- data/lib/clacky/web/billing.js +371 -99
- data/lib/clacky/web/i18n.js +48 -2
- data/lib/clacky/web/index.html +34 -1
- data/lib/clacky/web/sessions.js +213 -82
- data/lib/clacky/web/settings.js +59 -17
- data/lib/clacky/web/workspace.js +204 -0
- data/lib/clacky/web/ws-dispatcher.js +19 -3
- data/lib/clacky.rb +9 -3
- metadata +4 -5
- data/lib/clacky/tools/list_tasks.rb +0 -54
- data/lib/clacky/tools/redo_task.rb +0 -41
- data/lib/clacky/tools/undo_task.rb +0 -35
data/lib/clacky/web/app.js
CHANGED
|
@@ -158,7 +158,6 @@ const Router = (() => {
|
|
|
158
158
|
}
|
|
159
159
|
_setHash(`session/${id}`);
|
|
160
160
|
$("chat-panel").style.display = "flex";
|
|
161
|
-
$("chat-panel").style.flexDirection = "column";
|
|
162
161
|
Sessions.updateChatHeader(s);
|
|
163
162
|
Sessions.updateStatusBar(s.status);
|
|
164
163
|
Sessions.updateInfoBar(s);
|
data/lib/clacky/web/billing.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
const Billing = (() => {
|
|
5
5
|
let _summary = null;
|
|
6
6
|
let _daily = [];
|
|
7
|
-
let
|
|
7
|
+
let _allModels = []; // 保存完整的模型列表
|
|
8
|
+
let _currentPeriod = "day";
|
|
9
|
+
let _currentModel = "all";
|
|
10
|
+
let _clearPopupVisible = false;
|
|
8
11
|
|
|
9
12
|
// ── Currency Settings ─────────────────────────────────────────────────────
|
|
10
13
|
const CURRENCY_STORAGE_KEY = "clacky-currency";
|
|
@@ -69,15 +72,21 @@ const Billing = (() => {
|
|
|
69
72
|
container.innerHTML = `<div class="billing-loading">${I18n.t("billing.loading") || "Loading billing data..."}</div>`;
|
|
70
73
|
|
|
71
74
|
try {
|
|
75
|
+
const modelParam = (_currentModel && _currentModel !== "all") ? `&model=${encodeURIComponent(_currentModel)}` : "";
|
|
72
76
|
const [summaryRes, dailyRes] = await Promise.all([
|
|
73
|
-
fetch(`/api/billing/summary?period=${_currentPeriod}`),
|
|
74
|
-
fetch(
|
|
77
|
+
fetch(`/api/billing/summary?period=${_currentPeriod}${modelParam}`),
|
|
78
|
+
fetch(`/api/billing/daily?days=30${modelParam}`)
|
|
75
79
|
]);
|
|
76
80
|
|
|
77
81
|
_summary = await summaryRes.json();
|
|
78
82
|
const dailyData = await dailyRes.json();
|
|
79
83
|
_daily = dailyData.days || [];
|
|
80
84
|
|
|
85
|
+
// 保存完整模型列表(仅在未筛选时更新)
|
|
86
|
+
if (!_currentModel || _currentModel === "all") {
|
|
87
|
+
_allModels = _summary.by_model ? Object.keys(_summary.by_model) : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
_render();
|
|
82
91
|
} catch (e) {
|
|
83
92
|
container.innerHTML = `<div class="billing-error">${I18n.t("billing.error") || "Failed to load billing data"}: ${e.message}</div>`;
|
|
@@ -90,145 +99,397 @@ const Billing = (() => {
|
|
|
90
99
|
const container = document.getElementById("billing-content");
|
|
91
100
|
if (!container || !_summary) return;
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
// Period button group
|
|
103
|
+
const periods = ["day", "week", "month", "year", "all"];
|
|
104
|
+
const periodBtns = periods.map(p =>
|
|
105
|
+
`<button class="billing-period-btn ${p === _currentPeriod ? 'active' : ''}" data-period="${p}">${_periodLabel(p)}</button>`
|
|
95
106
|
).join("");
|
|
96
107
|
|
|
108
|
+
// Model filter options (使用完整模型列表)
|
|
109
|
+
const models = _allModels.length > 0 ? _allModels : (_summary.by_model ? Object.keys(_summary.by_model) : []);
|
|
110
|
+
const modelOptions = [`<option value="all">${I18n.t("billing.allModels") || "All Models"}</option>`]
|
|
111
|
+
.concat(models.map(m => `<option value="${_esc(m)}" ${m === _currentModel ? "selected" : ""}>${_esc(m)}</option>`))
|
|
112
|
+
.join("");
|
|
113
|
+
|
|
97
114
|
container.innerHTML = `
|
|
98
|
-
<div class="billing-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
115
|
+
<div class="billing-dashboard">
|
|
116
|
+
<div class="billing-top-bar">
|
|
117
|
+
<div class="billing-title-row">
|
|
118
|
+
<h2>${I18n.t("billing.title") || "Usage"}</h2>
|
|
119
|
+
<span class="billing-subtitle">${_getSummaryHint()}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="billing-controls">
|
|
122
|
+
<div class="billing-period-group">${periodBtns}</div>
|
|
123
|
+
<select id="billing-model-filter" class="billing-model-filter">${modelOptions}</select>
|
|
124
|
+
<div class="billing-clear-container">
|
|
125
|
+
<button id="billing-clear-btn" class="billing-clear-btn" title="${I18n.t('billing.clearData') || 'Clear Data'}">
|
|
126
|
+
🗑️
|
|
127
|
+
</button>
|
|
128
|
+
<div id="billing-clear-popup" class="billing-clear-popup" style="display: none;">
|
|
129
|
+
<button id="billing-clear-today" class="billing-clear-option">${I18n.t('billing.clearToday') || 'Clear Today'}</button>
|
|
130
|
+
<button id="billing-clear-all" class="billing-clear-option billing-clear-danger">${I18n.t('billing.clearAll') || 'Clear All'}</button>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
104
135
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
136
|
+
<div class="billing-stats-row">
|
|
137
|
+
<div class="billing-stat-card billing-stat-primary">
|
|
138
|
+
<div class="billing-stat-icon">💰</div>
|
|
139
|
+
<div class="billing-stat-content">
|
|
140
|
+
<div class="billing-stat-value">${_getCurrencySymbol()}${_formatCost(_convertCost(_summary.total_cost))}</div>
|
|
141
|
+
<div class="billing-stat-label">${I18n.t("billing.totalCost") || "Total Cost"}</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="billing-stat-card">
|
|
145
|
+
<div class="billing-stat-icon">📊</div>
|
|
146
|
+
<div class="billing-stat-content">
|
|
147
|
+
<div class="billing-stat-value">${_formatCompact(_summary.total_tokens)}</div>
|
|
148
|
+
<div class="billing-stat-label">${I18n.t("billing.totalTokens") || "Total Tokens"}</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="billing-stat-card">
|
|
152
|
+
<div class="billing-stat-icon">🔄</div>
|
|
153
|
+
<div class="billing-stat-content">
|
|
154
|
+
<div class="billing-stat-value">${_formatNumber(_summary.record_count)}</div>
|
|
155
|
+
<div class="billing-stat-label">${I18n.t("billing.requests") || "Requests"}</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="billing-stat-card">
|
|
159
|
+
<div class="billing-stat-icon">⚡</div>
|
|
160
|
+
<div class="billing-stat-content">
|
|
161
|
+
<div class="billing-stat-value">${_getCacheHitRate()}%</div>
|
|
162
|
+
<div class="billing-stat-label">${I18n.t("billing.cacheHit") || "Cache Hit"}</div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
109
165
|
</div>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
|
|
167
|
+
<div class="billing-bottom-grid">
|
|
168
|
+
${_renderTokenBreakdown()}
|
|
169
|
+
${_renderModelBreakdown()}
|
|
113
170
|
</div>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
171
|
+
|
|
172
|
+
<div class="billing-chart-row">
|
|
173
|
+
${_renderCombinedChart()}
|
|
117
174
|
</div>
|
|
118
175
|
</div>
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
// Bind period button handlers
|
|
179
|
+
document.querySelectorAll(".billing-period-btn").forEach(btn => {
|
|
180
|
+
btn.addEventListener("click", (e) => {
|
|
181
|
+
_currentPeriod = e.target.dataset.period;
|
|
182
|
+
_load();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Bind model filter handler
|
|
187
|
+
document.getElementById("billing-model-filter")?.addEventListener("change", (e) => {
|
|
188
|
+
_currentModel = e.target.value;
|
|
189
|
+
_load();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Bind clear button handlers
|
|
193
|
+
_bindClearHandlers();
|
|
194
|
+
|
|
195
|
+
// Bind chart tooltip handlers
|
|
196
|
+
_bindChartTooltip();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function _bindChartTooltip() {
|
|
200
|
+
const container = document.getElementById("billing-chart-container");
|
|
201
|
+
const tooltip = document.getElementById("billing-tooltip");
|
|
202
|
+
if (!container || !tooltip) return;
|
|
203
|
+
|
|
204
|
+
container.addEventListener("mousemove", (e) => {
|
|
205
|
+
const group = e.target.closest(".billing-bar-group");
|
|
206
|
+
if (!group) {
|
|
207
|
+
tooltip.style.display = "none";
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const date = group.dataset.date;
|
|
212
|
+
const total = group.dataset.total;
|
|
213
|
+
const cacheHit = group.dataset.cacheHit;
|
|
214
|
+
const cacheMiss = group.dataset.cacheMiss;
|
|
215
|
+
const output = group.dataset.output;
|
|
216
|
+
|
|
217
|
+
tooltip.innerHTML = `
|
|
218
|
+
<div class="tooltip-header">
|
|
219
|
+
<span class="tooltip-date">${date}</span>
|
|
220
|
+
<span class="tooltip-total">${total} tokens</span>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="tooltip-row">
|
|
223
|
+
<span class="tooltip-dot tooltip-cache-hit"></span>
|
|
224
|
+
<span class="tooltip-label">${I18n.t("billing.inputCacheHit") || "Input (Cache Hit)"}</span>
|
|
225
|
+
<span class="tooltip-value">${cacheHit}</span>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="tooltip-row">
|
|
228
|
+
<span class="tooltip-dot tooltip-cache-miss"></span>
|
|
229
|
+
<span class="tooltip-label">${I18n.t("billing.inputCacheMiss") || "Input (Cache Miss)"}</span>
|
|
230
|
+
<span class="tooltip-value">${cacheMiss}</span>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="tooltip-row">
|
|
233
|
+
<span class="tooltip-dot tooltip-output"></span>
|
|
234
|
+
<span class="tooltip-label">${I18n.t("billing.output") || "Output"}</span>
|
|
235
|
+
<span class="tooltip-value">${output}</span>
|
|
236
|
+
</div>
|
|
237
|
+
`;
|
|
238
|
+
tooltip.style.display = "block";
|
|
239
|
+
|
|
240
|
+
// Position tooltip following mouse
|
|
241
|
+
tooltip.style.left = `${e.clientX + 15}px`;
|
|
242
|
+
tooltip.style.top = `${e.clientY - 10}px`;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
container.addEventListener("mouseleave", () => {
|
|
246
|
+
tooltip.style.display = "none";
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function _bindClearHandlers() {
|
|
251
|
+
const clearBtn = document.getElementById("billing-clear-btn");
|
|
252
|
+
const clearPopup = document.getElementById("billing-clear-popup");
|
|
253
|
+
const clearToday = document.getElementById("billing-clear-today");
|
|
254
|
+
const clearAll = document.getElementById("billing-clear-all");
|
|
255
|
+
|
|
256
|
+
if (!clearBtn || !clearPopup) return;
|
|
257
|
+
|
|
258
|
+
// Toggle popup on button click
|
|
259
|
+
clearBtn.addEventListener("click", (e) => {
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
_clearPopupVisible = !_clearPopupVisible;
|
|
262
|
+
clearPopup.style.display = _clearPopupVisible ? "flex" : "none";
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Clear today
|
|
266
|
+
clearToday?.addEventListener("click", async (e) => {
|
|
267
|
+
e.stopPropagation();
|
|
268
|
+
await _clearData("today");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Clear all
|
|
272
|
+
clearAll?.addEventListener("click", async (e) => {
|
|
273
|
+
e.stopPropagation();
|
|
274
|
+
await _clearData("all");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Close popup when clicking outside
|
|
278
|
+
document.addEventListener("click", _closeClearPopup);
|
|
279
|
+
}
|
|
119
280
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
281
|
+
function _closeClearPopup(e) {
|
|
282
|
+
const clearPopup = document.getElementById("billing-clear-popup");
|
|
283
|
+
const clearBtn = document.getElementById("billing-clear-btn");
|
|
284
|
+
if (!clearPopup || !clearBtn) return;
|
|
285
|
+
|
|
286
|
+
// Check if click is outside popup and button
|
|
287
|
+
if (!clearPopup.contains(e.target) && !clearBtn.contains(e.target)) {
|
|
288
|
+
_clearPopupVisible = false;
|
|
289
|
+
clearPopup.style.display = "none";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function _clearData(scope) {
|
|
294
|
+
const clearPopup = document.getElementById("billing-clear-popup");
|
|
295
|
+
if (clearPopup) {
|
|
296
|
+
clearPopup.style.display = "none";
|
|
297
|
+
_clearPopupVisible = false;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const res = await fetch(`/api/billing/clear?scope=${scope}`, { method: "DELETE" });
|
|
302
|
+
const data = await res.json();
|
|
303
|
+
if (res.ok) {
|
|
304
|
+
// Reload data after clearing
|
|
305
|
+
_load();
|
|
306
|
+
} else {
|
|
307
|
+
alert(data.error || "Failed to clear data");
|
|
308
|
+
}
|
|
309
|
+
} catch (e) {
|
|
310
|
+
alert(`Error clearing data: ${e.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Helper functions for new UI
|
|
315
|
+
function _getSummaryHint() {
|
|
316
|
+
const cost = _convertCost(_summary.total_cost || 0);
|
|
317
|
+
const tokens = _summary.total_tokens || 0;
|
|
318
|
+
return `${_formatCompact(tokens)} tokens · ${_getCurrencySymbol()}${_formatCost(cost)}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function _getCacheHitRate() {
|
|
322
|
+
const prompt = _summary.prompt_tokens || 0;
|
|
323
|
+
const cacheRead = _summary.cache_read_tokens || 0;
|
|
324
|
+
if (prompt === 0) return "0";
|
|
325
|
+
return ((cacheRead / prompt) * 100).toFixed(1);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _formatCompact(num) {
|
|
329
|
+
if (num == null || num === 0) return "0";
|
|
330
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + "M";
|
|
331
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + "K";
|
|
332
|
+
return num.toLocaleString();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function _renderTokenBreakdown() {
|
|
336
|
+
return `
|
|
337
|
+
<div class="billing-section billing-token-section">
|
|
338
|
+
<h3>${I18n.t("billing.tokenBreakdown") || "Token Breakdown"}</h3>
|
|
339
|
+
<div class="billing-token-bars">
|
|
340
|
+
<div class="billing-token-bar-item">
|
|
341
|
+
<div class="billing-token-bar-header">
|
|
342
|
+
<span class="billing-token-bar-label">${I18n.t("billing.inputCacheHit") || "Input (Cache Hit)"}</span>
|
|
343
|
+
<span class="billing-token-bar-value">${_formatCompact(_summary.cache_read_tokens)}</span>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="billing-token-bar-track">
|
|
346
|
+
<div class="billing-token-bar-fill billing-bar-cache-read" style="width: ${_getTokenPercent('cache_read')}%"></div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div class="billing-token-bar-item">
|
|
350
|
+
<div class="billing-token-bar-header">
|
|
351
|
+
<span class="billing-token-bar-label">${I18n.t("billing.inputCacheMiss") || "Input (Cache Miss)"}</span>
|
|
352
|
+
<span class="billing-token-bar-value">${_formatCompact(_summary.prompt_tokens)}</span>
|
|
127
353
|
</div>
|
|
128
|
-
<div class="billing-token-
|
|
129
|
-
<
|
|
130
|
-
<span class="billing-token-value">${_formatNumber(_summary.completion_tokens)}</span>
|
|
354
|
+
<div class="billing-token-bar-track">
|
|
355
|
+
<div class="billing-token-bar-fill billing-bar-prompt" style="width: ${_getTokenPercent('prompt')}%"></div>
|
|
131
356
|
</div>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
357
|
+
</div>
|
|
358
|
+
<div class="billing-token-bar-item">
|
|
359
|
+
<div class="billing-token-bar-header">
|
|
360
|
+
<span class="billing-token-bar-label">${I18n.t("billing.output") || "Output"}</span>
|
|
361
|
+
<span class="billing-token-bar-value">${_formatCompact(_summary.completion_tokens)}</span>
|
|
135
362
|
</div>
|
|
136
|
-
<div class="billing-token-
|
|
137
|
-
<
|
|
138
|
-
<span class="billing-token-value">${_formatNumber(_summary.cache_write_tokens)}</span>
|
|
363
|
+
<div class="billing-token-bar-track">
|
|
364
|
+
<div class="billing-token-bar-fill billing-bar-completion" style="width: ${_getTokenPercent('completion')}%"></div>
|
|
139
365
|
</div>
|
|
140
366
|
</div>
|
|
141
367
|
</div>
|
|
142
|
-
|
|
143
|
-
${_renderModelBreakdown()}
|
|
144
|
-
${_renderDailyChart()}
|
|
145
368
|
</div>
|
|
146
369
|
`;
|
|
370
|
+
}
|
|
147
371
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
});
|
|
372
|
+
function _getTokenPercent(type) {
|
|
373
|
+
const total = _summary.total_tokens || 1;
|
|
374
|
+
const value = _summary[type + '_tokens'] || 0;
|
|
375
|
+
return Math.min((value / total) * 100, 100).toFixed(1);
|
|
153
376
|
}
|
|
154
377
|
|
|
155
378
|
function _renderModelBreakdown() {
|
|
156
|
-
|
|
157
|
-
|
|
379
|
+
const hasData = _summary.by_model && Object.keys(_summary.by_model).length > 0;
|
|
380
|
+
|
|
381
|
+
if (!hasData) {
|
|
382
|
+
return `
|
|
383
|
+
<div class="billing-section billing-model-section">
|
|
384
|
+
<h3>${I18n.t("billing.byModel") || "By Model"}</h3>
|
|
385
|
+
<div class="billing-model-empty">${I18n.t("billing.noData") || "No data"}</div>
|
|
386
|
+
</div>
|
|
387
|
+
`;
|
|
158
388
|
}
|
|
159
389
|
|
|
160
|
-
const
|
|
161
|
-
.sort((a, b) => (b[1].cost || b[1]) - (a[1].cost || a[1]))
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
390
|
+
const entries = Object.entries(_summary.by_model)
|
|
391
|
+
.sort((a, b) => (b[1].cost || b[1]) - (a[1].cost || a[1]));
|
|
392
|
+
|
|
393
|
+
const totalCost = entries.reduce((sum, [, data]) => sum + (typeof data === "object" ? data.cost : data), 0) || 1;
|
|
394
|
+
|
|
395
|
+
const rows = entries.map(([model, data]) => {
|
|
396
|
+
const cost = typeof data === "object" ? data.cost : data;
|
|
397
|
+
const requests = typeof data === "object" ? data.requests : 0;
|
|
398
|
+
const percent = ((cost / totalCost) * 100).toFixed(1);
|
|
399
|
+
return `
|
|
400
|
+
<div class="billing-model-row">
|
|
401
|
+
<div class="billing-model-info">
|
|
402
|
+
<span class="billing-model-name">${_esc(model)}</span>
|
|
403
|
+
<span class="billing-model-meta">${requests} ${I18n.t("billing.requests") || "requests"}</span>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="billing-model-bar-track">
|
|
406
|
+
<div class="billing-model-bar-fill" style="width: ${percent}%"></div>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="billing-model-cost">${_getCurrencySymbol()}${_formatCost(_convertCost(cost))}</div>
|
|
409
|
+
</div>
|
|
410
|
+
`;
|
|
411
|
+
}).join("");
|
|
173
412
|
|
|
174
413
|
return `
|
|
175
|
-
<div class="billing-section">
|
|
414
|
+
<div class="billing-section billing-model-section">
|
|
176
415
|
<h3>${I18n.t("billing.byModel") || "By Model"}</h3>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<th>${I18n.t("billing.model") || "Model"}</th>
|
|
181
|
-
<th>${I18n.t("billing.cost") || "Cost"}</th>
|
|
182
|
-
<th>${I18n.t("billing.requests") || "Requests"}</th>
|
|
183
|
-
</tr>
|
|
184
|
-
</thead>
|
|
185
|
-
<tbody>
|
|
186
|
-
${rows}
|
|
187
|
-
</tbody>
|
|
188
|
-
</table>
|
|
416
|
+
<div class="billing-model-list">
|
|
417
|
+
${rows}
|
|
418
|
+
</div>
|
|
189
419
|
</div>
|
|
190
420
|
`;
|
|
191
421
|
}
|
|
192
422
|
|
|
193
|
-
function
|
|
423
|
+
function _renderCombinedChart() {
|
|
194
424
|
if (!_daily || _daily.length === 0) {
|
|
195
|
-
return ""
|
|
425
|
+
return `<div class="billing-chart-card billing-chart-wide"><div class="billing-chart-empty">${I18n.t("billing.noData") || "No data available"}</div></div>`;
|
|
196
426
|
}
|
|
197
427
|
|
|
198
|
-
// Get last 14 days (show all days, not just those with activity)
|
|
199
428
|
const recentDays = _daily.slice(-14);
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
429
|
+
// Max values for scaling
|
|
430
|
+
const maxInput = Math.max(...recentDays.map(d => (d.prompt_tokens || 0) + (d.cache_read_tokens || 0)), 1);
|
|
431
|
+
const maxOutput = Math.max(...recentDays.map(d => d.completion_tokens || 0), 1);
|
|
432
|
+
const maxVal = Math.max(maxInput, maxOutput);
|
|
433
|
+
|
|
434
|
+
// Chart height in pixels
|
|
435
|
+
const chartHeight = 120;
|
|
436
|
+
|
|
437
|
+
// Generate bars: each date has Input (stacked: cache hit + cache miss) and Output
|
|
438
|
+
const chartBars = recentDays.map((d, i) => {
|
|
439
|
+
const cacheHit = d.cache_read_tokens || 0; // 命中缓存
|
|
440
|
+
const cacheMiss = d.prompt_tokens || 0; // 未命中缓存(实际发送的prompt)
|
|
441
|
+
const output = d.completion_tokens || 0;
|
|
442
|
+
const totalInput = cacheHit + cacheMiss;
|
|
443
|
+
const totalTokens = totalInput + output;
|
|
444
|
+
|
|
445
|
+
// Calculate heights in pixels
|
|
446
|
+
const cacheHitPx = Math.max((cacheHit / maxVal) * chartHeight, cacheHit > 0 ? 2 : 0);
|
|
447
|
+
const cacheMissPx = Math.max((cacheMiss / maxVal) * chartHeight, cacheMiss > 0 ? 2 : 0);
|
|
448
|
+
const outputPx = Math.max((output / maxVal) * chartHeight, output > 0 ? 2 : 0);
|
|
449
|
+
const date = d.date.slice(5);
|
|
450
|
+
const showLabel = i % 2 === 0 || i === recentDays.length - 1;
|
|
451
|
+
|
|
452
|
+
// Tooltip data attributes for custom tooltip
|
|
453
|
+
const tooltipData = `data-date="${d.date}" data-total="${_formatCompact(totalTokens)}" data-cache-hit="${_formatCompact(cacheHit)}" data-cache-miss="${_formatCompact(cacheMiss)}" data-output="${_formatCompact(output)}"`;
|
|
203
454
|
|
|
204
|
-
// Find max cost for scaling (minimum 0.0001 to avoid division by zero)
|
|
205
|
-
const maxCost = Math.max(...recentDays.map(d => d.cost || 0), 0.0001);
|
|
206
|
-
|
|
207
|
-
const bars = recentDays.map(d => {
|
|
208
|
-
const cost = d.cost || 0;
|
|
209
|
-
const height = cost > 0 ? Math.max((cost / maxCost) * 100, 5) : 0;
|
|
210
|
-
const date = d.date.slice(5); // MM-DD
|
|
211
|
-
const promptTokens = d.prompt_tokens || 0;
|
|
212
|
-
const completionTokens = d.completion_tokens || 0;
|
|
213
|
-
const cacheRead = d.cache_read_tokens || 0;
|
|
214
|
-
const cacheWrite = d.cache_write_tokens || 0;
|
|
215
|
-
const cacheHitRate = promptTokens > 0 ? ((cacheRead / promptTokens) * 100).toFixed(1) : 0;
|
|
216
|
-
const tooltip = `${d.date}\n${I18n.t("billing.totalCost") || "Cost"}: ${_getCurrencySymbol()}${_formatCost(_convertCost(cost))}\n${I18n.t("billing.promptTokens") || "Input"}: ${_formatNumber(promptTokens)}\n${I18n.t("billing.completionTokens") || "Output"}: ${_formatNumber(completionTokens)}\n${I18n.t("billing.cacheRead") || "Cache Read"}: ${_formatNumber(cacheRead)} (${cacheHitRate}%)\n${I18n.t("billing.cacheWrite") || "Cache Write"}: ${_formatNumber(cacheWrite)}\n${I18n.t("billing.requests") || "Requests"}: ${d.requests || 0}`;
|
|
217
455
|
return `
|
|
218
|
-
<div class="billing-
|
|
219
|
-
<div class="billing-
|
|
220
|
-
|
|
456
|
+
<div class="billing-bar-group" ${tooltipData}>
|
|
457
|
+
<div class="billing-bar-pair">
|
|
458
|
+
<div class="billing-input-stack">
|
|
459
|
+
<div class="billing-cache-hit" style="height: ${cacheHitPx}px"></div>
|
|
460
|
+
<div class="billing-cache-miss" style="height: ${cacheMissPx}px"></div>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="billing-output-bar" style="height: ${outputPx}px"></div>
|
|
463
|
+
</div>
|
|
464
|
+
${showLabel ? `<span class="billing-bar-date">${date}</span>` : '<span class="billing-bar-date"></span>'}
|
|
221
465
|
</div>
|
|
222
466
|
`;
|
|
223
467
|
}).join("");
|
|
224
468
|
|
|
225
469
|
return `
|
|
226
|
-
<div class="billing-
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
470
|
+
<div class="billing-chart-card billing-chart-wide">
|
|
471
|
+
<div class="billing-chart-header">
|
|
472
|
+
<h4>${I18n.t("billing.dailyUsage") || "Usage Details"}</h4>
|
|
473
|
+
<div class="billing-chart-legends">
|
|
474
|
+
<span class="billing-chart-legend">
|
|
475
|
+
<span class="billing-legend-dot billing-legend-cache-hit"></span>
|
|
476
|
+
${I18n.t("billing.inputCacheHit") || "Input (Cache Hit)"}
|
|
477
|
+
</span>
|
|
478
|
+
<span class="billing-chart-legend">
|
|
479
|
+
<span class="billing-legend-dot billing-legend-cache-miss"></span>
|
|
480
|
+
${I18n.t("billing.inputCacheMiss") || "Input (Cache Miss)"}
|
|
481
|
+
</span>
|
|
482
|
+
<span class="billing-chart-legend">
|
|
483
|
+
<span class="billing-legend-dot billing-legend-output"></span>
|
|
484
|
+
${I18n.t("billing.output") || "Output"}
|
|
485
|
+
</span>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
<div class="billing-combined-chart" id="billing-chart-container">
|
|
489
|
+
${chartBars}
|
|
230
490
|
</div>
|
|
231
491
|
</div>
|
|
492
|
+
<div class="billing-chart-tooltip" id="billing-tooltip"></div>
|
|
232
493
|
`;
|
|
233
494
|
}
|
|
234
495
|
|
|
@@ -244,6 +505,17 @@ const Billing = (() => {
|
|
|
244
505
|
return num.toLocaleString();
|
|
245
506
|
}
|
|
246
507
|
|
|
508
|
+
function _periodLabelShort(period) {
|
|
509
|
+
const labels = {
|
|
510
|
+
day: I18n.t("billing.period.day") || "Today",
|
|
511
|
+
week: I18n.t("billing.period.weekShort") || "Week",
|
|
512
|
+
month: I18n.t("billing.period.monthShort") || "Month",
|
|
513
|
+
year: I18n.t("billing.period.yearShort") || "Year",
|
|
514
|
+
all: I18n.t("billing.period.allShort") || "All"
|
|
515
|
+
};
|
|
516
|
+
return labels[period] || period;
|
|
517
|
+
}
|
|
518
|
+
|
|
247
519
|
function _periodLabel(period) {
|
|
248
520
|
const labels = {
|
|
249
521
|
day: I18n.t("billing.period.day") || "Today",
|