openclacky 1.2.4 → 1.2.6
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 +32 -0
- data/lib/clacky/agent/cost_tracker.rb +17 -9
- data/lib/clacky/agent/llm_caller.rb +25 -3
- data/lib/clacky/agent/tool_executor.rb +14 -0
- data/lib/clacky/agent/tool_registry.rb +0 -7
- data/lib/clacky/agent.rb +0 -8
- data/lib/clacky/billing/billing_store.rb +62 -4
- data/lib/clacky/brand_config.rb +11 -0
- data/lib/clacky/cli.rb +74 -23
- data/lib/clacky/client.rb +36 -2
- data/lib/clacky/default_parsers/wps_parser.rb +82 -0
- data/lib/clacky/default_skills/browser-setup/SKILL.md +16 -90
- data/lib/clacky/message_format/anthropic.rb +13 -3
- data/lib/clacky/message_format/bedrock.rb +2 -2
- data/lib/clacky/platform_http_client.rb +28 -1
- data/lib/clacky/providers.rb +0 -27
- data/lib/clacky/server/http_server.rb +24 -4
- data/lib/clacky/tools/browser.rb +4 -13
- data/lib/clacky/tools/terminal.rb +18 -7
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- data/lib/clacky/utils/scripts_manager.rb +0 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +422 -74
- data/lib/clacky/web/billing.js +371 -99
- data/lib/clacky/web/i18n.js +24 -2
- data/lib/clacky/web/index.html +1 -1
- data/lib/clacky/web/sessions.js +10 -68
- data/lib/clacky.rb +0 -3
- metadata +3 -6
- 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/scripts/wsl_network_doctor.ps1 +0 -196
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",
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -664,12 +664,23 @@ const I18n = (() => {
|
|
|
664
664
|
"billing.byModel": "By Model",
|
|
665
665
|
"billing.model": "Model",
|
|
666
666
|
"billing.cost": "Cost",
|
|
667
|
-
"billing.dailyUsage": "
|
|
667
|
+
"billing.dailyUsage": "Usage Details",
|
|
668
668
|
"billing.period.day": "Today",
|
|
669
669
|
"billing.period.week": "This Week",
|
|
670
670
|
"billing.period.month": "This Month",
|
|
671
671
|
"billing.period.year": "This Year",
|
|
672
672
|
"billing.period.all": "All Time",
|
|
673
|
+
"billing.clearData": "Clear Data",
|
|
674
|
+
"billing.clearToday": "Clear Today",
|
|
675
|
+
"billing.clearAll": "Clear All",
|
|
676
|
+
"billing.allModels": "All Models",
|
|
677
|
+
"billing.cacheHit": "Cache Hit",
|
|
678
|
+
"billing.inputCacheHit": "Input (Cache Hit)",
|
|
679
|
+
"billing.inputCacheMiss": "Input (Cache Miss)",
|
|
680
|
+
"billing.output": "Output",
|
|
681
|
+
"billing.tokenUsage": "Token Usage",
|
|
682
|
+
"billing.costTrend": "Cost Trend",
|
|
683
|
+
"billing.noData": "No data available",
|
|
673
684
|
},
|
|
674
685
|
|
|
675
686
|
zh: {
|
|
@@ -1321,12 +1332,23 @@ const I18n = (() => {
|
|
|
1321
1332
|
"billing.byModel": "按模型",
|
|
1322
1333
|
"billing.model": "模型",
|
|
1323
1334
|
"billing.cost": "费用",
|
|
1324
|
-
"billing.dailyUsage": "
|
|
1335
|
+
"billing.dailyUsage": "使用详情",
|
|
1325
1336
|
"billing.period.day": "今日",
|
|
1326
1337
|
"billing.period.week": "本周",
|
|
1327
1338
|
"billing.period.month": "本月",
|
|
1328
1339
|
"billing.period.year": "今年",
|
|
1329
1340
|
"billing.period.all": "全部",
|
|
1341
|
+
"billing.clearData": "清除数据",
|
|
1342
|
+
"billing.clearToday": "清除今日",
|
|
1343
|
+
"billing.clearAll": "全部清除",
|
|
1344
|
+
"billing.allModels": "所有模型",
|
|
1345
|
+
"billing.cacheHit": "缓存命中",
|
|
1346
|
+
"billing.inputCacheHit": "输入(命中缓存)",
|
|
1347
|
+
"billing.inputCacheMiss": "输入(未命中缓存)",
|
|
1348
|
+
"billing.output": "输出",
|
|
1349
|
+
"billing.tokenUsage": "Token 用量",
|
|
1350
|
+
"billing.costTrend": "费用趋势",
|
|
1351
|
+
"billing.noData": "暂无数据",
|
|
1330
1352
|
}
|
|
1331
1353
|
};
|
|
1332
1354
|
|
data/lib/clacky/web/index.html
CHANGED
|
@@ -381,7 +381,7 @@
|
|
|
381
381
|
<div id="image-preview-strip" style="display:none"></div>
|
|
382
382
|
<div id="input-bar">
|
|
383
383
|
<!-- Hidden file picker -->
|
|
384
|
-
<input type="file" id="image-file-input" accept="image/png,image/jpeg,image/gif,image/webp
|
|
384
|
+
<input type="file" id="image-file-input" accept="image/png,image/jpeg,image/gif,image/webp,*/*" multiple style="display:none">
|
|
385
385
|
<button id="btn-attach" title="Attach file (image, pdf, docx, md, tar.gz…) — drag & drop / Ctrl+V also work">
|
|
386
386
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
387
387
|
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66L9.41 17.41a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
|