@geminixiang/mikan 0.5.3 → 1.0.0-beta.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/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts +10 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +153 -95
- package/dist/agent.js.map +1 -1
- package/dist/commands/model.d.ts +2 -2
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +2 -5
- package/dist/commands/registry.js.map +1 -1
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +83 -5
- package/dist/config.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +2 -3
- package/dist/execution-resolver.js.map +1 -1
- package/dist/harness/auth.d.ts +16 -0
- package/dist/harness/auth.d.ts.map +1 -0
- package/dist/harness/auth.js +86 -0
- package/dist/harness/auth.js.map +1 -0
- package/dist/harness/extensions/loader.d.ts +43 -0
- package/dist/harness/extensions/loader.d.ts.map +1 -0
- package/dist/harness/extensions/loader.js +325 -0
- package/dist/harness/extensions/loader.js.map +1 -0
- package/dist/harness/extensions/registry.d.ts +23 -0
- package/dist/harness/extensions/registry.d.ts.map +1 -0
- package/dist/harness/extensions/registry.js +45 -0
- package/dist/harness/extensions/registry.js.map +1 -0
- package/dist/harness/extensions/types.d.ts +225 -0
- package/dist/harness/extensions/types.d.ts.map +1 -0
- package/dist/harness/extensions/types.js +2 -0
- package/dist/harness/extensions/types.js.map +1 -0
- package/dist/harness/http.d.ts +16 -0
- package/dist/harness/http.d.ts.map +1 -0
- package/dist/harness/http.js +71 -0
- package/dist/harness/http.js.map +1 -0
- package/dist/harness/index.d.ts +19 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +18 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/models.d.ts +34 -0
- package/dist/harness/models.d.ts.map +1 -0
- package/dist/harness/models.js +248 -0
- package/dist/harness/models.js.map +1 -0
- package/dist/harness/runner.d.ts +124 -0
- package/dist/harness/runner.d.ts.map +1 -0
- package/dist/harness/runner.js +436 -0
- package/dist/harness/runner.js.map +1 -0
- package/dist/harness/session-store.d.ts +77 -0
- package/dist/harness/session-store.d.ts.map +1 -0
- package/dist/harness/session-store.js +248 -0
- package/dist/harness/session-store.js.map +1 -0
- package/dist/harness/settings.d.ts +47 -0
- package/dist/harness/settings.d.ts.map +1 -0
- package/dist/harness/settings.js +29 -0
- package/dist/harness/settings.js.map +1 -0
- package/dist/harness/skills.d.ts +54 -0
- package/dist/harness/skills.d.ts.map +1 -0
- package/dist/harness/skills.js +203 -0
- package/dist/harness/skills.js.map +1 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +65 -3
- package/dist/main.js.map +1 -1
- package/dist/model-registry.d.ts +2 -2
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js.map +1 -1
- package/dist/runtime/conversation-runtime.js +1 -1
- package/dist/runtime/conversation-runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
- package/dist/sessions/agent-memory-file-manager.js +8 -8
- package/dist/sessions/agent-memory-file-manager.js.map +1 -1
- package/dist/sessions/metadata.js +2 -2
- package/dist/sessions/metadata.js.map +1 -1
- package/dist/sessions/store.d.ts +3 -3
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +4 -4
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +1 -1
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/trigger.d.ts.map +1 -1
- package/dist/trigger.js +5 -13
- package/dist/trigger.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +7 -1
- package/dist/vault/index.js.map +1 -1
- package/dist/web/admin/portal.d.ts.map +1 -1
- package/dist/web/admin/portal.js +328 -9
- package/dist/web/admin/portal.js.map +1 -1
- package/dist/web/admin/provider-models.d.ts +2 -2
- package/dist/web/admin/provider-models.d.ts.map +1 -1
- package/dist/web/admin/provider-models.js.map +1 -1
- package/dist/web/session-view/service.js +5 -5
- package/dist/web/session-view/service.js.map +1 -1
- package/package.json +3 -3
package/dist/web/admin/portal.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { basename, join, resolve as pathResolve, sep as pathSep } from "path";
|
|
4
|
-
import {
|
|
3
|
+
import { MikanModels, SessionStore } from "../../harness/index.js";
|
|
5
4
|
import { loadConversationAutoReplyConfig, loadGlobalSettings, resolveConversationSettings, saveConversationAutoReplyConfig, updateConversationSettings, updateGlobalSettings, } from "../../config.js";
|
|
6
5
|
import { escapeHtml } from "../../utils/html.js";
|
|
7
6
|
import { readRawBody } from "../../utils/http-body.js";
|
|
@@ -56,6 +55,10 @@ async function routeApiRequest(req, res, url, services) {
|
|
|
56
55
|
serveSessionUsage(res, services);
|
|
57
56
|
return;
|
|
58
57
|
}
|
|
58
|
+
if (url.pathname === "/admin/api/conversation-usage") {
|
|
59
|
+
serveConversationUsage(res, url, services);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
59
62
|
if (url.pathname === "/admin/api/conversation-state") {
|
|
60
63
|
serveConversationState(res, url, services, token);
|
|
61
64
|
return;
|
|
@@ -198,7 +201,9 @@ function listConversationDirs(workingDir) {
|
|
|
198
201
|
const dir = join(workingDir, name);
|
|
199
202
|
try {
|
|
200
203
|
const items = readdirSync(dir);
|
|
201
|
-
|
|
204
|
+
// Conversation settings migrated to the state dir, so a sessions/
|
|
205
|
+
// subdir is the primary marker for existing conversation dirs.
|
|
206
|
+
return items.some((item) => item === "sessions" || SETTINGS_FILES.has(item) || item.endsWith(".jsonl"));
|
|
202
207
|
}
|
|
203
208
|
catch {
|
|
204
209
|
return false;
|
|
@@ -289,7 +294,7 @@ function listConversationSessionUsage(workingDir, conversationId, label) {
|
|
|
289
294
|
}
|
|
290
295
|
function readSessionUsage(sessionFile, conversationId, label) {
|
|
291
296
|
try {
|
|
292
|
-
const manager =
|
|
297
|
+
const manager = SessionStore.open(sessionFile);
|
|
293
298
|
const header = manager.getHeader();
|
|
294
299
|
if (!header)
|
|
295
300
|
return [];
|
|
@@ -330,6 +335,114 @@ function readSessionUsage(sessionFile, conversationId, label) {
|
|
|
330
335
|
function numberOrZero(value) {
|
|
331
336
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
332
337
|
}
|
|
338
|
+
function localDayKey(d) {
|
|
339
|
+
const year = d.getFullYear();
|
|
340
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
341
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
342
|
+
return `${year}-${month}-${day}`;
|
|
343
|
+
}
|
|
344
|
+
function emptyBucket(date) {
|
|
345
|
+
return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 };
|
|
346
|
+
}
|
|
347
|
+
/** Per-conversation daily token usage over the last N days (N clamped to 1..7). */
|
|
348
|
+
function serveConversationUsage(res, url, services) {
|
|
349
|
+
const workingDir = requireAdminWorkingDir(res, services);
|
|
350
|
+
if (!workingDir)
|
|
351
|
+
return;
|
|
352
|
+
const conversationId = url.searchParams.get("conversationId") ?? "";
|
|
353
|
+
if (!conversationId || !listConversationDirs(workingDir).includes(conversationId)) {
|
|
354
|
+
jsonRes(res, 400, { error: "Unknown conversationId" });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const days = 14;
|
|
358
|
+
const today = new Date();
|
|
359
|
+
today.setHours(0, 0, 0, 0);
|
|
360
|
+
const buckets = new Map();
|
|
361
|
+
const order = [];
|
|
362
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
363
|
+
const d = new Date(today);
|
|
364
|
+
d.setDate(today.getDate() - i);
|
|
365
|
+
const key = localDayKey(d);
|
|
366
|
+
order.push(key);
|
|
367
|
+
buckets.set(key, emptyBucket(key));
|
|
368
|
+
}
|
|
369
|
+
const cutoff = new Date(today);
|
|
370
|
+
cutoff.setDate(today.getDate() - (days - 1));
|
|
371
|
+
const flags = { hasOlder: false };
|
|
372
|
+
const sessionDir = join(workingDir, conversationId, "sessions");
|
|
373
|
+
try {
|
|
374
|
+
for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
|
|
375
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
376
|
+
continue;
|
|
377
|
+
accumulateSessionUsageByDay(join(sessionDir, entry.name), cutoff, buckets, flags);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// No sessions directory yet — return empty buckets.
|
|
382
|
+
}
|
|
383
|
+
const series = order.map((key) => buckets.get(key) ?? emptyBucket(key));
|
|
384
|
+
const totals = series.reduce((sum, b) => {
|
|
385
|
+
sum.input += b.input;
|
|
386
|
+
sum.output += b.output;
|
|
387
|
+
sum.cacheRead += b.cacheRead;
|
|
388
|
+
sum.cacheWrite += b.cacheWrite;
|
|
389
|
+
sum.total += b.total;
|
|
390
|
+
sum.cost += b.cost;
|
|
391
|
+
return sum;
|
|
392
|
+
}, emptyBucket(""));
|
|
393
|
+
jsonRes(res, 200, {
|
|
394
|
+
conversationId,
|
|
395
|
+
label: conversationDisplayLabel(services, conversationId),
|
|
396
|
+
days,
|
|
397
|
+
hasOlder: flags.hasOlder,
|
|
398
|
+
buckets: series,
|
|
399
|
+
totals: {
|
|
400
|
+
input: totals.input,
|
|
401
|
+
output: totals.output,
|
|
402
|
+
cacheRead: totals.cacheRead,
|
|
403
|
+
cacheWrite: totals.cacheWrite,
|
|
404
|
+
total: totals.total,
|
|
405
|
+
cost: totals.cost,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function accumulateSessionUsageByDay(sessionFile, cutoff, buckets, flags) {
|
|
410
|
+
try {
|
|
411
|
+
const manager = SessionStore.open(sessionFile);
|
|
412
|
+
if (!manager.getHeader())
|
|
413
|
+
return;
|
|
414
|
+
for (const entry of manager.getEntries()) {
|
|
415
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
416
|
+
continue;
|
|
417
|
+
const usage = entry.message.usage;
|
|
418
|
+
if (!usage || !entry.timestamp)
|
|
419
|
+
continue;
|
|
420
|
+
const when = new Date(entry.timestamp);
|
|
421
|
+
if (Number.isNaN(when.getTime()))
|
|
422
|
+
continue;
|
|
423
|
+
if (when < cutoff) {
|
|
424
|
+
flags.hasOlder = true;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const bucket = buckets.get(localDayKey(when));
|
|
428
|
+
if (!bucket)
|
|
429
|
+
continue;
|
|
430
|
+
const input = numberOrZero(usage.input);
|
|
431
|
+
const output = numberOrZero(usage.output);
|
|
432
|
+
const cacheRead = numberOrZero(usage.cacheRead);
|
|
433
|
+
const cacheWrite = numberOrZero(usage.cacheWrite);
|
|
434
|
+
bucket.input += input;
|
|
435
|
+
bucket.output += output;
|
|
436
|
+
bucket.cacheRead += cacheRead;
|
|
437
|
+
bucket.cacheWrite += cacheWrite;
|
|
438
|
+
bucket.total += input + output + cacheRead + cacheWrite;
|
|
439
|
+
bucket.cost += numberOrZero(usage.cost?.total);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Skip unreadable session files.
|
|
444
|
+
}
|
|
445
|
+
}
|
|
333
446
|
function serveConversationState(res, url, services, token) {
|
|
334
447
|
const workingDir = requireAdminWorkingDir(res, services);
|
|
335
448
|
if (!workingDir)
|
|
@@ -386,8 +499,7 @@ function serveGlobalSettings(res) {
|
|
|
386
499
|
}
|
|
387
500
|
async function serveModelsList(res) {
|
|
388
501
|
try {
|
|
389
|
-
const
|
|
390
|
-
const registry = ModelRegistry.create(authStorage);
|
|
502
|
+
const registry = MikanModels.create();
|
|
391
503
|
const availableModels = await registry.getAvailable();
|
|
392
504
|
const statuses = await resolveAdminModelAccessStatuses(registry, availableModels);
|
|
393
505
|
const models = availableModels.map((model) => ({
|
|
@@ -1270,11 +1382,18 @@ function renderAdminPage(token) {
|
|
|
1270
1382
|
<header class="sect-head">
|
|
1271
1383
|
<div>
|
|
1272
1384
|
<p class="eyebrow">Token Usage</p>
|
|
1273
|
-
<h2 class="card-title">Top 20 sessions</h2>
|
|
1274
1385
|
</div>
|
|
1275
|
-
<button class="refresh-btn" onclick="
|
|
1386
|
+
<button class="refresh-btn" onclick="loadTokenUsage()">↻</button>
|
|
1276
1387
|
</header>
|
|
1388
|
+
<h2 class="card-subtitle" style="margin-bottom:10px">Top 20 sessions</h3>
|
|
1277
1389
|
<div id="session-usage-content"><div class="loading-msg">Loading…</div></div>
|
|
1390
|
+
<h2 class="card-subtitle" style="margin:24px 0 10px">Usage timeline</h3>
|
|
1391
|
+
<div class="timeline-controls">
|
|
1392
|
+
<label>Conversation
|
|
1393
|
+
<select id="timeline-conv" onchange="loadUsageTimeline()"></select>
|
|
1394
|
+
</label>
|
|
1395
|
+
</div>
|
|
1396
|
+
<div id="usage-timeline-content"><div class="loading-msg">Loading…</div></div>
|
|
1278
1397
|
</section>
|
|
1279
1398
|
|
|
1280
1399
|
<section class="card sect">
|
|
@@ -1805,7 +1924,7 @@ function renderAdminPage(token) {
|
|
|
1805
1924
|
if (globalLoaded) return;
|
|
1806
1925
|
globalLoaded = true;
|
|
1807
1926
|
loadAllConversations();
|
|
1808
|
-
|
|
1927
|
+
loadTokenUsage();
|
|
1809
1928
|
loadGlobalSettings();
|
|
1810
1929
|
loadGlobalSkills();
|
|
1811
1930
|
loadEvents();
|
|
@@ -1864,6 +1983,140 @@ function renderAdminPage(token) {
|
|
|
1864
1983
|
return Number(value || 0).toLocaleString('en-US');
|
|
1865
1984
|
}
|
|
1866
1985
|
|
|
1986
|
+
let timelineConvLoaded = false;
|
|
1987
|
+
let timelineData = null;
|
|
1988
|
+
let timelineFilter = null;
|
|
1989
|
+
async function ensureTimelineConvOptions() {
|
|
1990
|
+
if (timelineConvLoaded) return;
|
|
1991
|
+
const sel = document.getElementById('timeline-conv');
|
|
1992
|
+
const prev = sel.value;
|
|
1993
|
+
const data = await apiGet('/admin/api/conversations');
|
|
1994
|
+
if (!data.conversations.length) {
|
|
1995
|
+
sel.innerHTML = '<option value="">No conversations</option>';
|
|
1996
|
+
timelineConvLoaded = true;
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const want = prev || defaultConversationId;
|
|
2000
|
+
sel.innerHTML = data.conversations.map((c) =>
|
|
2001
|
+
'<option value="' + escAttr(c.conversationId) + '"' +
|
|
2002
|
+
(c.conversationId === want ? ' selected' : '') + '>' +
|
|
2003
|
+
escHtml(c.label || c.conversationId) + '</option>'
|
|
2004
|
+
).join('');
|
|
2005
|
+
timelineConvLoaded = true;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Shared refresh for the merged Token Usage section: reloads the session
|
|
2009
|
+
// ranking and re-fetches the conversation list (so new sessions appear),
|
|
2010
|
+
// preserving the currently selected conversation.
|
|
2011
|
+
async function loadTokenUsage() {
|
|
2012
|
+
loadSessionUsage();
|
|
2013
|
+
timelineConvLoaded = false;
|
|
2014
|
+
await loadUsageTimeline();
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
async function loadUsageTimeline() {
|
|
2018
|
+
const container = document.getElementById('usage-timeline-content');
|
|
2019
|
+
try {
|
|
2020
|
+
await ensureTimelineConvOptions();
|
|
2021
|
+
const conv = document.getElementById('timeline-conv').value;
|
|
2022
|
+
if (!conv) {
|
|
2023
|
+
container.innerHTML = '<div class="empty-state">No conversations found</div>';
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
2027
|
+
const data = await apiGet('/admin/api/conversation-usage?conversationId=' +
|
|
2028
|
+
encodeURIComponent(conv));
|
|
2029
|
+
timelineData = data;
|
|
2030
|
+
container.innerHTML = renderUsageTimeline(data);
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function tlCard(label, value) {
|
|
2037
|
+
return '<div class="tl-card"><div class="tl-card-label">' + label +
|
|
2038
|
+
'</div><div class="tl-card-value">' + value + '</div></div>';
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const TL_SERIES = [
|
|
2042
|
+
{ key: 'cacheRead', seg: 'tl-cache-read', sw: 'sw-cache-read', label: 'Cache read' },
|
|
2043
|
+
{ key: 'cacheWrite', seg: 'tl-cache-write', sw: 'sw-cache-write', label: 'Cache write' },
|
|
2044
|
+
{ key: 'input', seg: 'tl-input', sw: 'sw-input', label: 'Input' },
|
|
2045
|
+
{ key: 'output', seg: 'tl-output', sw: 'sw-output', label: 'Output' },
|
|
2046
|
+
];
|
|
2047
|
+
|
|
2048
|
+
// Click a legend item to show only that series; click it again for all.
|
|
2049
|
+
function toggleTimelineFilter(key) {
|
|
2050
|
+
timelineFilter = timelineFilter === key ? null : key;
|
|
2051
|
+
if (timelineData) {
|
|
2052
|
+
document.getElementById('usage-timeline-content').innerHTML = renderUsageTimeline(timelineData);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function renderUsageTimeline(data) {
|
|
2057
|
+
const buckets = data.buckets || [];
|
|
2058
|
+
const totals = data.totals || { total: 0, cost: 0, cacheRead: 0 };
|
|
2059
|
+
const cacheHit = totals.total > 0 ? Math.round((totals.cacheRead / totals.total) * 100) : 0;
|
|
2060
|
+
const cards = '<div class="tl-cards">' +
|
|
2061
|
+
tlCard('Total cost', totals.cost > 0 ? '$' + Number(totals.cost).toFixed(4) : '—') +
|
|
2062
|
+
tlCard('Total tokens', fmtNum(totals.total)) +
|
|
2063
|
+
tlCard('Cache hit', cacheHit + '%') +
|
|
2064
|
+
'</div>';
|
|
2065
|
+
|
|
2066
|
+
if (totals.total === 0) {
|
|
2067
|
+
const emptyNote = data.hasOlder
|
|
2068
|
+
? '<div class="tl-note">No usage in the last 14 days · earlier activity exists</div>'
|
|
2069
|
+
: '<div class="empty-state">No token usage in the last 14 days</div>';
|
|
2070
|
+
return cards + emptyNote;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const active = TL_SERIES.find((s) => s.key === timelineFilter) || null;
|
|
2074
|
+
const valueOf = (b) => active ? (b[active.key] || 0) : b.total;
|
|
2075
|
+
const max = Math.max(1, ...buckets.map(valueOf));
|
|
2076
|
+
const px = (v) => Math.round((v / max) * 180);
|
|
2077
|
+
|
|
2078
|
+
const legend = '<div class="tl-legend">' + TL_SERIES.map((s) => {
|
|
2079
|
+
const cls = 'tl-legend-item' +
|
|
2080
|
+
(active && active.key === s.key ? ' active' : (active ? ' dim' : ''));
|
|
2081
|
+
return '<span class="' + cls + '" onclick="toggleTimelineFilter(\\'' + s.key + '\\')">' +
|
|
2082
|
+
'<i class="sw ' + s.sw + '"></i>' + s.label + '</span>';
|
|
2083
|
+
}).join('') + '</div>';
|
|
2084
|
+
|
|
2085
|
+
const bars = buckets.map((b) => {
|
|
2086
|
+
const val = valueOf(b);
|
|
2087
|
+
const tip = active
|
|
2088
|
+
? b.date + ' · ' + active.label + ': ' + fmtNum(b[active.key] || 0) + ' tokens'
|
|
2089
|
+
: b.date + ' · ' + fmtNum(b.total) + ' tokens' +
|
|
2090
|
+
(b.cost > 0 ? ' · $' + Number(b.cost).toFixed(4) : '');
|
|
2091
|
+
let inner;
|
|
2092
|
+
if (val <= 0) {
|
|
2093
|
+
inner = '<span class="tl-empty"></span>';
|
|
2094
|
+
} else if (active) {
|
|
2095
|
+
inner = '<span class="tl-seg ' + active.seg + '" style="height:' + px(val) + 'px"></span>';
|
|
2096
|
+
} else {
|
|
2097
|
+
inner = '<span class="tl-seg tl-output" style="height:' + px(b.output) + 'px"></span>' +
|
|
2098
|
+
'<span class="tl-seg tl-input" style="height:' + px(b.input) + 'px"></span>' +
|
|
2099
|
+
'<span class="tl-seg tl-cache-write" style="height:' + px(b.cacheWrite) + 'px"></span>' +
|
|
2100
|
+
'<span class="tl-seg tl-cache-read" style="height:' + px(b.cacheRead) + 'px"></span>';
|
|
2101
|
+
}
|
|
2102
|
+
return '<div class="tl-bar">' +
|
|
2103
|
+
'<span class="tl-tip">' + escHtml(tip) + '</span>' +
|
|
2104
|
+
'<div class="tl-fill">' + inner + '</div>' +
|
|
2105
|
+
'</div>';
|
|
2106
|
+
}).join('');
|
|
2107
|
+
|
|
2108
|
+
const axis = buckets.length
|
|
2109
|
+
? '<div class="tl-axis"><span>' + escHtml(buckets[0].date.slice(5)) +
|
|
2110
|
+
'</span><span>' + escHtml(buckets[buckets.length - 1].date.slice(5)) + '</span></div>'
|
|
2111
|
+
: '';
|
|
2112
|
+
const note = data.hasOlder
|
|
2113
|
+
? '<div class="tl-note">Showing last 14 days · earlier activity not shown</div>'
|
|
2114
|
+
: '<div class="tl-note">Showing last 14 days</div>';
|
|
2115
|
+
const peak = '<div class="tl-peak" style="bottom:180px"><span class="tl-peak-label">' +
|
|
2116
|
+
fmtNum(max) + ' tokens</span></div>';
|
|
2117
|
+
return cards + legend + '<div class="tl-chart">' + peak + bars + '</div>' + axis + note;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1867
2120
|
async function loadGlobalSettings() {
|
|
1868
2121
|
const container = document.getElementById('global-settings-content');
|
|
1869
2122
|
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
@@ -2235,6 +2488,72 @@ const adminViewStyles = `
|
|
|
2235
2488
|
}
|
|
2236
2489
|
.usage-table code { font-size: 0.72rem; }
|
|
2237
2490
|
|
|
2491
|
+
.timeline-controls {
|
|
2492
|
+
display: flex; flex-wrap: wrap; gap: 18px; align-items: center; margin-bottom: 16px;
|
|
2493
|
+
}
|
|
2494
|
+
.timeline-controls label {
|
|
2495
|
+
display: flex; align-items: center; gap: 8px;
|
|
2496
|
+
font-size: 0.76rem; color: var(--muted);
|
|
2497
|
+
}
|
|
2498
|
+
.timeline-controls select {
|
|
2499
|
+
padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px;
|
|
2500
|
+
background: #fff; color: var(--text); font-size: 0.8rem; max-width: 240px;
|
|
2501
|
+
}
|
|
2502
|
+
.tl-note { margin-top: 8px; font-size: 0.72rem; color: var(--subtle); }
|
|
2503
|
+
.tl-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
2504
|
+
.tl-card { background: rgba(0,0,0,0.025); border-radius: 10px; padding: 10px 12px; }
|
|
2505
|
+
.tl-card-label { font-size: 0.73rem; color: var(--muted); margin-bottom: 4px; }
|
|
2506
|
+
.tl-card-value { font-size: 1.35rem; font-weight: 600; color: var(--text); }
|
|
2507
|
+
.tl-legend { display: flex; gap: 16px; font-size: 0.73rem; color: var(--muted); margin-bottom: 10px; }
|
|
2508
|
+
.tl-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
2509
|
+
.tl-legend-item { cursor: pointer; transition: opacity 100ms; }
|
|
2510
|
+
.tl-legend-item:hover { text-decoration: underline; }
|
|
2511
|
+
.tl-legend-item.active { color: var(--text); font-weight: 600; text-decoration: underline; }
|
|
2512
|
+
.tl-legend-item.dim { opacity: 0.4; }
|
|
2513
|
+
.sw { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
|
|
2514
|
+
.sw-cache-read { background: rgba(0,0,0,0.18); }
|
|
2515
|
+
.sw-cache-write { background: #3b6fb0; }
|
|
2516
|
+
.sw-input { background: var(--accent); }
|
|
2517
|
+
.sw-output { background: var(--ok-text); }
|
|
2518
|
+
.tl-chart {
|
|
2519
|
+
position: relative;
|
|
2520
|
+
display: flex; align-items: flex-end; gap: 6px;
|
|
2521
|
+
height: 216px; border-bottom: 1px solid var(--border);
|
|
2522
|
+
}
|
|
2523
|
+
.tl-peak {
|
|
2524
|
+
position: absolute; left: 0; right: 0; height: 0;
|
|
2525
|
+
border-top: 1px dashed var(--accent); pointer-events: none;
|
|
2526
|
+
}
|
|
2527
|
+
.tl-peak-label {
|
|
2528
|
+
position: absolute; left: 0; top: -15px;
|
|
2529
|
+
font-size: 0.7rem; color: var(--accent); white-space: nowrap;
|
|
2530
|
+
}
|
|
2531
|
+
.tl-bar {
|
|
2532
|
+
position: relative; flex: 1; min-width: 0; height: 180px;
|
|
2533
|
+
display: flex; flex-direction: column; justify-content: flex-end;
|
|
2534
|
+
border-radius: 6px 6px 0 0; transition: background 80ms ease;
|
|
2535
|
+
}
|
|
2536
|
+
.tl-bar:hover { background: rgba(0,0,0,0.06); }
|
|
2537
|
+
.tl-bar:hover .tl-seg { opacity: 0.55; }
|
|
2538
|
+
.tl-fill {
|
|
2539
|
+
display: flex; flex-direction: column; justify-content: flex-end;
|
|
2540
|
+
border-radius: 3px 3px 0 0; overflow: hidden;
|
|
2541
|
+
}
|
|
2542
|
+
.tl-tip {
|
|
2543
|
+
position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
|
|
2544
|
+
margin-bottom: 6px; padding: 5px 9px; border-radius: 6px;
|
|
2545
|
+
background: var(--text); color: #fafafa; font-size: 0.7rem; line-height: 1.35;
|
|
2546
|
+
white-space: nowrap; opacity: 0; pointer-events: none; z-index: 5;
|
|
2547
|
+
}
|
|
2548
|
+
.tl-bar:hover .tl-tip { opacity: 1; }
|
|
2549
|
+
.tl-seg { display: block; width: 100%; }
|
|
2550
|
+
.tl-seg.tl-output { background: var(--ok-text); }
|
|
2551
|
+
.tl-seg.tl-input { background: var(--accent); }
|
|
2552
|
+
.tl-seg.tl-cache-write { background: #3b6fb0; }
|
|
2553
|
+
.tl-seg.tl-cache-read { background: rgba(0,0,0,0.18); }
|
|
2554
|
+
.tl-empty { display: block; width: 100%; height: 3px; background: var(--border); }
|
|
2555
|
+
.tl-axis { display: flex; justify-content: space-between; margin-top: 6px; font-size: 0.7rem; color: var(--subtle); }
|
|
2556
|
+
|
|
2238
2557
|
.status-pill {
|
|
2239
2558
|
display: inline-flex; padding: 2px 9px; border-radius: 999px;
|
|
2240
2559
|
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
|