Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 129 additions & 9 deletions src/lib/OverviewPanel.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
<script lang="ts">
import { CalendarDays, Activity } from 'lucide-svelte';
import UsageHeatmap from '$lib/UsageHeatmap.svelte';
import { getDailyUsage, fmtTokens } from '$lib/usageStats';
import Segmented from '$lib/ui/Segmented.svelte';
import {
getDailyUsage,
fmtTokens,
sumDimension,
collectAgentLabels,
type UsageDimension
} from '$lib/usageStats';
import { t } from '$lib/i18n';

const DETAIL_DAYS = 15;
Expand All @@ -11,10 +18,39 @@
.filter(([, d]) => d.in + d.out > 0)
.sort((a, b) => (a[0] < b[0] ? 1 : -1))
.slice(0, DETAIL_DAYS);
const dayValues = days.map(([, d]) => d);
// days is newest-first; merge oldest-first so the latest ACP name wins.
const agentLabels = collectAgentLabels([...dayValues].reverse());

const provName = (p: string) => (p === 'other' ? t('settings.overview.other') : p);
const provRows = (d: (typeof days)[number][1]) =>
Object.entries(d.prov ?? {}).sort((a, b) => b[1].in + b[1].out - (a[1].in + a[1].out));
let dim = $state<UsageDimension>('prov');

// 稳定 agent key → 展示名;acp:<id> 用记录到的名称,缺失时退回 id。
const AGENT_NAMES: Record<string, string> = {
jucode: 'JuCode',
claude: 'Claude',
codex: 'Codex',
acp: 'ACP'
};

function keyName(key: string): string {
if (key === 'other') return t('settings.overview.other');
if (dim === 'agents') {
if (AGENT_NAMES[key]) return AGENT_NAMES[key];
if (key.startsWith('acp:')) return agentLabels[key] ?? key.slice(4);
}
return key;
}

const emptyKey = $derived(
dim === 'prov'
? 'settings.overview.noProvDetail'
: dim === 'models'
? 'settings.overview.noModelDetail'
: 'settings.overview.noAgentDetail'
);

const windowRows = $derived(sumDimension(dayValues, dim));
const windowMax = $derived(windowRows.length ? windowRows[0][1].in + windowRows[0][1].out : 0);
</script>

<div class="group">
Expand All @@ -23,21 +59,50 @@
<UsageHeatmap />

<div class="detail">
<div class="glabel"><Activity size={13} /> {t('settings.overview.detail')}</div>
<div class="dhead">
<div class="glabel"><Activity size={13} /> {t('settings.overview.detail')}</div>
<Segmented
value={dim}
options={[
{ value: 'prov', label: t('settings.overview.dimProvider') },
{ value: 'models', label: t('settings.overview.dimModel') },
{ value: 'agents', label: t('settings.overview.dimAgent') }
]}
onChange={(v) => (dim = v as UsageDimension)}
/>
</div>
{#if days.length === 0}
<p class="hint">{t('settings.overview.noData')}</p>
{:else}
{#if windowRows.length > 0}
<div class="win">
<div class="wtitle">{t('settings.overview.windowTotal', { n: DETAIL_DAYS })}</div>
{#each windowRows as [k, v] (k)}
<div class="wrow">
<span class="wname" title={k}>{keyName(k)}</span>
<span class="wbar">
<span
class="wfill"
style:width={`${windowMax ? Math.max(1, ((v.in + v.out) / windowMax) * 100) : 0}%`}
></span>
</span>
<span class="wval mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)} · {fmtTokens(v.in + v.out)}</span>
</div>
{/each}
</div>
{/if}
{#each days as [date, d] (date)}
{@const rows = sumDimension([d], dim)}
<div class="drow">
<span class="dd mono">{date}</span>
<span class="dt mono">↑{fmtTokens(d.in)} ↓{fmtTokens(d.out)} · {t('settings.overview.total')} {fmtTokens(d.in + d.out)}</span>
<span class="dp">
{#if d.prov && Object.keys(d.prov).length > 0}
{#each provRows(d) as [p, v] (p)}
<span class="chip">{provName(p)} <b class="mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)}</b></span>
{#if rows.length > 0}
{#each rows as [k, v] (k)}
<span class="chip" title={k}>{keyName(k)} <b class="mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)}</b></span>
{/each}
{:else}
<span class="chip dimmed">{t('settings.overview.noProvDetail')}</span>
<span class="chip dimmed">{t(emptyKey)}</span>
{/if}
</span>
</div>
Expand Down Expand Up @@ -69,6 +134,61 @@
.detail {
margin-top: 20px;
}
.dhead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.dhead .glabel {
margin-bottom: 0;
}
.win {
margin-bottom: 14px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
}
.wtitle {
font-size: 11px;
font-weight: 600;
color: var(--dim2);
margin-bottom: 8px;
}
.wrow {
display: grid;
grid-template-columns: minmax(60px, 160px) 1fr auto;
gap: 10px;
align-items: center;
padding: 3px 0;
}
.wname {
font-size: 12px;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.wbar {
height: 6px;
border-radius: 3px;
background: var(--surface2);
box-shadow: inset 0 0 0 1px var(--border);
overflow: hidden;
}
.wfill {
display: block;
height: 100%;
border-radius: 3px;
background: var(--accent);
}
.wval {
font-size: 11px;
color: var(--dim);
white-space: nowrap;
}
.drow {
display: grid;
grid-template-columns: 1fr auto;
Expand Down
14 changes: 13 additions & 1 deletion src/lib/chat.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,19 @@ export class ChatState {
this.totalOut += out;
// Estimate cost from tokens when the engine doesn't report it itself.
if (!this.#engineCost) this.cost += costUsd(this.model, inn, out);
recordUsage(inn, out, this.provider);
recordUsage(inn, out, {
provider: this.provider,
model: this.model,
// Stable agent keys: native backend ids as-is; acp sessions keyed
// by registry id so renames don't split history.
agent:
this.backendId === 'acp'
? this.acpAgentId
? `acp:${this.acpAgentId}`
: 'acp'
: this.backendId,
agentLabel: this.backendId === 'acp' ? this.acpAgentName : undefined
});
// Prefer the active assistant message (jucode reports usage per
// message, mid-turn). When it's already reset — e.g. claude reports
// one usage at the end of the turn, after the assistant finished —
Expand Down
12 changes: 12 additions & 0 deletions src/lib/i18n/messages/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ const settings = {
total: '共',
other: '其他',
noProvDetail: '无 Provider 明细',
noModelDetail: '无模型明细',
noAgentDetail: '无智能体明细',
dimProvider: '渠道',
dimModel: '模型',
dimAgent: '智能体',
windowTotal: '近 {n} 天合计',
heatmap: {
month: '{n}月',
ariaLabel: '每日 Token 用量热力图',
Expand Down Expand Up @@ -580,6 +586,12 @@ const settings = {
total: 'total',
other: 'Other',
noProvDetail: 'No provider breakdown',
noModelDetail: 'No model breakdown',
noAgentDetail: 'No agent breakdown',
dimProvider: 'Provider',
dimModel: 'Model',
dimAgent: 'Agent',
windowTotal: 'Last {n} days total',
heatmap: {
month: 'M{n}',
ariaLabel: 'Daily token usage heatmap',
Expand Down
Loading
Loading