dsh-all-usage 1.0.9 → 1.1.2
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 +54 -0
- package/README.md +59 -12
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/lib/client.js +1668 -100
- package/lib/index.js +1118 -167
- package/lib/pricing.js +645 -0
- package/package.json +3 -1
- package/screenshots.json +5 -0
package/lib/client.js
CHANGED
|
@@ -92,6 +92,10 @@ window.__ModuleLoader__.load({
|
|
|
92
92
|
if (n < 1000000000) return trim1(n / 1000000) + 'M'
|
|
93
93
|
return trim1(n / 1000000000) + 'B'
|
|
94
94
|
}
|
|
95
|
+
function fmtCount(n, language) {
|
|
96
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) return '0'
|
|
97
|
+
return Math.round(n).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN')
|
|
98
|
+
}
|
|
95
99
|
function rateOf(input, cacheRead) {
|
|
96
100
|
const denom = input + cacheRead
|
|
97
101
|
if (denom <= 0) return 0
|
|
@@ -106,6 +110,7 @@ window.__ModuleLoader__.load({
|
|
|
106
110
|
refresh: [React.createElement('path', { key: 'a', d: 'M19 9a7 7 0 1 0 1.1 5.2M19 4v5h-5', ...base })],
|
|
107
111
|
close: [React.createElement('path', { key: 'a', d: 'M6 6l12 12M18 6L6 18', ...base })],
|
|
108
112
|
chart: [React.createElement('path', { key: 'a', d: 'M4 19V5M4 19h16M7 15l3-4 3 2 5-7', ...base })],
|
|
113
|
+
list: [React.createElement('path', { key: 'a', d: 'M6 6h12M6 12h12M6 18h12', ...base }), React.createElement('circle', { key: 'b', cx: 3.5, cy: 6, r: .7, fill: 'currentColor' }), React.createElement('circle', { key: 'c', cx: 3.5, cy: 12, r: .7, fill: 'currentColor' }), React.createElement('circle', { key: 'd', cx: 3.5, cy: 18, r: .7, fill: 'currentColor' })],
|
|
109
114
|
cache: [React.createElement('path', { key: 'a', d: 'M12 4l7 4-7 4-7-4 7-4zM5 12l7 4 7-4M5 16l7 4 7-4', ...base })],
|
|
110
115
|
wallet: [React.createElement('path', { key: 'a', d: 'M4 7.5A2.5 2.5 0 0 1 6.5 5H18v14H6.5A2.5 2.5 0 0 1 4 16.5zM4 8h14M14 13h.01', ...base })],
|
|
111
116
|
clock: [React.createElement('path', { key: 'a', d: 'M12 6v6l4 2M20 12a8 8 0 1 1-16 0 8 8 0 0 1 16 0z', ...base })],
|
|
@@ -113,6 +118,7 @@ window.__ModuleLoader__.load({
|
|
|
113
118
|
language: [React.createElement('circle', { key: 'a', cx: 12, cy: 12, r: 8, ...base }), React.createElement('path', { key: 'b', d: 'M4 12h16M12 4c2.1 2.2 3.2 4.9 3.2 8S14.1 17.8 12 20M12 4C9.9 6.2 8.8 8.9 8.8 12s1.1 5.8 3.2 8', ...base })],
|
|
114
119
|
chevron: [React.createElement('path', { key: 'a', d: 'M7 10l5 5 5-5', ...base })],
|
|
115
120
|
check: [React.createElement('path', { key: 'a', d: 'M5 12.5l4.2 4.1L19 7.3', ...base })],
|
|
121
|
+
plus: [React.createElement('path', { key: 'a', d: 'M12 5v14M5 12h14', ...base })],
|
|
116
122
|
calendar: [React.createElement('path', { key: 'a', d: 'M6 4v3M18 4v3M4 9h16M5 6h14a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z', ...base })],
|
|
117
123
|
}
|
|
118
124
|
return React.createElement('svg', { className: 'uh-line-icon ' + (props.className || ''), width: size, height: size, viewBox: '0 0 24 24', 'aria-hidden': true }, paths[props.name] || paths.chart)
|
|
@@ -130,7 +136,101 @@ window.__ModuleLoader__.load({
|
|
|
130
136
|
function money(currency, n, language) {
|
|
131
137
|
if (n === null || n === undefined) return '—'
|
|
132
138
|
const sym = currency === 'CNY' ? '¥' : currency === 'USD' ? '$' : currency + ' '
|
|
133
|
-
return sym + n.toLocaleString(language === 'en' ? 'en-US' : 'zh-CN', { minimumFractionDigits:
|
|
139
|
+
return sym + n.toLocaleString(language === 'en' ? 'en-US' : 'zh-CN', { minimumFractionDigits: 4, maximumFractionDigits: 4 })
|
|
140
|
+
}
|
|
141
|
+
function decimalParts(value) {
|
|
142
|
+
const raw = typeof value === 'number' ? String(value) : typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
143
|
+
const match = raw.match(/^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/)
|
|
144
|
+
if (!match) return { digits: 0n, scale: 0 }
|
|
145
|
+
let digits = (match[1] || '') + (match[2] || '')
|
|
146
|
+
let scale = (match[2] || '').length - (match[3] ? Number(match[3]) : 0)
|
|
147
|
+
if (scale < 0) { digits += '0'.repeat(-scale); scale = 0 }
|
|
148
|
+
if (scale > digits.length) digits = '0'.repeat(scale - digits.length + 1) + digits
|
|
149
|
+
while (scale > 0 && digits.length > 1 && digits.endsWith('0')) { digits = digits.slice(0, -1); scale -= 1 }
|
|
150
|
+
return { digits: BigInt(digits.replace(/^0+(?=\d)/, '') || '0'), scale }
|
|
151
|
+
}
|
|
152
|
+
function decimalText(value) {
|
|
153
|
+
const parts = decimalParts(value)
|
|
154
|
+
if (parts.digits === 0n) return '0'
|
|
155
|
+
const raw = parts.digits.toString()
|
|
156
|
+
if (parts.scale === 0) return raw
|
|
157
|
+
const padded = raw.padStart(parts.scale + 1, '0')
|
|
158
|
+
const split = padded.length - parts.scale
|
|
159
|
+
return padded.slice(0, split) + '.' + padded.slice(split)
|
|
160
|
+
}
|
|
161
|
+
function decimalAdd(left, right) {
|
|
162
|
+
const a = decimalParts(left); const b = decimalParts(right)
|
|
163
|
+
const scale = Math.max(a.scale, b.scale)
|
|
164
|
+
const value = a.digits * 10n ** BigInt(scale - a.scale) + b.digits * 10n ** BigInt(scale - b.scale)
|
|
165
|
+
return decimalText(value.toString() + (scale > 0 ? 'e-' + scale : ''))
|
|
166
|
+
}
|
|
167
|
+
function emptyCostAggregate() {
|
|
168
|
+
return { currency: 'USD', input: '0', output: '0', cacheRead: '0', cacheWrite: '0', baseTotal: '0', total: '0', pricedCalls: 0, unpricedCalls: 0, ambiguousCalls: 0, unsupportedCalls: 0 }
|
|
169
|
+
}
|
|
170
|
+
function costAggregate(row) {
|
|
171
|
+
const source = row && row.cost && typeof row.cost === 'object' ? row.cost : (row && typeof row === 'object' ? row : {})
|
|
172
|
+
const value = emptyCostAggregate()
|
|
173
|
+
value.currency = typeof source.currency === 'string' && source.currency !== '' ? source.currency : 'USD'
|
|
174
|
+
if (source.breakdown && typeof source.breakdown === 'object') {
|
|
175
|
+
if (source.status === 'priced') {
|
|
176
|
+
value.input = decimalText(source.breakdown.input)
|
|
177
|
+
value.output = decimalText(source.breakdown.output)
|
|
178
|
+
value.cacheRead = decimalText(source.breakdown.cacheRead)
|
|
179
|
+
value.cacheWrite = decimalText(source.breakdown.cacheWrite)
|
|
180
|
+
value.baseTotal = decimalText(source.baseTotal)
|
|
181
|
+
value.total = decimalText(source.total)
|
|
182
|
+
value.pricedCalls = 1
|
|
183
|
+
} else if (source.status === 'ambiguous') value.ambiguousCalls = 1
|
|
184
|
+
else if (source.status === 'unsupported') value.unsupportedCalls = 1
|
|
185
|
+
else value.unpricedCalls = 1
|
|
186
|
+
return value
|
|
187
|
+
}
|
|
188
|
+
for (const key of ['input', 'output', 'cacheRead', 'cacheWrite', 'baseTotal', 'total']) value[key] = decimalText(source[key])
|
|
189
|
+
for (const key of ['pricedCalls', 'unpricedCalls', 'ambiguousCalls', 'unsupportedCalls']) value[key] = Number.isFinite(source[key]) ? source[key] : 0
|
|
190
|
+
return value
|
|
191
|
+
}
|
|
192
|
+
function addCostAggregate(target, row) {
|
|
193
|
+
const value = costAggregate(row)
|
|
194
|
+
for (const key of ['input', 'output', 'cacheRead', 'cacheWrite', 'baseTotal', 'total']) target[key] = decimalAdd(target[key], value[key])
|
|
195
|
+
target.pricedCalls += value.pricedCalls
|
|
196
|
+
target.unpricedCalls += value.unpricedCalls
|
|
197
|
+
target.ambiguousCalls += value.ambiguousCalls
|
|
198
|
+
target.unsupportedCalls += value.unsupportedCalls
|
|
199
|
+
return target
|
|
200
|
+
}
|
|
201
|
+
function costDisplay(row, language) {
|
|
202
|
+
const value = costAggregate(row)
|
|
203
|
+
if (value.pricedCalls <= 0) return '—'
|
|
204
|
+
const numeric = Number(value.total)
|
|
205
|
+
return Number.isFinite(numeric) ? money(value.currency, numeric, language) : value.currency + ' ' + value.total
|
|
206
|
+
}
|
|
207
|
+
function costCoverageLabel(row, language) {
|
|
208
|
+
const value = costAggregate(row)
|
|
209
|
+
if (value.pricedCalls > 0 && value.unpricedCalls === 0 && value.ambiguousCalls === 0 && value.unsupportedCalls === 0) return language === 'en' ? value.pricedCalls + ' priced' : value.pricedCalls + ' 次已计价'
|
|
210
|
+
const pending = value.unpricedCalls + value.ambiguousCalls + value.unsupportedCalls
|
|
211
|
+
return pending > 0 ? (language === 'en' ? pending + ' unpriced' : pending + ' 次未计价') : (language === 'en' ? 'No pricing' : '暂无价格')
|
|
212
|
+
}
|
|
213
|
+
function pricingDraftOf(pricing) {
|
|
214
|
+
const config = pricing && pricing.config && typeof pricing.config === 'object' ? pricing.config : {}
|
|
215
|
+
const sync = config.sync && typeof config.sync === 'object' ? config.sync : {}
|
|
216
|
+
return {
|
|
217
|
+
sync: { autoEnabled: sync.autoEnabled === true, intervalMs: Number.isFinite(sync.intervalMs) ? sync.intervalMs : 21600000 },
|
|
218
|
+
providerAliases: config.providerAliases && typeof config.providerAliases === 'object' ? Object.assign({}, config.providerAliases) : {},
|
|
219
|
+
mappings: Array.isArray(config.mappings) ? config.mappings.map((mapping) => Object.assign({}, mapping)) : [],
|
|
220
|
+
overrides: Array.isArray(config.overrides) ? config.overrides.map((entry) => Object.assign({}, entry)) : [],
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function pricingStatusLabel(status, language) {
|
|
224
|
+
const labels = { priced: ['已计价', 'priced'], unpriced: ['未计价', 'unpriced'], ambiguous: ['待确认', 'ambiguous'], unsupported: ['不支持', 'unsupported'] }
|
|
225
|
+
const pair = labels[status] || labels.unpriced
|
|
226
|
+
return language === 'en' ? pair[1] : pair[0]
|
|
227
|
+
}
|
|
228
|
+
function pricingModelKey(value) {
|
|
229
|
+
return String(value || '').trim().toLowerCase().replace(/^.*\//, '').split(':')[0]
|
|
230
|
+
}
|
|
231
|
+
function modelViewKey(value) {
|
|
232
|
+
const text = String(value || '').trim()
|
|
233
|
+
return text.includes(' / ') ? text : pricingModelKey(text)
|
|
134
234
|
}
|
|
135
235
|
function humanDate(date, language) {
|
|
136
236
|
const parts = date.split('-')
|
|
@@ -170,7 +270,7 @@ window.__ModuleLoader__.load({
|
|
|
170
270
|
return 'hsl(' + ((i * 137) % 360) + ', 70%, 55%)'
|
|
171
271
|
}
|
|
172
272
|
function rangeAgg(stats, range, utc, customRange) {
|
|
173
|
-
const empty = { totals: { turns: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }, perWs: [], perModel: [] }
|
|
273
|
+
const empty = { totals: { turns: 0, calls: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }, perWs: [], perModel: [] }
|
|
174
274
|
if (stats === null) return empty
|
|
175
275
|
if (range === 'all') return { totals: stats.totals, perWs: stats.perWorkspace, perModel: stats.perModel || [] }
|
|
176
276
|
const days = utc && Array.isArray(stats.byDayUtc) ? stats.byDayUtc : (Array.isArray(stats.byDay) ? stats.byDay : [])
|
|
@@ -188,7 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
188
288
|
} else {
|
|
189
289
|
start = fmtDate(shiftCalendarDate(new Date(), -89, utc), utc)
|
|
190
290
|
}
|
|
191
|
-
const t = { turns: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
291
|
+
const t = { turns: 0, calls: 0, sessions: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }
|
|
192
292
|
const per = new Map()
|
|
193
293
|
const models = new Map()
|
|
194
294
|
const sessionsInRange = new Set()
|
|
@@ -202,43 +302,143 @@ window.__ModuleLoader__.load({
|
|
|
202
302
|
t.cacheRead += day.tokens.cacheRead
|
|
203
303
|
t.cacheWrite += day.tokens.cacheWrite
|
|
204
304
|
t.reasoning += day.tokens.reasoning
|
|
305
|
+
addCostAggregate(t.cost, day.cost)
|
|
205
306
|
for (const w of day.byWorkspace) {
|
|
206
307
|
let p = per.get(w.workspaceId)
|
|
207
|
-
if (p === undefined) { p = { workspaceId: w.workspaceId, turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; per.set(w.workspaceId, p) }
|
|
308
|
+
if (p === undefined) { p = { workspaceId: w.workspaceId, turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }; per.set(w.workspaceId, p) }
|
|
208
309
|
p.input += w.input
|
|
209
310
|
p.output += w.output
|
|
210
311
|
p.cacheRead += w.cacheRead
|
|
211
312
|
p.cacheWrite += w.cacheWrite
|
|
212
313
|
p.reasoning += w.reasoning
|
|
314
|
+
addCostAggregate(p.cost, w.cost)
|
|
213
315
|
}
|
|
214
316
|
for (const w of day.perWorkspace) {
|
|
215
317
|
let p = per.get(w.workspaceId)
|
|
216
|
-
if (p === undefined) { p = { workspaceId: w.workspaceId, turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; per.set(w.workspaceId, p) }
|
|
318
|
+
if (p === undefined) { p = { workspaceId: w.workspaceId, turns: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }; per.set(w.workspaceId, p) }
|
|
217
319
|
p.turns += w.turns
|
|
218
320
|
}
|
|
219
321
|
for (const m of (day.byModel || [])) {
|
|
220
|
-
|
|
221
|
-
|
|
322
|
+
const key = m.identityKey || m.model
|
|
323
|
+
let p = models.get(key)
|
|
324
|
+
if (p === undefined) { p = { ...m, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }; models.set(key, p) }
|
|
222
325
|
p.calls += m.calls; p.input += m.input; p.output += m.output; p.cacheRead += m.cacheRead; p.cacheWrite += m.cacheWrite; p.reasoning += m.reasoning
|
|
326
|
+
t.calls += Number.isFinite(m.calls) ? m.calls : 0
|
|
327
|
+
addCostAggregate(p.cost, m.cost)
|
|
223
328
|
}
|
|
224
329
|
}
|
|
225
330
|
t.sessions = sessionsInRange.size
|
|
226
331
|
return { totals: t, perWs: Array.from(per.values()), perModel: Array.from(models.values()) }
|
|
227
332
|
}
|
|
228
|
-
function
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
|
|
333
|
+
function resolveRangeBounds(stats, range, utc, customRange) {
|
|
334
|
+
const days = utc && Array.isArray(stats && stats.byDayUtc) ? stats.byDayUtc : (Array.isArray(stats && stats.byDay) ? stats.byDay : [])
|
|
335
|
+
const latest = fmtDate(new Date(), utc)
|
|
336
|
+
if (range === 'custom') {
|
|
337
|
+
const normalized = normalizeCustomRange(customRange, utc)
|
|
338
|
+
return normalized === null ? null : { start: normalized.start, end: normalized.end }
|
|
339
|
+
}
|
|
340
|
+
if (range === 'today') return { start: latest, end: latest }
|
|
341
|
+
if (range === '30d') return { start: fmtDate(shiftCalendarDate(new Date(), -29, utc), utc), end: latest }
|
|
342
|
+
if (range === '90d') return { start: fmtDate(shiftCalendarDate(new Date(), -89, utc), utc), end: latest }
|
|
343
|
+
const bounds = availableDateBounds(days, latest)
|
|
344
|
+
return { start: bounds.min, end: bounds.max }
|
|
345
|
+
}
|
|
346
|
+
function makeUsageScope(stats, range, utc, customRange, workspaceId, provider, modelKey) {
|
|
347
|
+
const bounds = resolveRangeBounds(stats, range, utc, customRange)
|
|
348
|
+
if (bounds === null) return null
|
|
349
|
+
return { start: bounds.start, end: bounds.end, utc: utc === true, workspaceId: workspaceId || null, provider: provider || null, modelKey: modelKey || null }
|
|
350
|
+
}
|
|
351
|
+
function usageScopeKey(scope) {
|
|
352
|
+
return scope === null ? '' : JSON.stringify({ start: scope.start, end: scope.end, utc: scope.utc === true, workspaceId: scope.workspaceId || null, provider: scope.provider || null, modelKey: scope.modelKey || null })
|
|
353
|
+
}
|
|
354
|
+
function rowTokens(row) {
|
|
355
|
+
const tokens = row && row.tokens && typeof row.tokens === 'object' ? row.tokens : row || {}
|
|
356
|
+
return { input: Number.isFinite(tokens.input) ? tokens.input : 0, output: Number.isFinite(tokens.output) ? tokens.output : 0, cacheRead: Number.isFinite(tokens.cacheRead) ? tokens.cacheRead : 0, cacheWrite: Number.isFinite(tokens.cacheWrite) ? tokens.cacheWrite : 0, reasoning: Number.isFinite(tokens.reasoning) ? tokens.reasoning : 0 }
|
|
357
|
+
}
|
|
358
|
+
function buildTrendRows(rows, bounds, utc) {
|
|
359
|
+
if (bounds === null || typeof bounds !== 'object') return []
|
|
360
|
+
const source = new Map((Array.isArray(rows) ? rows : []).filter((row) => row && typeof row.date === 'string').map((row) => [row.date, row]))
|
|
361
|
+
const startParts = bounds.start.split('-').map(Number)
|
|
362
|
+
const endParts = bounds.end.split('-').map(Number)
|
|
363
|
+
const cursor = utc ? new Date(Date.UTC(startParts[0], startParts[1] - 1, startParts[2])) : new Date(startParts[0], startParts[1] - 1, startParts[2])
|
|
364
|
+
const end = utc ? new Date(Date.UTC(endParts[0], endParts[1] - 1, endParts[2])) : new Date(endParts[0], endParts[1] - 1, endParts[2])
|
|
365
|
+
const result = []
|
|
366
|
+
while (cursor.getTime() <= end.getTime()) {
|
|
367
|
+
const date = fmtDate(cursor, utc)
|
|
368
|
+
const row = source.get(date)
|
|
369
|
+
const tokens = rowTokens(row)
|
|
370
|
+
result.push({ date, turns: row && Number.isFinite(row.turns) ? row.turns : 0, calls: row && Number.isFinite(row.calls) ? row.calls : 0, sessions: row && Number.isFinite(row.sessions) ? row.sessions : 0, tokens, cost: costAggregate(row), total: tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite + tokens.reasoning })
|
|
371
|
+
if (utc) cursor.setUTCDate(cursor.getUTCDate() + 1)
|
|
372
|
+
else cursor.setDate(cursor.getDate() + 1)
|
|
373
|
+
}
|
|
374
|
+
return result
|
|
375
|
+
}
|
|
376
|
+
function buildTrendHourlyRows(rows, utc) {
|
|
377
|
+
const result = []
|
|
378
|
+
for (const row of (Array.isArray(rows) ? rows : [])) {
|
|
379
|
+
if (row === null || typeof row !== 'object') continue
|
|
380
|
+
const time = Number.isFinite(row.time) ? row.time : (typeof row.date === 'string' ? Date.parse(row.date) : NaN)
|
|
381
|
+
if (!Number.isFinite(time)) continue
|
|
382
|
+
const tokens = rowTokens(row)
|
|
383
|
+
result.push({ date: fmtDate(new Date(time), utc), time, turns: Number.isFinite(row.turns) ? row.turns : 0, calls: Number.isFinite(row.calls) ? row.calls : 0, sessions: Number.isFinite(row.sessions) ? row.sessions : 0, tokens, cost: costAggregate(row), total: tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite + tokens.reasoning })
|
|
384
|
+
}
|
|
385
|
+
return result.sort((a, b) => a.time - b.time)
|
|
386
|
+
}
|
|
387
|
+
function trendHourLabel(time, language, detailed) {
|
|
388
|
+
const options = detailed
|
|
389
|
+
? { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }
|
|
390
|
+
: { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }
|
|
391
|
+
if (language === 'en') options.timeZone = 'UTC'
|
|
392
|
+
return new Date(time).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN', options)
|
|
393
|
+
}
|
|
394
|
+
function trendRowLabel(row, language, detailed) {
|
|
395
|
+
if (row && Number.isFinite(row.time)) return trendHourLabel(row.time, language, detailed)
|
|
396
|
+
if (!row || typeof row.date !== 'string') return ''
|
|
397
|
+
return detailed ? humanDate(row.date, language) : row.date.slice(5)
|
|
398
|
+
}
|
|
399
|
+
function trendRowKey(row, index) {
|
|
400
|
+
return row && Number.isFinite(row.time) ? String(row.time) : (row && typeof row.date === 'string' ? row.date : String(index))
|
|
401
|
+
}
|
|
402
|
+
function trendRowDate(row) {
|
|
403
|
+
return row && typeof row.date === 'string' ? row.date : ''
|
|
404
|
+
}
|
|
405
|
+
function buildTrendGeometry(rows, visible, width = 900, height = 250) {
|
|
406
|
+
const keys = Array.isArray(visible) && visible.length > 0 ? visible : ['total']
|
|
407
|
+
const padding = { left: 46, right: 14, top: 14, bottom: 30 }
|
|
408
|
+
const innerWidth = Math.max(1, width - padding.left - padding.right)
|
|
409
|
+
const innerHeight = Math.max(1, height - padding.top - padding.bottom)
|
|
410
|
+
const values = (Array.isArray(rows) ? rows : []).flatMap((row) => keys.map((key) => key === 'total' ? row.total : row.tokens[key] || 0))
|
|
411
|
+
const max = Math.max(1, ...values)
|
|
412
|
+
const points = {}
|
|
413
|
+
for (const key of keys) points[key] = (Array.isArray(rows) ? rows : []).map((row, index) => ({ x: padding.left + (rows.length > 1 ? index * innerWidth / (rows.length - 1) : innerWidth / 2), y: padding.top + innerHeight - ((key === 'total' ? row.total : row.tokens[key] || 0) / max) * innerHeight, value: key === 'total' ? row.total : row.tokens[key] || 0 }))
|
|
414
|
+
return { width, height, padding, max, points }
|
|
415
|
+
}
|
|
416
|
+
function modelParts(row, unknownProvider, unknownModel) {
|
|
417
|
+
const structuredModel = typeof row.actualModel === 'string' && row.actualModel !== '' ? row.actualModel : (typeof row.requestedModel === 'string' && row.requestedModel !== '' ? row.requestedModel : '')
|
|
418
|
+
const displayModel = typeof row.model === 'string' && row.model !== '' ? row.model : unknownModel
|
|
419
|
+
const separator = displayModel.indexOf(' / ')
|
|
420
|
+
const rowProvider = typeof row.provider === 'string' && row.provider !== '' ? row.provider : ''
|
|
421
|
+
const provider = rowProvider || (separator > 0 ? displayModel.slice(0, separator) : unknownProvider)
|
|
422
|
+
const providerPrefix = provider !== unknownProvider ? provider + ' / ' : ''
|
|
423
|
+
const fallbackModel = structuredModel !== '' ? displayModel : providerPrefix !== '' && displayModel.startsWith(providerPrefix) ? displayModel.slice(providerPrefix.length) : separator > 0 ? displayModel.slice(separator + 3) : displayModel
|
|
424
|
+
const model = modelViewKey(structuredModel || fallbackModel) || unknownModel
|
|
425
|
+
return { provider, model }
|
|
426
|
+
}
|
|
427
|
+
function modelOptionLabel(row, unknownProvider, unknownModel) {
|
|
428
|
+
const parts = modelParts(row, unknownProvider, unknownModel)
|
|
429
|
+
const base = parts.provider + ' / ' + parts.model
|
|
430
|
+
return row && row.requestedModel && row.actualModel && row.requestedModel !== row.actualModel ? base + ' ← ' + row.requestedModel : base
|
|
232
431
|
}
|
|
233
432
|
function aggregateModelRows(rows, view, unknownProvider, unknownModel) {
|
|
234
433
|
if (view === 'route') return rows.slice()
|
|
235
434
|
const grouped = new Map()
|
|
236
435
|
for (const row of rows) {
|
|
237
|
-
const parts = modelParts(row
|
|
436
|
+
const parts = modelParts(row, unknownProvider, unknownModel)
|
|
238
437
|
const key = view === 'model' ? parts.model : parts.provider
|
|
239
438
|
let item = grouped.get(key)
|
|
240
|
-
if (item === undefined) { item = { model: key, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }; grouped.set(key, item) }
|
|
439
|
+
if (item === undefined) { item = { model: key, provider: view === 'provider' ? key : parts.provider, calls: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, cost: emptyCostAggregate() }; grouped.set(key, item) }
|
|
241
440
|
item.calls += row.calls; item.input += row.input; item.output += row.output; item.cacheRead += row.cacheRead; item.cacheWrite += row.cacheWrite; item.reasoning += row.reasoning
|
|
441
|
+
addCostAggregate(item.cost, row.cost)
|
|
242
442
|
}
|
|
243
443
|
return Array.from(grouped.values())
|
|
244
444
|
}
|
|
@@ -295,20 +495,375 @@ window.__ModuleLoader__.load({
|
|
|
295
495
|
return state.value
|
|
296
496
|
}
|
|
297
497
|
|
|
498
|
+
function smoothTrendPath(points) {
|
|
499
|
+
if (!Array.isArray(points) || points.length === 0) return ''
|
|
500
|
+
if (points.length === 1) return 'M' + points[0].x.toFixed(2) + ' ' + points[0].y.toFixed(2)
|
|
501
|
+
const slopes = []
|
|
502
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
503
|
+
const dx = points[i + 1].x - points[i].x
|
|
504
|
+
slopes.push(dx === 0 ? 0 : (points[i + 1].y - points[i].y) / dx)
|
|
505
|
+
}
|
|
506
|
+
const tangents = new Array(points.length).fill(0)
|
|
507
|
+
tangents[0] = slopes[0]
|
|
508
|
+
tangents[points.length - 1] = slopes[slopes.length - 1]
|
|
509
|
+
for (let i = 1; i < points.length - 1; i += 1) {
|
|
510
|
+
const before = slopes[i - 1]
|
|
511
|
+
const after = slopes[i]
|
|
512
|
+
tangents[i] = before * after <= 0 ? 0 : (before + after) / 2
|
|
513
|
+
}
|
|
514
|
+
// Fritsch-Carlson limiting keeps the smooth curve monotone between points.
|
|
515
|
+
for (let i = 0; i < slopes.length; i += 1) {
|
|
516
|
+
if (slopes[i] === 0) { tangents[i] = 0; tangents[i + 1] = 0; continue }
|
|
517
|
+
const a = tangents[i] / slopes[i]
|
|
518
|
+
const b = tangents[i + 1] / slopes[i]
|
|
519
|
+
const magnitude = a * a + b * b
|
|
520
|
+
if (magnitude > 9) {
|
|
521
|
+
const scale = 3 / Math.sqrt(magnitude)
|
|
522
|
+
tangents[i] = scale * a * slopes[i]
|
|
523
|
+
tangents[i + 1] = scale * b * slopes[i]
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
let path = 'M' + points[0].x.toFixed(2) + ' ' + points[0].y.toFixed(2)
|
|
527
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
528
|
+
const dx = points[i + 1].x - points[i].x
|
|
529
|
+
const c1x = points[i].x + dx / 3
|
|
530
|
+
const c1y = points[i].y + tangents[i] * dx / 3
|
|
531
|
+
const c2x = points[i + 1].x - dx / 3
|
|
532
|
+
const c2y = points[i + 1].y - tangents[i + 1] * dx / 3
|
|
533
|
+
path += ' C' + c1x.toFixed(2) + ' ' + c1y.toFixed(2) + ' ' + c2x.toFixed(2) + ' ' + c2y.toFixed(2) + ' ' + points[i + 1].x.toFixed(2) + ' ' + points[i + 1].y.toFixed(2)
|
|
534
|
+
}
|
|
535
|
+
return path
|
|
536
|
+
}
|
|
537
|
+
function trendPathLength(points) {
|
|
538
|
+
if (!Array.isArray(points) || points.length < 2) return 1
|
|
539
|
+
let length = 0
|
|
540
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
541
|
+
const dx = points[i].x - points[i - 1].x
|
|
542
|
+
const dy = points[i].y - points[i - 1].y
|
|
543
|
+
length += Math.sqrt(dx * dx + dy * dy)
|
|
544
|
+
}
|
|
545
|
+
return Math.max(1, Math.ceil(length * 1.35 + 2))
|
|
546
|
+
}
|
|
547
|
+
const DONUT_COLORS = ['#0a84ff', '#30d158', '#bf5af2', '#ff9f0a', '#ff375f', '#64d2ff']
|
|
548
|
+
function tokenMagnitude(value, language) {
|
|
549
|
+
const magnitude = chineseMagnitude(value, language)
|
|
550
|
+
return magnitude !== '' ? magnitude : fmtCompact(value)
|
|
551
|
+
}
|
|
552
|
+
function tokenDisplay(value, language) {
|
|
553
|
+
return tokenMagnitude(value, language) + (language === 'en' ? ' tokens' : ' Token')
|
|
554
|
+
}
|
|
555
|
+
function buildDonutSegments(items, otherLabel, limit = 5) {
|
|
556
|
+
const topLimit = Math.max(1, Number.isInteger(limit) ? limit : 5)
|
|
557
|
+
const normalized = (Array.isArray(items) ? items : []).map((item, index) => ({
|
|
558
|
+
label: item && item.label !== undefined ? String(item.label) : '',
|
|
559
|
+
value: Number(item && item.value),
|
|
560
|
+
color: item && typeof item.color === 'string' && item.color !== '' ? item.color : DONUT_COLORS[index % DONUT_COLORS.length],
|
|
561
|
+
cost: costAggregate(item),
|
|
562
|
+
})).filter((item) => item.label !== '' && Number.isFinite(item.value) && item.value > 0).sort((a, b) => b.value - a.value)
|
|
563
|
+
const total = normalized.reduce((sum, item) => sum + item.value, 0)
|
|
564
|
+
if (total <= 0) return { total: 0, segments: [] }
|
|
565
|
+
const segments = normalized.slice(0, topLimit)
|
|
566
|
+
const remainderItems = normalized.slice(topLimit)
|
|
567
|
+
const remainder = remainderItems.reduce((sum, item) => sum + item.value, 0)
|
|
568
|
+
if (remainder > 0) {
|
|
569
|
+
const remainderCost = emptyCostAggregate()
|
|
570
|
+
for (const item of remainderItems) addCostAggregate(remainderCost, item.cost)
|
|
571
|
+
segments.push({ label: otherLabel + ' (' + (normalized.length - topLimit) + ')', value: remainder, color: '#b8c2cf', cost: remainderCost, other: true })
|
|
572
|
+
}
|
|
573
|
+
let angle = -Math.PI / 2
|
|
574
|
+
return {
|
|
575
|
+
total,
|
|
576
|
+
segments: segments.map((item, index) => {
|
|
577
|
+
const sweep = item.value / total * Math.PI * 2
|
|
578
|
+
const gap = segments.length > 1 ? Math.min(.018, sweep / 3) : 0
|
|
579
|
+
const startAngle = angle + gap
|
|
580
|
+
const endAngle = angle + sweep - gap
|
|
581
|
+
angle += sweep
|
|
582
|
+
return { ...item, index, percentage: item.value / total * 100, startAngle: endAngle <= startAngle ? angle - sweep : startAngle, endAngle: endAngle <= startAngle ? angle : endAngle }
|
|
583
|
+
}),
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function donutArcPath(cx, cy, outerRadius, innerRadius, startAngle, endAngle) {
|
|
587
|
+
const sweep = Math.max(0, endAngle - startAngle)
|
|
588
|
+
const point = (radius, angle) => ({ x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) })
|
|
589
|
+
const outerStart = point(outerRadius, startAngle)
|
|
590
|
+
const innerStart = point(innerRadius, startAngle)
|
|
591
|
+
if (sweep >= Math.PI * 2 - .0001) {
|
|
592
|
+
const outerMid = point(outerRadius, startAngle + Math.PI)
|
|
593
|
+
const innerMid = point(innerRadius, startAngle + Math.PI)
|
|
594
|
+
return 'M' + outerStart.x.toFixed(2) + ' ' + outerStart.y.toFixed(2) + ' A' + outerRadius + ' ' + outerRadius + ' 0 1 1 ' + outerMid.x.toFixed(2) + ' ' + outerMid.y.toFixed(2) + ' A' + outerRadius + ' ' + outerRadius + ' 0 1 1 ' + outerStart.x.toFixed(2) + ' ' + outerStart.y.toFixed(2) + ' L' + innerStart.x.toFixed(2) + ' ' + innerStart.y.toFixed(2) + ' A' + innerRadius + ' ' + innerRadius + ' 0 1 0 ' + innerMid.x.toFixed(2) + ' ' + innerMid.y.toFixed(2) + ' A' + innerRadius + ' ' + innerRadius + ' 0 1 0 ' + innerStart.x.toFixed(2) + ' ' + innerStart.y.toFixed(2) + ' Z'
|
|
595
|
+
}
|
|
596
|
+
const outerEnd = point(outerRadius, endAngle)
|
|
597
|
+
const innerEnd = point(innerRadius, endAngle)
|
|
598
|
+
const largeArc = sweep > Math.PI ? 1 : 0
|
|
599
|
+
return 'M' + outerStart.x.toFixed(2) + ' ' + outerStart.y.toFixed(2) + ' A' + outerRadius + ' ' + outerRadius + ' 0 ' + largeArc + ' 1 ' + outerEnd.x.toFixed(2) + ' ' + outerEnd.y.toFixed(2) + ' L' + innerEnd.x.toFixed(2) + ' ' + innerEnd.y.toFixed(2) + ' A' + innerRadius + ' ' + innerRadius + ' 0 ' + largeArc + ' 0 ' + innerStart.x.toFixed(2) + ' ' + innerStart.y.toFixed(2) + ' Z'
|
|
600
|
+
}
|
|
601
|
+
function donutArcLinePath(cx, cy, radius, startAngle, endAngle) {
|
|
602
|
+
const sweep = Math.max(0, endAngle - startAngle)
|
|
603
|
+
const point = (angle) => ({ x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) })
|
|
604
|
+
const start = point(startAngle)
|
|
605
|
+
if (sweep >= Math.PI * 2 - .0001) {
|
|
606
|
+
const mid = point(startAngle + Math.PI)
|
|
607
|
+
return 'M' + start.x.toFixed(2) + ' ' + start.y.toFixed(2) + ' A' + radius + ' ' + radius + ' 0 1 1 ' + mid.x.toFixed(2) + ' ' + mid.y.toFixed(2) + ' A' + radius + ' ' + radius + ' 0 1 1 ' + start.x.toFixed(2) + ' ' + start.y.toFixed(2)
|
|
608
|
+
}
|
|
609
|
+
const end = point(endAngle)
|
|
610
|
+
return 'M' + start.x.toFixed(2) + ' ' + start.y.toFixed(2) + ' A' + radius + ' ' + radius + ' 0 ' + (sweep > Math.PI ? 1 : 0) + ' 1 ' + end.x.toFixed(2) + ' ' + end.y.toFixed(2)
|
|
611
|
+
}
|
|
612
|
+
function UsageDonutChart(props) {
|
|
613
|
+
const language = props.language === 'en' ? 'en' : 'zh'
|
|
614
|
+
const tr = (zh, en) => language === 'en' ? en : zh
|
|
615
|
+
const [activeIndex, setActiveIndex] = React.useState(null)
|
|
616
|
+
const [tooltipPosition, setTooltipPosition] = React.useState(null)
|
|
617
|
+
const data = buildDonutSegments(props.items, tr('其他', 'Other'))
|
|
618
|
+
const activeSegment = activeIndex === null ? null : (data.segments[activeIndex] || null)
|
|
619
|
+
if (data.total <= 0) return null
|
|
620
|
+
const cx = 130
|
|
621
|
+
const cy = 130
|
|
622
|
+
const outerRadius = 94
|
|
623
|
+
const innerRadius = 61
|
|
624
|
+
const percentText = (value) => (value >= 10 ? Math.round(value) : Math.round(value * 10) / 10) + '%'
|
|
625
|
+
const updatePointer = (event) => {
|
|
626
|
+
const visual = event.currentTarget.ownerSVGElement?.parentElement
|
|
627
|
+
const box = visual?.getBoundingClientRect()
|
|
628
|
+
if (!box) return
|
|
629
|
+
const tooltipWidth = 198
|
|
630
|
+
const tooltipHeight = 82
|
|
631
|
+
setTooltipPosition({ left: Math.max(8, Math.min(Math.max(8, box.width - tooltipWidth), event.clientX - box.left + 14)), top: Math.max(8, Math.min(Math.max(8, box.height - tooltipHeight), event.clientY - box.top + 14)) })
|
|
632
|
+
}
|
|
633
|
+
const clearPointer = () => { setActiveIndex(null); setTooltipPosition(null) }
|
|
634
|
+
return React.createElement('div', { className: 'uh-donut-chart', 'aria-label': props.title },
|
|
635
|
+
React.createElement('div', { className: 'uh-donut-title' }, React.createElement(LineIcon, { name: props.icon || 'chart', size: 16 }), props.title),
|
|
636
|
+
React.createElement('div', { className: 'uh-donut-layout' },
|
|
637
|
+
React.createElement('div', { className: 'uh-donut-visual' },
|
|
638
|
+
React.createElement('svg', { className: 'uh-donut-svg', viewBox: '0 0 260 260', role: 'img', 'aria-label': props.title + ' ' + tokenDisplay(data.total, language) },
|
|
639
|
+
React.createElement('circle', { cx, cy, r: (outerRadius + innerRadius) / 2, className: 'uh-donut-track', fill: 'none', stroke: 'var(--dsw-alias-bg-layer-2)', strokeWidth: outerRadius - innerRadius }),
|
|
640
|
+
data.segments.map((segment) => React.createElement('path', { key: 'donut-' + segment.index, d: donutArcLinePath(cx, cy, (outerRadius + innerRadius) / 2, segment.startAngle, segment.endAngle), className: 'uh-donut-segment' + (activeIndex === segment.index ? ' uh-active' : ''), fill: 'none', stroke: segment.color, strokeWidth: outerRadius - innerRadius, strokeLinecap: 'butt', strokeLinejoin: 'round', pathLength: 1, style: { animationDelay: (segment.index * 90) + 'ms' }, tabIndex: 0, 'aria-label': segment.label + ' ' + tokenDisplay(segment.value, language) + ' ' + percentText(segment.percentage) + ' ' + costDisplay(segment.cost, language), onMouseEnter: (event) => { setActiveIndex(segment.index); updatePointer(event) }, onMouseMove: updatePointer, onMouseLeave: clearPointer, onFocus: () => { setActiveIndex(segment.index); setTooltipPosition({ left: 12, top: 12 }) }, onBlur: clearPointer })),
|
|
641
|
+
),
|
|
642
|
+
activeSegment ? React.createElement('div', { className: 'uh-donut-tooltip', style: tooltipPosition ? { left: tooltipPosition.left, top: tooltipPosition.top } : undefined },
|
|
643
|
+
React.createElement('span', { className: 'uh-donut-dot', style: { background: activeSegment.color } }),
|
|
644
|
+
React.createElement('div', {},
|
|
645
|
+
React.createElement('strong', {}, activeSegment.label),
|
|
646
|
+
React.createElement('span', {}, tokenDisplay(activeSegment.value, language) + ' · ' + percentText(activeSegment.percentage)),
|
|
647
|
+
React.createElement('span', { className: 'uh-donut-tooltip-cost' }, costDisplay(activeSegment.cost, language)),
|
|
648
|
+
),
|
|
649
|
+
) : null,
|
|
650
|
+
React.createElement('div', { className: 'uh-donut-center' },
|
|
651
|
+
React.createElement('strong', {}, tokenMagnitude(data.total, language)),
|
|
652
|
+
React.createElement('span', {}, language === 'en' ? 'tokens' : 'Token'),
|
|
653
|
+
),
|
|
654
|
+
),
|
|
655
|
+
React.createElement('div', { className: 'uh-donut-legend', role: 'list' },
|
|
656
|
+
data.segments.map((segment) => React.createElement('div', { key: 'legend-' + segment.index, className: 'uh-donut-legend-row', role: 'listitem' },
|
|
657
|
+
React.createElement('span', { className: 'uh-donut-dot', style: { background: segment.color } }),
|
|
658
|
+
React.createElement('div', { className: 'uh-donut-legend-copy' },
|
|
659
|
+
React.createElement('strong', { title: segment.label }, segment.label),
|
|
660
|
+
),
|
|
661
|
+
React.createElement('div', { className: 'uh-donut-legend-metrics' },
|
|
662
|
+
React.createElement('span', {}, tokenDisplay(segment.value, language)),
|
|
663
|
+
React.createElement('span', { className: 'uh-donut-cost' }, costDisplay(segment.cost, language)),
|
|
664
|
+
),
|
|
665
|
+
React.createElement('strong', { className: 'uh-donut-percent' }, percentText(segment.percentage)),
|
|
666
|
+
)),
|
|
667
|
+
),
|
|
668
|
+
),
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
function trendSeriesLabel(key, language) {
|
|
672
|
+
const labels = {
|
|
673
|
+
total: language === 'en' ? 'Total' : '总处理',
|
|
674
|
+
input: language === 'en' ? 'Input' : '输入',
|
|
675
|
+
cacheRead: language === 'en' ? 'Cache hits' : '缓存命中',
|
|
676
|
+
cacheWrite: language === 'en' ? 'Cache writes' : '缓存写入',
|
|
677
|
+
output: language === 'en' ? 'Output' : '输出',
|
|
678
|
+
reasoning: language === 'en' ? 'Reasoning' : '推理',
|
|
679
|
+
}
|
|
680
|
+
return labels[key] || key
|
|
681
|
+
}
|
|
682
|
+
function trendSeriesValue(row, key) {
|
|
683
|
+
return key === 'total' ? row.total : (row.tokens && Number.isFinite(row.tokens[key]) ? row.tokens[key] : 0)
|
|
684
|
+
}
|
|
685
|
+
function UsageTrendChart(props) {
|
|
686
|
+
const language = props.language === 'en' ? 'en' : 'zh'
|
|
687
|
+
const tr = (zh, en) => language === 'en' ? en : zh
|
|
688
|
+
const rows = Array.isArray(props.rows) ? props.rows : []
|
|
689
|
+
const visible = Array.isArray(props.visible) && props.visible.length > 0 ? props.visible : ['total']
|
|
690
|
+
const [hoverIndex, setHoverIndex] = React.useState(null)
|
|
691
|
+
const [tooltipIndex, setTooltipIndex] = React.useState(null)
|
|
692
|
+
const width = 900
|
|
693
|
+
const height = 280
|
|
694
|
+
const geometry = buildTrendGeometry(rows, visible, width, height)
|
|
695
|
+
const colors = { total: '#f4c542', input: '#5aa9ff', cacheRead: '#44d483', cacheWrite: '#d98bff', output: '#ff8c66', reasoning: '#aab4c4' }
|
|
696
|
+
const pathFor = (points) => smoothTrendPath(points)
|
|
697
|
+
const bottomY = height - geometry.padding.bottom
|
|
698
|
+
const areaPathFor = (points) => {
|
|
699
|
+
if (!Array.isArray(points) || points.length === 0) return ''
|
|
700
|
+
return pathFor(points) + ' L' + points[points.length - 1].x.toFixed(2) + ' ' + bottomY + ' L' + points[0].x.toFixed(2) + ' ' + bottomY + ' Z'
|
|
701
|
+
}
|
|
702
|
+
const gradientOpacity = { total: .20, input: .16, cacheRead: .18, cacheWrite: .14, output: .16, reasoning: .10 }
|
|
703
|
+
const tickIndexes = rows.length <= 1 ? [0] : Array.from(new Set([0, Math.floor((rows.length - 1) / 4), Math.floor((rows.length - 1) / 2), Math.floor((rows.length - 1) * 3 / 4), rows.length - 1]))
|
|
704
|
+
const chartReady = !props.loading && !props.error && rows.length > 0
|
|
705
|
+
const hourly = rows.length > 0 && Number.isFinite(rows[0].time)
|
|
706
|
+
const chartAriaLabel = hourly ? tr('每小时 Token 使用趋势,选择小时查看当天请求日志', 'Hourly Token usage trend; select an hour to view request logs') : tr('每日 Token 使用趋势,选择日期查看请求日志', 'Daily Token usage trend; select a date to view request logs')
|
|
707
|
+
const hovered = hoverIndex === null ? null : (rows[hoverIndex] || null)
|
|
708
|
+
const hoverPoint = hoverIndex === null ? null : ((geometry.points[visible[0]] || [])[hoverIndex] || null)
|
|
709
|
+
const tooltipRow = tooltipIndex === null ? null : (rows[tooltipIndex] || null)
|
|
710
|
+
const tooltipPoint = tooltipIndex === null ? null : ((geometry.points[visible[0]] || [])[tooltipIndex] || null)
|
|
711
|
+
const tooltipVisible = hoverIndex !== null && tooltipRow !== null && tooltipPoint !== null
|
|
712
|
+
const tooltipSide = tooltipPoint !== null && tooltipPoint.x > width * .68 ? ' uh-left' : ' uh-right'
|
|
713
|
+
const tooltipStyle = tooltipPoint === null ? undefined : { left: (tooltipPoint.x / width * 100).toFixed(2) + '%', top: Math.max(23, Math.min(77, tooltipPoint.y / height * 100)).toFixed(2) + '%' }
|
|
714
|
+
const activateHover = (index) => { setHoverIndex(index); setTooltipIndex(index) }
|
|
715
|
+
const toggle = (key) => {
|
|
716
|
+
if (typeof props.onToggle === 'function') props.onToggle(key)
|
|
717
|
+
}
|
|
718
|
+
const chartBody = props.loading
|
|
719
|
+
? React.createElement('div', { className: 'uh-trend-stage uh-trend-loading', role: 'status', 'aria-label': tr('正在加载趋势', 'Loading trend') }, React.createElement('span', { className: 'uh-trend-spinner', 'aria-hidden': true }))
|
|
720
|
+
: props.error
|
|
721
|
+
? React.createElement('div', { className: 'uh-trend-stage uh-trend-message', role: 'alert' }, props.error)
|
|
722
|
+
: rows.length === 0
|
|
723
|
+
? React.createElement('div', { className: 'uh-trend-stage uh-trend-message' }, tr('该范围内暂无趋势数据', 'No trend data in this range'))
|
|
724
|
+
: React.createElement('div', { className: 'uh-trend-chart-wrap' },
|
|
725
|
+
React.createElement('svg', { className: 'uh-trend-svg', viewBox: '0 0 ' + width + ' ' + height, role: 'group', 'aria-label': chartAriaLabel },
|
|
726
|
+
[0, 0.5, 1].map((ratio) => React.createElement(React.Fragment, { key: ratio },
|
|
727
|
+
React.createElement('line', { x1: geometry.padding.left, x2: width - geometry.padding.right, y1: geometry.padding.top + (height - geometry.padding.top - geometry.padding.bottom) * ratio, y2: geometry.padding.top + (height - geometry.padding.top - geometry.padding.bottom) * ratio, className: 'uh-trend-grid' }),
|
|
728
|
+
React.createElement('text', { x: geometry.padding.left - 7, y: geometry.padding.top + (height - geometry.padding.top - geometry.padding.bottom) * ratio + 4, className: 'uh-trend-axis-label', textAnchor: 'end' }, fmtCompact(Math.round(geometry.max * (1 - ratio)))),
|
|
729
|
+
)),
|
|
730
|
+
React.createElement('defs', {},
|
|
731
|
+
visible.map((key) => React.createElement('linearGradient', { key: key, id: 'uh-trend-gradient-' + key, x1: '0', y1: '0', x2: '0', y2: '1' },
|
|
732
|
+
React.createElement('stop', { offset: '4%', stopColor: colors[key] || '#9aa4b2', stopOpacity: gradientOpacity[key] || .12 }),
|
|
733
|
+
React.createElement('stop', { offset: '96%', stopColor: colors[key] || '#9aa4b2', stopOpacity: 0 }),
|
|
734
|
+
)),
|
|
735
|
+
),
|
|
736
|
+
visible.map((key, seriesIndex) => {
|
|
737
|
+
const areaPath = areaPathFor(geometry.points[key] || [])
|
|
738
|
+
return areaPath === '' ? null : React.createElement('path', { key: 'area-' + key, d: areaPath, className: 'uh-trend-area', 'data-series': key, fill: 'url(#uh-trend-gradient-' + key + ')', style: { animationDelay: (80 + seriesIndex * 80) + 'ms' } })
|
|
739
|
+
}),
|
|
740
|
+
visible.map((key) => React.createElement('path', { key: 'line-base-' + key, d: pathFor(geometry.points[key] || []), className: 'uh-trend-line', 'data-series': key, stroke: colors[key] || '#9aa4b2' })),
|
|
741
|
+
visible.map((key, seriesIndex) => {
|
|
742
|
+
const points = geometry.points[key] || []
|
|
743
|
+
const drawLength = trendPathLength(points)
|
|
744
|
+
return React.createElement('path', { key: 'line-draw-' + key, d: pathFor(points), className: 'uh-trend-line-draw', 'data-series': key, stroke: colors[key] || '#9aa4b2', style: { '--uh-draw-length': drawLength + 'px', animationDelay: (seriesIndex * 90) + 'ms' } })
|
|
745
|
+
}),
|
|
746
|
+
visible.map((key) => {
|
|
747
|
+
const points = geometry.points[key] || []
|
|
748
|
+
if (points.length !== 1) return null
|
|
749
|
+
const point = points[0]
|
|
750
|
+
return React.createElement('circle', { key: 'single-point-' + key, cx: point.x, cy: point.y, r: 4, className: 'uh-trend-point', fill: colors[key] || '#9aa4b2' })
|
|
751
|
+
}),
|
|
752
|
+
hoverIndex !== null && hoverPoint ? React.createElement(React.Fragment, { key: 'hover-' + hoverIndex },
|
|
753
|
+
React.createElement('line', { x1: hoverPoint.x, x2: hoverPoint.x, y1: geometry.padding.top, y2: bottomY, className: 'uh-trend-cursor' }),
|
|
754
|
+
visible.map((key) => { const point = (geometry.points[key] || [])[hoverIndex]; return point ? React.createElement('circle', { key: key, cx: point.x, cy: point.y, r: 4, className: 'uh-trend-point', fill: colors[key] || '#9aa4b2' }) : null }),
|
|
755
|
+
) : null,
|
|
756
|
+
rows.map((row, index) => {
|
|
757
|
+
const point = (geometry.points[visible[0]] || [])[index]
|
|
758
|
+
if (!point) return null
|
|
759
|
+
const next = (geometry.points[visible[0]] || [])[index + 1]
|
|
760
|
+
const cellWidth = next ? Math.max(8, next.x - point.x) : (index > 0 ? Math.max(8, point.x - (geometry.points[visible[0]] || [])[index - 1].x) : 24)
|
|
761
|
+
return React.createElement('rect', { key: trendRowKey(row, index), x: Math.max(geometry.padding.left, point.x - cellWidth / 2), y: geometry.padding.top, width: cellWidth, height: height - geometry.padding.top - geometry.padding.bottom, className: 'uh-trend-hit', tabIndex: 0, role: 'button', 'aria-label': trendRowLabel(row, language, true) + ' ' + trendSeriesLabel('total', language) + ' ' + fmtCompact(row.total), onMouseEnter: () => activateHover(index), onMouseLeave: () => setHoverIndex(null), onFocus: () => activateHover(index), onBlur: () => setHoverIndex(null), onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); if (typeof props.onPointClick === 'function') props.onPointClick(trendRowDate(row)) } }, onClick: () => { if (typeof props.onPointClick === 'function') props.onPointClick(trendRowDate(row)) } })
|
|
762
|
+
}),
|
|
763
|
+
tickIndexes.map((index) => {
|
|
764
|
+
const point = (geometry.points[visible[0]] || [])[index]
|
|
765
|
+
const row = rows[index]
|
|
766
|
+
return point && row ? React.createElement('text', { key: trendRowKey(row, index), x: point.x, y: height - 8, className: 'uh-trend-axis-label', textAnchor: index === 0 ? 'start' : index === rows.length - 1 ? 'end' : 'middle' }, trendRowLabel(row, language, false)) : null
|
|
767
|
+
}),
|
|
768
|
+
),
|
|
769
|
+
tooltipRow && tooltipPoint ? React.createElement('div', { className: 'uh-trend-tooltip' + tooltipSide + (tooltipVisible ? ' uh-visible' : ''), style: tooltipStyle, 'aria-hidden': !tooltipVisible },
|
|
770
|
+
React.createElement('strong', { className: 'uh-trend-tooltip-title' }, trendRowLabel(tooltipRow, language, true)),
|
|
771
|
+
visible.map((key) => React.createElement('div', { key, className: 'uh-trend-tooltip-row', style: { color: colors[key] || '#9aa4b2' } },
|
|
772
|
+
React.createElement('span', { className: 'uh-trend-dot', style: { background: colors[key] || '#9aa4b2' } }),
|
|
773
|
+
React.createElement('span', { className: 'uh-trend-tooltip-label' }, trendSeriesLabel(key, language)),
|
|
774
|
+
React.createElement('strong', { className: 'uh-trend-tooltip-value' }, fmtCompact(trendSeriesValue(tooltipRow, key))),
|
|
775
|
+
)),
|
|
776
|
+
) : null,
|
|
777
|
+
)
|
|
778
|
+
return React.createElement('div', { className: 'uh-panel uh-trend-panel' },
|
|
779
|
+
React.createElement('div', { className: 'uh-trend-head' },
|
|
780
|
+
React.createElement('div', {}, React.createElement('h3', { className: 'uh-tbl-title uh-title-with-icon' }, React.createElement(LineIcon, { name: 'chart', size: 16 }), tr('Token 使用趋势', 'Token Usage Trend')), React.createElement('div', { className: 'uh-note' }, props.rangeLabel || '')),
|
|
781
|
+
chartReady ? React.createElement('div', { className: 'uh-note' }, tr('点击数据点查看当日明细', 'Click a point to inspect that day')) : null,
|
|
782
|
+
),
|
|
783
|
+
chartBody,
|
|
784
|
+
chartReady ? React.createElement('div', { className: 'uh-trend-legend' },
|
|
785
|
+
['total', 'input', 'cacheRead', 'cacheWrite', 'output', 'reasoning'].map((key) => React.createElement('button', { key, type: 'button', className: 'uh-trend-legend-item' + (visible.includes(key) ? ' uh-on' : ''), onClick: () => toggle(key), 'aria-pressed': visible.includes(key) }, React.createElement('span', { className: 'uh-trend-dot', style: { background: colors[key] || '#9aa4b2' } }), trendSeriesLabel(key, language))),
|
|
786
|
+
) : null,
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function UsageFilterMenu(props) {
|
|
791
|
+
const options = Array.isArray(props.options) ? props.options : []
|
|
792
|
+
const value = props.value === undefined || props.value === null ? '' : String(props.value)
|
|
793
|
+
const selected = options.find((option) => String(option.value) === value)
|
|
794
|
+
const [open, setOpen] = React.useState(false)
|
|
795
|
+
const menuRef = React.useRef(null)
|
|
796
|
+
React.useEffect(() => {
|
|
797
|
+
if (!open || typeof document === 'undefined') return undefined
|
|
798
|
+
const closeMenu = (event) => {
|
|
799
|
+
if (menuRef.current && !menuRef.current.contains(event.target)) setOpen(false)
|
|
800
|
+
}
|
|
801
|
+
document.addEventListener('pointerdown', closeMenu)
|
|
802
|
+
return () => document.removeEventListener('pointerdown', closeMenu)
|
|
803
|
+
}, [open])
|
|
804
|
+
const choose = (next) => {
|
|
805
|
+
if (typeof props.onChange === 'function') props.onChange(next)
|
|
806
|
+
setOpen(false)
|
|
807
|
+
}
|
|
808
|
+
return React.createElement('div', { className: 'uh-language-menu uh-filter-menu' + (props.className ? ' ' + props.className : '') + (open ? ' uh-open' : ''), ref: menuRef, onKeyDown: (event) => { if (event.key === 'Escape' && open) { event.preventDefault(); event.stopPropagation(); setOpen(false) } } },
|
|
809
|
+
React.createElement('button', {
|
|
810
|
+
type: 'button',
|
|
811
|
+
className: 'uh-language-trigger uh-filter-trigger' + (open ? ' uh-open' : ''),
|
|
812
|
+
title: selected ? selected.label : props.label,
|
|
813
|
+
'aria-label': props.ariaLabel || props.label,
|
|
814
|
+
'aria-haspopup': 'listbox',
|
|
815
|
+
'aria-expanded': open,
|
|
816
|
+
onClick: () => setOpen((current) => !current),
|
|
817
|
+
},
|
|
818
|
+
React.createElement(LineIcon, { name: props.icon || 'chart', size: 14 }),
|
|
819
|
+
React.createElement('span', { className: 'uh-filter-label' }, selected ? selected.label : props.label),
|
|
820
|
+
React.createElement(LineIcon, { name: 'chevron', size: 13, className: 'uh-language-caret' }),
|
|
821
|
+
),
|
|
822
|
+
open ? React.createElement('div', { className: 'uh-language-options uh-filter-options', role: 'listbox', 'aria-label': props.ariaLabel || props.label },
|
|
823
|
+
options.map((option) => {
|
|
824
|
+
const optionValue = String(option.value)
|
|
825
|
+
const active = optionValue === value
|
|
826
|
+
return React.createElement('button', {
|
|
827
|
+
key: optionValue,
|
|
828
|
+
type: 'button',
|
|
829
|
+
role: 'option',
|
|
830
|
+
'aria-selected': active,
|
|
831
|
+
className: 'uh-language-option' + (active ? ' uh-on' : ''),
|
|
832
|
+
onClick: () => choose(optionValue),
|
|
833
|
+
},
|
|
834
|
+
React.createElement(LineIcon, { name: props.icon || 'chart', size: 14 }),
|
|
835
|
+
React.createElement('span', { className: 'uh-filter-option-label' }, option.label),
|
|
836
|
+
active ? React.createElement(LineIcon, { name: 'check', size: 14, className: 'uh-language-option-check' }) : null,
|
|
837
|
+
)
|
|
838
|
+
}),
|
|
839
|
+
) : null,
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
|
|
298
843
|
const CSS = `
|
|
299
844
|
.uh-page { display:flex; flex-direction:column; gap:14px; padding:2px 2px 28px; font-family:inherit; }
|
|
300
|
-
.uh-head { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
|
|
845
|
+
.uh-head { position:relative; z-index:20; display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; }
|
|
301
846
|
.uh-title { margin:0; font-size:15px; font-weight:600; color:var(--dsw-alias-label-primary); }
|
|
302
847
|
.uh-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
303
|
-
.uh-language-menu { position:relative; z-index:12; }
|
|
848
|
+
.uh-language-menu, .uh-filter-menu { position:relative; z-index:12; }
|
|
849
|
+
.uh-filter-menu { flex:0 1 auto; min-width:0; }
|
|
850
|
+
.uh-filter-workspace { width:180px; }
|
|
851
|
+
.uh-filter-provider { width:180px; }
|
|
852
|
+
.uh-filter-model { width:260px; }
|
|
853
|
+
.uh-filter-menu.uh-open { z-index:14; }
|
|
304
854
|
.uh-language-trigger { display:inline-flex; align-items:center; gap:6px; min-height:30px; padding:4px 9px 4px 10px; border:1px solid transparent; border-radius:15px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 7%, var(--dsw-alias-bg-layer-1)); color:var(--dsw-alias-label-primary); font:inherit; font-size:12px; font-weight:600; line-height:1; cursor:pointer; transition:border-color .15s ease, background-color .15s ease, transform .1s ease; }
|
|
855
|
+
.uh-filter-trigger { width:100%; min-width:0; justify-content:flex-start; }
|
|
305
856
|
.uh-language-trigger:hover, .uh-language-trigger.uh-open { border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 58%, var(--dsw-alias-border-l2)); background:color-mix(in srgb, var(--dsw-alias-brand-primary) 13%, var(--dsw-alias-bg-layer-1)); }
|
|
306
857
|
.uh-language-trigger:active { transform:scale(.96); }
|
|
307
858
|
.uh-language-label { min-width:26px; text-align:left; }
|
|
859
|
+
.uh-filter-label { min-width:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; }
|
|
308
860
|
.uh-language-caret { color:var(--dsw-alias-label-secondary); transition:transform .18s ease; }
|
|
309
861
|
.uh-language-trigger.uh-open .uh-language-caret { transform:rotate(180deg); }
|
|
862
|
+
.uh-language-menu.uh-open { z-index:30; }
|
|
310
863
|
.uh-language-options { position:absolute; top:calc(100% + 7px); right:0; min-width:142px; padding:5px; border:1px solid var(--dsw-alias-border-l2); border-radius:12px; background:var(--dsw-alias-bg-layer-1); box-shadow:0 14px 28px color-mix(in srgb, #000 24%, transparent); animation:uh-menu-in .16s ease both; }
|
|
864
|
+
.uh-filter-options { left:0; right:auto; min-width:100%; max-width:300px; }
|
|
311
865
|
.uh-language-option { display:flex; align-items:center; gap:8px; width:100%; min-height:32px; padding:6px 8px; border:0; border-radius:8px; background:transparent; color:var(--dsw-alias-label-primary); font:inherit; font-size:12px; text-align:left; cursor:pointer; transition:background-color .14s ease, color .14s ease; }
|
|
866
|
+
.uh-filter-option-label { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
312
867
|
.uh-language-option:hover, .uh-language-option:focus-visible { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, var(--dsw-alias-bg-layer-2)); outline:0; }
|
|
313
868
|
.uh-language-option.uh-on { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 19%, var(--dsw-alias-bg-layer-2)); color:var(--dsw-alias-label-primary); font-weight:600; }
|
|
314
869
|
.uh-language-option-check { margin-left:auto; color:var(--dsw-alias-brand-primary); }
|
|
@@ -345,10 +900,123 @@ window.__ModuleLoader__.load({
|
|
|
345
900
|
.uh-alias-ok { border:1px solid var(--dsw-alias-brand-primary); background:color-mix(in srgb, var(--dsw-alias-brand-primary) 16%, transparent); color:var(--dsw-alias-label-primary); border-radius:6px; font-size:12px; padding:3px 12px; cursor:pointer; font-family:inherit; flex:none; transition:transform .1s ease; }
|
|
346
901
|
.uh-alias-ok:active { transform:scale(.96); }
|
|
347
902
|
.uh-anim-panel { animation:uh-panel-in .28s ease both; }
|
|
903
|
+
.uh-pricing-panel { display:flex; flex-direction:column; gap:12px; }
|
|
904
|
+
.uh-pricing-head, .uh-pricing-toolbar, .uh-pricing-section-head, .uh-pricing-foot { display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; }
|
|
905
|
+
.uh-pricing-note { color:var(--dsw-alias-label-secondary); font-size:12px; line-height:1.55; }
|
|
906
|
+
.uh-pricing-toolbar { padding:10px 0; border-top:1px solid var(--dsw-alias-border-l1); border-bottom:1px solid var(--dsw-alias-border-l1); }
|
|
907
|
+
.uh-pricing-switch { display:inline-flex; align-items:center; gap:7px; color:var(--dsw-alias-label-primary); font-size:12px; }
|
|
908
|
+
.uh-pricing-section { display:flex; flex-direction:column; gap:8px; }
|
|
909
|
+
.uh-pricing-table-wrap { max-height:392px; overflow-x:auto; overflow-y:scroll; scrollbar-gutter:stable; scrollbar-width:auto; scrollbar-color:#707780 #1d1f22; border:1px solid var(--dsw-alias-border-l1); border-radius:8px; background:var(--dsw-alias-bg-layer-2); }
|
|
910
|
+
.uh-pricing-model-table { width:100%; min-width:920px; border-collapse:collapse; table-layout:fixed; font-size:11px; }
|
|
911
|
+
.uh-pricing-model-table th, .uh-pricing-model-table td { min-width:0; padding:8px 9px; border-bottom:1px solid var(--dsw-alias-border-l1); text-align:left; vertical-align:middle; }
|
|
912
|
+
.uh-pricing-model-table th { position:sticky; top:0; z-index:1; color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-2); font-weight:650; white-space:nowrap; }
|
|
913
|
+
.uh-pricing-model-table th:nth-child(1) { width:27%; }
|
|
914
|
+
.uh-pricing-model-table th:nth-child(2) { width:88px; }
|
|
915
|
+
.uh-pricing-model-table th:nth-child(3) { width:23%; }
|
|
916
|
+
.uh-pricing-model-table th:nth-child(n+4) { width:105px; text-align:right; }
|
|
917
|
+
.uh-pricing-model-table td:nth-child(n+4) { text-align:right; }
|
|
918
|
+
.uh-pricing-model-table tbody tr:last-child td { border-bottom:0; }
|
|
919
|
+
.uh-pricing-model-table tbody tr:hover { background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 65%, transparent); }
|
|
920
|
+
.uh-pricing-model-name, .uh-pricing-model-target { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--dsw-alias-label-primary); }
|
|
921
|
+
.uh-pricing-model-rate { color:var(--dsw-alias-label-secondary); font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
922
|
+
.uh-pricing-status { display:inline-flex; justify-content:center; padding:3px 6px; border-radius:6px; font-size:10px; font-weight:650; }
|
|
923
|
+
.uh-pricing-status-priced { color:#157347; background:color-mix(in srgb, #30d158 22%, transparent); }
|
|
924
|
+
.uh-pricing-status-unpriced, .uh-pricing-status-ambiguous, .uh-pricing-status-unsupported { color:#9a5b00; background:color-mix(in srgb, #ff9f0a 20%, transparent); }
|
|
925
|
+
.uh-pricing-used-model-picker { position:relative; z-index:2; min-width:0; }
|
|
926
|
+
.uh-pricing-used-model-picker:focus-within { z-index:30; }
|
|
927
|
+
.uh-pricing-used-model-input { box-sizing:border-box; width:100%; min-width:0; min-height:30px; border:1px solid var(--dsw-alias-border-l2); border-radius:6px; padding:4px 7px; background:var(--dsw-alias-bg-base); color:var(--dsw-alias-label-primary); font:inherit; font-size:11px; outline:none; }
|
|
928
|
+
.uh-pricing-used-model-input:focus { border-color:var(--dsw-alias-brand-primary); }
|
|
929
|
+
.uh-pricing-used-model-options { top:calc(100% + 7px); left:0; right:auto; width:100%; min-width:280px; max-height:240px; overflow-y:auto; z-index:40; }
|
|
930
|
+
.uh-pricing-model-search { position:relative; z-index:2; min-width:0; }
|
|
931
|
+
.uh-pricing-model-search:focus-within { z-index:30; }
|
|
932
|
+
.uh-pricing-model-search-input { box-sizing:border-box; width:100%; min-width:0; min-height:30px; border:1px solid var(--dsw-alias-border-l2); border-radius:6px; padding:4px 7px; background:var(--dsw-alias-bg-base); color:var(--dsw-alias-label-primary); font:inherit; font-size:11px; outline:none; }
|
|
933
|
+
.uh-pricing-model-search-input:focus { border-color:var(--dsw-alias-brand-primary); }
|
|
934
|
+
.uh-pricing-model-options { top:calc(100% + 7px); left:0; right:auto; width:100%; min-width:280px; max-height:240px; overflow-y:auto; z-index:40; }
|
|
935
|
+
.uh-pricing-model-option { align-items:flex-start; }
|
|
936
|
+
.uh-pricing-model-option-name { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
937
|
+
.uh-pricing-model-option-id { margin-left:auto; padding-left:10px; color:var(--dsw-alias-label-secondary); font-size:10px; white-space:nowrap; }
|
|
938
|
+
.uh-pricing-edit-row { display:grid; grid-template-columns:minmax(280px,1.35fr) minmax(300px,1.45fr) minmax(78px,.5fr) 32px; gap:10px; align-items:center; min-width:790px; }
|
|
939
|
+
.uh-pricing-price-row { grid-template-columns:repeat(5,minmax(108px,1fr)) 32px; min-width:650px; }
|
|
940
|
+
.uh-pricing-price-head { display:grid; grid-template-columns:repeat(5,minmax(108px,1fr)) 32px; gap:10px; align-items:center; min-width:650px; color:var(--dsw-alias-label-secondary); font-size:10px; }
|
|
941
|
+
.uh-pricing-price-head span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
942
|
+
.uh-pricing-edit-row input, .uh-pricing-edit-row select { box-sizing:border-box; min-width:0; min-height:30px; border:1px solid var(--dsw-alias-border-l2); border-radius:6px; padding:4px 7px; background:var(--dsw-alias-bg-base); color:var(--dsw-alias-label-primary); font:inherit; font-size:11px; outline:none; }
|
|
943
|
+
.uh-pricing-edit-row input:focus, .uh-pricing-edit-row select:focus { border-color:var(--dsw-alias-brand-primary); }
|
|
944
|
+
.uh-pricing-edit-row .uh-refresh { min-height:30px; padding:0; }
|
|
945
|
+
.uh-pricing-error { color:var(--dsw-alias-warning, #a55b00); font-size:12px; line-height:1.45; }
|
|
946
|
+
.uh-pricing-foot { padding-top:4px; }
|
|
947
|
+
.uh-cost-num { color:var(--dsw-alias-label-primary); }
|
|
348
948
|
.uh-progress { font-size:12px; color:var(--dsw-alias-label-secondary); display:flex; align-items:center; gap:10px; }
|
|
349
949
|
.uh-sync-health { margin-top:8px; padding:8px 12px; display:flex; align-items:center; flex-wrap:wrap; gap:6px; border:1px solid var(--dsw-alias-border-l1); border-radius:10px; color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-1); font-size:11px; line-height:1.45; }
|
|
350
950
|
.uh-sync-health.uh-stale { color:var(--dsw-alias-warning, #a55b00); border-color:color-mix(in srgb, var(--dsw-alias-warning, #d9822b) 45%, var(--dsw-alias-border-l1)); }
|
|
351
951
|
.uh-sync-retry { border:0; background:transparent; color:inherit; font:inherit; text-decoration:underline; cursor:pointer; padding:0 2px; }
|
|
952
|
+
.uh-trend-panel { min-height:300px; animation:uh-panel-in .38s ease both; }
|
|
953
|
+
.uh-trend-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:10px; }
|
|
954
|
+
.uh-trend-chart-wrap { position:relative; min-height:250px; width:100%; overflow:hidden; }
|
|
955
|
+
.uh-trend-stage { display:grid; place-items:center; min-height:250px; width:100%; }
|
|
956
|
+
.uh-trend-message { color:var(--dsw-alias-label-secondary); font-size:12px; }
|
|
957
|
+
.uh-trend-spinner { width:24px; height:24px; border:2px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 22%, var(--dsw-alias-border-l2)); border-top-color:var(--dsw-alias-brand-primary); border-radius:50%; animation:uh-spinner-turn .78s linear infinite; }
|
|
958
|
+
.uh-trend-svg { display:block; width:100%; height:auto; min-height:220px; }
|
|
959
|
+
.uh-trend-grid { stroke:var(--dsw-alias-border-l1); stroke-width:1; stroke-dasharray:3 4; opacity:.8; }
|
|
960
|
+
.uh-trend-cursor { stroke:var(--dsw-alias-label-secondary); stroke-width:1; stroke-dasharray:3 4; opacity:.65; pointer-events:none; }
|
|
961
|
+
.uh-trend-point { stroke:var(--dsw-alias-bg-layer-1); stroke-width:2; vector-effect:non-scaling-stroke; pointer-events:none; }
|
|
962
|
+
.uh-trend-axis-label { fill:var(--dsw-alias-label-secondary); font-size:11px; font-family:inherit; }
|
|
963
|
+
.uh-trend-line { fill:none; stroke-width:2.2; vector-effect:non-scaling-stroke; stroke-linecap:round; stroke-linejoin:round; opacity:.22; }
|
|
964
|
+
.uh-trend-line-draw { fill:none; stroke-width:2.2; vector-effect:non-scaling-stroke; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:var(--uh-draw-length); stroke-dashoffset:var(--uh-draw-length); opacity:.96; pointer-events:none; animation:uh-trend-draw .95s cubic-bezier(.22,.61,.36,1) both; }
|
|
965
|
+
.uh-trend-area { opacity:1; animation:uh-trend-fill .8s ease; }
|
|
966
|
+
.uh-trend-hit { fill:transparent; cursor:crosshair; outline:none; }
|
|
967
|
+
.uh-trend-hit:focus { fill:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); outline:1px solid var(--dsw-alias-brand-primary); outline-offset:2px; }
|
|
968
|
+
.uh-trend-tooltip { position:absolute; z-index:4; min-width:166px; padding:10px 11px; border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2) 88%, transparent); border-radius:8px; background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 94%, transparent); box-shadow:0 10px 24px rgba(0,0,0,.22); backdrop-filter:blur(10px); color:var(--dsw-alias-label-primary); font-size:12px; line-height:1.45; pointer-events:none; opacity:0; visibility:hidden; transform:translate(14px,-50%) scale(.985); transform-origin:left center; transition:left .16s cubic-bezier(.22,.61,.36,1), top .16s cubic-bezier(.22,.61,.36,1), opacity .12s ease, transform .16s cubic-bezier(.22,.61,.36,1), visibility 0s linear .16s; }
|
|
969
|
+
.uh-trend-tooltip.uh-left { transform:translate(calc(-100% - 14px),-50%) scale(.985); transform-origin:right center; }
|
|
970
|
+
.uh-trend-tooltip.uh-visible { opacity:1; visibility:visible; transform:translate(14px,-50%) scale(1); transition-delay:0s; }
|
|
971
|
+
.uh-trend-tooltip.uh-left.uh-visible { transform:translate(calc(-100% - 14px),-50%) scale(1); }
|
|
972
|
+
.uh-trend-tooltip-title { display:block; margin-bottom:6px; color:var(--dsw-alias-label-primary); font-size:12px; font-weight:650; }
|
|
973
|
+
.uh-trend-tooltip-row { display:grid; grid-template-columns:8px minmax(0,1fr) auto; align-items:center; gap:7px; min-width:0; margin-top:3px; font-size:11px; }
|
|
974
|
+
.uh-trend-tooltip-row .uh-trend-dot { width:8px; height:8px; margin:0; }
|
|
975
|
+
.uh-trend-tooltip-label { overflow:hidden; font-weight:600; text-overflow:ellipsis; white-space:nowrap; }
|
|
976
|
+
.uh-trend-tooltip-value { color:inherit; font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
977
|
+
.uh-trend-dot { display:inline-block; width:7px; height:7px; margin-right:5px; border-radius:50%; vertical-align:1px; }
|
|
978
|
+
.uh-trend-legend { display:flex; flex-wrap:wrap; gap:5px 8px; margin-top:5px; }
|
|
979
|
+
.uh-trend-legend-item { display:inline-flex; align-items:center; gap:3px; border:0; border-radius:7px; padding:3px 6px; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:11px; cursor:pointer; transition:color .15s ease; }
|
|
980
|
+
.uh-trend-legend-item:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); }
|
|
981
|
+
.uh-trend-legend-item.uh-on { background:var(--dsw-alias-interactive-bg-hover); color:var(--dsw-alias-label-primary); }
|
|
982
|
+
.uh-filter-bar { position:relative; z-index:10; display:flex; align-items:center; flex-wrap:wrap; gap:7px; }
|
|
983
|
+
.uh-filter-clear { border:0; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:11px; cursor:pointer; text-decoration:underline; }
|
|
984
|
+
.uh-query-note { color:var(--dsw-alias-label-secondary); font-size:11px; }
|
|
985
|
+
.uh-detail-tabs { display:flex; align-items:center; flex-wrap:wrap; gap:4px; padding:4px; border:1px solid var(--dsw-alias-border-l1); border-radius:9px; background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 58%, transparent); }
|
|
986
|
+
.uh-detail-tab { display:inline-flex; align-items:center; gap:6px; min-height:32px; padding:5px 11px; border:0; border-radius:7px; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:12px; cursor:pointer; transition:background-color .15s ease, color .15s ease, transform .12s ease; }
|
|
987
|
+
.uh-detail-tab:hover { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-2); }
|
|
988
|
+
.uh-detail-tab.uh-on { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 3px rgba(0,0,0,.14); }
|
|
989
|
+
.uh-records-panel { animation:uh-panel-in .28s ease both; }
|
|
990
|
+
.uh-records-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:7px; }
|
|
991
|
+
.uh-records-note { margin:8px 0 10px; color:var(--dsw-alias-label-secondary); font-size:11px; line-height:1.45; }
|
|
992
|
+
.uh-records-error { margin:7px 0; color:var(--dsw-alias-warning, #a55b00); font-size:11px; }
|
|
993
|
+
.uh-records-scroll { overflow:auto; border:1px solid var(--dsw-alias-border-l1); border-radius:8px; }
|
|
994
|
+
.uh-record-grid { display:grid; grid-template-columns:112px minmax(190px,1.45fr) 78px repeat(4,minmax(76px,.72fr)) 96px 82px; gap:0; min-width:900px; align-items:center; }
|
|
995
|
+
.uh-record-grid > div { min-width:0; padding:8px 7px; border-bottom:1px solid var(--dsw-alias-border-l1); font-size:11px; }
|
|
996
|
+
.uh-record-header { color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-2); font-weight:600; }
|
|
997
|
+
.uh-record-header > div { white-space:nowrap; }
|
|
998
|
+
.uh-record-row { color:var(--dsw-alias-label-primary); cursor:pointer; outline:none; transition:background-color .14s ease, box-shadow .14s ease; }
|
|
999
|
+
.uh-record-row:hover { background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 68%, transparent); }
|
|
1000
|
+
.uh-record-row.uh-on { background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, var(--dsw-alias-bg-layer-1)); box-shadow:inset 3px 0 var(--dsw-alias-brand-primary); }
|
|
1001
|
+
.uh-record-row:focus-visible { box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary); }
|
|
1002
|
+
.uh-record-row:last-child > div { border-bottom:0; }
|
|
1003
|
+
.uh-record-time, .uh-record-num { color:var(--dsw-alias-label-secondary); font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
1004
|
+
.uh-record-num { text-align:right; }
|
|
1005
|
+
.uh-record-model { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:550; }
|
|
1006
|
+
.uh-record-model small { display:block; overflow:hidden; color:var(--dsw-alias-label-secondary); font-size:10px; font-weight:400; text-overflow:ellipsis; white-space:nowrap; }
|
|
1007
|
+
.uh-record-source { color:var(--dsw-alias-label-secondary); white-space:nowrap; }
|
|
1008
|
+
.uh-records-footer { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:9px; }
|
|
1009
|
+
.uh-record-detail { margin-top:12px; padding:10px 11px; border-top:1px solid var(--dsw-alias-border-l2); background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 42%, transparent); animation:uh-detail-in .24s ease both; }
|
|
1010
|
+
.uh-record-detail-head, .uh-record-detail-meta { display:flex; align-items:center; flex-wrap:wrap; gap:7px 14px; }
|
|
1011
|
+
.uh-record-detail-head { justify-content:space-between; margin-bottom:5px; color:var(--dsw-alias-label-primary); font-size:12px; }
|
|
1012
|
+
.uh-record-detail-meta { color:var(--dsw-alias-label-secondary); font-size:11px; }
|
|
1013
|
+
.uh-record-token-strip { display:grid; grid-template-columns:repeat(5,minmax(72px,1fr)) repeat(2,minmax(82px,1.1fr)); gap:6px; margin-top:9px; }
|
|
1014
|
+
.uh-record-token-strip > div { display:flex; flex-direction:column; gap:2px; min-width:0; padding:6px 7px; border-radius:6px; background:var(--dsw-alias-bg-layer-2); }
|
|
1015
|
+
.uh-record-token-strip span { color:var(--dsw-alias-label-secondary); font-size:10px; }
|
|
1016
|
+
.uh-record-token-strip strong { color:var(--dsw-alias-label-primary); font-size:12px; font-variant-numeric:tabular-nums; }
|
|
1017
|
+
.uh-record-token-total { border:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 38%, var(--dsw-alias-border-l1)) !important; }
|
|
1018
|
+
@keyframes uh-detail-in { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:translateY(0); } }
|
|
1019
|
+
@media (max-width:640px) { .uh-trend-head { flex-direction:column; } .uh-filter-menu { flex:1 1 130px; width:auto; } .uh-filter-trigger { max-width:100%; } .uh-trend-tooltip { min-width:116px; } .uh-records-head { flex-direction:column; } .uh-record-token-strip { grid-template-columns:repeat(2,minmax(0,1fr)); } .uh-record-token-total { grid-column:1 / -1; } }
|
|
352
1020
|
.uh-bar { flex:1; height:6px; border-radius:3px; background:var(--dsw-alias-bg-layer-2); overflow:hidden; max-width:340px; }
|
|
353
1021
|
.uh-fill { height:100%; background:var(--dsw-alias-brand-primary); border-radius:3px; transition:width .3s ease; }
|
|
354
1022
|
.uh-cards { display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:10px; }
|
|
@@ -390,8 +1058,8 @@ window.__ModuleLoader__.load({
|
|
|
390
1058
|
.uh-tip-tokens { font-size:11px; color:var(--dsw-alias-label-secondary); margin-top:6px; border-top:1px solid var(--dsw-alias-border-l1); padding-top:6px; }
|
|
391
1059
|
.uh-tbl-title { font-size:13px; font-weight:600; color:var(--dsw-alias-label-primary); margin:0 0 10px; }
|
|
392
1060
|
.uh-tbl-scroll { overflow-x:auto; }
|
|
393
|
-
.uh-hrow, .uh-row { display:grid; grid-template-columns:minmax(160px,2.2fr) .7fr .9fr .9fr .9fr .9fr 1.1fr .8fr 1fr; gap:8px; align-items:center; min-width:
|
|
394
|
-
.uh-model-hrow, .uh-model-row { display:grid; grid-template-columns:minmax(190px,2.2fr) .7fr .9fr .9fr .9fr .9fr 1.1fr .8fr; gap:8px; align-items:center; min-width:
|
|
1061
|
+
.uh-hrow, .uh-row { display:grid; grid-template-columns:minmax(160px,2.2fr) .7fr .9fr .9fr .9fr .9fr 1.1fr .9fr .8fr 1fr; gap:8px; align-items:center; min-width:900px; padding:7px 10px; border-radius:8px; font-size:12px; }
|
|
1062
|
+
.uh-model-hrow, .uh-model-row { display:grid; grid-template-columns:minmax(190px,2.2fr) .7fr .9fr .9fr .9fr .9fr 1.1fr .9fr .8fr; gap:8px; align-items:center; min-width:860px; padding:7px 10px; border-radius:8px; font-size:12px; }
|
|
395
1063
|
.uh-hrow { color:var(--dsw-alias-label-secondary); font-size:11px; }
|
|
396
1064
|
.uh-row { cursor:pointer; border:1px solid transparent; transition:background-color .15s ease, border-color .15s ease; }
|
|
397
1065
|
.uh-row:hover { background:var(--dsw-alias-bg-layer-2); }
|
|
@@ -399,6 +1067,7 @@ window.__ModuleLoader__.load({
|
|
|
399
1067
|
.uh-num { text-align:right; font-variant-numeric:tabular-nums; color:var(--dsw-alias-label-primary); }
|
|
400
1068
|
.uh-hrow .uh-num { color:var(--dsw-alias-label-secondary); }
|
|
401
1069
|
.uh-ws-title { color:var(--dsw-alias-label-primary); font-weight:550; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1070
|
+
.uh-row-title-wrap { min-width:0; }
|
|
402
1071
|
.uh-ws-path { color:var(--dsw-alias-label-secondary); font-size:11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
403
1072
|
.uh-barwrap { height:5px; border-radius:3px; background:var(--dsw-alias-bg-layer-2); overflow:hidden; margin-top:3px; }
|
|
404
1073
|
.uh-barwrap.uh-bar-thin { height:3px; margin-top:1px; }
|
|
@@ -409,13 +1078,20 @@ window.__ModuleLoader__.load({
|
|
|
409
1078
|
.uh-side-entry:hover { background:var(--dsw-alias-interactive-bg-hover); color:var(--dsw-alias-label-primary); }
|
|
410
1079
|
.uh-side-entry-icon { width:18px; text-align:center; flex:none; font-size:15px; }
|
|
411
1080
|
.uh-side-entry-label { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1081
|
+
.uh-boundary-fallback { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; min-height:360px; padding:24px; border:1px solid var(--dsw-alias-border-l1); border-radius:12px; background:var(--dsw-alias-bg-layer-1); text-align:center; }
|
|
1082
|
+
.uh-boundary-title { color:var(--dsw-alias-label-primary); font-size:15px; font-weight:650; }
|
|
1083
|
+
.uh-boundary-note { max-width:420px; color:var(--dsw-alias-label-secondary); font-size:12px; line-height:1.6; }
|
|
412
1084
|
.uh-side-modal { position:fixed; inset:0; z-index:1100; background:color-mix(in srgb, #000 44%, transparent); display:flex; align-items:stretch; justify-content:center; padding:26px; }
|
|
413
|
-
.uh-side-dialog { width:min(1120px, 100%); overflow:auto; background:var(--dsw-alias-bg-base); border:1px solid var(--dsw-alias-border-l2); border-radius:14px; box-shadow:0 18px 52px rgba(0,0,0,.35); padding:18px; }
|
|
1085
|
+
.uh-side-dialog { width:min(1120px, 100%); overflow-x:hidden; overflow-y:scroll; scrollbar-gutter:stable; scrollbar-width:auto; scrollbar-color:#707780 #1d1f22; background:var(--dsw-alias-bg-base); border:1px solid var(--dsw-alias-border-l2); border-radius:14px; box-shadow:0 18px 52px rgba(0,0,0,.35); padding:18px; }
|
|
1086
|
+
.uh-side-dialog::-webkit-scrollbar, .uh-pricing-table-wrap::-webkit-scrollbar { width:12px; height:12px; }
|
|
1087
|
+
.uh-side-dialog::-webkit-scrollbar-track, .uh-pricing-table-wrap::-webkit-scrollbar-track { background:#1d1f22; border-left:1px solid #363a40; }
|
|
1088
|
+
.uh-side-dialog::-webkit-scrollbar-thumb, .uh-pricing-table-wrap::-webkit-scrollbar-thumb { background:#707780; border:3px solid #1d1f22; border-radius:6px; }
|
|
1089
|
+
.uh-side-dialog::-webkit-scrollbar-thumb:hover, .uh-pricing-table-wrap::-webkit-scrollbar-thumb:hover { background:#9aa1aa; }
|
|
414
1090
|
.uh-side-dialog-head { display:flex; justify-content:flex-end; margin-bottom:8px; }
|
|
415
1091
|
@media (max-width: 640px) { .uh-side-modal { padding:0; } .uh-side-dialog { border-radius:0; border:0; padding:14px; } }
|
|
416
1092
|
/* iOS-style dashboard: grouped surfaces, tactile controls, and an elevated sheet. */
|
|
417
1093
|
.uh-page { gap:18px; max-width:1160px; margin:0 auto; padding:4px 2px 34px; font-family:-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif; }
|
|
418
|
-
.uh-head { position:sticky; top:-18px; z-index:
|
|
1094
|
+
.uh-head { position:sticky; top:-18px; z-index:20; margin:0 -2px; padding:18px 2px 14px; background:color-mix(in srgb, var(--dsw-alias-bg-base) 88%, transparent); backdrop-filter:blur(18px) saturate(150%); border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 76%, transparent); }
|
|
419
1095
|
.uh-title { font-size:22px; line-height:1.2; font-weight:700; letter-spacing:0; }
|
|
420
1096
|
.uh-actions { gap:8px; }
|
|
421
1097
|
.uh-range { padding:2px; gap:2px; border:0; border-radius:9px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent); overflow:visible; }
|
|
@@ -468,37 +1144,98 @@ window.__ModuleLoader__.load({
|
|
|
468
1144
|
.uh-ios-tabs { display:grid; grid-template-columns:repeat(3, 1fr); gap:4px; padding:4px; border-radius:14px; background:color-mix(in srgb, var(--dsw-alias-label-primary) 9%, transparent); }
|
|
469
1145
|
.uh-ios-tab { min-height:32px; border:0; border-radius:10px; background:transparent; color:var(--dsw-alias-label-secondary); font:inherit; font-size:13px; font-weight:600; cursor:pointer; }
|
|
470
1146
|
.uh-ios-tab.uh-on { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 4px rgba(0,0,0,.16); }
|
|
471
|
-
.uh-ios-summary { display:
|
|
472
|
-
.uh-ios-summary-
|
|
473
|
-
.uh-ios-summary-total {
|
|
474
|
-
.uh-ios-summary-
|
|
1147
|
+
.uh-ios-summary { display:flex; flex-direction:column; gap:12px; background:transparent; box-shadow:none; }
|
|
1148
|
+
.uh-ios-summary-hero { display:grid; grid-template-columns:minmax(0,1fr) minmax(320px,.48fr); min-height:142px; padding:20px 22px; border:1px solid var(--dsw-alias-border-l1); border-radius:18px; background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 2px rgba(0,0,0,.06), 0 8px 22px rgba(0,0,0,.06); }
|
|
1149
|
+
.uh-ios-summary-total { min-width:0; min-height:0; padding:0; border-radius:0; display:flex; align-items:center; justify-content:flex-start; gap:16px; background:transparent; box-shadow:none; }
|
|
1150
|
+
.uh-ios-summary-total-icon { display:grid; place-items:center; flex:none; width:54px; height:54px; border-radius:16px; background:color-mix(in srgb,#0a84ff 18%,var(--dsw-alias-bg-layer-2)); color:#0a84ff; }
|
|
1151
|
+
.uh-ios-summary-total-copy { min-width:0; }
|
|
475
1152
|
.uh-ios-summary-label { font-size:13px; font-weight:600; color:var(--dsw-alias-label-secondary); }
|
|
476
|
-
.uh-ios-summary-
|
|
1153
|
+
.uh-ios-summary-total .uh-ios-summary-label { font-size:14px; }
|
|
1154
|
+
.uh-ios-summary-value { margin-top:7px; font-size:40px; line-height:1; font-weight:750; letter-spacing:0; color:var(--dsw-alias-label-primary); }
|
|
477
1155
|
.uh-unit { margin-left:6px; color:var(--dsw-alias-label-secondary); font-size:.4em; font-weight:650; white-space:nowrap; vertical-align:baseline; }
|
|
478
1156
|
.uh-wsbar-num .uh-unit { font-size:.78em; margin-left:3px; }
|
|
479
1157
|
.uh-ios-summary-caption { margin-top:8px; font-size:12px; color:var(--dsw-alias-label-secondary); }
|
|
1158
|
+
.uh-ios-summary-meta { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); align-items:center; min-width:0; gap:0; padding:0 0 0 22px; border-left:1px solid var(--dsw-alias-border-l1); background:transparent; font-size:12px; color:var(--dsw-alias-label-secondary); }
|
|
1159
|
+
.uh-ios-summary-meta-stat { min-width:0; padding:4px 22px; }
|
|
1160
|
+
.uh-ios-summary-meta-stat + .uh-ios-summary-meta-stat { border-left:1px solid var(--dsw-alias-border-l1); }
|
|
1161
|
+
.uh-ios-summary-meta-label { display:flex; align-items:center; gap:7px; color:var(--dsw-alias-label-secondary); font-size:12px; font-weight:600; white-space:nowrap; }
|
|
1162
|
+
.uh-ios-summary-meta-value { margin-top:7px; color:var(--dsw-alias-label-primary); font-size:24px; line-height:1; font-weight:700; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
1163
|
+
.uh-ios-summary-meta-cost .uh-ios-summary-meta-value { color:#30d158; }
|
|
1164
|
+
.uh-ios-summary-meta-caption { margin-top:7px; color:var(--dsw-alias-label-secondary); font-size:11px; white-space:nowrap; }
|
|
1165
|
+
.uh-ios-metrics { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:10px; }
|
|
1166
|
+
.uh-ios-metric { min-width:0; min-height:108px; padding:16px 18px; border:1px solid var(--dsw-alias-border-l1); border-radius:18px; display:flex; flex-direction:column; justify-content:center; gap:10px; background:var(--dsw-alias-bg-layer-1); box-shadow:0 1px 2px rgba(0,0,0,.06), 0 8px 20px rgba(0,0,0,.05); animation:uh-card-in .35s ease both; }
|
|
1167
|
+
.uh-ios-metrics > .uh-card { min-width:0; min-height:141px; padding:16px 18px; gap:4px; border:0; border-radius:18px; }
|
|
1168
|
+
.uh-ios-metrics > .uh-card .uh-card-value { min-width:0; font-size:23px; line-height:1.2; font-weight:700; white-space:nowrap; }
|
|
1169
|
+
.uh-ios-metrics > .uh-card .uh-card-sub { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1170
|
+
.uh-ios-metrics > .uh-card:nth-child(-n+3) { justify-content:center; }
|
|
1171
|
+
.uh-ios-metric-label { display:flex; align-items:center; gap:8px; min-width:0; color:var(--dsw-alias-label-secondary); font-size:13px; font-weight:600; white-space:nowrap; }
|
|
1172
|
+
.uh-ios-metric-label .uh-line-icon { flex:none; }
|
|
1173
|
+
.uh-ios-metric-value { min-width:0; color:var(--dsw-alias-label-primary); font-size:26px; line-height:1.05; font-weight:700; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
|
1174
|
+
.uh-ios-metric-input { background:color-mix(in srgb,#0a84ff 11%,var(--dsw-alias-bg-layer-1)); }
|
|
1175
|
+
.uh-ios-metric-input .uh-line-icon { color:#0a84ff; }
|
|
1176
|
+
.uh-ios-metric-output { background:color-mix(in srgb,#bf5af2 10%,var(--dsw-alias-bg-layer-1)); }
|
|
1177
|
+
.uh-ios-metric-output .uh-line-icon { color:#bf5af2; }
|
|
1178
|
+
.uh-ios-metric-write { background:color-mix(in srgb,#ff9f0a 11%,var(--dsw-alias-bg-layer-1)); }
|
|
1179
|
+
.uh-ios-metric-write .uh-line-icon { color:#ff9f0a; }
|
|
1180
|
+
.uh-ios-metric-read { background:color-mix(in srgb,#30d158 11%,var(--dsw-alias-bg-layer-1)); }
|
|
1181
|
+
.uh-ios-metric-read .uh-line-icon { color:#30d158; }
|
|
1182
|
+
.uh-ios-metric-rate { background:var(--dsw-alias-bg-layer-1); }
|
|
1183
|
+
.uh-ios-metric-rate .uh-line-icon { color:#30d158; }
|
|
1184
|
+
.uh-ios-metric-rate-head { display:flex; align-items:baseline; justify-content:space-between; gap:8px; min-width:0; }
|
|
1185
|
+
.uh-ios-metric-rate-value { flex:none; color:#30d158; font-size:24px; line-height:1; font-weight:700; font-variant-numeric:tabular-nums; }
|
|
1186
|
+
.uh-ios-metric-rate-detail { min-width:0; overflow:hidden; color:var(--dsw-alias-label-secondary); font-size:14px; line-height:1.2; font-weight:600; font-variant-numeric:tabular-nums; text-overflow:ellipsis; white-space:nowrap; }
|
|
1187
|
+
.uh-ios-metric-bar { height:7px; border-radius:4px; background:var(--dsw-alias-bg-layer-2); overflow:hidden; }
|
|
1188
|
+
.uh-ios-metric-fill { height:100%; border-radius:inherit; background:#30d158; transition:width .35s ease; }
|
|
480
1189
|
.uh-token-semantics { display:flex; align-items:flex-start; gap:8px; padding:10px 12px; border-radius:12px; background:color-mix(in srgb, var(--dsw-alias-brand-primary) 8%, var(--dsw-alias-bg-layer-1)); color:var(--dsw-alias-label-secondary); font-size:12px; line-height:1.55; }
|
|
481
1190
|
.uh-token-semantics .uh-line-icon { margin-top:1px; color:var(--dsw-alias-brand-primary); }
|
|
482
|
-
.uh-ios-summary-meta { display:grid; align-content:center; grid-template-columns:1fr auto; gap:7px 12px; padding:20px 22px; background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 64%, transparent); font-size:12px; color:var(--dsw-alias-label-secondary); }
|
|
483
|
-
.uh-ios-summary-meta strong { color:var(--dsw-alias-label-primary); font-size:15px; font-variant-numeric:tabular-nums; }
|
|
484
1191
|
.uh-ios-list-panel { min-height:360px; }
|
|
1192
|
+
.uh-donut-chart { margin:0 0 18px; }
|
|
1193
|
+
.uh-donut-title { display:flex; align-items:center; gap:7px; margin-bottom:12px; color:var(--dsw-alias-label-primary); font-size:14px; font-weight:650; }
|
|
1194
|
+
.uh-donut-layout { display:grid; grid-template-columns:minmax(220px,300px) minmax(0,1fr); gap:24px; align-items:center; }
|
|
1195
|
+
.uh-donut-visual { position:relative; width:min(100%,280px); aspect-ratio:1; margin:0 auto; }
|
|
1196
|
+
.uh-donut-svg { display:block; width:100%; height:100%; overflow:visible; }
|
|
1197
|
+
.uh-donut-track { opacity:.78; }
|
|
1198
|
+
.uh-donut-segment { fill:none; stroke-dasharray:1; stroke-dashoffset:1; animation:uh-donut-draw .95s cubic-bezier(.22,.61,.36,1) both; cursor:pointer; outline:none; transition:filter .15s ease, opacity .15s ease; }
|
|
1199
|
+
.uh-donut-segment:hover, .uh-donut-segment:focus-visible, .uh-donut-segment.uh-active { filter:brightness(1.12); }
|
|
1200
|
+
.uh-donut-tooltip { position:absolute; top:0; left:0; z-index:3; display:flex; align-items:flex-start; gap:8px; max-width:190px; padding:9px 10px; border:1px solid var(--dsw-alias-border-l2); border-radius:8px; background:color-mix(in srgb, var(--dsw-alias-bg-base) 94%, transparent); box-shadow:0 10px 24px rgba(0,0,0,.22); backdrop-filter:blur(10px); color:var(--dsw-alias-label-primary); pointer-events:none; font-size:12px; line-height:1.4; transition:left .12s cubic-bezier(.22,.61,.36,1), top .12s cubic-bezier(.22,.61,.36,1); }
|
|
1201
|
+
.uh-donut-tooltip > div { min-width:0; display:flex; flex-direction:column; gap:4px; }
|
|
1202
|
+
.uh-donut-tooltip strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; }
|
|
1203
|
+
.uh-donut-tooltip span:not(.uh-donut-dot) { color:var(--dsw-alias-label-secondary); font-size:11px; }
|
|
1204
|
+
.uh-donut-tooltip-cost { color:var(--dsw-alias-label-primary) !important; font-variant-numeric:tabular-nums; }
|
|
1205
|
+
.uh-donut-center { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; pointer-events:none; }
|
|
1206
|
+
.uh-donut-center strong { color:var(--dsw-alias-label-primary); font-size:28px; line-height:1; font-weight:750; font-variant-numeric:tabular-nums; }
|
|
1207
|
+
.uh-donut-center span { margin-top:5px; color:var(--dsw-alias-label-secondary); font-size:13px; }
|
|
1208
|
+
.uh-donut-legend { min-width:0; }
|
|
1209
|
+
.uh-donut-legend-row { display:grid; grid-template-columns:12px minmax(180px,1fr) minmax(250px,.8fr) 54px; gap:10px; align-items:center; min-height:58px; padding:8px 0; border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 78%, transparent); }
|
|
1210
|
+
.uh-donut-legend-row:last-child { border-bottom:0; }
|
|
1211
|
+
.uh-donut-dot { width:12px; height:12px; border-radius:50%; }
|
|
1212
|
+
.uh-donut-legend-copy { min-width:0; display:flex; flex-direction:column; gap:5px; }
|
|
1213
|
+
.uh-donut-legend-copy strong { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--dsw-alias-label-primary); font-size:13px; font-weight:650; }
|
|
1214
|
+
.uh-donut-legend-metrics { display:grid; grid-template-columns:minmax(120px,1fr) minmax(92px,auto); align-items:center; gap:14px; min-width:0; }
|
|
1215
|
+
.uh-donut-legend-metrics span { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--dsw-alias-label-secondary); font-size:12px; font-variant-numeric:tabular-nums; text-align:right; }
|
|
1216
|
+
.uh-donut-legend-metrics .uh-donut-cost { color:var(--dsw-alias-label-secondary); font-size:12px; }
|
|
1217
|
+
.uh-donut-percent { min-width:48px; color:var(--dsw-alias-label-secondary); font-size:12px; font-variant-numeric:tabular-nums; text-align:right; }
|
|
485
1218
|
/* Each detail table owns its scrolling and sticky header; sections must not overlap in the page scroll. */
|
|
486
1219
|
.uh-tbl-scroll { max-height:360px; overflow:auto; border-radius:12px; background:color-mix(in srgb, var(--dsw-alias-bg-layer-2) 55%, transparent); }
|
|
487
1220
|
.uh-hrow, .uh-model-hrow { position:sticky; top:0; z-index:3; border-bottom:1px solid var(--dsw-alias-border-l1); box-shadow:0 1px 0 color-mix(in srgb, var(--dsw-alias-bg-base) 70%, transparent); }
|
|
488
1221
|
.uh-row, .uh-model-row { min-height:48px; border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1) 72%, transparent); }
|
|
489
1222
|
.uh-row:last-child, .uh-model-row:last-child { border-bottom:0; }
|
|
490
1223
|
@media (max-width:640px) { .uh-tbl-scroll { max-height:300px; border-radius:10px; } }
|
|
491
|
-
@media (max-width:640px) { .uh-hm-body { min-width:720px; } .uh-ios-summary { grid-template-columns:1fr; } .uh-ios-summary-total
|
|
1224
|
+
@media (max-width:640px) { .uh-hm-body { min-width:720px; } .uh-donut-legend-row { grid-template-columns:12px minmax(0,1fr) 48px; gap:8px; } .uh-donut-legend-copy { grid-column:2; grid-row:1; } .uh-donut-legend-metrics { grid-column:2 / -1; grid-row:2; grid-template-columns:minmax(0,1fr) minmax(0,auto); gap:8px; } .uh-donut-percent { grid-column:3; grid-row:1; } .uh-ios-summary-hero { grid-template-columns:1fr; min-height:0; gap:18px; padding:18px; } .uh-ios-summary-total { align-items:flex-start; } .uh-ios-summary-meta { grid-template-columns:repeat(2,minmax(0,1fr)); padding:16px 0 0; border-left:0; border-top:1px solid var(--dsw-alias-border-l1); } .uh-ios-summary-meta-stat { padding:0 12px; } .uh-ios-summary-meta-stat:first-child { padding-left:0; } .uh-ios-summary-meta-stat:last-child { padding-right:0; } .uh-ios-summary-value { font-size:31px; } .uh-ios-metrics { grid-template-columns:repeat(2,minmax(0,1fr)); } .uh-ios-metric:last-child { grid-column:1 / -1; } .uh-ios-metric-value { font-size:24px; } .uh-donut-layout { grid-template-columns:1fr; gap:12px; } .uh-donut-visual { width:min(100%,250px); } }
|
|
492
1225
|
@keyframes uh-cell-in { from { opacity:0; transform:scale(.4); } to { opacity:1; transform:scale(1); } }
|
|
493
1226
|
@keyframes uh-glow { 0% { box-shadow:0 0 0 0 rgba(46,160,67,.5); } 70% { box-shadow:0 0 0 5px rgba(46,160,67,0); } 100% { box-shadow:0 0 0 0 rgba(46,160,67,0); } }
|
|
494
1227
|
@keyframes uh-card-in { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
|
|
495
1228
|
@keyframes uh-bar-grow { from { transform:scaleX(0); } to { transform:scaleX(1); } }
|
|
496
1229
|
@keyframes uh-panel-in { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:translateY(0); } }
|
|
1230
|
+
@keyframes uh-trend-draw { from { stroke-dashoffset:var(--uh-draw-length); opacity:.2; } to { stroke-dashoffset:0; opacity:1; } }
|
|
1231
|
+
@keyframes uh-trend-fill { from { opacity:0; } to { opacity:1; } }
|
|
1232
|
+
@keyframes uh-donut-draw { from { stroke-dashoffset:1; opacity:.25; } to { stroke-dashoffset:0; opacity:1; } }
|
|
1233
|
+
@keyframes uh-spinner-turn { to { transform:rotate(360deg); } }
|
|
497
1234
|
@keyframes uh-menu-in { from { opacity:0; transform:translateY(-4px) scale(.97); } to { opacity:1; transform:translateY(0) scale(1); } }
|
|
498
1235
|
@keyframes uh-tip-in { from { opacity:0; } to { opacity:1; } }
|
|
499
1236
|
@media (prefers-reduced-motion: reduce) {
|
|
500
|
-
.uh-cell, .uh-card, .uh-barfill, .uh-anim-panel, .uh-tip, .uh-language-options { animation:none !important; }
|
|
501
|
-
.uh-card, .uh-cell, .uh-barfill, .uh-fill, .uh-refresh, .uh-chip, .uh-row, .uh-tip-row, .uh-language-trigger, .uh-language-caret, .uh-language-option { transition:none !important; }
|
|
1237
|
+
.uh-cell, .uh-card, .uh-ios-metric, .uh-barfill, .uh-anim-panel, .uh-trend-panel, .uh-trend-line-draw, .uh-trend-area, .uh-trend-point, .uh-trend-spinner, .uh-donut-segment, .uh-records-panel, .uh-record-detail, .uh-tip, .uh-language-options { animation:none !important; stroke-dashoffset:0 !important; opacity:1 !important; }
|
|
1238
|
+
.uh-card, .uh-cell, .uh-ios-metric-fill, .uh-barfill, .uh-fill, .uh-refresh, .uh-chip, .uh-row, .uh-tip-row, .uh-trend-tooltip, .uh-language-trigger, .uh-language-caret, .uh-language-option { transition:none !important; }
|
|
502
1239
|
}
|
|
503
1240
|
`
|
|
504
1241
|
const cssTagId = "dsh-all-usage/styles.css"
|
|
@@ -522,6 +1259,27 @@ window.__ModuleLoader__.load({
|
|
|
522
1259
|
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
523
1260
|
return r.json()
|
|
524
1261
|
})
|
|
1262
|
+
const getUsageQuery = (scope) => {
|
|
1263
|
+
const params = new URLSearchParams({ start: scope.start, end: scope.end, utc: scope.utc ? '1' : '0' })
|
|
1264
|
+
if (scope.workspaceId) params.set('workspaceId', scope.workspaceId)
|
|
1265
|
+
if (scope.provider) params.set('provider', scope.provider)
|
|
1266
|
+
if (scope.modelKey) params.set('modelKey', scope.modelKey)
|
|
1267
|
+
return fetch('/api/all-usage/query?' + params.toString(), { headers: { accept: 'application/json' } }).then((r) => {
|
|
1268
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
1269
|
+
return r.json()
|
|
1270
|
+
})
|
|
1271
|
+
}
|
|
1272
|
+
const getUsageRecords = (scope, cursor, limit) => {
|
|
1273
|
+
const params = new URLSearchParams({ start: scope.start, end: scope.end, utc: scope.utc ? '1' : '0', limit: String(limit || 100) })
|
|
1274
|
+
if (scope.workspaceId) params.set('workspaceId', scope.workspaceId)
|
|
1275
|
+
if (scope.provider) params.set('provider', scope.provider)
|
|
1276
|
+
if (scope.modelKey) params.set('modelKey', scope.modelKey)
|
|
1277
|
+
if (cursor) params.set('cursor', cursor)
|
|
1278
|
+
return fetch('/api/all-usage/records?' + params.toString(), { headers: { accept: 'application/json' } }).then((r) => {
|
|
1279
|
+
if (!r.ok) { const error = new Error('HTTP ' + r.status); error.status = r.status; throw error }
|
|
1280
|
+
return r.json()
|
|
1281
|
+
})
|
|
1282
|
+
}
|
|
525
1283
|
const getBalance = (force, requestToken) => fetch('/api/all-usage/balance' + (force ? '?force=1' : ''), { headers: { accept: 'application/json', 'x-all-usage-request-token': requestToken } }).then((r) => {
|
|
526
1284
|
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
527
1285
|
return r.json()
|
|
@@ -534,6 +1292,33 @@ window.__ModuleLoader__.load({
|
|
|
534
1292
|
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
535
1293
|
return r.json()
|
|
536
1294
|
})
|
|
1295
|
+
const getPricing = () => fetch('/api/all-usage/pricing', { headers: { accept: 'application/json' } }).then((r) => {
|
|
1296
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
1297
|
+
return r.json()
|
|
1298
|
+
})
|
|
1299
|
+
const getPricingModels = (query) => {
|
|
1300
|
+
const params = new URLSearchParams({ q: String(query || '').slice(0, 120), limit: '30' })
|
|
1301
|
+
return fetch('/api/all-usage/pricing/models?' + params.toString(), { headers: { accept: 'application/json' } }).then((r) => {
|
|
1302
|
+
if (!r.ok) throw new Error('HTTP ' + r.status)
|
|
1303
|
+
return r.json()
|
|
1304
|
+
})
|
|
1305
|
+
}
|
|
1306
|
+
const setPricingRpc = (pricing, backfill, writeToken) => fetch('/api/all-usage/pricing', {
|
|
1307
|
+
method: 'POST',
|
|
1308
|
+
headers: { 'content-type': 'application/json', 'x-all-usage-request-token': writeToken },
|
|
1309
|
+
body: JSON.stringify({ pricing, backfill: backfill === true }),
|
|
1310
|
+
}).then((r) => {
|
|
1311
|
+
if (!r.ok) { const error = new Error('HTTP ' + r.status); error.status = r.status; throw error }
|
|
1312
|
+
return r.json()
|
|
1313
|
+
})
|
|
1314
|
+
const syncPricingRpc = (writeToken) => fetch('/api/all-usage/pricing/sync', {
|
|
1315
|
+
method: 'POST',
|
|
1316
|
+
headers: { 'content-type': 'application/json', 'x-all-usage-request-token': writeToken },
|
|
1317
|
+
body: '{}',
|
|
1318
|
+
}).then((r) => {
|
|
1319
|
+
if (!r.ok) { const error = new Error('HTTP ' + r.status); error.status = r.status; throw error }
|
|
1320
|
+
return r.json()
|
|
1321
|
+
})
|
|
537
1322
|
|
|
538
1323
|
const LANGUAGE_STORAGE_KEY = 'dsh-all-usage.language'
|
|
539
1324
|
function storedLanguage() {
|
|
@@ -542,12 +1327,35 @@ window.__ModuleLoader__.load({
|
|
|
542
1327
|
function persistLanguage(language) {
|
|
543
1328
|
try { window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language) } catch (_) {}
|
|
544
1329
|
}
|
|
1330
|
+
const USAGE_UI_STATE_KEY = 'dsh-all-usage.ui-state'
|
|
1331
|
+
function storedUsageUiState() {
|
|
1332
|
+
try {
|
|
1333
|
+
const raw = window.localStorage.getItem(USAGE_UI_STATE_KEY)
|
|
1334
|
+
const value = raw ? JSON.parse(raw) : {}
|
|
1335
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return {}
|
|
1336
|
+
const state = {}
|
|
1337
|
+
if (['logs', 'model', 'workspace'].includes(value.detailView)) state.detailView = value.detailView
|
|
1338
|
+
if (['route', 'model', 'provider'].includes(value.modelView)) state.modelView = value.modelView
|
|
1339
|
+
if (['today', '30d', '90d', 'all'].includes(value.range)) state.range = value.range
|
|
1340
|
+
if (typeof value.pricingAutoSync === 'boolean') state.pricingAutoSync = value.pricingAutoSync
|
|
1341
|
+
return state
|
|
1342
|
+
} catch (_) { return {} }
|
|
1343
|
+
}
|
|
1344
|
+
function persistUsageUiState(patch) {
|
|
1345
|
+
try {
|
|
1346
|
+
const current = storedUsageUiState()
|
|
1347
|
+
window.localStorage.setItem(USAGE_UI_STATE_KEY, JSON.stringify(Object.assign({}, current, patch)))
|
|
1348
|
+
} catch (_) {}
|
|
1349
|
+
}
|
|
545
1350
|
|
|
546
1351
|
function UsagePage(props) {
|
|
547
1352
|
const timer = props.timerCtx
|
|
548
1353
|
const language = props.language === 'en' ? 'en' : 'zh'
|
|
549
1354
|
const tr = (zh, en) => language === 'en' ? en : zh
|
|
550
1355
|
const useUtc = language === 'en'
|
|
1356
|
+
const usageUiStateRef = React.useRef(null)
|
|
1357
|
+
if (usageUiStateRef.current === null) usageUiStateRef.current = storedUsageUiState()
|
|
1358
|
+
const usageUiState = usageUiStateRef.current
|
|
551
1359
|
const calendarNow = new Date()
|
|
552
1360
|
const latestCalendarDate = fmtDate(calendarNow, useUtc)
|
|
553
1361
|
const [stats, setStats] = React.useState(null)
|
|
@@ -555,29 +1363,83 @@ window.__ModuleLoader__.load({
|
|
|
555
1363
|
const [statsError, setStatsError] = React.useState('')
|
|
556
1364
|
const [lastStatsAt, setLastStatsAt] = React.useState(0)
|
|
557
1365
|
const [balance, setBalance] = React.useState(null)
|
|
558
|
-
const [range, setRange] = React.useState('
|
|
1366
|
+
const [range, setRange] = React.useState(() => usageUiState.range || 'today')
|
|
559
1367
|
const [customRange, setCustomRange] = React.useState({ start: '', end: '' })
|
|
560
1368
|
const [customDraft, setCustomDraft] = React.useState({ start: '', end: '' })
|
|
561
1369
|
const [customRangeOpen, setCustomRangeOpen] = React.useState(false)
|
|
562
|
-
const [modelView, setModelView] = React.useState('route')
|
|
1370
|
+
const [modelView, setModelView] = React.useState(() => usageUiState.modelView || 'route')
|
|
563
1371
|
const [wsFilter, setWsFilter] = React.useState(null)
|
|
1372
|
+
const [providerFilter, setProviderFilter] = React.useState(null)
|
|
1373
|
+
const [modelFilter, setModelFilter] = React.useState(null)
|
|
1374
|
+
const [queryResult, setQueryResult] = React.useState(null)
|
|
1375
|
+
const [queryResultKey, setQueryResultKey] = React.useState('')
|
|
1376
|
+
const [queryLoading, setQueryLoading] = React.useState(false)
|
|
1377
|
+
const [queryError, setQueryError] = React.useState('')
|
|
1378
|
+
const [trendVisible, setTrendVisible] = React.useState(['total', 'input', 'cacheRead', 'output'])
|
|
1379
|
+
const [detailView, setDetailView] = React.useState(() => usageUiState.detailView || 'logs')
|
|
1380
|
+
const [detailSelection, setDetailSelection] = React.useState(null)
|
|
1381
|
+
const [auditSelectedId, setAuditSelectedId] = React.useState(null)
|
|
1382
|
+
const [auditRows, setAuditRows] = React.useState([])
|
|
1383
|
+
const [auditCursor, setAuditCursor] = React.useState(null)
|
|
1384
|
+
const [auditHasMore, setAuditHasMore] = React.useState(false)
|
|
1385
|
+
const [auditReload, setAuditReload] = React.useState(0)
|
|
1386
|
+
const [auditLoading, setAuditLoading] = React.useState(false)
|
|
1387
|
+
const [auditExporting, setAuditExporting] = React.useState(false)
|
|
1388
|
+
const [auditError, setAuditError] = React.useState('')
|
|
564
1389
|
const [hover, setHover] = React.useState(null)
|
|
565
1390
|
const [aliasOpen, setAliasOpen] = React.useState(false)
|
|
566
1391
|
const [aliasDrafts, setAliasDrafts] = React.useState({})
|
|
1392
|
+
const [pricingOpen, setPricingOpen] = React.useState(false)
|
|
1393
|
+
const [pricingDraft, setPricingDraft] = React.useState(null)
|
|
1394
|
+
const [pricingSaving, setPricingSaving] = React.useState(false)
|
|
1395
|
+
const [pricingSyncing, setPricingSyncing] = React.useState(false)
|
|
1396
|
+
const [pricingSyncSaving, setPricingSyncSaving] = React.useState(false)
|
|
1397
|
+
const [pricingError, setPricingError] = React.useState('')
|
|
1398
|
+
const [pricingModelSearchOptions, setPricingModelSearchOptions] = React.useState({})
|
|
1399
|
+
const [pricingModelSearchOpen, setPricingModelSearchOpen] = React.useState(null)
|
|
1400
|
+
const [pricingUsedModelSearchText, setPricingUsedModelSearchText] = React.useState({})
|
|
1401
|
+
const [pricingUsedModelOpen, setPricingUsedModelOpen] = React.useState(null)
|
|
1402
|
+
const [pricingOverrideSearchText, setPricingOverrideSearchText] = React.useState({})
|
|
1403
|
+
const [pricingOverrideOpen, setPricingOverrideOpen] = React.useState(null)
|
|
1404
|
+
const pricingModelSearchSeqRef = React.useRef({})
|
|
1405
|
+
const pricingModelSearchTimerRef = React.useRef({})
|
|
567
1406
|
const [languageMenuOpen, setLanguageMenuOpen] = React.useState(false)
|
|
568
1407
|
const languageMenuRef = React.useRef(null)
|
|
1408
|
+
const recordsPanelRef = React.useRef(null)
|
|
569
1409
|
const statsGateRef = React.useRef(null)
|
|
570
1410
|
if (statsGateRef.current === null) statsGateRef.current = createRequestGate()
|
|
571
1411
|
const statusGateRef = React.useRef(null)
|
|
572
1412
|
if (statusGateRef.current === null) statusGateRef.current = createRequestGate()
|
|
573
1413
|
const balanceGateRef = React.useRef(null)
|
|
574
1414
|
if (balanceGateRef.current === null) balanceGateRef.current = createRequestGate()
|
|
1415
|
+
const queryGateRef = React.useRef(null)
|
|
1416
|
+
if (queryGateRef.current === null) queryGateRef.current = createRequestGate()
|
|
1417
|
+
const recordsGateRef = React.useRef(null)
|
|
1418
|
+
if (recordsGateRef.current === null) recordsGateRef.current = createRequestGate()
|
|
575
1419
|
const statsGate = statsGateRef.current
|
|
576
1420
|
const statusGate = statusGateRef.current
|
|
577
1421
|
const balanceGate = balanceGateRef.current
|
|
1422
|
+
const queryGate = queryGateRef.current
|
|
1423
|
+
const recordsGate = recordsGateRef.current
|
|
578
1424
|
const refreshRef = React.useRef(() => {})
|
|
579
1425
|
const setLanguage = (next) => { if (typeof props.onLanguageChange === 'function') props.onLanguageChange(next === 'en' ? 'en' : 'zh') }
|
|
580
1426
|
const chooseLanguage = (next) => { setLanguage(next); setLanguageMenuOpen(false) }
|
|
1427
|
+
React.useEffect(() => { persistUsageUiState({ detailView }) }, [detailView])
|
|
1428
|
+
React.useEffect(() => { persistUsageUiState({ modelView }) }, [modelView])
|
|
1429
|
+
React.useEffect(() => { if (range !== 'custom') persistUsageUiState({ range }) }, [range])
|
|
1430
|
+
|
|
1431
|
+
const queryScope = stats === null ? null : makeUsageScope(stats, range, useUtc, customRange, wsFilter, providerFilter, modelFilter)
|
|
1432
|
+
const queryKey = usageScopeKey(queryScope)
|
|
1433
|
+
const selectedDetailScope = detailSelection !== null && detailSelection.baseKey === queryKey ? detailSelection.scope : queryScope
|
|
1434
|
+
const detailKey = usageScopeKey(selectedDetailScope)
|
|
1435
|
+
const previousQueryKeyRef = React.useRef(queryKey)
|
|
1436
|
+
React.useEffect(() => {
|
|
1437
|
+
if (previousQueryKeyRef.current !== queryKey) {
|
|
1438
|
+
previousQueryKeyRef.current = queryKey
|
|
1439
|
+
setDetailSelection(null)
|
|
1440
|
+
setAuditSelectedId(null)
|
|
1441
|
+
}
|
|
1442
|
+
}, [queryKey])
|
|
581
1443
|
|
|
582
1444
|
React.useEffect(() => {
|
|
583
1445
|
let alive = true
|
|
@@ -678,7 +1540,13 @@ window.__ModuleLoader__.load({
|
|
|
678
1540
|
refreshStats()
|
|
679
1541
|
refreshBalance(true)
|
|
680
1542
|
}
|
|
681
|
-
return () => {
|
|
1543
|
+
return () => {
|
|
1544
|
+
alive = false
|
|
1545
|
+
clearRetry()
|
|
1546
|
+
fast(); slow(); bal()
|
|
1547
|
+
for (const timerId of Object.values(pricingModelSearchTimerRef.current)) clearTimeout(timerId)
|
|
1548
|
+
pricingModelSearchTimerRef.current = {}
|
|
1549
|
+
}
|
|
682
1550
|
}, [])
|
|
683
1551
|
|
|
684
1552
|
React.useEffect(() => {
|
|
@@ -690,10 +1558,126 @@ window.__ModuleLoader__.load({
|
|
|
690
1558
|
return () => document.removeEventListener('pointerdown', closeLanguageMenu)
|
|
691
1559
|
}, [languageMenuOpen])
|
|
692
1560
|
|
|
1561
|
+
React.useEffect(() => {
|
|
1562
|
+
if (queryScope === null || queryKey === '') return undefined
|
|
1563
|
+
const seq = queryGate.next()
|
|
1564
|
+
setQueryLoading(true)
|
|
1565
|
+
setQueryError('')
|
|
1566
|
+
getUsageQuery(queryScope).then((data) => {
|
|
1567
|
+
if (!queryGate.isCurrent(seq)) return
|
|
1568
|
+
if (data === null || typeof data !== 'object' || data.revision !== (stats && stats.revision)) {
|
|
1569
|
+
setQueryError('stale')
|
|
1570
|
+
setQueryLoading(false)
|
|
1571
|
+
return
|
|
1572
|
+
}
|
|
1573
|
+
setQueryResult(data)
|
|
1574
|
+
setQueryResultKey(queryKey)
|
|
1575
|
+
setQueryLoading(false)
|
|
1576
|
+
setQueryError('')
|
|
1577
|
+
}, () => {
|
|
1578
|
+
if (!queryGate.isCurrent(seq)) return
|
|
1579
|
+
setQueryLoading(false)
|
|
1580
|
+
setQueryError('query')
|
|
1581
|
+
})
|
|
1582
|
+
return undefined
|
|
1583
|
+
}, [queryKey, stats && stats.revision])
|
|
1584
|
+
|
|
1585
|
+
const openAuditForScope = (scope) => {
|
|
1586
|
+
if (scope === null || queryKey === '') return
|
|
1587
|
+
setDetailSelection({ baseKey: queryKey, scope: { ...scope } })
|
|
1588
|
+
setDetailView('logs')
|
|
1589
|
+
setAuditSelectedId(null)
|
|
1590
|
+
setAuditError('')
|
|
1591
|
+
}
|
|
1592
|
+
const openAuditForDate = (date) => {
|
|
1593
|
+
if (queryScope === null || typeof date !== 'string') return
|
|
1594
|
+
openAuditForScope({ ...queryScope, start: date, end: date })
|
|
1595
|
+
}
|
|
1596
|
+
React.useEffect(() => {
|
|
1597
|
+
if (selectedDetailScope === null || detailKey === '') return undefined
|
|
1598
|
+
const seq = recordsGate.next()
|
|
1599
|
+
setAuditLoading(true)
|
|
1600
|
+
setAuditError('')
|
|
1601
|
+
setAuditRows([])
|
|
1602
|
+
setAuditSelectedId(null)
|
|
1603
|
+
setAuditCursor(null)
|
|
1604
|
+
setAuditHasMore(false)
|
|
1605
|
+
getUsageRecords(selectedDetailScope, null, 20).then((data) => {
|
|
1606
|
+
if (!recordsGate.isCurrent(seq)) return
|
|
1607
|
+
if (data === null || typeof data !== 'object' || !Array.isArray(data.items)) {
|
|
1608
|
+
setAuditError('audit')
|
|
1609
|
+
setAuditLoading(false)
|
|
1610
|
+
return
|
|
1611
|
+
}
|
|
1612
|
+
setAuditRows(data.items)
|
|
1613
|
+
setAuditSelectedId(data.items[0] ? data.items[0].id : null)
|
|
1614
|
+
setAuditCursor(data.nextCursor || null)
|
|
1615
|
+
setAuditHasMore(data.hasMore === true)
|
|
1616
|
+
setAuditLoading(false)
|
|
1617
|
+
setAuditError('')
|
|
1618
|
+
}, (reason) => {
|
|
1619
|
+
if (!recordsGate.isCurrent(seq)) return
|
|
1620
|
+
if (reason && reason.status === 409) {
|
|
1621
|
+
setAuditError('stale')
|
|
1622
|
+
setAuditReload((value) => value + 1)
|
|
1623
|
+
return
|
|
1624
|
+
}
|
|
1625
|
+
setAuditLoading(false)
|
|
1626
|
+
setAuditError('audit')
|
|
1627
|
+
})
|
|
1628
|
+
return undefined
|
|
1629
|
+
}, [detailKey, stats && stats.revision, auditReload])
|
|
1630
|
+
const loadMoreAudit = () => {
|
|
1631
|
+
if (selectedDetailScope === null || auditCursor === null || auditLoading) return
|
|
1632
|
+
const seq = recordsGate.next()
|
|
1633
|
+
setAuditLoading(true)
|
|
1634
|
+
getUsageRecords(selectedDetailScope, auditCursor, 20).then((data) => {
|
|
1635
|
+
if (!recordsGate.isCurrent(seq)) return
|
|
1636
|
+
if (data === null || typeof data !== 'object' || !Array.isArray(data.items)) {
|
|
1637
|
+
setAuditError('audit')
|
|
1638
|
+
setAuditLoading(false)
|
|
1639
|
+
return
|
|
1640
|
+
}
|
|
1641
|
+
setAuditRows((prev) => prev.concat(data.items))
|
|
1642
|
+
setAuditCursor(data.nextCursor || null)
|
|
1643
|
+
setAuditHasMore(data.hasMore === true)
|
|
1644
|
+
setAuditLoading(false)
|
|
1645
|
+
setAuditError('')
|
|
1646
|
+
}, (reason) => {
|
|
1647
|
+
if (!recordsGate.isCurrent(seq)) return
|
|
1648
|
+
if (reason && reason.status === 409) {
|
|
1649
|
+
setAuditRows([])
|
|
1650
|
+
setAuditSelectedId(null)
|
|
1651
|
+
setAuditCursor(null)
|
|
1652
|
+
setAuditHasMore(false)
|
|
1653
|
+
setAuditError('stale')
|
|
1654
|
+
setAuditReload((value) => value + 1)
|
|
1655
|
+
return
|
|
1656
|
+
}
|
|
1657
|
+
setAuditLoading(false)
|
|
1658
|
+
setAuditError('audit')
|
|
1659
|
+
})
|
|
1660
|
+
}
|
|
1661
|
+
React.useEffect(() => {
|
|
1662
|
+
if (detailSelection === null || detailView !== 'logs' || recordsPanelRef.current === null) return undefined
|
|
1663
|
+
recordsPanelRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
1664
|
+
return undefined
|
|
1665
|
+
}, [detailSelection && usageScopeKey(detailSelection.scope), detailView])
|
|
693
1666
|
const onRefresh = () => { refreshRef.current() }
|
|
694
1667
|
const toggleFilter = (id) => {
|
|
695
1668
|
setWsFilter((prev) => (prev === id ? null : id))
|
|
696
1669
|
}
|
|
1670
|
+
const clearFilters = () => {
|
|
1671
|
+
setWsFilter(null)
|
|
1672
|
+
setProviderFilter(null)
|
|
1673
|
+
setModelFilter(null)
|
|
1674
|
+
}
|
|
1675
|
+
const chooseProvider = (value) => {
|
|
1676
|
+
setProviderFilter(value || null)
|
|
1677
|
+
}
|
|
1678
|
+
const chooseModel = (value) => {
|
|
1679
|
+
setModelFilter(value || null)
|
|
1680
|
+
}
|
|
697
1681
|
const activeDayRows = useUtc && Array.isArray(stats && stats.byDayUtc) ? stats.byDayUtc : (Array.isArray(stats && stats.byDay) ? stats.byDay : [])
|
|
698
1682
|
const availableDateRange = availableDateBounds(activeDayRows, latestCalendarDate)
|
|
699
1683
|
const earliestAvailableDate = availableDateRange.min
|
|
@@ -718,10 +1702,45 @@ window.__ModuleLoader__.load({
|
|
|
718
1702
|
setCustomRangeOpen(false)
|
|
719
1703
|
}
|
|
720
1704
|
|
|
721
|
-
const
|
|
1705
|
+
const queryReady = queryResult !== null && queryResultKey === queryKey && queryResult.revision === (stats && stats.revision)
|
|
1706
|
+
const displayedDays = queryReady && Array.isArray(queryResult.daily) ? queryResult.daily : activeDayRows
|
|
1707
|
+
const displayedHeatmap = queryReady && Array.isArray(queryResult.heatmap) ? queryResult.heatmap : activeDayRows
|
|
1708
|
+
const activeCustomRangeKey = activeCustomRange === null ? '' : activeCustomRange.start + ':' + activeCustomRange.end
|
|
1709
|
+
const rangeOnlyAgg = React.useMemo(() => rangeAgg(stats, range, useUtc, activeCustomRange), [stats, range, useUtc, activeCustomRangeKey])
|
|
1710
|
+
const agg = React.useMemo(() => queryReady
|
|
1711
|
+
? { totals: queryResult.totals, perWs: queryResult.perWorkspace || [], perModel: queryResult.perModel || [] }
|
|
1712
|
+
: rangeOnlyAgg, [queryReady, queryResult, rangeOnlyAgg])
|
|
722
1713
|
const animatedTotal = useCountUp(agg.totals.input + agg.totals.output + agg.totals.cacheRead + agg.totals.cacheWrite + agg.totals.reasoning, timer)
|
|
723
1714
|
const animatedRate = useCountUp(Math.round(rateOf(agg.totals.input, agg.totals.cacheRead) * 10), timer)
|
|
724
|
-
const
|
|
1715
|
+
const scopedCountIsCalls = providerFilter !== null || modelFilter !== null
|
|
1716
|
+
const requestCount = Number.isFinite(agg.totals.calls) && agg.totals.calls > 0 ? agg.totals.calls : agg.totals.turns
|
|
1717
|
+
const displayedCount = scopedCountIsCalls ? requestCount : agg.totals.turns
|
|
1718
|
+
const animatedTurns = useCountUp(displayedCount, timer)
|
|
1719
|
+
const animatedRequests = useCountUp(requestCount, timer)
|
|
1720
|
+
const wsTotal = (w) => w.input + w.output + w.cacheRead + w.cacheWrite + w.reasoning
|
|
1721
|
+
const rows = React.useMemo(() => (Array.isArray(agg.perWs) ? agg.perWs : []).slice().sort((a, b) => wsTotal(b) - wsTotal(a)), [agg.perWs])
|
|
1722
|
+
const modelRows = React.useMemo(() => aggregateModelRows(agg.perModel || [], modelView, tr('未知供应商', 'Unknown provider'), tr('未知模型', 'Unknown model')).sort((a, b) => wsTotal(b) - wsTotal(a)), [agg.perModel, modelView, language])
|
|
1723
|
+
const rangeModelOptions = Array.isArray(rangeOnlyAgg.perModel) ? rangeOnlyAgg.perModel.filter((row) => row && typeof row === 'object') : []
|
|
1724
|
+
const providerOptions = Array.from(new Set(rangeModelOptions.map((row) => typeof row.provider === 'string' && row.provider !== '' ? row.provider : null).filter((value) => value !== null))).sort()
|
|
1725
|
+
const modelFilterValue = (row) => {
|
|
1726
|
+
const structured = typeof row.actualModel === 'string' && row.actualModel !== '' ? row.actualModel : typeof row.requestedModel === 'string' && row.requestedModel !== '' ? row.requestedModel : null
|
|
1727
|
+
if (structured !== null) return structured
|
|
1728
|
+
const display = typeof row.model === 'string' && row.model !== '' ? row.model : tr('未知模型', 'Unknown model')
|
|
1729
|
+
const separator = display.indexOf(' / ')
|
|
1730
|
+
const legacyProvider = separator > 0 ? display.slice(0, separator) : ''
|
|
1731
|
+
return separator > 0 && providerOptions.includes(legacyProvider) ? display.slice(separator + 3) : display
|
|
1732
|
+
}
|
|
1733
|
+
const modelOptions = Array.from(new Set(rangeModelOptions.map(modelFilterValue))).sort((a, b) => a.localeCompare(b))
|
|
1734
|
+
const workspaces = stats && Array.isArray(stats.workspaces) ? stats.workspaces : []
|
|
1735
|
+
const rangeWorkspaceTotals = new Map((Array.isArray(rangeOnlyAgg.perWs) ? rangeOnlyAgg.perWs : []).map((row) => [row.workspaceId, row]))
|
|
1736
|
+
const workspaceHasUsage = (row) => row !== undefined && (Number(row.turns) > 0 || Number(row.calls) > 0 || Number(row.input) > 0 || Number(row.output) > 0 || Number(row.cacheRead) > 0 || Number(row.cacheWrite) > 0 || Number(row.reasoning) > 0)
|
|
1737
|
+
const rangeWorkspaceOptions = workspaces.filter((workspace) => workspaceHasUsage(rangeWorkspaceTotals.get(workspace.id)))
|
|
1738
|
+
const rangeWorkspaceIds = new Set(rangeWorkspaceOptions.map((workspace) => workspace.id))
|
|
1739
|
+
React.useEffect(() => {
|
|
1740
|
+
if (wsFilter !== null && !rangeWorkspaceIds.has(wsFilter)) setWsFilter(null)
|
|
1741
|
+
if (providerFilter !== null && !providerOptions.includes(providerFilter)) setProviderFilter(null)
|
|
1742
|
+
if (modelFilter !== null && !modelOptions.includes(modelFilter)) setModelFilter(null)
|
|
1743
|
+
}, [wsFilter, providerFilter, modelFilter, Array.from(rangeWorkspaceIds).sort().join('\0'), providerOptions.join('\0'), modelOptions.join('\0')])
|
|
725
1744
|
|
|
726
1745
|
if (stats === null) {
|
|
727
1746
|
const failed = statsError !== ''
|
|
@@ -741,14 +1760,15 @@ window.__ModuleLoader__.load({
|
|
|
741
1760
|
const statusPayload = status !== null && typeof status === 'object' ? status : stats
|
|
742
1761
|
const scan = statusPayload && statusPayload.scan ? statusPayload.scan : (stats.scan || { done: true, started: true, scanned: 0, total: 0, failed: 0 })
|
|
743
1762
|
const sync = statusPayload && statusPayload.sync ? statusPayload.sync : (stats.sync || {})
|
|
744
|
-
const workspaces = Array.isArray(stats.workspaces) ? stats.workspaces : []
|
|
745
1763
|
const aliases = stats.aliases && typeof stats.aliases === 'object' ? stats.aliases : {}
|
|
746
1764
|
const wsById = new Map()
|
|
747
1765
|
const wsIndex = new Map()
|
|
748
1766
|
workspaces.forEach((w, i) => { wsById.set(w.id, w); wsIndex.set(w.id, i) })
|
|
749
|
-
const dayRows =
|
|
1767
|
+
const dayRows = displayedDays
|
|
1768
|
+
const heatmapMap = new Map()
|
|
1769
|
+
for (const d of displayedHeatmap) heatmapMap.set(d.date, d)
|
|
750
1770
|
const dayMap = new Map()
|
|
751
|
-
for (const d of
|
|
1771
|
+
for (const d of activeDayRows) dayMap.set(d.date, d)
|
|
752
1772
|
const wsTitle = (id) => {
|
|
753
1773
|
const alias = aliases[id]
|
|
754
1774
|
if (typeof alias === 'string' && alias !== '') return alias
|
|
@@ -778,9 +1798,213 @@ window.__ModuleLoader__.load({
|
|
|
778
1798
|
}
|
|
779
1799
|
setAliasOpen(false)
|
|
780
1800
|
}
|
|
1801
|
+
const currentPricing = stats.pricing && typeof stats.pricing === 'object' ? stats.pricing : {}
|
|
1802
|
+
const pricingUsedModels = Array.isArray(currentPricing.usedModels) ? currentPricing.usedModels : []
|
|
1803
|
+
const pricingUsedModelOptions = pricingUsedModels.slice().sort((left, right) => {
|
|
1804
|
+
const rank = { unpriced: 0, ambiguous: 1, unsupported: 2, priced: 3 }
|
|
1805
|
+
return (rank[left.status] === undefined ? 9 : rank[left.status]) - (rank[right.status] === undefined ? 9 : rank[right.status]) || String(left.model || '').localeCompare(String(right.model || ''))
|
|
1806
|
+
}).map((model) => ({
|
|
1807
|
+
value: String(model.identityKey || model.model || ''),
|
|
1808
|
+
label: (model.model || tr('未知模型', 'Unknown model')) + ' · ' + pricingStatusLabel(model.status, language),
|
|
1809
|
+
model: pricingModelKey(model.actualModel || model.requestedModel || model.pricingModel),
|
|
1810
|
+
officialModel: pricingModelKey(model.pricingModel),
|
|
1811
|
+
})).filter((option) => option.value !== '')
|
|
1812
|
+
const openPricingPanel = () => {
|
|
1813
|
+
const draft = pricingDraftOf(currentPricing)
|
|
1814
|
+
const uiState = storedUsageUiState()
|
|
1815
|
+
if (typeof uiState.pricingAutoSync === 'boolean') draft.sync.autoEnabled = uiState.pricingAutoSync
|
|
1816
|
+
setPricingDraft(draft)
|
|
1817
|
+
setPricingUsedModelSearchText({})
|
|
1818
|
+
setPricingOverrideSearchText({})
|
|
1819
|
+
setPricingUsedModelOpen(null)
|
|
1820
|
+
setPricingOverrideOpen(null)
|
|
1821
|
+
setPricingModelSearchOptions({})
|
|
1822
|
+
setPricingModelSearchOpen(null)
|
|
1823
|
+
setPricingError('')
|
|
1824
|
+
setPricingOpen(true)
|
|
1825
|
+
setAliasOpen(false)
|
|
1826
|
+
}
|
|
1827
|
+
const savePricingSettings = (backfill) => {
|
|
1828
|
+
if (pricingDraft === null || pricingSaving || pricingSyncing || pricingSyncSaving) return
|
|
1829
|
+
const requestToken = typeof stats.requestToken === 'string' ? stats.requestToken : ''
|
|
1830
|
+
if (requestToken === '') { setPricingError('token'); return }
|
|
1831
|
+
setPricingSaving(true)
|
|
1832
|
+
setPricingError('')
|
|
1833
|
+
setPricingRpc(pricingDraft, backfill, requestToken).then((data) => {
|
|
1834
|
+
if (!data || data.ok !== true || !data.pricing) { setPricingError('save'); return }
|
|
1835
|
+
setStats((prev) => prev === null ? prev : Object.assign({}, prev, { pricing: data.pricing }))
|
|
1836
|
+
setPricingDraft(pricingDraftOf(data.pricing))
|
|
1837
|
+
setPricingUsedModelSearchText({})
|
|
1838
|
+
setPricingOverrideSearchText({})
|
|
1839
|
+
setPricingUsedModelOpen(null)
|
|
1840
|
+
setPricingOverrideOpen(null)
|
|
1841
|
+
setPricingOpen(false)
|
|
1842
|
+
refreshRef.current()
|
|
1843
|
+
}, (reason) => { setPricingError(reason && reason.status === 403 ? 'forbidden' : 'save') }).finally(() => setPricingSaving(false))
|
|
1844
|
+
}
|
|
1845
|
+
const syncPricingNow = () => {
|
|
1846
|
+
if (pricingSaving || pricingSyncing || pricingSyncSaving) return
|
|
1847
|
+
const requestToken = typeof stats.requestToken === 'string' ? stats.requestToken : ''
|
|
1848
|
+
if (requestToken === '') { setPricingError('token'); return }
|
|
1849
|
+
setPricingSyncing(true)
|
|
1850
|
+
setPricingError('')
|
|
1851
|
+
syncPricingRpc(requestToken).then((data) => {
|
|
1852
|
+
if (!data || data.ok !== true || !data.pricing) { setPricingError('sync'); return }
|
|
1853
|
+
setStats((prev) => prev === null ? prev : Object.assign({}, prev, { pricing: data.pricing }))
|
|
1854
|
+
setPricingDraft(pricingDraftOf(data.pricing))
|
|
1855
|
+
setPricingUsedModelSearchText({})
|
|
1856
|
+
setPricingOverrideSearchText({})
|
|
1857
|
+
setPricingUsedModelOpen(null)
|
|
1858
|
+
setPricingOverrideOpen(null)
|
|
1859
|
+
refreshRef.current()
|
|
1860
|
+
}, (reason) => { setPricingError(reason && reason.status === 403 ? 'forbidden' : 'sync') }).finally(() => setPricingSyncing(false))
|
|
1861
|
+
}
|
|
1862
|
+
const updatePricingSync = (enabled) => {
|
|
1863
|
+
if (pricingDraft === null || pricingSyncSaving || pricingSaving || pricingSyncing) return
|
|
1864
|
+
const nextEnabled = enabled === true
|
|
1865
|
+
const previousEnabled = pricingDraft.sync && pricingDraft.sync.autoEnabled === true
|
|
1866
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { sync: Object.assign({}, prev.sync, { autoEnabled: nextEnabled }) }))
|
|
1867
|
+
persistUsageUiState({ pricingAutoSync: nextEnabled })
|
|
1868
|
+
const requestToken = typeof stats.requestToken === 'string' ? stats.requestToken : ''
|
|
1869
|
+
if (requestToken === '') {
|
|
1870
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { sync: Object.assign({}, prev.sync, { autoEnabled: previousEnabled }) }))
|
|
1871
|
+
persistUsageUiState({ pricingAutoSync: previousEnabled })
|
|
1872
|
+
setPricingError('token')
|
|
1873
|
+
return
|
|
1874
|
+
}
|
|
1875
|
+
const rollback = (error) => {
|
|
1876
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { sync: Object.assign({}, prev.sync, { autoEnabled: previousEnabled }) }))
|
|
1877
|
+
persistUsageUiState({ pricingAutoSync: previousEnabled })
|
|
1878
|
+
setPricingError(error)
|
|
1879
|
+
}
|
|
1880
|
+
setPricingSyncSaving(true)
|
|
1881
|
+
setPricingError('')
|
|
1882
|
+
setPricingRpc({ sync: { autoEnabled: nextEnabled } }, false, requestToken).then((data) => {
|
|
1883
|
+
if (!data || data.ok !== true || !data.pricing) { rollback('save'); return }
|
|
1884
|
+
const savedEnabled = data.pricing.sync && data.pricing.sync.autoEnabled === true
|
|
1885
|
+
setStats((prev) => prev === null ? prev : Object.assign({}, prev, { pricing: data.pricing }))
|
|
1886
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { sync: Object.assign({}, prev.sync, { autoEnabled: savedEnabled, intervalMs: data.pricing.sync && data.pricing.sync.intervalMs }) }))
|
|
1887
|
+
persistUsageUiState({ pricingAutoSync: savedEnabled })
|
|
1888
|
+
}, (reason) => rollback(reason && reason.status === 403 ? 'forbidden' : 'save')).finally(() => setPricingSyncSaving(false))
|
|
1889
|
+
}
|
|
1890
|
+
const updatePricingMapping = (index, field, value) => {
|
|
1891
|
+
setPricingDraft((prev) => {
|
|
1892
|
+
if (prev === null || !Array.isArray(prev.mappings) || !prev.mappings[index]) return prev
|
|
1893
|
+
const mappings = prev.mappings.slice()
|
|
1894
|
+
mappings[index] = Object.assign({}, mappings[index], { [field]: value })
|
|
1895
|
+
return Object.assign({}, prev, { mappings })
|
|
1896
|
+
})
|
|
1897
|
+
}
|
|
1898
|
+
const selectPricingUsedModel = (index, value) => {
|
|
1899
|
+
const selected = pricingUsedModels.find((model) => String(model.identityKey || model.model || '') === String(value))
|
|
1900
|
+
if (!selected) return
|
|
1901
|
+
const modelAlias = selected.actualModel || selected.requestedModel || selected.pricingModel || ''
|
|
1902
|
+
const officialModel = selected.status === 'priced' ? (selected.pricingModel || '') : ''
|
|
1903
|
+
setPricingDraft((prev) => {
|
|
1904
|
+
if (prev === null || !Array.isArray(prev.mappings) || !prev.mappings[index]) return prev
|
|
1905
|
+
const mappings = prev.mappings.slice()
|
|
1906
|
+
mappings[index] = Object.assign({}, mappings[index], { usageIdentityKey: value, model: modelAlias, catalogModelId: officialModel, catalogProviderId: selected.providerId || '' })
|
|
1907
|
+
return Object.assign({}, prev, { mappings })
|
|
1908
|
+
})
|
|
1909
|
+
setPricingUsedModelSearchText((prev) => Object.assign({}, prev, { [index]: selected.model || modelAlias }))
|
|
1910
|
+
setPricingUsedModelOpen(null)
|
|
1911
|
+
setPricingModelSearchOpen(null)
|
|
1912
|
+
}
|
|
1913
|
+
const searchUsedModels = (index, value) => {
|
|
1914
|
+
setPricingUsedModelSearchText((prev) => Object.assign({}, prev, { [index]: value }))
|
|
1915
|
+
setPricingUsedModelOpen(index)
|
|
1916
|
+
setPricingDraft((prev) => {
|
|
1917
|
+
if (prev === null || !Array.isArray(prev.mappings) || !prev.mappings[index]) return prev
|
|
1918
|
+
const mappings = prev.mappings.slice()
|
|
1919
|
+
mappings[index] = Object.assign({}, mappings[index], { usageIdentityKey: '', model: '', catalogModelId: '', catalogProviderId: '' })
|
|
1920
|
+
return Object.assign({}, prev, { mappings })
|
|
1921
|
+
})
|
|
1922
|
+
}
|
|
1923
|
+
const searchOfficialModels = (index, value) => {
|
|
1924
|
+
updatePricingMapping(index, 'catalogModelId', value)
|
|
1925
|
+
setPricingModelSearchOpen(index)
|
|
1926
|
+
const previousTimer = pricingModelSearchTimerRef.current[index]
|
|
1927
|
+
if (previousTimer !== undefined) {
|
|
1928
|
+
clearTimeout(previousTimer)
|
|
1929
|
+
delete pricingModelSearchTimerRef.current[index]
|
|
1930
|
+
}
|
|
1931
|
+
const nextSeq = (pricingModelSearchSeqRef.current[index] || 0) + 1
|
|
1932
|
+
pricingModelSearchSeqRef.current[index] = nextSeq
|
|
1933
|
+
if (String(value || '').trim() === '') {
|
|
1934
|
+
setPricingModelSearchOptions((prev) => Object.assign({}, prev, { [index]: [] }))
|
|
1935
|
+
return
|
|
1936
|
+
}
|
|
1937
|
+
const timerId = setTimeout(() => {
|
|
1938
|
+
delete pricingModelSearchTimerRef.current[index]
|
|
1939
|
+
getPricingModels(value).then((data) => {
|
|
1940
|
+
if (pricingModelSearchSeqRef.current[index] !== nextSeq) return
|
|
1941
|
+
setPricingModelSearchOptions((prev) => Object.assign({}, prev, { [index]: Array.isArray(data && data.items) ? data.items : [] }))
|
|
1942
|
+
}, () => {
|
|
1943
|
+
if (pricingModelSearchSeqRef.current[index] === nextSeq) setPricingModelSearchOptions((prev) => Object.assign({}, prev, { [index]: [] }))
|
|
1944
|
+
})
|
|
1945
|
+
}, 180)
|
|
1946
|
+
pricingModelSearchTimerRef.current[index] = timerId
|
|
1947
|
+
}
|
|
1948
|
+
const chooseOfficialModel = (index, option) => {
|
|
1949
|
+
if (!option || typeof option.value !== 'string') return
|
|
1950
|
+
const pendingTimer = pricingModelSearchTimerRef.current[index]
|
|
1951
|
+
if (pendingTimer !== undefined) {
|
|
1952
|
+
clearTimeout(pendingTimer)
|
|
1953
|
+
delete pricingModelSearchTimerRef.current[index]
|
|
1954
|
+
}
|
|
1955
|
+
pricingModelSearchSeqRef.current[index] = (pricingModelSearchSeqRef.current[index] || 0) + 1
|
|
1956
|
+
setPricingDraft((prev) => {
|
|
1957
|
+
if (prev === null || !Array.isArray(prev.mappings) || !prev.mappings[index]) return prev
|
|
1958
|
+
const mappings = prev.mappings.slice()
|
|
1959
|
+
mappings[index] = Object.assign({}, mappings[index], { catalogModelId: option.value, catalogProviderId: option.providerId || '' })
|
|
1960
|
+
return Object.assign({}, prev, { mappings })
|
|
1961
|
+
})
|
|
1962
|
+
setPricingModelSearchOpen(null)
|
|
1963
|
+
}
|
|
1964
|
+
const addPricingMapping = () => {
|
|
1965
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { mappings: prev.mappings.concat([{ usageIdentityKey: '', model: '', catalogProviderId: '', catalogModelId: '', inputTokenSemantics: 'fresh', multiplier: '1' }]) }))
|
|
1966
|
+
}
|
|
1967
|
+
const removePricingMapping = (index) => {
|
|
1968
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { mappings: prev.mappings.filter((_, itemIndex) => itemIndex !== index) }))
|
|
1969
|
+
}
|
|
1970
|
+
const updatePricingOverride = (index, field, value) => {
|
|
1971
|
+
setPricingDraft((prev) => {
|
|
1972
|
+
if (prev === null || !Array.isArray(prev.overrides) || !prev.overrides[index]) return prev
|
|
1973
|
+
const overrides = prev.overrides.slice()
|
|
1974
|
+
overrides[index] = Object.assign({}, overrides[index], { [field]: value })
|
|
1975
|
+
return Object.assign({}, prev, { overrides })
|
|
1976
|
+
})
|
|
1977
|
+
}
|
|
1978
|
+
const selectPricingOverrideModel = (index, value) => {
|
|
1979
|
+
const selected = pricingUsedModels.find((model) => String(model.identityKey || model.model || '') === String(value))
|
|
1980
|
+
if (!selected) return
|
|
1981
|
+
const modelId = selected.pricingModel || selected.actualModel || selected.requestedModel || ''
|
|
1982
|
+
setPricingDraft((prev) => {
|
|
1983
|
+
if (prev === null || !Array.isArray(prev.overrides) || !prev.overrides[index]) return prev
|
|
1984
|
+
const overrides = prev.overrides.slice()
|
|
1985
|
+
overrides[index] = Object.assign({}, overrides[index], { modelId })
|
|
1986
|
+
return Object.assign({}, prev, { overrides })
|
|
1987
|
+
})
|
|
1988
|
+
setPricingOverrideSearchText((prev) => Object.assign({}, prev, { [index]: modelId }))
|
|
1989
|
+
setPricingOverrideOpen(null)
|
|
1990
|
+
}
|
|
1991
|
+
const searchPricingOverrideModels = (index, value) => {
|
|
1992
|
+
setPricingOverrideSearchText((prev) => Object.assign({}, prev, { [index]: value }))
|
|
1993
|
+
setPricingOverrideOpen(index)
|
|
1994
|
+
updatePricingOverride(index, 'modelId', value)
|
|
1995
|
+
}
|
|
1996
|
+
const addPricingOverride = () => {
|
|
1997
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { overrides: prev.overrides.concat([{ providerId: '', modelId: '', displayName: '', input: '', output: '', cacheRead: '', cacheWrite: '' }]) }))
|
|
1998
|
+
}
|
|
1999
|
+
const removePricingOverride = (index) => {
|
|
2000
|
+
setPricingDraft((prev) => prev === null ? prev : Object.assign({}, prev, { overrides: prev.overrides.filter((_, itemIndex) => itemIndex !== index) }))
|
|
2001
|
+
}
|
|
781
2002
|
|
|
782
2003
|
const totalTokens = agg.totals.input + agg.totals.output + agg.totals.cacheRead + agg.totals.cacheWrite + agg.totals.reasoning
|
|
783
2004
|
const cacheRate = rateOf(agg.totals.input, agg.totals.cacheRead)
|
|
2005
|
+
const scopedCost = costAggregate(agg.totals)
|
|
2006
|
+
const costValue = costDisplay(agg.totals, language)
|
|
2007
|
+
const costCoverage = costCoverageLabel(agg.totals, language)
|
|
784
2008
|
const st = streaks(dayMap, useUtc)
|
|
785
2009
|
|
|
786
2010
|
const today = calendarNow
|
|
@@ -804,7 +2028,7 @@ window.__ModuleLoader__.load({
|
|
|
804
2028
|
const weekdayLabels = language === 'en' ? ['', 'Mon', '', 'Wed', '', 'Fri', ''] : ['', '周一', '', '周三', '', '周五', '']
|
|
805
2029
|
|
|
806
2030
|
const onEnter = (cell, ev) => {
|
|
807
|
-
setHover({ date: cell.date, x: ev.clientX, y: ev.clientY, day:
|
|
2031
|
+
setHover({ date: cell.date, x: ev.clientX, y: ev.clientY, day: heatmapMap.get(cell.date) })
|
|
808
2032
|
}
|
|
809
2033
|
const onMove = (cell, ev) => {
|
|
810
2034
|
setHover((prev) => (prev !== null && prev.date === cell.date ? { date: prev.date, x: ev.clientX, y: ev.clientY, day: prev.day } : prev))
|
|
@@ -812,12 +2036,12 @@ window.__ModuleLoader__.load({
|
|
|
812
2036
|
const onLeave = () => setHover(null)
|
|
813
2037
|
|
|
814
2038
|
const cellElements = cells.map((cell, i) => {
|
|
815
|
-
const day =
|
|
2039
|
+
const day = heatmapMap.get(cell.date)
|
|
816
2040
|
let count = 0
|
|
817
2041
|
if (day !== undefined) {
|
|
818
|
-
if (wsFilter === null) count = day.turns
|
|
2042
|
+
if (queryReady || wsFilter === null) count = day.turns
|
|
819
2043
|
else {
|
|
820
|
-
const w = day.perWorkspace.find((x) => x.workspaceId === wsFilter)
|
|
2044
|
+
const w = Array.isArray(day.perWorkspace) ? day.perWorkspace.find((x) => x.workspaceId === wsFilter) : undefined
|
|
821
2045
|
if (w !== undefined) count = w.turns
|
|
822
2046
|
}
|
|
823
2047
|
}
|
|
@@ -837,6 +2061,7 @@ window.__ModuleLoader__.load({
|
|
|
837
2061
|
onMouseEnter: (ev) => onEnter(cell, ev),
|
|
838
2062
|
onMouseMove: (ev) => onMove(cell, ev),
|
|
839
2063
|
onMouseLeave: onLeave,
|
|
2064
|
+
onClick: () => openAuditForDate(cell.date),
|
|
840
2065
|
})
|
|
841
2066
|
})
|
|
842
2067
|
|
|
@@ -872,9 +2097,15 @@ window.__ModuleLoader__.load({
|
|
|
872
2097
|
React.createElement('div', { className: 'uh-card-value' }, value),
|
|
873
2098
|
React.createElement('div', { className: 'uh-card-sub' }, sub),
|
|
874
2099
|
)
|
|
2100
|
+
const summaryRateMetric = React.createElement('div', { className: 'uh-ios-metric uh-ios-metric-rate', style: { animationDelay: '280ms' } },
|
|
2101
|
+
React.createElement('div', { className: 'uh-ios-metric-rate-head' },
|
|
2102
|
+
React.createElement('div', { className: 'uh-ios-metric-label' }, React.createElement(LineIcon, { name: 'cache', size: 18 }), tr('缓存命中率', 'Cache Hit Rate')),
|
|
2103
|
+
React.createElement('div', { className: 'uh-ios-metric-rate-value' }, (cacheRate).toFixed(1) + '%'),
|
|
2104
|
+
),
|
|
2105
|
+
React.createElement('div', { className: 'uh-ios-metric-bar' }, React.createElement('div', { className: 'uh-ios-metric-fill', style: { width: Math.max(0, Math.min(100, cacheRate)) + '%' } })),
|
|
2106
|
+
React.createElement('div', { className: 'uh-ios-metric-rate-detail' }, language === 'en' ? 'Context reused ' + fmtCompact(agg.totals.cacheRead) + ' tokens' : '复用上下文 ' + fmtCompact(agg.totals.cacheRead) + ' Token'),
|
|
2107
|
+
)
|
|
875
2108
|
|
|
876
|
-
const wsTotal = (w) => w.input + w.output + w.cacheRead + w.cacheWrite + w.reasoning
|
|
877
|
-
const rows = agg.perWs.slice().sort((a, b) => wsTotal(b) - wsTotal(a))
|
|
878
2109
|
const maxTotal = rows.length > 0 ? wsTotal(rows[0]) : 0
|
|
879
2110
|
|
|
880
2111
|
const tokenCardRows = rows.slice(0, 3).map((w) => {
|
|
@@ -924,7 +2155,7 @@ window.__ModuleLoader__.load({
|
|
|
924
2155
|
className: 'uh-row' + (selected ? ' uh-sel' : ''),
|
|
925
2156
|
onClick: () => toggleFilter(w.workspaceId),
|
|
926
2157
|
},
|
|
927
|
-
React.createElement('div', {},
|
|
2158
|
+
React.createElement('div', { className: 'uh-row-title-wrap' },
|
|
928
2159
|
React.createElement('div', { className: 'uh-ws-title' }, title),
|
|
929
2160
|
React.createElement('div', { className: 'uh-ws-path' }, subText),
|
|
930
2161
|
),
|
|
@@ -939,6 +2170,7 @@ window.__ModuleLoader__.load({
|
|
|
939
2170
|
React.createElement('div', { className: 'uh-barfill', style: { width: maxTotal > 0 ? Math.max(2, (total / maxTotal) * 100) + '%' : '0%', background: color } }),
|
|
940
2171
|
),
|
|
941
2172
|
),
|
|
2173
|
+
React.createElement('div', { className: 'uh-num uh-cost-num' }, costDisplay(w, language)),
|
|
942
2174
|
React.createElement('div', { className: 'uh-num' }, rate.toFixed(1) + '%'),
|
|
943
2175
|
React.createElement('div', { className: 'uh-num' }, maxTotal > 0 ? ((total / maxTotal) * 100).toFixed(0) + '%' : '0%'),
|
|
944
2176
|
)
|
|
@@ -946,29 +2178,47 @@ window.__ModuleLoader__.load({
|
|
|
946
2178
|
|
|
947
2179
|
const modelViewLabel = modelView === 'route' ? tr('混合查看', 'Combined View') : modelView === 'model' ? tr('按模型合并', 'Grouped by Model') : tr('按供应商汇总', 'Grouped by Provider')
|
|
948
2180
|
const modelColumnLabel = modelView === 'route' ? tr('供应商 / 模型', 'Provider / Model') : modelView === 'model' ? tr('模型', 'Model') : tr('供应商', 'Provider')
|
|
949
|
-
const
|
|
2181
|
+
const modelDonutChart = detailView !== 'model' || modelRows.length === 0 ? null : React.createElement(UsageDonutChart, {
|
|
2182
|
+
key: 'model-donut-' + detailView + ':' + queryKey + ':' + stats.revision + ':' + modelView,
|
|
2183
|
+
title: modelView === 'provider' ? tr('供应商用量', 'Provider Usage') : tr('模型用量', 'Model Usage'),
|
|
2184
|
+
icon: 'chart',
|
|
2185
|
+
language,
|
|
2186
|
+
items: modelRows.map((row, index) => ({ label: row.model, value: wsTotal(row), cost: row.cost, color: DONUT_COLORS[index % DONUT_COLORS.length] })),
|
|
2187
|
+
})
|
|
2188
|
+
const workspaceDonutChart = detailView !== 'workspace' || rows.length === 0 ? null : React.createElement(UsageDonutChart, {
|
|
2189
|
+
key: 'workspace-donut-' + detailView + ':' + queryKey + ':' + stats.revision,
|
|
2190
|
+
title: tr('工作区用量', 'Workspace Usage'),
|
|
2191
|
+
icon: 'folder',
|
|
2192
|
+
language,
|
|
2193
|
+
items: rows.map((row, index) => ({ label: wsTitle(row.workspaceId), value: wsTotal(row), cost: row.cost, color: DONUT_COLORS[index % DONUT_COLORS.length] })),
|
|
2194
|
+
})
|
|
950
2195
|
const exportCsv = () => {
|
|
951
2196
|
const quote = (value) => '"' + String(value === undefined || value === null ? '' : value).replace(/"/g, '""') + '"'
|
|
952
2197
|
const line = (values) => values.map(quote).join(',')
|
|
953
2198
|
const allTokens = (entry) => entry.input + entry.output + entry.cacheRead + entry.cacheWrite + entry.reasoning
|
|
954
|
-
const tokenHeaders = [tr('输入 Token', 'Input Tokens'), tr('缓存命中 Token', 'Cache-Hit Tokens'), tr('缓存写入 Token', 'Cache-Write Tokens'), tr('输出 Token', 'Output Tokens'), tr('推理 Token', 'Reasoning Tokens'), tr('总处理 Token', 'Total Tokens Processed'), tr('缓存命中率', 'Cache Hit Rate')]
|
|
2199
|
+
const tokenHeaders = [tr('输入 Token', 'Input Tokens'), tr('缓存命中 Token', 'Cache-Hit Tokens'), tr('缓存写入 Token', 'Cache-Write Tokens'), tr('输出 Token', 'Output Tokens'), tr('推理 Token', 'Reasoning Tokens'), tr('总处理 Token', 'Total Tokens Processed'), tr('成本', 'Cost'), tr('缓存命中率', 'Cache Hit Rate')]
|
|
955
2200
|
const output = [
|
|
956
2201
|
line([tr('DSH 用量统计导出', 'DSH Usage Statistics Export')]),
|
|
957
2202
|
line([tr('导出时间', 'Exported At'), useUtc ? new Date().toLocaleString('en-US', { timeZone: 'UTC', timeZoneName: 'short' }) : new Date().toLocaleString('zh-CN')]),
|
|
958
2203
|
line([tr('时间范围', 'Time Range'), rangeLabel]),
|
|
2204
|
+
line([tr('时区', 'Timezone'), useUtc ? 'UTC' : tr('本地', 'Local')]),
|
|
2205
|
+
line([tr('工作区筛选', 'Workspace Filter'), wsFilter || tr('全部', 'All')]),
|
|
2206
|
+
line([tr('供应商筛选', 'Provider Filter'), providerFilter || tr('全部', 'All')]),
|
|
2207
|
+
line([tr('模型筛选', 'Model Filter'), modelFilter || tr('全部', 'All')]),
|
|
2208
|
+
line([tr('统计 revision', 'Stats Revision'), stats.revision || '']),
|
|
959
2209
|
line([tr('模型查看模式', 'Model View Mode'), modelViewLabel]),
|
|
960
2210
|
'',
|
|
961
2211
|
line([tr('汇总', 'Summary')]),
|
|
962
2212
|
line([tr('回合', 'Turns'), tr('会话', 'Sessions'), ...tokenHeaders]),
|
|
963
|
-
line([agg.totals.turns, agg.totals.sessions, agg.totals.input, agg.totals.cacheRead, agg.totals.cacheWrite, agg.totals.output, agg.totals.reasoning, allTokens(agg.totals), rateOf(agg.totals.input, agg.totals.cacheRead).toFixed(2) + '%']),
|
|
2213
|
+
line([agg.totals.turns, agg.totals.sessions, agg.totals.input, agg.totals.cacheRead, agg.totals.cacheWrite, agg.totals.output, agg.totals.reasoning, allTokens(agg.totals), costDisplay(agg.totals, language), rateOf(agg.totals.input, agg.totals.cacheRead).toFixed(2) + '%']),
|
|
964
2214
|
'',
|
|
965
2215
|
line([tr('模型用量明细', 'Model Usage Details')]),
|
|
966
2216
|
line([modelColumnLabel, tr('调用', 'Calls'), ...tokenHeaders]),
|
|
967
|
-
...modelRows.map((m) => line([m.model, m.calls, m.input, m.cacheRead, m.cacheWrite, m.output, m.reasoning, allTokens(m), rateOf(m.input, m.cacheRead).toFixed(2) + '%'])),
|
|
2217
|
+
...modelRows.map((m) => line([m.model, m.calls, m.input, m.cacheRead, m.cacheWrite, m.output, m.reasoning, allTokens(m), costDisplay(m, language), rateOf(m.input, m.cacheRead).toFixed(2) + '%'])),
|
|
968
2218
|
'',
|
|
969
2219
|
line([tr('工作区明细', 'Workspace Details')]),
|
|
970
2220
|
line([tr('工作区', 'Workspace'), tr('路径', 'Path'), tr('回合', 'Turns'), ...tokenHeaders]),
|
|
971
|
-
...rows.map((w) => { const meta = wsById.get(w.workspaceId); return line([wsTitle(w.workspaceId), meta ? meta.path : '', w.turns, w.input, w.cacheRead, w.cacheWrite, w.output, w.reasoning, allTokens(w), rateOf(w.input, w.cacheRead).toFixed(2) + '%']) }),
|
|
2221
|
+
...rows.map((w) => { const meta = wsById.get(w.workspaceId); return line([wsTitle(w.workspaceId), meta ? meta.path : '', w.turns, w.input, w.cacheRead, w.cacheWrite, w.output, w.reasoning, allTokens(w), costDisplay(w, language), rateOf(w.input, w.cacheRead).toFixed(2) + '%']) }),
|
|
972
2222
|
]
|
|
973
2223
|
const blob = new Blob(['\uFEFF' + output.join('\r\n')], { type: 'text/csv;charset=utf-8' })
|
|
974
2224
|
const url = URL.createObjectURL(blob)
|
|
@@ -979,20 +2229,23 @@ window.__ModuleLoader__.load({
|
|
|
979
2229
|
URL.revokeObjectURL(url)
|
|
980
2230
|
}
|
|
981
2231
|
|
|
982
|
-
const modelElements = modelRows.map((m) => {
|
|
2232
|
+
const modelElements = detailView === 'model' ? modelRows.map((m) => {
|
|
983
2233
|
const total = wsTotal(m)
|
|
984
2234
|
const rate = rateOf(m.input, m.cacheRead)
|
|
985
|
-
return React.createElement('div', { key: m.model, className: 'uh-model-row uh-row' },
|
|
986
|
-
React.createElement('div', { className: 'uh-
|
|
2235
|
+
return React.createElement('div', { key: m.identityKey || m.model, className: 'uh-model-row uh-row' },
|
|
2236
|
+
React.createElement('div', { className: 'uh-row-title-wrap' },
|
|
2237
|
+
React.createElement('div', { className: 'uh-ws-title', title: m.model }, m.model),
|
|
2238
|
+
),
|
|
987
2239
|
React.createElement('div', { className: 'uh-num' }, fmtCompact(m.calls)),
|
|
988
2240
|
React.createElement('div', { className: 'uh-num' }, fmtCompact(m.input)),
|
|
989
2241
|
React.createElement('div', { className: 'uh-num' }, fmtCompact(m.cacheRead)),
|
|
990
2242
|
React.createElement('div', { className: 'uh-num' }, fmtCompact(m.output)),
|
|
991
2243
|
React.createElement('div', { className: 'uh-num' }, fmtCompact(m.reasoning)),
|
|
992
2244
|
React.createElement('div', { className: 'uh-num' }, fmtCompact(total)),
|
|
2245
|
+
React.createElement('div', { className: 'uh-num uh-cost-num' }, costDisplay(m, language)),
|
|
993
2246
|
React.createElement('div', { className: 'uh-num' }, rate.toFixed(1) + '%'),
|
|
994
2247
|
)
|
|
995
|
-
})
|
|
2248
|
+
}) : []
|
|
996
2249
|
|
|
997
2250
|
const aliasPanel = aliasOpen
|
|
998
2251
|
? React.createElement('div', { className: 'uh-panel uh-anim-panel' },
|
|
@@ -1022,13 +2275,133 @@ window.__ModuleLoader__.load({
|
|
|
1022
2275
|
)
|
|
1023
2276
|
: null
|
|
1024
2277
|
|
|
2278
|
+
|
|
2279
|
+
const pricingSync = currentPricing.sync && typeof currentPricing.sync === 'object' ? currentPricing.sync : {}
|
|
2280
|
+
const pricingPanel = pricingOpen && pricingDraft !== null ? React.createElement('div', { className: 'uh-panel uh-pricing-panel uh-anim-panel' },
|
|
2281
|
+
React.createElement('div', { className: 'uh-pricing-head' },
|
|
2282
|
+
React.createElement('div', { className: 'uh-title-with-icon' }, React.createElement(LineIcon, { name: 'wallet', size: 16 }), React.createElement('strong', {}, tr('成本统计设置', 'Cost Statistics'))),
|
|
2283
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh uh-icon-button', title: tr('关闭成本设置', 'Close cost settings'), 'aria-label': tr('关闭成本设置', 'Close cost settings'), onClick: () => setPricingOpen(false) }, React.createElement(LineIcon, { name: 'close', size: 16 })),
|
|
2284
|
+
),
|
|
2285
|
+
React.createElement('div', { className: 'uh-pricing-note' }, tr('价格来自 models.dev 的公开目录,单位为 USD / 1M Token。已保存的历史成本不会因目录更新重算;同步只回填当前未计价的调用。', 'Prices come from the public models.dev catalog in USD per 1M tokens. Saved historical costs are not recalculated; sync only backfills currently unpriced calls.')),
|
|
2286
|
+
React.createElement('div', { className: 'uh-pricing-toolbar' },
|
|
2287
|
+
React.createElement('label', { className: 'uh-pricing-switch' },
|
|
2288
|
+
React.createElement('input', { type: 'checkbox', checked: pricingDraft.sync.autoEnabled === true, disabled: pricingSaving || pricingSyncing || pricingSyncSaving, onChange: (event) => updatePricingSync(event.target.checked) }),
|
|
2289
|
+
React.createElement('span', {}, tr('启用 6 小时自动同步', 'Enable 6-hour automatic sync')),
|
|
2290
|
+
),
|
|
2291
|
+
React.createElement('span', { className: 'uh-note' }, pricingSyncSaving ? tr('保存中…', 'Saving…') : (pricingSync.lastSuccessAt > 0 ? tr('上次成功:', 'Last success: ') + new Date(pricingSync.lastSuccessAt).toLocaleString() : tr('尚未同步', 'Not synced yet'))),
|
|
2292
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: syncPricingNow, disabled: pricingSyncing || pricingSaving || pricingSyncSaving }, React.createElement(LineIcon, { name: 'refresh', size: 14 }), pricingSyncing ? tr('同步中…', 'Syncing…') : tr('立即同步', 'Sync now')),
|
|
2293
|
+
),
|
|
2294
|
+
pricingSync.lastError ? React.createElement('div', { className: 'uh-pricing-error', role: 'alert' }, tr('上次同步失败:', 'Last sync failed: ') + pricingSync.lastError) : null,
|
|
2295
|
+
React.createElement('div', { className: 'uh-pricing-section' },
|
|
2296
|
+
React.createElement('div', { className: 'uh-pricing-section-head' }, React.createElement('strong', {}, tr('当前用量匹配', 'Usage matches')), React.createElement('span', { className: 'uh-note' }, pricingUsedModels.length + ' ' + tr('个模型', 'models'))),
|
|
2297
|
+
pricingUsedModels.length === 0 ? React.createElement('div', { className: 'uh-empty', style: { padding: '12px 0' } }, tr('暂无模型用量', 'No model usage yet')) : React.createElement('div', { className: 'uh-pricing-table-wrap' },
|
|
2298
|
+
React.createElement('table', { className: 'uh-pricing-model-table' },
|
|
2299
|
+
React.createElement('thead', {}, React.createElement('tr', {},
|
|
2300
|
+
React.createElement('th', { scope: 'col' }, tr('当前模型', 'Usage model')),
|
|
2301
|
+
React.createElement('th', { scope: 'col' }, tr('状态', 'Status')),
|
|
2302
|
+
React.createElement('th', { scope: 'col' }, tr('官方模型', 'Official model')),
|
|
2303
|
+
React.createElement('th', { scope: 'col', title: tr('输入价格(USD / 1M)', 'Input price (USD / 1M)') }, tr('输入', 'Input')),
|
|
2304
|
+
React.createElement('th', { scope: 'col', title: tr('输出价格(USD / 1M)', 'Output price (USD / 1M)') }, tr('输出', 'Output')),
|
|
2305
|
+
React.createElement('th', { scope: 'col', title: tr('缓存读取价格(USD / 1M)', 'Cache read price (USD / 1M)') }, tr('缓存读', 'Cache read')),
|
|
2306
|
+
React.createElement('th', { scope: 'col', title: tr('缓存写入价格(USD / 1M)', 'Cache write price (USD / 1M)') }, tr('缓存写', 'Cache write')),
|
|
2307
|
+
)),
|
|
2308
|
+
React.createElement('tbody', {}, pricingUsedModels.map((model) => React.createElement('tr', { key: model.identityKey },
|
|
2309
|
+
React.createElement('td', { className: 'uh-pricing-model-name', title: model.model }, model.model || tr('未知模型', 'Unknown model')),
|
|
2310
|
+
React.createElement('td', { title: model.reason || '' }, React.createElement('span', { className: 'uh-pricing-status uh-pricing-status-' + (model.status || 'unpriced') }, pricingStatusLabel(model.status || 'unpriced', language))),
|
|
2311
|
+
React.createElement('td', { className: 'uh-pricing-model-target', title: model.pricingModel || '' }, model.pricingModel || tr('未匹配', 'No match')),
|
|
2312
|
+
React.createElement('td', { className: 'uh-pricing-model-rate' }, model.status === 'priced' && model.rates ? model.rates.input : '—'),
|
|
2313
|
+
React.createElement('td', { className: 'uh-pricing-model-rate' }, model.status === 'priced' && model.rates ? model.rates.output : '—'),
|
|
2314
|
+
React.createElement('td', { className: 'uh-pricing-model-rate' }, model.status === 'priced' && model.rates ? model.rates.cacheRead : '—'),
|
|
2315
|
+
React.createElement('td', { className: 'uh-pricing-model-rate' }, model.status === 'priced' && model.rates ? model.rates.cacheWrite : '—'),
|
|
2316
|
+
)),
|
|
2317
|
+
),
|
|
2318
|
+
),
|
|
2319
|
+
),
|
|
2320
|
+
),
|
|
2321
|
+
React.createElement('div', { className: 'uh-pricing-section' },
|
|
2322
|
+
React.createElement('div', { className: 'uh-pricing-section-head' }, React.createElement('strong', {}, tr('模型映射', 'Model mappings')), React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: addPricingMapping }, React.createElement(LineIcon, { name: 'plus', size: 13 }), tr('添加映射', 'Add mapping'))),
|
|
2323
|
+
pricingDraft.mappings.length === 0 ? React.createElement('div', { className: 'uh-empty', style: { padding: '12px 0' } }, tr('选择当前模型后,再指定对应的官方模型。DSH Provider 不参与计价。', 'Select a used model, then choose its official model. The DSH provider is ignored.')) : pricingDraft.mappings.map((mapping, index) => {
|
|
2324
|
+
const usedModelQuery = String(pricingUsedModelSearchText[index] || '').trim().toLowerCase()
|
|
2325
|
+
const mappingModelKey = pricingModelKey(mapping.model)
|
|
2326
|
+
const mappingOfficialModelKey = pricingModelKey(mapping.catalogModelId)
|
|
2327
|
+
const selectedUsedModel = pricingUsedModelOptions.find((option) => option.value === String(mapping.usageIdentityKey || '')) || pricingUsedModelOptions.find((option) => mappingModelKey !== '' && option.model === mappingModelKey) || pricingUsedModelOptions.find((option) => mappingOfficialModelKey !== '' && option.officialModel === mappingOfficialModelKey)
|
|
2328
|
+
const usedModelOptions = pricingUsedModelOptions.filter((option) => usedModelQuery === '' || option.label.toLowerCase().includes(usedModelQuery))
|
|
2329
|
+
return React.createElement('div', { key: index, className: 'uh-pricing-edit-row' },
|
|
2330
|
+
React.createElement('div', { className: 'uh-pricing-used-model-picker' },
|
|
2331
|
+
React.createElement('input', { type: 'text', className: 'uh-pricing-used-model-input', placeholder: tr('选择当前用过的模型', 'Select a used model'), value: pricingUsedModelSearchText[index] !== undefined ? pricingUsedModelSearchText[index] : (selectedUsedModel ? selectedUsedModel.label : ''), 'aria-label': tr('当前用过的模型', 'Used model'), 'aria-haspopup': 'listbox', 'aria-expanded': pricingUsedModelOpen === index, onFocus: () => { setPricingUsedModelOpen(index); setPricingUsedModelSearchText((prev) => Object.assign({}, prev, { [index]: '' })) }, onClick: () => setPricingUsedModelOpen(index), onBlur: () => setTimeout(() => { setPricingUsedModelOpen((current) => current === index ? null : current); if (selectedUsedModel) setPricingUsedModelSearchText((prev) => Object.assign({}, prev, { [index]: selectedUsedModel.label })) }, 120), onKeyDown: (event) => { if (event.key === 'Escape') setPricingUsedModelOpen(null) }, onChange: (event) => searchUsedModels(index, event.target.value) }),
|
|
2332
|
+
pricingUsedModelOpen === index && usedModelOptions.length > 0 ? React.createElement('div', { className: 'uh-language-options uh-pricing-used-model-options', role: 'listbox', 'aria-label': tr('当前用过的模型', 'Used models') },
|
|
2333
|
+
usedModelOptions.map((option) => React.createElement('button', { key: option.value, type: 'button', role: 'option', className: 'uh-language-option uh-pricing-model-option', onMouseDown: (event) => event.preventDefault(), onClick: () => selectPricingUsedModel(index, option.value) },
|
|
2334
|
+
React.createElement(LineIcon, { name: 'list', size: 14 }),
|
|
2335
|
+
React.createElement('span', { className: 'uh-pricing-model-option-name' }, option.label),
|
|
2336
|
+
)),
|
|
2337
|
+
) : null,
|
|
2338
|
+
),
|
|
2339
|
+
React.createElement('div', { className: 'uh-pricing-model-search' },
|
|
2340
|
+
React.createElement('input', { type: 'text', className: 'uh-pricing-model-search-input', placeholder: tr('输入官方模型 ID 检索', 'Type official model ID to search'), value: mapping.catalogModelId || '', 'aria-label': tr('官方模型 ID', 'Official model ID'), 'aria-autocomplete': 'list', onFocus: () => setPricingModelSearchOpen(index), onBlur: () => setTimeout(() => setPricingModelSearchOpen((current) => current === index ? null : current), 120), onKeyDown: (event) => { if (event.key === 'Escape') setPricingModelSearchOpen(null) }, onChange: (event) => searchOfficialModels(index, event.target.value) }),
|
|
2341
|
+
pricingModelSearchOpen === index && Array.isArray(pricingModelSearchOptions[index]) && pricingModelSearchOptions[index].length > 0 ? React.createElement('div', { className: 'uh-language-options uh-pricing-model-options', role: 'listbox', 'aria-label': tr('官方模型匹配结果', 'Official model matches') },
|
|
2342
|
+
pricingModelSearchOptions[index].map((option) => React.createElement('button', { key: option.value, type: 'button', role: 'option', className: 'uh-language-option uh-pricing-model-option', onMouseDown: (event) => event.preventDefault(), onClick: () => chooseOfficialModel(index, option) },
|
|
2343
|
+
React.createElement(LineIcon, { name: 'list', size: 14 }),
|
|
2344
|
+
React.createElement('span', { className: 'uh-pricing-model-option-name' }, option.label || option.value),
|
|
2345
|
+
React.createElement('span', { className: 'uh-pricing-model-option-id' }, option.value),
|
|
2346
|
+
)),
|
|
2347
|
+
) : null,
|
|
2348
|
+
),
|
|
2349
|
+
React.createElement('input', { type: 'number', min: '0', step: 'any', title: tr('成本倍率', 'Cost multiplier'), 'aria-label': tr('成本倍率', 'Cost multiplier'), value: mapping.multiplier || '1', onChange: (event) => updatePricingMapping(index, 'multiplier', event.target.value) }),
|
|
2350
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh uh-icon-button', title: tr('删除映射', 'Remove mapping'), 'aria-label': tr('删除映射', 'Remove mapping'), onClick: () => removePricingMapping(index) }, React.createElement(LineIcon, { name: 'close', size: 14 })),
|
|
2351
|
+
)
|
|
2352
|
+
}),
|
|
2353
|
+
),
|
|
2354
|
+
React.createElement('div', { className: 'uh-pricing-section' },
|
|
2355
|
+
React.createElement('div', { className: 'uh-pricing-section-head' }, React.createElement('strong', {}, tr('显式价格覆盖', 'Explicit price overrides')), React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: addPricingOverride }, React.createElement(LineIcon, { name: 'plus', size: 13 }), tr('添加价格', 'Add price'))),
|
|
2356
|
+
pricingDraft.overrides.length === 0 ? React.createElement('div', { className: 'uh-empty', style: { padding: '12px 0' } }, tr('仅在官方目录未覆盖或有明确官方账单时添加;价格按模型 ID 生效。', 'Add an override only when the official catalog lacks the model or you have an authoritative official price. It applies by model ID.')) : React.createElement(React.Fragment, null,
|
|
2357
|
+
React.createElement('div', { className: 'uh-pricing-price-head' },
|
|
2358
|
+
React.createElement('span', {}, tr('官方模型 ID', 'Official model ID')),
|
|
2359
|
+
React.createElement('span', {}, tr('输入价 / 1M', 'Input / 1M')),
|
|
2360
|
+
React.createElement('span', {}, tr('输出价 / 1M', 'Output / 1M')),
|
|
2361
|
+
React.createElement('span', {}, tr('缓存读 / 1M', 'Cache read / 1M')),
|
|
2362
|
+
React.createElement('span', {}, tr('缓存写 / 1M', 'Cache write / 1M')),
|
|
2363
|
+
React.createElement('span', {}, ''),
|
|
2364
|
+
),
|
|
2365
|
+
pricingDraft.overrides.map((entry, index) => {
|
|
2366
|
+
const overrideModelQuery = String(pricingOverrideSearchText[index] || '').trim().toLowerCase()
|
|
2367
|
+
const overrideModelOptions = pricingUsedModelOptions.filter((option) => overrideModelQuery === '' || option.label.toLowerCase().includes(overrideModelQuery))
|
|
2368
|
+
return React.createElement('div', { key: index, className: 'uh-pricing-edit-row uh-pricing-price-row' },
|
|
2369
|
+
React.createElement('div', { className: 'uh-pricing-used-model-picker' },
|
|
2370
|
+
React.createElement('input', { type: 'text', className: 'uh-pricing-used-model-input', placeholder: tr('选择当前用过的模型', 'Select a used model'), value: pricingOverrideSearchText[index] !== undefined ? pricingOverrideSearchText[index] : (entry.modelId || ''), 'aria-label': tr('覆盖模型 ID', 'Override model ID'), 'aria-haspopup': 'listbox', 'aria-expanded': pricingOverrideOpen === index, onFocus: () => { setPricingOverrideOpen(index); setPricingOverrideSearchText((prev) => Object.assign({}, prev, { [index]: '' })) }, onClick: () => setPricingOverrideOpen(index), onBlur: () => setTimeout(() => { setPricingOverrideOpen((current) => current === index ? null : current); if (entry.modelId) setPricingOverrideSearchText((prev) => Object.assign({}, prev, { [index]: entry.modelId })) }, 120), onKeyDown: (event) => { if (event.key === 'Escape') setPricingOverrideOpen(null) }, onChange: (event) => searchPricingOverrideModels(index, event.target.value) }),
|
|
2371
|
+
pricingOverrideOpen === index && overrideModelOptions.length > 0 ? React.createElement('div', { className: 'uh-language-options uh-pricing-used-model-options', role: 'listbox', 'aria-label': tr('当前用过的模型', 'Used models') },
|
|
2372
|
+
overrideModelOptions.map((option) => React.createElement('button', { key: option.value, type: 'button', role: 'option', className: 'uh-language-option uh-pricing-model-option', onMouseDown: (event) => event.preventDefault(), onClick: () => selectPricingOverrideModel(index, option.value) },
|
|
2373
|
+
React.createElement(LineIcon, { name: 'list', size: 14 }),
|
|
2374
|
+
React.createElement('span', { className: 'uh-pricing-model-option-name' }, option.label),
|
|
2375
|
+
)),
|
|
2376
|
+
) : null,
|
|
2377
|
+
),
|
|
2378
|
+
React.createElement('input', { type: 'number', min: '0', step: 'any', placeholder: tr('输入价 / 1M', 'Input / 1M'), title: tr('输入价格,美元 / 100 万 Token', 'Input price, USD / 1M tokens'), 'aria-label': tr('输入价格 / 1M', 'Input price / 1M'), value: entry.input === undefined ? '' : entry.input, onChange: (event) => updatePricingOverride(index, 'input', event.target.value) }),
|
|
2379
|
+
React.createElement('input', { type: 'number', min: '0', step: 'any', placeholder: tr('输出价 / 1M', 'Output / 1M'), title: tr('输出价格,美元 / 100 万 Token', 'Output price, USD / 1M tokens'), 'aria-label': tr('输出价格 / 1M', 'Output price / 1M'), value: entry.output === undefined ? '' : entry.output, onChange: (event) => updatePricingOverride(index, 'output', event.target.value) }),
|
|
2380
|
+
React.createElement('input', { type: 'number', min: '0', step: 'any', placeholder: tr('缓存读 / 1M', 'Cache read / 1M'), title: tr('缓存读取价格,美元 / 100 万 Token', 'Cache read price, USD / 1M tokens'), 'aria-label': tr('缓存读取价格 / 1M', 'Cache read price / 1M'), value: entry.cacheRead === undefined ? '' : entry.cacheRead, onChange: (event) => updatePricingOverride(index, 'cacheRead', event.target.value) }),
|
|
2381
|
+
React.createElement('input', { type: 'number', min: '0', step: 'any', placeholder: tr('缓存写 / 1M', 'Cache write / 1M'), title: tr('缓存写入价格,美元 / 100 万 Token', 'Cache write price, USD / 1M tokens'), 'aria-label': tr('缓存写入价格 / 1M', 'Cache write price / 1M'), value: entry.cacheWrite === undefined ? '' : entry.cacheWrite, onChange: (event) => updatePricingOverride(index, 'cacheWrite', event.target.value) }),
|
|
2382
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh uh-icon-button', title: tr('删除价格覆盖', 'Remove price override'), 'aria-label': tr('删除价格覆盖', 'Remove price override'), onClick: () => removePricingOverride(index) }, React.createElement(LineIcon, { name: 'close', size: 14 })),
|
|
2383
|
+
)
|
|
2384
|
+
}),
|
|
2385
|
+
),
|
|
2386
|
+
),
|
|
2387
|
+
pricingError !== '' ? React.createElement('div', { className: 'uh-pricing-error', role: 'alert' }, pricingError === 'forbidden' ? tr('没有权限保存成本设置', 'Not allowed to save cost settings') : pricingError === 'token' ? tr('当前进程令牌不可用,请刷新看板', 'The process capability is unavailable; refresh the dashboard') : pricingError === 'sync' ? tr('models.dev 同步失败,已保留上次成功目录', 'models.dev sync failed; the last good catalog was kept') : tr('成本设置保存失败,请检查输入', 'Cost settings could not be saved; check the inputs')) : null,
|
|
2388
|
+
React.createElement('div', { className: 'uh-pricing-foot' },
|
|
2389
|
+
React.createElement('span', { className: 'uh-note' }, tr('保存不会重算已有正成本;回填只处理未计价调用。', 'Saving does not recalculate existing positive costs; backfill only handles unpriced calls.')),
|
|
2390
|
+
React.createElement('div', { className: 'uh-actions' },
|
|
2391
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: () => setPricingOpen(false) }, tr('取消', 'Cancel')),
|
|
2392
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh', disabled: pricingSaving || pricingSyncing, onClick: () => savePricingSettings(false) }, pricingSaving ? tr('保存中…', 'Saving…') : tr('保存', 'Save')),
|
|
2393
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh uh-pricing-backfill', disabled: pricingSaving || pricingSyncing, onClick: () => savePricingSettings(true) }, tr('保存并回填', 'Save and backfill')),
|
|
2394
|
+
),
|
|
2395
|
+
),
|
|
2396
|
+
) : null
|
|
2397
|
+
|
|
1025
2398
|
let tip = null
|
|
1026
2399
|
if (hover !== null && hover !== undefined) {
|
|
1027
2400
|
const day = hover.day
|
|
1028
2401
|
let rowsContent = []
|
|
1029
2402
|
let tokensText = ''
|
|
1030
2403
|
if (day !== undefined) {
|
|
1031
|
-
const sorted = day.perWorkspace.slice().sort((a, b) => b.turns - a.turns)
|
|
2404
|
+
const sorted = (Array.isArray(day.perWorkspace) ? day.perWorkspace : []).slice().sort((a, b) => b.turns - a.turns)
|
|
1032
2405
|
rowsContent = sorted.map((entry) => {
|
|
1033
2406
|
const idx = wsIndex.get(entry.workspaceId)
|
|
1034
2407
|
return React.createElement('div', {
|
|
@@ -1041,8 +2414,9 @@ window.__ModuleLoader__.load({
|
|
|
1041
2414
|
React.createElement('span', { className: 'uh-n' }, language === 'en' ? entry.turns + ' uses' : entry.turns + ' 次'),
|
|
1042
2415
|
)
|
|
1043
2416
|
})
|
|
1044
|
-
|
|
1045
|
-
|
|
2417
|
+
const dayTokenValues = rowTokens(day)
|
|
2418
|
+
if (dayTokenValues.input + dayTokenValues.output + dayTokenValues.cacheRead > 0) {
|
|
2419
|
+
tokensText = language === 'en' ? 'Tokens: Input ' + fmtCompact(dayTokenValues.input) + ' · Cache hits ' + fmtCompact(dayTokenValues.cacheRead) + ' · Output ' + fmtCompact(dayTokenValues.output) : 'Token:输入 ' + fmtCompact(dayTokenValues.input) + ' · 缓存命中 ' + fmtCompact(dayTokenValues.cacheRead) + ' · 输出 ' + fmtCompact(dayTokenValues.output)
|
|
1046
2420
|
}
|
|
1047
2421
|
}
|
|
1048
2422
|
const flip = hover.x > 640
|
|
@@ -1095,9 +2469,120 @@ window.__ModuleLoader__.load({
|
|
|
1095
2469
|
),
|
|
1096
2470
|
customRangeErrorText !== '' ? React.createElement('div', { className: 'uh-custom-range-error', role: 'alert' }, customRangeErrorText) : null,
|
|
1097
2471
|
) : null
|
|
2472
|
+
const trendBounds = queryScope !== null ? { start: queryScope.start, end: queryScope.end } : resolveRangeBounds(stats, range, useUtc, activeCustomRange)
|
|
2473
|
+
const hourlyTrendRows = queryReady && queryScope !== null && queryScope.start === queryScope.end && queryResult && Array.isArray(queryResult.hourly) ? buildTrendHourlyRows(queryResult.hourly, useUtc) : []
|
|
2474
|
+
const trendRows = hourlyTrendRows.length > 0 ? hourlyTrendRows : buildTrendRows(queryReady && queryResult && Array.isArray(queryResult.daily) ? queryResult.daily : activeDayRows, trendBounds, useUtc)
|
|
2475
|
+
const trendAnimationKey = queryReady && queryResult ? queryKey + ':' + queryResult.revision : queryKey
|
|
2476
|
+
const toggleTrendSeries = (key) => {
|
|
2477
|
+
setTrendVisible((prev) => {
|
|
2478
|
+
if (prev.includes(key)) return prev.length <= 1 ? prev : prev.filter((item) => item !== key)
|
|
2479
|
+
return prev.concat(key)
|
|
2480
|
+
})
|
|
2481
|
+
}
|
|
2482
|
+
const trendPanel = React.createElement(UsageTrendChart, {
|
|
2483
|
+
key: trendAnimationKey,
|
|
2484
|
+
rows: trendRows,
|
|
2485
|
+
visible: trendVisible,
|
|
2486
|
+
language,
|
|
2487
|
+
rangeLabel,
|
|
2488
|
+
loading: queryLoading && !queryReady,
|
|
2489
|
+
error: queryError !== '' && queryError !== 'stale' && !queryReady ? tr('趋势数据加载失败', 'Trend data unavailable') : '',
|
|
2490
|
+
onToggle: toggleTrendSeries,
|
|
2491
|
+
onPointClick: openAuditForDate,
|
|
2492
|
+
})
|
|
2493
|
+
const detailScopeLabel = selectedDetailScope === null
|
|
2494
|
+
? rangeLabel
|
|
2495
|
+
: selectedDetailScope.start === selectedDetailScope.end
|
|
2496
|
+
? selectedDetailScope.start
|
|
2497
|
+
: selectedDetailScope.start + ' → ' + selectedDetailScope.end
|
|
2498
|
+
const auditToken = (row, key) => Number(row && row.values && row.values[key]) || 0
|
|
2499
|
+
const auditTotal = (row) => auditToken(row, 'input') + auditToken(row, 'cacheRead') + auditToken(row, 'cacheWrite') + auditToken(row, 'output') + auditToken(row, 'reasoning')
|
|
2500
|
+
const auditSource = (row) => row && row.materialization === 'ledger-recovery' ? tr('账本恢复', 'Ledger recovery') : row && row.materialization === 'ledger-reuse' ? tr('账本复用', 'Ledger reuse') : row && row.materialization === 'scan' ? tr('扫描', 'Scan') : row && row.materialization === 'live' ? tr('实时', 'Live') : tr('未知', 'Unknown')
|
|
2501
|
+
const auditTime = (row, detailed) => row && Number.isFinite(row.time) ? new Date(row.time).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN', language === 'en' ? (detailed ? { timeZone: 'UTC' } : { timeZone: 'UTC', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : (detailed ? undefined : { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })) : '—'
|
|
2502
|
+
const selectedAudit = auditRows.find((row) => row.id === auditSelectedId) || auditRows[0] || null
|
|
2503
|
+
const exportAuditCsv = async () => {
|
|
2504
|
+
if (selectedDetailScope === null || auditExporting) return
|
|
2505
|
+
setAuditExporting(true)
|
|
2506
|
+
setAuditError('')
|
|
2507
|
+
try {
|
|
2508
|
+
let cursor = null
|
|
2509
|
+
const all = []
|
|
2510
|
+
for (let page = 0; page < 50; page += 1) {
|
|
2511
|
+
const data = await getUsageRecords(selectedDetailScope, cursor, 200)
|
|
2512
|
+
if (data === null || typeof data !== 'object' || !Array.isArray(data.items)) throw new Error('audit export failed')
|
|
2513
|
+
all.push(...data.items)
|
|
2514
|
+
if (!data.hasMore || !data.nextCursor) break
|
|
2515
|
+
cursor = data.nextCursor
|
|
2516
|
+
}
|
|
2517
|
+
const quote = (value) => '"' + String(value === undefined || value === null ? '' : value).replace(/"/g, '""') + '"'
|
|
2518
|
+
const line = (values) => values.map(quote).join(',')
|
|
2519
|
+
const headers = [tr('时间', 'Time'), tr('日期', 'Date'), tr('Provider', 'Provider'), tr('请求模型', 'Requested model'), tr('实际模型', 'Actual model'), tr('显示模型', 'Display model'), 'turn', 'step', 'seq', tr('输入', 'Input'), tr('缓存命中', 'Cache read'), tr('缓存写入', 'Cache write'), tr('输出', 'Output'), tr('推理', 'Reasoning'), tr('成本', 'Cost'), tr('计价状态', 'Cost status'), tr('计价模型', 'Pricing model'), tr('来源', 'Source')]
|
|
2520
|
+
const lines = [line([tr('DSH 用量明细导出', 'DSH Usage Audit Export')]), line([tr('范围', 'Scope'), selectedDetailScope.start + ' → ' + selectedDetailScope.end]), line([tr('时区', 'Timezone'), selectedDetailScope.utc ? 'UTC' : tr('本地', 'Local')]), line(headers)]
|
|
2521
|
+
for (const row of all) lines.push(line([row.time, row.date, row.provider, row.requestedModel, row.actualModel, row.model, row.turn, row.step, row.seq, auditToken(row, 'input'), auditToken(row, 'cacheRead'), auditToken(row, 'cacheWrite'), auditToken(row, 'output'), auditToken(row, 'reasoning'), row.cost && row.cost.status === 'priced' ? row.cost.total : '', row.cost && row.cost.status ? row.cost.status : 'unpriced', row.cost && row.cost.pricingModel ? row.cost.pricingModel : '', row.materialization || 'unknown']))
|
|
2522
|
+
const blob = new Blob(['\uFEFF' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' })
|
|
2523
|
+
const url = URL.createObjectURL(blob)
|
|
2524
|
+
const anchor = document.createElement('a')
|
|
2525
|
+
anchor.href = url
|
|
2526
|
+
anchor.download = 'dsh-all-usage-audit-' + selectedDetailScope.start + '-to-' + selectedDetailScope.end + '.csv'
|
|
2527
|
+
document.body.appendChild(anchor); anchor.click(); anchor.remove()
|
|
2528
|
+
URL.revokeObjectURL(url)
|
|
2529
|
+
} catch (err) {
|
|
2530
|
+
setAuditError('audit-export')
|
|
2531
|
+
} finally {
|
|
2532
|
+
setAuditExporting(false)
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const recordsPanel = React.createElement('div', { className: 'uh-panel uh-records-panel', ref: recordsPanelRef, style: { display: detailView === 'logs' ? 'block' : 'none' } },
|
|
2536
|
+
React.createElement('div', { className: 'uh-records-head' },
|
|
2537
|
+
React.createElement('div', {},
|
|
2538
|
+
React.createElement('h3', { className: 'uh-tbl-title uh-title-with-icon' }, React.createElement(LineIcon, { name: 'list', size: 16 }), tr('请求日志', 'Request Logs')),
|
|
2539
|
+
React.createElement('div', { className: 'uh-note' }, detailScopeLabel + (selectedDetailScope && selectedDetailScope.utc ? ' · UTC' : '')),
|
|
2540
|
+
),
|
|
2541
|
+
React.createElement('div', { className: 'uh-actions' },
|
|
2542
|
+
auditLoading ? React.createElement('span', { className: 'uh-query-note' }, tr('同步中…', 'Refreshing…')) : null,
|
|
2543
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh', title: tr('导出当前日志', 'Export current logs'), onClick: exportAuditCsv, disabled: auditExporting || selectedDetailScope === null }, React.createElement(LineIcon, { name: 'export', size: 13 }), auditExporting ? tr('导出中…', 'Exporting…') : tr('导出日志', 'Export logs')),
|
|
2544
|
+
),
|
|
2545
|
+
),
|
|
2546
|
+
auditError !== '' ? React.createElement('div', { className: 'uh-records-error', role: 'alert' }, auditError === 'stale' ? tr('数据已更新,正在重新加载日志…', 'Data changed; reloading logs…') : auditError === 'audit-export' ? tr('日志导出失败', 'Unable to export logs') : tr('日志加载失败,请重试', 'Unable to load logs')) : null,
|
|
2547
|
+
React.createElement('div', { className: 'uh-records-note' }, tr('按时间倒序显示可审计的 Token 调用;选择一行查看 turn / step 和完整 Token 分桶。', 'Token calls are newest first; select a row to inspect its turn / step and token buckets.')),
|
|
2548
|
+
auditRows.length === 0 && !auditLoading ? React.createElement('div', { className: 'uh-empty' }, tr('当前范围没有可审计的 Token 调用', 'No auditable Token calls in this scope')) : React.createElement('div', { className: 'uh-records-scroll' },
|
|
2549
|
+
React.createElement('div', { className: 'uh-record-grid uh-record-header' },
|
|
2550
|
+
React.createElement('div', {}, tr('时间', 'Time')), React.createElement('div', {}, tr('Provider / 模型', 'Provider / Model')), React.createElement('div', { className: 'uh-record-num' }, 'turn / step'), React.createElement('div', { className: 'uh-record-num' }, tr('输入', 'Input')), React.createElement('div', { className: 'uh-record-num' }, tr('缓存命中', 'Cache read')), React.createElement('div', { className: 'uh-record-num' }, tr('缓存写入', 'Cache write')), React.createElement('div', { className: 'uh-record-num' }, tr('输出', 'Output')), React.createElement('div', { className: 'uh-record-num' }, tr('成本', 'Cost')), React.createElement('div', {}, tr('来源', 'Source')),
|
|
2551
|
+
),
|
|
2552
|
+
auditRows.map((row) => React.createElement('div', { key: row.id, className: 'uh-record-grid uh-record-row' + (selectedAudit && selectedAudit.id === row.id ? ' uh-on' : ''), role: 'button', tabIndex: 0, 'aria-pressed': selectedAudit && selectedAudit.id === row.id, onClick: () => setAuditSelectedId(row.id), onKeyDown: (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setAuditSelectedId(row.id) } } },
|
|
2553
|
+
React.createElement('div', { className: 'uh-record-time' }, auditTime(row, false)),
|
|
2554
|
+
React.createElement('div', { className: 'uh-record-model', title: row.model || '' }, row.model || tr('未知模型', 'Unknown model'), row.requestedModel && row.actualModel && row.requestedModel !== row.actualModel ? React.createElement('small', {}, row.requestedModel + ' → ' + row.actualModel) : null),
|
|
2555
|
+
React.createElement('div', { className: 'uh-record-num' }, (row.turn === null || row.turn === undefined ? '—' : row.turn) + ' / ' + (row.step === null || row.step === undefined ? '—' : row.step)),
|
|
2556
|
+
React.createElement('div', { className: 'uh-record-num' }, fmtCompact(auditToken(row, 'input'))),
|
|
2557
|
+
React.createElement('div', { className: 'uh-record-num' }, fmtCompact(auditToken(row, 'cacheRead'))),
|
|
2558
|
+
React.createElement('div', { className: 'uh-record-num' }, fmtCompact(auditToken(row, 'cacheWrite'))),
|
|
2559
|
+
React.createElement('div', { className: 'uh-record-num' }, fmtCompact(auditToken(row, 'output'))),
|
|
2560
|
+
React.createElement('div', { className: 'uh-record-num uh-cost-num' }, costDisplay(row, language)),
|
|
2561
|
+
React.createElement('div', { className: 'uh-record-source' }, auditSource(row)),
|
|
2562
|
+
)),
|
|
2563
|
+
),
|
|
2564
|
+
React.createElement('div', { className: 'uh-records-footer' },
|
|
2565
|
+
React.createElement('span', { className: 'uh-note' }, auditRows.length > 0 ? (auditHasMore ? tr('已显示 ' + auditRows.length + ' 条,继续加载可查看更多', auditRows.length + ' shown; load more for additional records') : tr('共显示 ' + auditRows.length + ' 条', auditRows.length + ' records shown')) : ''),
|
|
2566
|
+
auditHasMore ? React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: loadMoreAudit, disabled: auditLoading }, auditLoading ? tr('加载中…', 'Loading…') : tr('加载更多', 'Load more')) : null,
|
|
2567
|
+
),
|
|
2568
|
+
selectedAudit ? React.createElement('div', { className: 'uh-record-detail' },
|
|
2569
|
+
React.createElement('div', { className: 'uh-record-detail-head' }, React.createElement('strong', {}, tr('选中调用', 'Selected call')), React.createElement('span', { className: 'uh-note' }, auditTime(selectedAudit, true))),
|
|
2570
|
+
React.createElement('div', { className: 'uh-record-detail-meta' },
|
|
2571
|
+
React.createElement('span', {}, (selectedAudit.provider || tr('未知供应商', 'Unknown provider')) + ' / ' + (selectedAudit.actualModel || selectedAudit.requestedModel || selectedAudit.model || tr('未知模型', 'Unknown model'))),
|
|
2572
|
+
React.createElement('span', {}, 'turn ' + (selectedAudit.turn === null || selectedAudit.turn === undefined ? '—' : selectedAudit.turn) + ' · step ' + (selectedAudit.step === null || selectedAudit.step === undefined ? '—' : selectedAudit.step)),
|
|
2573
|
+
React.createElement('span', {}, tr('来源:', 'Source: ') + auditSource(selectedAudit)),
|
|
2574
|
+
React.createElement('span', {}, tr('计价模型:', 'Pricing model: ') + (selectedAudit.cost && selectedAudit.cost.pricingModel ? selectedAudit.cost.pricingModel : tr('未计价', 'unpriced'))),
|
|
2575
|
+
),
|
|
2576
|
+
React.createElement('div', { className: 'uh-record-token-strip' },
|
|
2577
|
+
['input', 'cacheRead', 'cacheWrite', 'output', 'reasoning'].map((key) => React.createElement('div', { key }, React.createElement('span', {}, key === 'cacheRead' ? tr('缓存命中', 'Cache read') : key === 'cacheWrite' ? tr('缓存写入', 'Cache write') : key === 'reasoning' ? tr('推理', 'Reasoning') : key === 'input' ? tr('输入', 'Input') : tr('输出', 'Output')), React.createElement('strong', {}, fmtCompact(auditToken(selectedAudit, key))))),
|
|
2578
|
+
React.createElement('div', { className: 'uh-record-token-total' }, React.createElement('span', {}, tr('总处理', 'Total')), React.createElement('strong', {}, fmtCompact(auditTotal(selectedAudit)))),
|
|
2579
|
+
React.createElement('div', { className: 'uh-record-token-total' }, React.createElement('span', {}, tr('成本', 'Cost')), React.createElement('strong', {}, costDisplay(selectedAudit, language))),
|
|
2580
|
+
),
|
|
2581
|
+
) : null,
|
|
2582
|
+
)
|
|
1098
2583
|
const scanning = !scan.done
|
|
1099
2584
|
const pct = scan.total > 0 ? Math.min(100, Math.round((scan.scanned / scan.total) * 100)) : 40
|
|
1100
|
-
const isEmpty = scan.done && dayRows.length === 0 &&
|
|
2585
|
+
const isEmpty = scan.done && dayRows.length === 0 && agg.totals.turns === 0 && agg.totals.calls === 0
|
|
1101
2586
|
const syncCompletedAt = typeof sync.lastCompletedAt === 'number' && sync.lastCompletedAt > 0 ? new Date(sync.lastCompletedAt).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN') : ''
|
|
1102
2587
|
const lastStatsText = lastStatsAt > 0 ? new Date(lastStatsAt).toLocaleString(language === 'en' ? 'en-US' : 'zh-CN') : ''
|
|
1103
2588
|
const healthTitle = syncCompletedAt === '' ? undefined : (language === 'en' ? 'Historical scan completed ' + syncCompletedAt : '历史扫描完成于 ' + syncCompletedAt)
|
|
@@ -1129,8 +2614,13 @@ window.__ModuleLoader__.load({
|
|
|
1129
2614
|
title: tr('管理工作区别名', 'Manage workspace aliases'),
|
|
1130
2615
|
onClick: () => { if (aliasOpen) setAliasOpen(false); else openAliasPanel() },
|
|
1131
2616
|
}, React.createElement(LineIcon, { name: 'edit', size: 14 }), tr('工作区别名', 'Workspace Aliases')),
|
|
2617
|
+
React.createElement('button', {
|
|
2618
|
+
className: 'uh-refresh',
|
|
2619
|
+
title: tr('配置模型价格与同步', 'Configure model prices and sync'),
|
|
2620
|
+
onClick: () => { if (pricingOpen) setPricingOpen(false); else openPricingPanel() },
|
|
2621
|
+
}, React.createElement(LineIcon, { name: 'wallet', size: 14 }), tr('成本设置', 'Cost Settings')),
|
|
1132
2622
|
React.createElement('div', {
|
|
1133
|
-
className: 'uh-language-menu',
|
|
2623
|
+
className: 'uh-language-menu' + (languageMenuOpen ? ' uh-open' : ''),
|
|
1134
2624
|
ref: languageMenuRef,
|
|
1135
2625
|
onKeyDown: (event) => { if (event.key === 'Escape') { event.preventDefault(); setLanguageMenuOpen(false) } },
|
|
1136
2626
|
},
|
|
@@ -1175,7 +2665,40 @@ window.__ModuleLoader__.load({
|
|
|
1175
2665
|
React.createElement('button', { className: 'uh-refresh uh-icon-button', title: tr('刷新统计数据', 'Refresh usage statistics'), 'aria-label': tr('刷新统计数据', 'Refresh usage statistics'), onClick: onRefresh }, React.createElement(LineIcon, { name: 'refresh', size: 16 })),
|
|
1176
2666
|
),
|
|
1177
2667
|
),
|
|
2668
|
+
React.createElement('div', { className: 'uh-filter-bar', role: 'group', 'aria-label': tr('统一筛选', 'Unified filters') },
|
|
2669
|
+
React.createElement(UsageFilterMenu, {
|
|
2670
|
+
label: tr('全部工作区', 'All workspaces'),
|
|
2671
|
+
ariaLabel: tr('工作区筛选', 'Workspace filter'),
|
|
2672
|
+
className: 'uh-filter-workspace',
|
|
2673
|
+
icon: 'folder',
|
|
2674
|
+
value: wsFilter || '',
|
|
2675
|
+
options: [{ value: '', label: tr('全部工作区', 'All workspaces') }].concat(rangeWorkspaceOptions.map((w) => ({ value: w.id, label: wsTitle(w.id) }))),
|
|
2676
|
+
onChange: (value) => setWsFilter(value || null),
|
|
2677
|
+
}),
|
|
2678
|
+
React.createElement(UsageFilterMenu, {
|
|
2679
|
+
label: tr('全部供应商', 'All providers'),
|
|
2680
|
+
ariaLabel: tr('供应商筛选', 'Provider filter'),
|
|
2681
|
+
className: 'uh-filter-provider',
|
|
2682
|
+
icon: 'chart',
|
|
2683
|
+
value: providerFilter || '',
|
|
2684
|
+
options: [{ value: '', label: tr('全部供应商', 'All providers') }].concat(providerOptions.map((value) => ({ value, label: value }))),
|
|
2685
|
+
onChange: (value) => chooseProvider(value),
|
|
2686
|
+
}),
|
|
2687
|
+
React.createElement(UsageFilterMenu, {
|
|
2688
|
+
label: tr('全部模型', 'All models'),
|
|
2689
|
+
ariaLabel: tr('模型筛选', 'Model filter'),
|
|
2690
|
+
className: 'uh-filter-model',
|
|
2691
|
+
icon: 'cache',
|
|
2692
|
+
value: modelFilter || '',
|
|
2693
|
+
options: [{ value: '', label: tr('全部模型', 'All models') }].concat(modelOptions.map((value) => ({ value, label: value }))),
|
|
2694
|
+
onChange: (value) => chooseModel(value),
|
|
2695
|
+
}),
|
|
2696
|
+
(wsFilter !== null || providerFilter !== null || modelFilter !== null) ? React.createElement('button', { type: 'button', className: 'uh-filter-clear', onClick: clearFilters }, tr('清除筛选', 'Clear filters')) : null,
|
|
2697
|
+
queryLoading ? React.createElement('span', { className: 'uh-query-note' }, tr('正在更新筛选结果…', 'Updating filtered data…')) : null,
|
|
2698
|
+
queryError !== '' && queryError !== 'stale' ? React.createElement('span', { className: 'uh-query-note', role: 'alert' }, tr('筛选结果加载失败', 'Filtered data unavailable')) : null,
|
|
2699
|
+
),
|
|
1178
2700
|
aliasOpen ? aliasPanel : null,
|
|
2701
|
+
pricingPanel,
|
|
1179
2702
|
customRangePanel,
|
|
1180
2703
|
scanning ? React.createElement('div', { className: 'uh-progress' },
|
|
1181
2704
|
React.createElement('span', {}, language === 'en' ? 'Scanning historical sessions: ' + scan.scanned + ' / ' + scan.total + (scan.failed > 0 ? ' (' + scan.failed + ' failed to read)' : '') : '正在统计历史会话 ' + scan.scanned + ' / ' + scan.total + (scan.failed > 0 ? '(' + scan.failed + ' 个读取失败)' : '')),
|
|
@@ -1191,27 +2714,40 @@ window.__ModuleLoader__.load({
|
|
|
1191
2714
|
) : React.createElement(React.Fragment, null,
|
|
1192
2715
|
React.createElement(React.Fragment, null,
|
|
1193
2716
|
React.createElement('div', { className: 'uh-ios-summary' },
|
|
1194
|
-
React.createElement('div', { className: 'uh-ios-summary-
|
|
1195
|
-
React.createElement('div', { className: 'uh-ios-summary-
|
|
1196
|
-
|
|
1197
|
-
|
|
2717
|
+
React.createElement('div', { className: 'uh-ios-summary-hero' },
|
|
2718
|
+
React.createElement('div', { className: 'uh-ios-summary-total' },
|
|
2719
|
+
React.createElement('div', { className: 'uh-ios-summary-total-icon' }, React.createElement(LineIcon, { name: 'chart', size: 24 })),
|
|
2720
|
+
React.createElement('div', { className: 'uh-ios-summary-total-copy' },
|
|
2721
|
+
React.createElement('div', { className: 'uh-ios-summary-label' }, tr('总处理 Token', 'Total Tokens Processed')),
|
|
2722
|
+
React.createElement('div', { className: 'uh-ios-summary-value' }, valueWithMagnitude(fmtCompact(animatedTotal), totalTokens, language)),
|
|
2723
|
+
React.createElement('div', { className: 'uh-ios-summary-caption' }, language === 'en' ? rangeLabel + ' · ' + fmtCompact(animatedTurns) + (scopedCountIsCalls ? ' calls' : ' uses') + ' · includes cache reads/writes and reasoning' : rangeLabel + ' · ' + fmtCompact(animatedTurns) + (scopedCountIsCalls ? ' 次调用' : ' 次使用') + ' · 含缓存读写与推理'),
|
|
2724
|
+
),
|
|
2725
|
+
),
|
|
2726
|
+
React.createElement('div', { className: 'uh-ios-summary-meta' },
|
|
2727
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-stat' },
|
|
2728
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-label' }, React.createElement(LineIcon, { name: 'chart', size: 16 }), tr('总请求数', 'Total Requests')),
|
|
2729
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-value' }, fmtCount(animatedRequests, language)),
|
|
2730
|
+
),
|
|
2731
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-stat uh-ios-summary-meta-cost' },
|
|
2732
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-label' }, React.createElement(LineIcon, { name: 'wallet', size: 16 }), tr('估算成本', 'Estimated Cost')),
|
|
2733
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-value' }, costValue),
|
|
2734
|
+
React.createElement('div', { className: 'uh-ios-summary-meta-caption' }, costCoverage),
|
|
2735
|
+
),
|
|
2736
|
+
),
|
|
1198
2737
|
),
|
|
1199
|
-
React.createElement('div', { className: 'uh-ios-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
2738
|
+
React.createElement('div', { className: 'uh-ios-metrics' },
|
|
2739
|
+
card(tr('DeepSeek 账户余额', 'DeepSeek Account Balance'), balanceValue, balanceSub, 0, 'wallet'),
|
|
2740
|
+
card(scopedCountIsCalls ? tr('匹配调用次数', 'Matching Calls') : tr('总使用次数', 'Total Uses'), fmtCompact(animatedTurns), range === 'all' && !scopedCountIsCalls ? (language === 'en' ? agg.totals.sessions + ' sessions' : agg.totals.sessions + ' 个会话') : (language === 'en' ? (scopedCountIsCalls ? 'Calls in ' : 'Turns in ') + rangeLabel : rangeLabel + (scopedCountIsCalls ? '内的调用数' : '内的回合数')), 1, 'chart'),
|
|
2741
|
+
card(tr('连续使用', 'Current Streak'), language === 'en' ? st.streak + ' days' : st.streak + ' 天', language === 'en' ? 'Longest streak: ' + st.best + ' days' : '最长连续 ' + st.best + ' 天', 2, 'clock'),
|
|
2742
|
+
tokenCard,
|
|
2743
|
+
summaryRateMetric,
|
|
1203
2744
|
),
|
|
1204
2745
|
),
|
|
1205
2746
|
React.createElement('div', { className: 'uh-token-semantics' },
|
|
1206
2747
|
React.createElement(LineIcon, { name: 'cache', size: 16 }),
|
|
1207
2748
|
tr('总处理 Token = 输入 + 输出 + 缓存读写 + 推理。缓存命中代表复用上下文,不等于新生成 Token 或实际费用。', 'Total tokens processed = input + output + cache reads/writes + reasoning. Cache hits represent reused context; they are not newly generated tokens or actual cost.'),
|
|
1208
2749
|
),
|
|
1209
|
-
|
|
1210
|
-
card(tr('DeepSeek 账户余额', 'DeepSeek Account Balance'), balanceValue, balanceSub, 0, 'wallet'),
|
|
1211
|
-
card(tr('总使用次数', 'Total Uses'), fmtCompact(animatedTurns), range === 'all' ? (language === 'en' ? agg.totals.sessions + ' sessions' : agg.totals.sessions + ' 个会话') : (language === 'en' ? 'Turns in ' + rangeLabel : rangeLabel + '内的回合数'), 4, 'chart'),
|
|
1212
|
-
card(tr('连续使用', 'Current Streak'), language === 'en' ? st.streak + ' days' : st.streak + ' 天', language === 'en' ? 'Longest streak: ' + st.best + ' days' : '最长连续 ' + st.best + ' 天', 5, 'clock'),
|
|
1213
|
-
tokenCard,
|
|
1214
|
-
),
|
|
2750
|
+
trendPanel,
|
|
1215
2751
|
React.createElement('div', { className: 'uh-panel' },
|
|
1216
2752
|
React.createElement('div', { className: 'uh-section-title' }, React.createElement(LineIcon, { name: 'calendar', size: 16 }), tr('使用热力图', 'Usage Heatmap')),
|
|
1217
2753
|
React.createElement('div', { className: 'uh-hm-head' },
|
|
@@ -1242,8 +2778,12 @@ window.__ModuleLoader__.load({
|
|
|
1242
2778
|
),
|
|
1243
2779
|
React.createElement('div', { className: 'uh-note', style: { marginTop: 10 } }, tr('口径:每完成一个回合点亮一次(含子代理会话);悬停查看按工作区明细,点击工作区可筛选热力图与明细表。日期按本地时区。', 'Methodology: one cell lights up for each completed turn, including subagent sessions. Hover to view workspace details; click a workspace to filter the heatmap and detail tables. English dates and day boundaries use UTC.')),
|
|
1244
2780
|
),
|
|
2781
|
+
React.createElement('div', { className: 'uh-detail-tabs', role: 'tablist', 'aria-label': tr('用量明细视图', 'Usage detail views') },
|
|
2782
|
+
[['logs', tr('请求日志', 'Request Logs'), 'list'], ['model', tr('模型统计', 'Model Stats'), 'chart'], ['workspace', tr('工作区统计', 'Workspace Stats'), 'folder']].map((entry) => React.createElement('button', { key: entry[0], type: 'button', role: 'tab', 'aria-selected': detailView === entry[0], className: 'uh-detail-tab' + (detailView === entry[0] ? ' uh-on' : ''), onClick: () => setDetailView(entry[0]) }, React.createElement(LineIcon, { name: entry[2], size: 14 }), entry[1])),
|
|
2783
|
+
),
|
|
2784
|
+
recordsPanel,
|
|
1245
2785
|
),
|
|
1246
|
-
React.createElement('div', { className: 'uh-panel uh-ios-list-panel' },
|
|
2786
|
+
detailView === 'model' ? React.createElement('div', { className: 'uh-panel uh-ios-list-panel' },
|
|
1247
2787
|
React.createElement('div', { className: 'uh-hm-head' },
|
|
1248
2788
|
React.createElement('h3', { className: 'uh-tbl-title uh-title-with-icon', style: { margin: 0 } }, React.createElement(LineIcon, { name: 'chart', size: 16 }), language === 'en' ? 'Model Usage Details (' + rangeLabel + ')' : '模型用量明细(' + rangeLabel + ')'),
|
|
1249
2789
|
React.createElement('div', { className: 'uh-range' },
|
|
@@ -1254,49 +2794,77 @@ window.__ModuleLoader__.load({
|
|
|
1254
2794
|
),
|
|
1255
2795
|
modelRows.length === 0
|
|
1256
2796
|
? React.createElement('div', { className: 'uh-empty' }, tr('尚无带模型路由信息的用量记录', 'No usage records with model-routing information yet'))
|
|
1257
|
-
: React.createElement(
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
React.createElement('div', { className: 'uh-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
2797
|
+
: React.createElement(React.Fragment, null,
|
|
2798
|
+
modelDonutChart,
|
|
2799
|
+
React.createElement('div', { className: 'uh-tbl-scroll' },
|
|
2800
|
+
React.createElement('div', { className: 'uh-model-hrow uh-hrow' },
|
|
2801
|
+
React.createElement('div', {}, modelColumnLabel),
|
|
2802
|
+
React.createElement('div', { className: 'uh-num' }, tr('调用', 'Calls')),
|
|
2803
|
+
React.createElement('div', { className: 'uh-num' }, tr('输入', 'Input')),
|
|
2804
|
+
React.createElement('div', { className: 'uh-num' }, tr('缓存命中', 'Cache Hits')),
|
|
2805
|
+
React.createElement('div', { className: 'uh-num' }, tr('输出', 'Output')),
|
|
2806
|
+
React.createElement('div', { className: 'uh-num' }, tr('推理', 'Reasoning')),
|
|
2807
|
+
React.createElement('div', { className: 'uh-num' }, tr('总处理', 'Total Processed')),
|
|
2808
|
+
React.createElement('div', { className: 'uh-num' }, tr('成本', 'Cost')),
|
|
2809
|
+
React.createElement('div', { className: 'uh-num' }, tr('命中率', 'Hit Rate')),
|
|
2810
|
+
),
|
|
2811
|
+
modelElements,
|
|
1267
2812
|
),
|
|
1268
|
-
modelElements,
|
|
1269
2813
|
),
|
|
1270
2814
|
React.createElement('div', { className: 'uh-note', style: { marginTop: 10 } }, language === 'en' ? modelViewLabel + ': Combined View distinguishes “Provider / Model”; By Model merges identically named models across providers; By Provider aggregates all of a provider’s models. Historical records without routing information are grouped as “Unknown.”' : modelViewLabel + ':混合查看按“供应商 / 模型”区分;按模型会跨供应商合并同名模型;按供应商则汇总其全部模型。缺少路由信息的历史记录会归为“未知”。'),
|
|
1271
|
-
),
|
|
1272
|
-
React.createElement('div', { className: 'uh-panel uh-ios-list-panel' },
|
|
2815
|
+
) : null,
|
|
2816
|
+
detailView === 'workspace' ? React.createElement('div', { className: 'uh-panel uh-ios-list-panel' },
|
|
1273
2817
|
React.createElement('h3', { className: 'uh-tbl-title uh-title-with-icon' }, React.createElement(LineIcon, { name: 'folder', size: 16 }), language === 'en' ? 'Workspace Details (' + rangeLabel + ')' : '工作区明细(' + rangeLabel + ')'),
|
|
1274
2818
|
rows.length === 0
|
|
1275
2819
|
? React.createElement('div', { className: 'uh-empty' }, tr('该时间范围内没有使用记录', 'No usage records in this time range'))
|
|
1276
|
-
: React.createElement(
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
React.createElement('div', { className: 'uh-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
2820
|
+
: React.createElement(React.Fragment, null,
|
|
2821
|
+
workspaceDonutChart,
|
|
2822
|
+
React.createElement('div', { className: 'uh-tbl-scroll' },
|
|
2823
|
+
React.createElement('div', { className: 'uh-hrow' },
|
|
2824
|
+
React.createElement('div', {}, tr('工作区', 'Workspace')),
|
|
2825
|
+
React.createElement('div', { className: 'uh-num' }, tr('回合', 'Turns')),
|
|
2826
|
+
React.createElement('div', { className: 'uh-num' }, tr('输入', 'Input')),
|
|
2827
|
+
React.createElement('div', { className: 'uh-num' }, tr('缓存命中', 'Cache Hits')),
|
|
2828
|
+
React.createElement('div', { className: 'uh-num' }, tr('输出', 'Output')),
|
|
2829
|
+
React.createElement('div', { className: 'uh-num' }, tr('推理', 'Reasoning')),
|
|
2830
|
+
React.createElement('div', { className: 'uh-num' }, tr('总处理', 'Total Processed')),
|
|
2831
|
+
React.createElement('div', { className: 'uh-num' }, tr('成本', 'Cost')),
|
|
2832
|
+
React.createElement('div', { className: 'uh-num' }, tr('命中率', 'Hit Rate')),
|
|
2833
|
+
React.createElement('div', { className: 'uh-num' }, tr('占比', 'Share')),
|
|
2834
|
+
),
|
|
2835
|
+
rowElements,
|
|
1287
2836
|
),
|
|
1288
|
-
rowElements,
|
|
1289
2837
|
),
|
|
1290
|
-
),
|
|
2838
|
+
) : null,
|
|
1291
2839
|
),
|
|
1292
2840
|
tip,
|
|
1293
2841
|
)
|
|
1294
2842
|
}
|
|
1295
2843
|
|
|
2844
|
+
class UsageDashboardBoundary extends React.Component {
|
|
2845
|
+
constructor(props) { super(props); this.state = { error: null, resetKey: props.resetKey } }
|
|
2846
|
+
static getDerivedStateFromError(error) { return { error } }
|
|
2847
|
+
componentDidUpdate(prevProps) {
|
|
2848
|
+
if (prevProps.resetKey !== this.props.resetKey && this.state.error !== null) this.setState({ error: null, resetKey: this.props.resetKey })
|
|
2849
|
+
}
|
|
2850
|
+
render() {
|
|
2851
|
+
if (this.state.error !== null) return this.props.fallback(this.state.error)
|
|
2852
|
+
return this.props.children
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
1296
2855
|
function UsageSidebarEntry(props) {
|
|
1297
2856
|
const [open, setOpen] = React.useState(false)
|
|
2857
|
+
const [dashboardResetKey, setDashboardResetKey] = React.useState(0)
|
|
1298
2858
|
const [language, setLanguage] = React.useState(storedLanguage)
|
|
1299
2859
|
const tr = (zh, en) => language === 'en' ? en : zh
|
|
2860
|
+
const dashboardFallback = () => React.createElement('div', { className: 'uh-boundary-fallback', role: 'alert' },
|
|
2861
|
+
React.createElement('div', { className: 'uh-boundary-title' }, tr('用量统计暂时无法显示', 'Usage statistics is temporarily unavailable')),
|
|
2862
|
+
React.createElement('div', { className: 'uh-boundary-note' }, tr('当前范围加载失败,入口仍然可用。', 'The selected range failed to render; the sidebar entry is still available.')),
|
|
2863
|
+
React.createElement('div', { className: 'uh-actions' },
|
|
2864
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: () => setDashboardResetKey((value) => value + 1) }, React.createElement(LineIcon, { name: 'refresh', size: 14 }), tr('重试', 'Retry')),
|
|
2865
|
+
React.createElement('button', { type: 'button', className: 'uh-refresh', onClick: () => setOpen(false) }, React.createElement(LineIcon, { name: 'close', size: 14 }), tr('关闭', 'Close')),
|
|
2866
|
+
),
|
|
2867
|
+
)
|
|
1300
2868
|
const changeLanguage = (next) => {
|
|
1301
2869
|
const value = next === 'en' ? 'en' : 'zh'
|
|
1302
2870
|
setLanguage(value)
|
|
@@ -1317,7 +2885,7 @@ window.__ModuleLoader__.load({
|
|
|
1317
2885
|
React.createElement('div', { className: 'uh-side-dialog-head' },
|
|
1318
2886
|
React.createElement('button', { className: 'uh-refresh uh-close-button', type: 'button', title: tr('关闭用量统计', 'Close Usage Statistics'), 'aria-label': tr('关闭用量统计', 'Close Usage Statistics'), onClick: () => setOpen(false) }, React.createElement(LineIcon, { name: 'close', size: 18 })),
|
|
1319
2887
|
),
|
|
1320
|
-
React.createElement(UsagePage, { timerCtx: props.timerCtx, language, onLanguageChange: changeLanguage }),
|
|
2888
|
+
React.createElement(UsageDashboardBoundary, { resetKey: dashboardResetKey, fallback: dashboardFallback }, React.createElement(UsagePage, { timerCtx: props.timerCtx, language, onLanguageChange: changeLanguage })),
|
|
1321
2889
|
),
|
|
1322
2890
|
) : null,
|
|
1323
2891
|
)
|