|
1 | 1 | <script lang="ts"> |
2 | 2 | import { CalendarDays, Activity } from 'lucide-svelte'; |
3 | 3 | import UsageHeatmap from '$lib/UsageHeatmap.svelte'; |
4 | | - import { getDailyUsage, fmtTokens } from '$lib/usageStats'; |
| 4 | + import Segmented from '$lib/ui/Segmented.svelte'; |
| 5 | + import { |
| 6 | + getDailyUsage, |
| 7 | + fmtTokens, |
| 8 | + sumDimension, |
| 9 | + collectAgentLabels, |
| 10 | + type UsageDimension |
| 11 | + } from '$lib/usageStats'; |
5 | 12 | import { t } from '$lib/i18n'; |
6 | 13 |
|
7 | 14 | const DETAIL_DAYS = 15; |
|
11 | 18 | .filter(([, d]) => d.in + d.out > 0) |
12 | 19 | .sort((a, b) => (a[0] < b[0] ? 1 : -1)) |
13 | 20 | .slice(0, DETAIL_DAYS); |
| 21 | + const dayValues = days.map(([, d]) => d); |
| 22 | + // days is newest-first; merge oldest-first so the latest ACP name wins. |
| 23 | + const agentLabels = collectAgentLabels([...dayValues].reverse()); |
14 | 24 |
|
15 | | - const provName = (p: string) => (p === 'other' ? t('settings.overview.other') : p); |
16 | | - const provRows = (d: (typeof days)[number][1]) => |
17 | | - Object.entries(d.prov ?? {}).sort((a, b) => b[1].in + b[1].out - (a[1].in + a[1].out)); |
| 25 | + let dim = $state<UsageDimension>('prov'); |
| 26 | +
|
| 27 | + // 稳定 agent key → 展示名;acp:<id> 用记录到的名称,缺失时退回 id。 |
| 28 | + const AGENT_NAMES: Record<string, string> = { |
| 29 | + jucode: 'JuCode', |
| 30 | + claude: 'Claude', |
| 31 | + codex: 'Codex', |
| 32 | + acp: 'ACP' |
| 33 | + }; |
| 34 | +
|
| 35 | + function keyName(key: string): string { |
| 36 | + if (key === 'other') return t('settings.overview.other'); |
| 37 | + if (dim === 'agents') { |
| 38 | + if (AGENT_NAMES[key]) return AGENT_NAMES[key]; |
| 39 | + if (key.startsWith('acp:')) return agentLabels[key] ?? key.slice(4); |
| 40 | + } |
| 41 | + return key; |
| 42 | + } |
| 43 | +
|
| 44 | + const emptyKey = $derived( |
| 45 | + dim === 'prov' |
| 46 | + ? 'settings.overview.noProvDetail' |
| 47 | + : dim === 'models' |
| 48 | + ? 'settings.overview.noModelDetail' |
| 49 | + : 'settings.overview.noAgentDetail' |
| 50 | + ); |
| 51 | +
|
| 52 | + const windowRows = $derived(sumDimension(dayValues, dim)); |
| 53 | + const windowMax = $derived(windowRows.length ? windowRows[0][1].in + windowRows[0][1].out : 0); |
18 | 54 | </script> |
19 | 55 |
|
20 | 56 | <div class="group"> |
|
23 | 59 | <UsageHeatmap /> |
24 | 60 |
|
25 | 61 | <div class="detail"> |
26 | | - <div class="glabel"><Activity size={13} /> {t('settings.overview.detail')}</div> |
| 62 | + <div class="dhead"> |
| 63 | + <div class="glabel"><Activity size={13} /> {t('settings.overview.detail')}</div> |
| 64 | + <Segmented |
| 65 | + value={dim} |
| 66 | + options={[ |
| 67 | + { value: 'prov', label: t('settings.overview.dimProvider') }, |
| 68 | + { value: 'models', label: t('settings.overview.dimModel') }, |
| 69 | + { value: 'agents', label: t('settings.overview.dimAgent') } |
| 70 | + ]} |
| 71 | + onChange={(v) => (dim = v as UsageDimension)} |
| 72 | + /> |
| 73 | + </div> |
27 | 74 | {#if days.length === 0} |
28 | 75 | <p class="hint">{t('settings.overview.noData')}</p> |
29 | 76 | {:else} |
| 77 | + {#if windowRows.length > 0} |
| 78 | + <div class="win"> |
| 79 | + <div class="wtitle">{t('settings.overview.windowTotal', { n: DETAIL_DAYS })}</div> |
| 80 | + {#each windowRows as [k, v] (k)} |
| 81 | + <div class="wrow"> |
| 82 | + <span class="wname" title={k}>{keyName(k)}</span> |
| 83 | + <span class="wbar"> |
| 84 | + <span |
| 85 | + class="wfill" |
| 86 | + style:width={`${windowMax ? Math.max(1, ((v.in + v.out) / windowMax) * 100) : 0}%`} |
| 87 | + ></span> |
| 88 | + </span> |
| 89 | + <span class="wval mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)} · {fmtTokens(v.in + v.out)}</span> |
| 90 | + </div> |
| 91 | + {/each} |
| 92 | + </div> |
| 93 | + {/if} |
30 | 94 | {#each days as [date, d] (date)} |
| 95 | + {@const rows = sumDimension([d], dim)} |
31 | 96 | <div class="drow"> |
32 | 97 | <span class="dd mono">{date}</span> |
33 | 98 | <span class="dt mono">↑{fmtTokens(d.in)} ↓{fmtTokens(d.out)} · {t('settings.overview.total')} {fmtTokens(d.in + d.out)}</span> |
34 | 99 | <span class="dp"> |
35 | | - {#if d.prov && Object.keys(d.prov).length > 0} |
36 | | - {#each provRows(d) as [p, v] (p)} |
37 | | - <span class="chip">{provName(p)} <b class="mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)}</b></span> |
| 100 | + {#if rows.length > 0} |
| 101 | + {#each rows as [k, v] (k)} |
| 102 | + <span class="chip" title={k}>{keyName(k)} <b class="mono">↑{fmtTokens(v.in)} ↓{fmtTokens(v.out)}</b></span> |
38 | 103 | {/each} |
39 | 104 | {:else} |
40 | | - <span class="chip dimmed">{t('settings.overview.noProvDetail')}</span> |
| 105 | + <span class="chip dimmed">{t(emptyKey)}</span> |
41 | 106 | {/if} |
42 | 107 | </span> |
43 | 108 | </div> |
|
69 | 134 | .detail { |
70 | 135 | margin-top: 20px; |
71 | 136 | } |
| 137 | + .dhead { |
| 138 | + display: flex; |
| 139 | + align-items: center; |
| 140 | + justify-content: space-between; |
| 141 | + gap: 12px; |
| 142 | + margin-bottom: 10px; |
| 143 | + } |
| 144 | + .dhead .glabel { |
| 145 | + margin-bottom: 0; |
| 146 | + } |
| 147 | + .win { |
| 148 | + margin-bottom: 14px; |
| 149 | + padding: 10px 12px; |
| 150 | + border: 1px solid var(--border); |
| 151 | + border-radius: 8px; |
| 152 | + background: var(--surface); |
| 153 | + } |
| 154 | + .wtitle { |
| 155 | + font-size: 11px; |
| 156 | + font-weight: 600; |
| 157 | + color: var(--dim2); |
| 158 | + margin-bottom: 8px; |
| 159 | + } |
| 160 | + .wrow { |
| 161 | + display: grid; |
| 162 | + grid-template-columns: minmax(60px, 160px) 1fr auto; |
| 163 | + gap: 10px; |
| 164 | + align-items: center; |
| 165 | + padding: 3px 0; |
| 166 | + } |
| 167 | + .wname { |
| 168 | + font-size: 12px; |
| 169 | + color: var(--text); |
| 170 | + overflow: hidden; |
| 171 | + text-overflow: ellipsis; |
| 172 | + white-space: nowrap; |
| 173 | + } |
| 174 | + .wbar { |
| 175 | + height: 6px; |
| 176 | + border-radius: 3px; |
| 177 | + background: var(--surface2); |
| 178 | + box-shadow: inset 0 0 0 1px var(--border); |
| 179 | + overflow: hidden; |
| 180 | + } |
| 181 | + .wfill { |
| 182 | + display: block; |
| 183 | + height: 100%; |
| 184 | + border-radius: 3px; |
| 185 | + background: var(--accent); |
| 186 | + } |
| 187 | + .wval { |
| 188 | + font-size: 11px; |
| 189 | + color: var(--dim); |
| 190 | + white-space: nowrap; |
| 191 | + } |
72 | 192 | .drow { |
73 | 193 | display: grid; |
74 | 194 | grid-template-columns: 1fr auto; |
|
0 commit comments