downcity 1.1.410 → 1.1.411
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/bin/federation/web-ui/app.js +17 -5
- package/package.json +1 -1
|
@@ -106,8 +106,8 @@ async function render_overview() {
|
|
|
106
106
|
]);
|
|
107
107
|
const calls_chart = mixed_daily_chart(usage.days);
|
|
108
108
|
const top_users = await api(analytics_url("users"));
|
|
109
|
-
const ranking = top_users.items.slice(0, 6).map((item, index) => `<div class="ranking-row"><span class="rank">${index + 1}</span><div class="ranking-user"><strong>${escape_html(item.email || item.user_id)}</strong><small>${escape_html(item.top_model_id || "暂无模型")}</small></div><div class="ranking-value"><strong>${compact_number(item.total_tokens)}</strong><small>${number(item.execution_count)} calls</small></div></div>`).join("") || `<div class="empty compact-empty">暂无活跃用户</div>`;
|
|
110
|
-
content.innerHTML = `${metric_cards(metrics)}<div class="analytics-grid wide-left">${panel("活跃用户趋势", activity_chart, usage.timezone)}${panel("
|
|
109
|
+
const ranking = sort_usage_users(top_users.items, "total_tokens").slice(0, 6).map((item, index) => `<div class="ranking-row"><span class="rank">${index + 1}</span><div class="ranking-user"><strong>${escape_html(item.email || item.user_id)}</strong><small>${escape_html(item.top_model_id || "暂无模型")}</small></div><div class="ranking-value"><strong>${compact_number(item.total_tokens)}</strong><small>${number(item.execution_count)} calls</small></div></div>`).join("") || `<div class="empty compact-empty">暂无活跃用户</div>`;
|
|
110
|
+
content.innerHTML = `${metric_cards(metrics)}<div class="analytics-grid wide-left">${panel("活跃用户趋势", activity_chart, usage.timezone)}${panel("Token 消耗排行", ranking, "TOTAL TOKENS · TOP 6")}</div><div class="analytics-grid">${panel("调用与成功率", calls_chart, range.options[range.selectedIndex].text)}${panel("Token 构成", stacked_chart(usage.days, [["未缓存输入", "uncached_input_tokens", "#2563eb"], ["缓存输入", "cached_input_tokens", "#60a5fa"], ["输出", "output_tokens", "#8b5cf6"], ["推理", "reasoning_tokens", "#c4b5fd"]]), "STACKED")}</div>`;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
async function render_activity() {
|
|
@@ -215,7 +215,7 @@ async function render_usage_users() {
|
|
|
215
215
|
const data = await api(analytics_url("users"));
|
|
216
216
|
state.usage_users = data.items;
|
|
217
217
|
state.usage_page = 1;
|
|
218
|
-
content.innerHTML = `<div class="toolbar"><div class="search-field"><span>⌕</span><input id="usage-search" placeholder="搜索邮箱或 user_id"></div><select id="usage-sort"><option value="
|
|
218
|
+
content.innerHTML = `<div class="toolbar"><div class="search-field"><span>⌕</span><input id="usage-search" placeholder="搜索邮箱或 user_id"></div><select id="usage-sort"><option value="total_tokens">按 Token 消耗</option><option value="credits_used">按 Credits 消耗</option><option value="execution_count">按调用量</option><option value="p95_duration_ms">按 P95 耗时</option><option value="last_active_at">按最后活跃</option></select><span class="badge">${data.items.length} USERS</span></div><div id="usage-table"></div><div id="usage-drawer"></div>`;
|
|
219
219
|
document.querySelector("#usage-search").addEventListener("input", () => { state.usage_page = 1; render_usage_table(); });
|
|
220
220
|
document.querySelector("#usage-sort").addEventListener("change", () => { state.usage_page = 1; render_usage_table(); });
|
|
221
221
|
render_usage_table();
|
|
@@ -223,8 +223,11 @@ async function render_usage_users() {
|
|
|
223
223
|
|
|
224
224
|
function render_usage_table() {
|
|
225
225
|
const query = value("usage-search").toLowerCase();
|
|
226
|
-
const sort_key = value("usage-sort") || "
|
|
227
|
-
const items =
|
|
226
|
+
const sort_key = value("usage-sort") || "total_tokens";
|
|
227
|
+
const items = sort_usage_users(
|
|
228
|
+
state.usage_users.filter((item) => `${item.user_id} ${item.email}`.toLowerCase().includes(query)),
|
|
229
|
+
sort_key,
|
|
230
|
+
);
|
|
228
231
|
const page_size = 25;
|
|
229
232
|
const page_count = Math.max(1, Math.ceil(items.length / page_size));
|
|
230
233
|
state.usage_page = Math.min(state.usage_page, page_count);
|
|
@@ -235,6 +238,15 @@ function render_usage_table() {
|
|
|
235
238
|
document.querySelectorAll("[data-usage-user]").forEach((button) => button.addEventListener("click", () => show_usage_detail(decodeURIComponent(button.dataset.usageUser))));
|
|
236
239
|
}
|
|
237
240
|
|
|
241
|
+
function sort_usage_users(items, sort_key) {
|
|
242
|
+
return [...items].sort((left, right) => {
|
|
243
|
+
const difference = sort_key === "last_active_at"
|
|
244
|
+
? String(right.last_active_at || "").localeCompare(String(left.last_active_at || ""))
|
|
245
|
+
: Number(right[sort_key] || 0) - Number(left[sort_key] || 0);
|
|
246
|
+
return difference || String(left.user_id || "").localeCompare(String(right.user_id || ""));
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
238
250
|
function usage_table(items) {
|
|
239
251
|
if (!items.length) return `<div class="empty">没有匹配的用户</div>`;
|
|
240
252
|
return `<div class="table-wrap"><table><thead><tr><th>用户</th><th>最后活跃</th><th>调用</th><th>Total Tokens</th><th>Credits</th><th>成功率</th><th>平均 / P95</th><th>Top Model</th><th></th></tr></thead><tbody>${items.map((item) => `<tr><td><div class="user-cell"><span class="avatar">${escape_html((item.email || item.user_id || "U").slice(0, 1).toUpperCase())}</span><div><strong>${escape_html(item.email || "—")}</strong><small>${escape_html(item.user_id)}</small></div></div></td><td>${item.last_active_at ? new Date(item.last_active_at).toLocaleString() : "从未活跃"}</td><td>${number(item.execution_count)}</td><td>${compact_number(item.total_tokens)}</td><td>${compact_number(item.credits_used)}</td><td>${percent(item.success_rate)}</td><td>${duration(item.average_duration_ms)} / ${duration(item.p95_duration_ms)}</td><td><span class="model-pill">${escape_html(item.top_model_id || "—")}</span></td><td><button class="quiet-button" data-usage-user="${encodeURIComponent(item.user_id)}">详情 →</button></td></tr>`).join("")}</tbody></table></div>`;
|