@tzyc/dsh-model-visibility 1.10.3 → 1.12.1

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/client.js CHANGED
@@ -312,6 +312,7 @@ window.__ModuleLoader__.load({
312
312
  let state = { status: 'idle', snapshot: null, error: null }
313
313
  let inflight = null
314
314
  let timer = null
315
+ let timerMs = null
315
316
 
316
317
  const publish = (next) => {
317
318
  state = next
@@ -343,16 +344,33 @@ window.__ModuleLoader__.load({
343
344
  return run
344
345
  }
345
346
 
346
- /** One poll now and another every POLL_MS while someone subscribes. */
347
+ /**
348
+ * Poll cadence follows the ledger's own progress: a cold scan of the session
349
+ * tree takes seconds, so the strip asks again quickly until it settles
350
+ * instead of showing em dashes for half a minute.
351
+ */
352
+ const LIVE_POLL_MS = 3_000
347
353
  const POLL_MS = 30_000
348
- const startTimer = () => {
349
- if (timer !== null) return
350
- timer = setInterval(() => read(), POLL_MS)
354
+ const rearm = () => {
355
+ // Same rule as the verify store: no observer, no armed timer.
356
+ if (listeners.size === 0) {
357
+ stopTimer()
358
+ return
359
+ }
360
+ const wantMs = state.snapshot?.computing === true ? LIVE_POLL_MS : POLL_MS
361
+ if (timer !== null && timerMs === wantMs) return
362
+ if (timer !== null) clearInterval(timer)
363
+ timerMs = wantMs
364
+ timer = setInterval(() => {
365
+ void read().then(rearm, rearm)
366
+ }, wantMs)
367
+ if (typeof timer === 'object' && timer !== null && typeof timer.unref === 'function') timer.unref()
351
368
  }
352
369
  const stopTimer = () => {
353
370
  if (timer !== null) {
354
371
  clearInterval(timer)
355
372
  timer = null
373
+ timerMs = null
356
374
  }
357
375
  }
358
376
 
@@ -360,7 +378,11 @@ window.__ModuleLoader__.load({
360
378
  subscribe(fn) {
361
379
  const first = listeners.size === 0
362
380
  listeners.add(fn)
363
- if (first) startTimer()
381
+ // Read once immediately: waiting a full poll interval is why the strip
382
+ // looked empty on every first visit.
383
+ if (first) {
384
+ void read().then(rearm, rearm)
385
+ }
364
386
  return () => {
365
387
  listeners.delete(fn)
366
388
  if (listeners.size === 0) stopTimer()
@@ -370,12 +392,21 @@ window.__ModuleLoader__.load({
370
392
  return state
371
393
  },
372
394
  load() {
373
- return read()
395
+ return read().then(
396
+ (value) => {
397
+ rearm()
398
+ return value
399
+ },
400
+ (error) => {
401
+ rearm()
402
+ throw error
403
+ },
404
+ )
374
405
  },
375
406
  refresh() {
376
407
  // A POST rescan is never needed: the Host rescan throttle refreshes on
377
408
  // reads anyway. A plain read is enough here.
378
- return read()
409
+ return this.load()
379
410
  },
380
411
  }
381
412
  }
@@ -390,6 +421,8 @@ window.__ModuleLoader__.load({
390
421
  let state = { status: 'idle', snapshot: null, error: null }
391
422
  let inflight = null
392
423
  let timer = null
424
+ /** Cadence the armed timer currently runs at, so rearm can compare. */
425
+ let timerMs = null
393
426
 
394
427
  const publish = (next) => {
395
428
  state = next
@@ -451,16 +484,30 @@ window.__ModuleLoader__.load({
451
484
 
452
485
  const SWEEP_POLL_MS = 2_000
453
486
  const IDLE_POLL_MS = 60_000
454
- const startTimer = () => {
455
- if (timer !== null) return
487
+ /** Re-arm the timer at the cadence the *current* snapshot needs. */
488
+ const rearm = () => {
489
+ // Never arm a timer nobody will observe: a bare `load()` (a test, an
490
+ // explicit refresh) must not leave an interval holding the page alive.
491
+ if (listeners.size === 0) {
492
+ stopTimer()
493
+ return
494
+ }
495
+ const wantFast = state.snapshot?.state?.running === true
496
+ const wantMs = wantFast ? SWEEP_POLL_MS : IDLE_POLL_MS
497
+ if (timer !== null && timerMs === wantMs) return
498
+ if (timer !== null) clearInterval(timer)
499
+ timerMs = wantMs
456
500
  timer = setInterval(() => {
457
- void read()
458
- }, state.snapshot?.state?.running === true ? SWEEP_POLL_MS : IDLE_POLL_MS)
501
+ void read().then(rearm, rearm)
502
+ }, wantMs)
503
+ // A polling strip must not keep a non-browser process alive.
504
+ if (typeof timer === 'object' && timer !== null && typeof timer.unref === 'function') timer.unref()
459
505
  }
460
506
  const stopTimer = () => {
461
507
  if (timer !== null) {
462
508
  clearInterval(timer)
463
509
  timer = null
510
+ timerMs = null
464
511
  }
465
512
  }
466
513
 
@@ -469,8 +516,8 @@ window.__ModuleLoader__.load({
469
516
  const first = listeners.size === 0
470
517
  listeners.add(fn)
471
518
  if (first) {
472
- void read()
473
- startTimer()
519
+ void read().then(rearm, rearm)
520
+ rearm()
474
521
  }
475
522
  return () => {
476
523
  listeners.delete(fn)
@@ -481,13 +528,40 @@ window.__ModuleLoader__.load({
481
528
  return state
482
529
  },
483
530
  load() {
484
- return read()
531
+ return read().then(
532
+ (value) => {
533
+ rearm()
534
+ return value
535
+ },
536
+ (error) => {
537
+ rearm()
538
+ throw error
539
+ },
540
+ )
485
541
  },
486
542
  start(payload) {
487
- return post(payload)
543
+ return post(payload).then(
544
+ (value) => {
545
+ rearm()
546
+ return value
547
+ },
548
+ (error) => {
549
+ rearm()
550
+ throw error
551
+ },
552
+ )
488
553
  },
489
554
  cancel() {
490
- return post({ cancel: true })
555
+ return post({ cancel: true }).then(
556
+ (value) => {
557
+ rearm()
558
+ return value
559
+ },
560
+ (error) => {
561
+ rearm()
562
+ throw error
563
+ },
564
+ )
491
565
  },
492
566
  }
493
567
  }
@@ -504,12 +578,13 @@ window.__ModuleLoader__.load({
504
578
  /**
505
579
  * The online capability verification panel.
506
580
  *
507
- * The user picks a checker route (provider + model) and a write-back switch,
508
- * runs the sweep, and the panel polls the Host for progress and the checker's
509
- * token spend. The last route choice is kept in browser storage only: the
510
- * Host persists run facts, not UI selections.
581
+ * Collapsed to a single summary line: this is a maintenance action, not the
582
+ * point of the page. Expanding it reveals the checker picker, the write-back
583
+ * switch and — the part that matters when a sweep fails — the per-batch errors
584
+ * with their real messages instead of a bare count.
511
585
  *
512
- * @module client-src/checker-view
586
+ * The last route choice is kept in browser storage only: the Host persists run
587
+ * facts, not UI selections.
513
588
  */
514
589
 
515
590
  const React = require('react')
@@ -519,6 +594,10 @@ window.__ModuleLoader__.load({
519
594
  const { useSource } = require("hooks")
520
595
 
521
596
  const PICK_KEY = 'dsh-model-visibility:checker-pick:v1'
597
+ /** How many failure lines to show before summarising the rest. */
598
+ const MAX_ERROR_LINES = 6
599
+ /** Concurrency choices offered in the panel; mirrors lib/check-catalog MAX_CONCURRENCY. */
600
+ const CONCURRENCY_CHOICES = [1, 2, 3, 4, 6, 8]
522
601
 
523
602
  function readPick() {
524
603
  try {
@@ -528,10 +607,14 @@ window.__ModuleLoader__.load({
528
607
  provider: typeof parsed.provider === 'string' ? parsed.provider : '',
529
608
  model: typeof parsed.model === 'string' ? parsed.model : '',
530
609
  writeBack: parsed.writeBack !== false,
610
+ concurrency:
611
+ Number.isFinite(parsed.concurrency) && parsed.concurrency > 0
612
+ ? Math.min(8, Math.max(1, Math.trunc(parsed.concurrency)))
613
+ : 2,
531
614
  }
532
- : { provider: '', model: '', writeBack: true }
615
+ : { provider: '', model: '', writeBack: true, concurrency: 2 }
533
616
  } catch (_error) {
534
- return { provider: '', model: '', writeBack: true }
617
+ return { provider: '', model: '', writeBack: true, concurrency: 2 }
535
618
  }
536
619
  }
537
620
 
@@ -562,10 +645,12 @@ window.__ModuleLoader__.load({
562
645
  .find((group) => group.id === pick.provider)?.models ?? []
563
646
 
564
647
  if (!canUse) {
565
- return h('div', { className: 'mvp_fallback' }, t('checker.hostStale'))
648
+ return h('div', { className: 'mvp_banner' }, t('checker.hostStale'))
566
649
  }
567
650
 
568
- const tokensSpent = progress ? formatTokens((progress.tokenUsage?.[0] ?? 0) + (progress.tokenUsage?.[1] ?? 0)) : null
651
+ const tokensSpent = progress
652
+ ? formatTokens((progress.tokenUsage?.[0] ?? 0) + (progress.tokenUsage?.[1] ?? 0))
653
+ : null
569
654
 
570
655
  const commit = (next) => {
571
656
  setPick(next)
@@ -575,11 +660,11 @@ window.__ModuleLoader__.load({
575
660
  const run = () => {
576
661
  setMessage(null)
577
662
  verify
578
- .start({ provider: pick.provider, model: pick.model, writeBack: pick.writeBack })
579
- .then((result) => {
580
- if (result !== undefined && result.ok === false) {
581
- setMessage(result.message ?? t('checker.failed'))
582
- }
663
+ .start({
664
+ provider: pick.provider,
665
+ model: pick.model,
666
+ writeBack: pick.writeBack,
667
+ concurrency: pick.concurrency,
583
668
  })
584
669
  .catch((error) => setMessage(String(error?.message ?? error)))
585
670
  }
@@ -588,6 +673,24 @@ window.__ModuleLoader__.load({
588
673
  verify.cancel().catch((error) => setMessage(String(error?.message ?? error)))
589
674
  }
590
675
 
676
+ const summary = running
677
+ ? t('checker.running', {
678
+ phase: t('checker.phase.' + (progress?.phase ?? 'verifying')),
679
+ done: progress?.done ?? 0,
680
+ total: progress?.total ?? 0,
681
+ tokens: tokensSpent,
682
+ })
683
+ : finished
684
+ ? t('checker.finished', {
685
+ done: progress?.done ?? 0,
686
+ total: progress?.total ?? 0,
687
+ tokens: tokensSpent,
688
+ })
689
+ : t('checker.idle')
690
+
691
+ const errors = Array.isArray(progress?.errors) ? progress.errors : []
692
+ const visibleErrors = errors.slice(0, MAX_ERROR_LINES)
693
+
591
694
  return h(
592
695
  'div',
593
696
  { className: 'mvp_checker' + (open ? '' : ' mvp_checker--closed') },
@@ -599,26 +702,16 @@ window.__ModuleLoader__.load({
599
702
  'aria-expanded': open,
600
703
  onClick: () => setOpen(!open),
601
704
  },
705
+ h('span', { className: 'mvp_chevron' }, open ? '▾' : '▸'),
602
706
  h('span', { className: 'mvp_checkerTitle' }, t('checker.title')),
603
- h(
604
- 'span',
605
- { className: 'mvp_checkerSummary' },
606
- running
607
- ? t('checker.running', {
608
- done: progress?.done ?? 0,
609
- total: progress?.total ?? 0,
610
- phase: t(`checker.phase.${progress?.phase ?? 'checker'}`),
611
- })
612
- : finished
613
- ? t('checker.finished', { done: progress?.done ?? 0, total: progress?.total ?? 0, tokens: tokensSpent })
614
- : t('checker.idle'),
615
- ),
707
+ h('span', { className: 'mvp_checkerSummary' }, summary),
708
+ errors.length > 0 ? h('span', { className: 'mvp_badge' }, t('checker.errorBadge', { count: errors.length })) : null,
616
709
  ),
617
710
  open
618
711
  ? h(
619
712
  'div',
620
713
  { className: 'mvp_checkerBody' },
621
- h('div', { className: 'mvp_checkerHint' }, t('checker.description')),
714
+ h('p', { className: 'mvp_checkerHint' }, t('checker.description')),
622
715
  h(
623
716
  'div',
624
717
  { className: 'mvp_checkerForm' },
@@ -657,67 +750,141 @@ window.__ModuleLoader__.load({
657
750
  ),
658
751
  ),
659
752
  ),
660
- ),
661
- h(
662
- 'div',
663
- { className: 'mvp_checkerActions' },
664
753
  h(
665
- 'button',
666
- {
667
- type: 'button',
668
- role: 'switch',
669
- className: 'mvp_switch',
670
- 'aria-checked': pick.writeBack,
671
- title: t('checker.writeBackTitle'),
672
- disabled: running,
673
- onClick: () => commit({ ...pick, writeBack: !pick.writeBack }),
674
- },
675
- h('span', { className: 'mvp_thumb' }),
754
+ 'label',
755
+ { className: 'mvp_field', title: t('checker.concurrencyTitle') },
756
+ h('span', { className: 'mvp_fieldLabel' }, t('checker.concurrency')),
757
+ h(
758
+ 'select',
759
+ {
760
+ className: 'mvp_select',
761
+ value: String(pick.concurrency),
762
+ 'aria-label': t('checker.concurrency'),
763
+ disabled: running,
764
+ onChange: (event) =>
765
+ commit({ ...pick, concurrency: Number(event.target.value) }),
766
+ },
767
+ CONCURRENCY_CHOICES.map((value) =>
768
+ h('option', { key: value, value: String(value) }, t('checker.concurrencyValue', { count: value })),
769
+ ),
770
+ ),
676
771
  ),
677
772
  h(
678
- 'button',
679
- {
680
- type: 'button',
681
- className: 'mvp_button',
682
- title: t('checker.writeBackTitle'),
683
- onClick: () => commit({ ...pick, writeBack: !pick.writeBack }),
684
- },
685
- t('checker.writeBack'),
773
+ 'label',
774
+ { className: 'mvp_check', title: t('checker.writeBackTitle') },
775
+ h('input', {
776
+ type: 'checkbox',
777
+ checked: pick.writeBack,
778
+ disabled: running,
779
+ onChange: (event) => commit({ ...pick, writeBack: event.target.checked }),
780
+ }),
781
+ h('span', null, t('checker.writeBack')),
686
782
  ),
687
783
  h(
688
784
  'button',
689
785
  {
690
786
  type: 'button',
691
787
  className: running ? 'mvp_button' : 'mvp_button mvp_button--accent',
692
- disabled: running ? false : pick.provider === '' || pick.model === '',
788
+ disabled: !running && (pick.provider === '' || pick.model === ''),
693
789
  title: running ? t('checker.cancelTitle') : t('checker.startTitle'),
694
790
  onClick: running ? cancel : run,
695
791
  },
696
792
  running ? t('checker.cancel') : t('checker.start'),
697
793
  ),
698
794
  ),
699
- progress !== null && (running || finished)
795
+ running || finished
700
796
  ? h(
701
797
  'div',
702
798
  { className: 'mvp_checkerProgress' },
703
- t('checker.progress', {
704
- done: progress?.done ?? 0,
705
- total: progress?.total ?? 0,
706
- tokens: tokensSpent,
707
- }),
799
+ h('div', {
800
+ className: 'mvp_progressBar',
801
+ role: 'progressbar',
802
+ 'aria-valuemin': 0,
803
+ 'aria-valuemax': progress?.total ?? 0,
804
+ 'aria-valuenow': progress?.done ?? 0,
805
+ }, h('span', {
806
+ className: 'mvp_progressFill',
807
+ style: {
808
+ width:
809
+ (progress?.total ?? 0) > 0
810
+ ? Math.min(100, Math.round(((progress?.done ?? 0) / progress.total) * 100)) + '%'
811
+ : '0%',
812
+ },
813
+ })),
814
+ h(
815
+ 'span',
816
+ { className: 'mvp_checkerProgressText' },
817
+ t('checker.progress', {
818
+ done: progress?.done ?? 0,
819
+ total: progress?.total ?? 0,
820
+ tokens: tokensSpent,
821
+ }),
822
+ ),
823
+ h(
824
+ 'span',
825
+ { className: 'mvp_checkerProgressText' },
826
+ t('checker.threads', { count: progress?.concurrency ?? pick.concurrency }),
827
+ ),
708
828
  )
709
829
  : null,
710
- (progress?.errors?.length ?? 0) > 0
830
+ // Which public catalogs were read, and which were not. Without this
831
+ // "the catalogs returned nothing" and "the catalogs errored" look
832
+ // identical, which is exactly how a broken fetch stayed invisible.
833
+ (progress?.sources?.length ?? 0) > 0
711
834
  ? h(
712
835
  'div',
713
- {
714
- className: 'mvp_usageHint mvp_usageHint--warn',
715
- title: progress.errors.join('\n'),
716
- },
717
- t('checker.errors', { count: progress.errors.length }),
836
+ { className: 'mvp_sources' },
837
+ h('div', { className: 'mvp_sourcesTitle' }, t('checker.sourcesTitle')),
838
+ h(
839
+ 'ul',
840
+ { className: 'mvp_sourceList' },
841
+ progress.sources.map((source, index) =>
842
+ h(
843
+ 'li',
844
+ { key: index, className: source.status === 'ok' ? 'mvp_source--ok' : 'mvp_source--bad' },
845
+ h('span', { className: 'mvp_sourceName' }, String(source.name)),
846
+ h('span', { className: 'mvp_sourceState' }, t('checker.source.' + String(source.status))),
847
+ source.models > 0
848
+ ? h('span', { className: 'mvp_sourceCount' }, t('checker.sourceModels', { count: source.models }))
849
+ : null,
850
+ source.message
851
+ ? h('span', { className: 'mvp_sourceMsg' }, String(source.message))
852
+ : null,
853
+ ),
854
+ ),
855
+ ),
718
856
  )
719
857
  : null,
720
- message !== null ? h('div', { className: 'mvp_usageHint mvp_usageHint--warn' }, message) : null,
858
+ (progress?.publicOnly ?? 0) > 0
859
+ ? h(
860
+ 'div',
861
+ { className: 'mvp_usageHint' },
862
+ t('checker.publicOnly', { count: progress.publicOnly }),
863
+ )
864
+ : null,
865
+ errors.length > 0
866
+ ? h(
867
+ 'div',
868
+ { className: 'mvp_checkerErrors' },
869
+ h('div', { className: 'mvp_checkerErrorsTitle' }, t('checker.errorTitle')),
870
+ h(
871
+ 'ul',
872
+ { className: 'mvp_errorList' },
873
+ visibleErrors.map((entry, index) =>
874
+ h('li', { key: index }, typeof entry === 'string' ? entry : String(entry)),
875
+ ),
876
+ ),
877
+ errors.length > visibleErrors.length
878
+ ? h(
879
+ 'div',
880
+ { className: 'mvp_usageHint' },
881
+ t('checker.errorMore', { count: errors.length - visibleErrors.length }),
882
+ )
883
+ : null,
884
+ )
885
+ : null,
886
+ message !== null ? h('div', { className: 'mvp_banner mvp_banner--error' }, message) : null,
887
+ state.error !== null ? h('div', { className: 'mvp_banner mvp_banner--error' }, state.error) : null,
721
888
  )
722
889
  : null,
723
890
  )
@@ -922,47 +1089,116 @@ window.__ModuleLoader__.load({
922
1089
  // -------------------------------------------------------- css
923
1090
  define("css", function (require, module, exports) {
924
1091
  const CSS = `
1092
+ /* ---------------------------------------------------------------- shell */
925
1093
  .mvp_page{box-sizing:border-box;width:100%;height:100%;min-height:0;flex-direction:column;display:flex;overflow-y:auto;padding-bottom:24px}
926
- .mvp_head{flex:none;flex-direction:column;gap:6px;padding:4px 0 14px;display:flex}
1094
+
1095
+ /* --------------------------------------------------------------- header */
1096
+ .mvp_head{flex:none;align-items:center;gap:12px;padding:2px 0 10px;display:flex;flex-wrap:wrap}
1097
+ .mvp_headMain{flex:1;min-width:0;align-items:baseline;gap:10px;display:flex;flex-wrap:wrap}
927
1098
  .mvp_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
928
- .mvp_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
929
- .mvp_toolbar{border-bottom:.5px solid var(--dsw-alias-border-l2);flex:none;justify-content:space-between;align-items:center;gap:12px;padding:10px 0;display:flex}
930
- .mvp_summary{color:var(--dsw-alias-label-secondary);flex:1;min-width:0;font-size:12px;line-height:18px}
931
- .mvp_actions{flex:none;align-items:center;justify-content:flex-end;gap:8px;display:flex;flex-wrap:wrap}
932
- .mvp_button{box-sizing:border-box;height:28px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 10px;font-family:inherit;font-size:12px;line-height:18px}
1099
+ .mvp_summary{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:nowrap}
1100
+ .mvp_summary--dim{color:var(--dsw-alias-label-tertiary)}
1101
+ .mvp_headActions{flex:none;align-items:center;gap:6px;display:flex;flex-wrap:wrap}
1102
+ .mvp_about{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);margin-bottom:10px;padding:8px 12px;background:transparent;background:color-mix(in srgb, var(--dsw-alias-label-primary) 2%, transparent)}
1103
+ .mvp_aboutP{color:var(--dsw-alias-label-secondary);margin:0 0 6px;font-size:12px;line-height:18px}
1104
+ .mvp_aboutP:last-child{margin-bottom:0}
1105
+
1106
+ /* -------------------------------------------------------------- buttons */
1107
+ .mvp_button{box-sizing:border-box;height:26px;color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;background:0 0;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 9px;font-family:inherit;font-size:12px;line-height:18px}
1108
+ .mvp_button--quiet{color:var(--dsw-alias-label-secondary);border-color:transparent}
1109
+ .mvp_button--quiet:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
933
1110
  .mvp_button--accent{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent)}
934
1111
  .mvp_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
935
1112
  .mvp_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
936
- .mvp_button:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
1113
+ .mvp_button:disabled{color:var(--dsw-alias-label-tertiary);cursor:default;opacity:.6}
1114
+
1115
+ /* ---------------------------------------------------------------- usage */
1116
+ .mvp_usage{flex:none;color:var(--dsw-alias-label-secondary);align-items:center;gap:14px;border-bottom:.5px solid var(--dsw-alias-border-l1);padding:6px 0 10px;font-size:12px;line-height:18px;display:flex;flex-wrap:wrap}
1117
+ .mvp_usage--off{color:var(--dsw-alias-label-tertiary)}
1118
+ .mvp_usageLabel{color:var(--dsw-alias-label-tertiary);align-items:baseline;gap:6px;flex:none;display:inline-flex}
1119
+ .mvp_usageComputing{color:var(--dsw-alias-brand-primary);font-size:11px}
1120
+ .mvp_usageRanges{flex:1;min-width:0;align-items:baseline;gap:16px;display:flex;flex-wrap:wrap}
1121
+ .mvp_stat{align-items:baseline;gap:5px;display:inline-flex;white-space:nowrap}
1122
+ .mvp_statLabel{color:var(--dsw-alias-label-tertiary);font-size:11px}
1123
+ .mvp_statValue{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;font-size:13px;font-weight:500}
1124
+ .mvp_stat--total .mvp_statValue{color:var(--dsw-alias-brand-primary)}
1125
+ .mvp_usageActions{flex:none;align-items:center;gap:4px;display:flex}
1126
+ .mvp_usageHint{width:100%;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1127
+ .mvp_usageHint--warn{color:var(--dsw-alias-state-error-primary)}
1128
+
1129
+ /* --------------------------------------------------------------- filters */
1130
+ .mvp_filters{flex:none;align-items:center;gap:8px;border-bottom:.5px solid var(--dsw-alias-border-l2);padding:8px 0;display:flex;flex-wrap:wrap}
1131
+ .mvp_searchInput{box-sizing:border-box;flex:1 1 180px;min-width:150px;max-width:320px;height:26px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 8px;font-family:inherit;font-size:12px}
1132
+ .mvp_searchInput:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
1133
+ .mvp_filterGroup{align-items:center;gap:4px;display:flex;flex-wrap:wrap}
1134
+ .mvp_filterGroup--push{margin-left:auto}
1135
+ .mvp_filter{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 9px;font-family:inherit;font-size:11px;line-height:16px}
1136
+ .mvp_filter[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 50%, transparent)}
1137
+ .mvp_filter:hover:not([aria-pressed="true"]){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}
1138
+ .mvp_filter:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1139
+ .mvp_contextSelect{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-base);border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 6px;font-family:inherit;font-size:11px}
1140
+
1141
+ /* ------------------------------------------------------- applyInput row */
1142
+ .mvp_applyRow{flex:none;align-items:center;gap:10px;border-bottom:.5px solid var(--dsw-alias-border-l1);padding:8px 0;display:flex}
1143
+ .mvp_applyText{flex:1;min-width:0;align-items:baseline;gap:8px;display:flex;flex-wrap:wrap}
1144
+ .mvp_applyTitle{color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px}
1145
+ .mvp_applyHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1146
+
1147
+ /* --------------------------------------------------------------- banners */
1148
+ .mvp_banner{flex:none;color:var(--dsw-alias-label-secondary);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);margin:8px 0;padding:6px 10px;font-size:12px;line-height:18px}
1149
+ .mvp_banner--warn{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 7%, transparent)}
1150
+ .mvp_banner--error{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 7%, transparent)}
1151
+ .mvp_note{flex:none;color:var(--dsw-alias-label-tertiary);padding:6px 0;font-size:11px;line-height:16px}
1152
+
1153
+ /* ---------------------------------------------------------------- groups */
1154
+ .mvp_groups{flex:none;flex-direction:column;display:flex}
937
1155
  .mvp_group{flex:none;flex-direction:column;display:flex}
938
- .mvp_groupHead{justify-content:space-between;align-items:center;gap:12px;padding:14px 0 6px;display:flex}
939
- .mvp_groupTitle{color:var(--dsw-alias-label-primary);align-items:baseline;row-gap:2px;gap:8px;font-size:14px;font-weight:500;line-height:22px;display:flex;flex-wrap:wrap}
940
- .mvp_groupName{min-width:0}
941
- .mvp_collapse{box-sizing:border-box;flex:none;align-self:center;width:18px;height:18px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:4px;justify-content:center;align-items:center;padding:0;font-family:inherit;font-size:11px;line-height:18px;display:inline-flex}
942
- .mvp_collapse:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
943
- .mvp_collapse:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
944
- .mvp_groupCount{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
945
- .mvp_row{border-bottom:.5px solid var(--dsw-alias-border-l1);justify-content:space-between;align-items:center;gap:16px;padding:10px 0;display:flex}
946
- .mvp_row--gen{background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent)}
947
- .mvp_row--kind-image{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 70%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 4%, transparent)}
948
- .mvp_row--kind-video{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-success-primary) 70%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 4%, transparent)}
949
- .mvp_row--kind-embedding{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-warn-primary) 70%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 4%, transparent)}
950
- .mvp_row--kind-rerank{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 3%, transparent)}
951
- .mvp_row--kind-audio{box-shadow:inset 2px 0 0 color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary)) 70%, transparent);background:transparent;background:color-mix(in srgb, color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary)) 4%, transparent)}
952
- .mvp_row--kind-classifier{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-error-secondary) 60%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-error-secondary) 3%, transparent)}
1156
+ .mvp_groupHead{justify-content:space-between;align-items:center;gap:12px;border-bottom:.5px solid var(--dsw-alias-border-l2);padding:10px 0 6px;display:flex}
1157
+ .mvp_groupTitle{flex:1;min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:0;align-items:baseline;column-gap:8px;row-gap:2px;padding:0;font-family:inherit;font-size:13px;font-weight:500;line-height:20px;display:flex;flex-wrap:wrap}
1158
+ .mvp_groupTitle:hover .mvp_groupName{color:var(--dsw-alias-brand-primary)}
1159
+ .mvp_groupTitle:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;border-radius:4px}
1160
+ .mvp_chevron{flex:none;align-self:center;width:12px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px}
1161
+ .mvp_groupName{min-width:0;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
1162
+ .mvp_groupCount{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400;font-variant-numeric:tabular-nums}
1163
+ .mvp_groupCap{color:var(--dsw-alias-label-tertiary);align-items:baseline;gap:3px;font-size:11px;font-weight:400;display:inline-flex}
1164
+ .mvp_sep{opacity:.5}
1165
+ .mvp_kindCount{font-variant-numeric:tabular-nums}
1166
+ .mvp_kindCount--image{color:var(--dsw-alias-state-business-primary)}
1167
+ .mvp_kindCount--video{color:var(--dsw-alias-state-success-primary)}
1168
+ .mvp_kindCount--other{color:var(--dsw-alias-state-warn-primary)}
1169
+ .mvp_groupProbe{color:var(--dsw-alias-label-tertiary);border:.5px dashed var(--dsw-alias-border-l2);border-radius:4px;max-width:100%;white-space:nowrap;text-overflow:ellipsis;padding:0 5px;font-size:10px;line-height:15px;overflow:hidden}
1170
+ .mvp_groupProbe--warn{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent)}
1171
+
1172
+ /* ------------------------------------------------------------------ rows */
1173
+ .mvp_row{justify-content:space-between;align-items:center;gap:16px;border-bottom:.5px solid var(--dsw-alias-border-l1);padding:8px 0 8px 6px;display:flex}
1174
+ .mvp_row--off{opacity:.62}
1175
+ .mvp_row--gen{background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 3%, transparent)}
1176
+ .mvp_row--kind-image{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 70%, transparent)}
1177
+ .mvp_row--kind-video{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-success-primary) 70%, transparent)}
1178
+ .mvp_row--kind-embedding{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-warn-primary) 70%, transparent)}
1179
+ .mvp_row--kind-rerank{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-error-primary) 60%, transparent)}
1180
+ .mvp_row--kind-audio{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-business-primary) 50%, var(--dsw-alias-state-error-primary))}
1181
+ .mvp_row--kind-classifier{box-shadow:inset 2px 0 0 color-mix(in srgb, var(--dsw-alias-state-error-secondary) 60%, transparent)}
953
1182
  .mvp_row--kind-translation{box-shadow:inset 2px 0 0 var(--dsw-alias-border-l2)}
954
- .mvp_rowText{flex-direction:column;gap:2px;min-width:0;display:flex}
955
- .mvp_rowTitle{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}
956
- .mvp_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}
957
- .mvp_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;padding:6px 0}
958
- .mvp_switch{position:relative;flex:none;box-sizing:border-box;width:36px;height:20px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l2);cursor:pointer;transition:background-color .15s ease}
959
- .mvp_switch[aria-checked="true"]{background:var(--dsw-alias-bg-base)}
1183
+ .mvp_rowText{flex-direction:column;gap:3px;min-width:0;display:flex}
1184
+ .mvp_rowHead{align-items:baseline;gap:8px;min-width:0;display:flex;flex-wrap:wrap}
1185
+ .mvp_rowTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
1186
+ .mvp_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-size:11px;line-height:18px;overflow:hidden}
1187
+ .mvp_rowActions{flex:none;align-items:center;gap:6px;display:flex}
1188
+ .mvp_editButton{height:24px;padding:0 8px;font-size:11px}
1189
+
1190
+ /* ---------------------------------------------------------------- switch */
1191
+ .mvp_switch{position:relative;flex:none;box-sizing:border-box;width:34px;height:19px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l2);cursor:pointer;transition:background-color .15s ease}
1192
+ .mvp_switch[aria-checked="true"]{background:var(--dsw-alias-brand-primary)}
960
1193
  .mvp_switch:disabled{opacity:.45;cursor:default}
961
1194
  .mvp_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
962
- .mvp_thumb{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:var(--dsw-alias-label-secondary);transition:transform .15s ease,background-color .15s ease}
963
- .mvp_switch[aria-checked="true"] .mvp_thumb{background:var(--dsw-alias-label-primary);transform:translateX(16px)}
964
- .mvp_tags{flex-wrap:wrap;align-items:center;gap:4px;margin-top:4px;display:flex}
965
- .mvp_tag{box-sizing:border-box;max-width:100%;height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:4px;align-items:center;padding:0 6px;font-size:11px;line-height:16px;display:inline-flex;overflow:hidden;text-overflow:ellipsis}
1195
+ .mvp_thumb{position:absolute;top:3px;left:3px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .15s ease}
1196
+ .mvp_switch[aria-checked="true"] .mvp_thumb{transform:translateX(15px)}
1197
+ .mvp_switch[aria-checked="false"] .mvp_thumb{background:var(--dsw-alias-label-secondary)}
1198
+
1199
+ /* ------------------------------------------------------------------ tags */
1200
+ .mvp_tags{flex-wrap:wrap;align-items:center;gap:4px;display:flex}
1201
+ .mvp_tag{box-sizing:border-box;max-width:100%;height:17px;color:var(--dsw-alias-label-secondary);white-space:nowrap;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:4px;align-items:center;padding:0 5px;font-size:10px;line-height:15px;display:inline-flex;overflow:hidden;text-overflow:ellipsis}
966
1202
  .mvp_tag--accent{color:var(--dsw-alias-brand-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, transparent)}
967
1203
  .mvp_tag--vision{color:var(--dsw-alias-state-business-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary) 45%, transparent)}
968
1204
  .mvp_tag--kind{font-weight:500}
@@ -976,198 +1212,61 @@ window.__ModuleLoader__.load({
976
1212
  .mvp_tag--kind-unknown{color:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-border-l2)}
977
1213
  .mvp_tag--inferred{border-style:dashed}
978
1214
  .mvp_tag--muted{color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l1)}
979
- .mvp_groupCap{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400;line-height:16px}
980
- .mvp_kindCount--image{color:var(--dsw-alias-state-business-primary)}
981
- .mvp_kindCount--video{color:var(--dsw-alias-state-success-primary)}
982
- .mvp_kindCount--other{color:var(--dsw-alias-state-warn-primary)}
983
- .mvp_applyRow{flex:none;align-items:flex-start;gap:10px;margin:8px 0 2px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:8px 10px;display:flex;background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent)}
984
- .mvp_applyText{flex-direction:column;gap:2px;min-width:0;display:flex}
985
- .mvp_applyTitle{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:18px}
986
- .mvp_applyHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
987
- .mvp_applyRow .mvp_switch{margin-top:1px}
988
- .mvp_groupProbe{color:var(--dsw-alias-label-tertiary);border:.5px dashed var(--dsw-alias-border-l2);border-radius:4px;max-width:100%;white-space:nowrap;text-overflow:ellipsis;padding:0 5px;font-size:10px;line-height:15px;overflow:hidden}
989
- .mvp_filters{align-items:center;gap:6px;padding:8px 0 2px;display:flex;flex-wrap:wrap}
990
- .mvp_filterLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
991
- .mvp_search{align-items:center;gap:6px;display:flex;min-width:220px;flex:1 1 220px}
992
- .mvp_searchInput{box-sizing:border-box;width:100%;height:28px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 8px;font-family:inherit;font-size:12px;line-height:18px}
993
- .mvp_searchInput:focus-visible,.mvp_contextSelect:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
994
- .mvp_context{align-items:center;gap:6px;display:flex}
995
- .mvp_contextSelect{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-base);border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 6px;font-family:inherit;font-size:11px;line-height:18px}
996
- .mvp_filter{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 9px;font-family:inherit;font-size:11px;line-height:18px}
997
- .mvp_filter:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
998
- .mvp_filter[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent)}
999
- .mvp_filter:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1000
- .mvp_note{color:var(--dsw-alias-label-tertiary);flex:none;padding:6px 0 0;font-size:10px;line-height:15px}
1001
- .mvp_legend{color:var(--dsw-alias-label-tertiary);flex:none;padding:8px 0 2px;font-size:11px;line-height:16px}
1002
- .mvp_fallback{color:var(--dsw-alias-label-secondary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 10%, transparent);border-radius:var(--dsw-radius-sm,6px);padding:6px 8px;font-size:11px;line-height:16px;margin-top:8px}
1003
- .mvp_rowActions{flex:none;align-items:center;gap:8px;display:flex}
1004
- .mvp_editButton{height:24px;padding:0 8px;font-size:11px;line-height:18px}
1005
- .mvp_editButton[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent)}
1006
- .mvp_editor{box-sizing:border-box;border:.5px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, transparent);border-radius:var(--dsw-radius-sm,6px);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent);margin:2px 0 12px;padding:10px 12px;flex-direction:column;gap:10px;display:flex}
1007
- .mvp_editorHead{flex-direction:column;gap:2px;display:flex}
1008
- .mvp_editorTitle{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:18px}
1215
+
1216
+ /* --------------------------------------------------------------- editor */
1217
+ .mvp_editor{border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);margin:4px 0 8px;padding:10px 12px;background:transparent;background:color-mix(in srgb, var(--dsw-alias-label-primary) 2%, transparent)}
1218
+ .mvp_editorHead{margin-bottom:8px}
1219
+ .mvp_editorTitle{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}
1009
1220
  .mvp_editorSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1010
- .mvp_editorGrid{flex-wrap:wrap;align-items:flex-end;gap:10px 16px;display:flex}
1011
- .mvp_field{flex-direction:column;gap:4px;min-width:140px;display:flex}
1221
+ .mvp_editorGrid{flex-wrap:wrap;align-items:flex-start;gap:8px 14px;display:flex}
1222
+ .mvp_field{flex-direction:column;gap:3px;min-width:140px;display:flex}
1012
1223
  .mvp_field--wide{flex:1 1 100%}
1013
- .mvp_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}
1014
- .mvp_select,.mvp_number{box-sizing:border-box;height:26px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 8px;font-family:inherit;font-size:12px;line-height:18px}
1015
- .mvp_number{width:180px}
1016
- .mvp_select:focus-visible,.mvp_number:focus-visible,.mvp_chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1017
- .mvp_chips{flex-wrap:wrap;align-items:center;gap:6px;display:flex}
1018
- .mvp_chip{box-sizing:border-box;height:24px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 10px;font-family:inherit;font-size:11px;line-height:18px}
1019
- .mvp_chip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1020
- .mvp_chip[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent)}
1224
+ .mvp_fieldLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1225
+ .mvp_select,.mvp_number{box-sizing:border-box;height:26px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);padding:0 6px;font-family:inherit;font-size:12px}
1226
+ .mvp_chips{flex-wrap:wrap;gap:4px;display:flex}
1227
+ .mvp_chip{box-sizing:border-box;height:23px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:.5px solid var(--dsw-alias-border-l2);border-radius:999px;padding:0 9px;font-family:inherit;font-size:11px}
1228
+ .mvp_chip[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 50%, transparent)}
1021
1229
  .mvp_chip:disabled{opacity:.45;cursor:default}
1022
- .mvp_editorFooter{justify-content:space-between;align-items:center;gap:12px;display:flex;flex-wrap:wrap}
1023
- .mvp_editorNote{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px;flex:1 1 220px;min-width:0}
1024
- .mvp_check{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
1025
- .mvp_check input{accent-color:var(--dsw-alias-brand-primary, #4c8dff);margin:0}
1026
- .mvp_editorActions{flex:none;align-items:center;gap:6px;display:flex;flex-wrap:wrap}
1027
- /* ---------------------------------------------------- usage & checker */
1028
-
1029
- .mvp_usage {
1030
- margin: 0 0 12px;
1031
- padding: 10px 12px;
1032
- border: 1px solid var(--dsw-alias-border, transparent);
1033
- border-radius: 10px;
1034
- background: transparent;
1035
- background: color-mix(in srgb, currentColor 4%, transparent);
1036
- }
1037
-
1038
- .mvp_usage--off {
1039
- opacity: 0.75;
1040
- }
1041
-
1042
- .mvp_usageHead {
1043
- display: flex;
1044
- align-items: center;
1045
- gap: 10px;
1046
- }
1047
-
1048
- .mvp_usageTitle {
1049
- font-weight: 600;
1050
- font-size: 12px;
1051
- }
1052
-
1053
- .mvp_usageComputing {
1054
- color: var(--dsw-alias-text-tertiary, currentColor);
1055
- font-size: 11px;
1056
- }
1057
-
1058
- .mvp_usageActions {
1059
- display: flex;
1060
- gap: 8px;
1061
- margin-left: auto;
1062
- }
1063
-
1064
- .mvp_usageRanges {
1065
- display: flex;
1066
- gap: 18px;
1067
- flex-wrap: wrap;
1068
- margin-top: 8px;
1069
- }
1070
-
1071
- .mvp_stat {
1072
- display: flex;
1073
- flex-direction: column;
1074
- gap: 2px;
1075
- }
1076
-
1077
- .mvp_statLabel {
1078
- font-size: 11px;
1079
- color: var(--dsw-alias-text-tertiary, currentColor);
1080
- }
1081
-
1082
- .mvp_statValue {
1083
- font-size: 12px;
1084
- font-variant-numeric: tabular-nums;
1085
- }
1086
-
1087
- .mvp_stat--total .mvp_statValue {
1088
- font-weight: 600;
1089
- }
1090
-
1091
- .mvp_usageHint {
1092
- margin-top: 8px;
1093
- font-size: 11px;
1094
- color: var(--dsw-alias-text-tertiary, currentColor);
1095
- }
1096
-
1097
- .mvp_usageHint--warn {
1098
- color: var(--dsw-alias-danger, #d0605e);
1099
- }
1100
-
1101
- .mvp_checker {
1102
- margin: 0 0 12px;
1103
- padding: 10px 12px;
1104
- border: 1px solid var(--dsw-alias-border, transparent);
1105
- border-radius: 10px;
1106
- }
1107
-
1108
- .mvp_checkerHead {
1109
- display: flex;
1110
- width: 100%;
1111
- align-items: center;
1112
- gap: 10px;
1113
- padding: 0;
1114
- border: 0;
1115
- background: transparent;
1116
- color: inherit;
1117
- text-align: left;
1118
- cursor: pointer;
1119
- }
1120
-
1121
- .mvp_checkerTitle {
1122
- font-weight: 600;
1123
- font-size: 12px;
1124
- }
1125
-
1126
- .mvp_checkerSummary {
1127
- margin-left: auto;
1128
- font-size: 11px;
1129
- color: var(--dsw-alias-text-tertiary, currentColor);
1130
- }
1131
-
1132
- .mvp_checker--closed.mvp_checker {
1133
- padding: 8px 12px;
1134
- }
1135
-
1136
- .mvp_checkerBody {
1137
- margin-top: 10px;
1138
- display: flex;
1139
- flex-direction: column;
1140
- gap: 10px;
1141
- }
1142
-
1143
- .mvp_checkerHint {
1144
- font-size: 11px;
1145
- color: var(--dsw-alias-text-tertiary, currentColor);
1146
- }
1147
-
1148
- .mvp_checkerForm {
1149
- display: grid;
1150
- grid-template-columns: 1fr 1fr;
1151
- gap: 10px;
1152
- max-width: 560px;
1153
- }
1154
-
1155
- .mvp_checkerActions {
1156
- display: flex;
1157
- align-items: center;
1158
- gap: 10px;
1159
- }
1160
-
1161
- .mvp_checkerSwitchText {
1162
- font-size: 12px;
1163
- }
1164
-
1165
- .mvp_checkerProgress {
1166
- font-size: 11px;
1167
- font-variant-numeric: tabular-nums;
1168
- color: var(--dsw-alias-text-secondary, currentColor);
1169
- }
1170
-
1230
+ .mvp_check{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:18px;display:inline-flex;cursor:pointer}
1231
+ .mvp_editorNote{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1232
+ .mvp_editorError{color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;margin-top:6px}
1233
+ .mvp_editorFooter{justify-content:space-between;align-items:flex-end;gap:12px;margin-top:10px;display:flex;flex-wrap:wrap}
1234
+ .mvp_editorActions{align-items:center;gap:6px;display:flex}
1235
+
1236
+ /* --------------------------------------------------------------- checker */
1237
+ .mvp_checker{flex:none;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,6px);margin-top:12px;overflow:hidden}
1238
+ .mvp_checker--closed{background:transparent}
1239
+ .mvp_checkerHead{box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:0;align-items:baseline;gap:8px;padding:7px 10px;font-family:inherit;font-size:12px;line-height:18px;display:flex;flex-wrap:wrap}
1240
+ .mvp_checkerHead:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.08))}
1241
+ .mvp_checkerHead:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1242
+ .mvp_checkerTitle{font-weight:500;flex:none}
1243
+ .mvp_checkerSummary{color:var(--dsw-alias-label-tertiary);flex:1;min-width:0;font-size:11px}
1244
+ .mvp_badge{flex:none;color:var(--dsw-alias-state-warn-primary);border:.5px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent);border-radius:999px;padding:0 6px;font-size:10px;line-height:15px}
1245
+ .mvp_checkerBody{border-top:.5px solid var(--dsw-alias-border-l2);padding:10px}
1246
+ .mvp_checkerHint{color:var(--dsw-alias-label-tertiary);margin:0 0 8px;font-size:11px;line-height:16px}
1247
+ .mvp_checkerForm{align-items:flex-end;gap:10px;display:flex;flex-wrap:wrap}
1248
+ .mvp_checkerProgress{align-items:center;gap:10px;margin-top:10px;display:flex;flex-wrap:wrap}
1249
+ .mvp_progressBar{flex:1 1 160px;min-width:120px;height:4px;border-radius:999px;background:var(--dsw-alias-border-l2);overflow:hidden}
1250
+ .mvp_progressFill{height:100%;background:var(--dsw-alias-brand-primary);transition:width .2s ease;display:block}
1251
+ .mvp_checkerProgressText{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1252
+ .mvp_checkerErrors{margin-top:10px}
1253
+ .mvp_checkerErrorsTitle{color:var(--dsw-alias-state-warn-primary);margin-bottom:4px;font-size:11px;line-height:16px}
1254
+ .mvp_errorList{max-height:150px;color:var(--dsw-alias-label-secondary);margin:0;padding-left:18px;font-size:11px;line-height:16px;overflow-y:auto}
1255
+ .mvp_errorList li{margin-bottom:3px;word-break:break-word}
1256
+ .mvp_sources{margin-top:10px}
1257
+ .mvp_sourcesTitle{color:var(--dsw-alias-label-tertiary);margin-bottom:4px;font-size:11px;line-height:16px}
1258
+ .mvp_sourceList{max-height:150px;margin:0;padding:0;list-style:none;font-size:11px;line-height:16px;overflow-y:auto}
1259
+ .mvp_sourceList li{flex-wrap:wrap;align-items:baseline;gap:6px;padding:2px 0;display:flex}
1260
+ .mvp_sourceName{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
1261
+ .mvp_sourceState{flex:none;border-radius:4px;padding:0 4px;font-size:10px;line-height:14px;border:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}
1262
+ .mvp_source--ok .mvp_sourceState{color:var(--dsw-alias-state-success-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-success-primary) 45%, transparent)}
1263
+ .mvp_source--bad .mvp_sourceState{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 45%, transparent)}
1264
+ .mvp_sourceCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}
1265
+ .mvp_sourceMsg{flex:1 1 100%;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px}
1266
+
1267
+ /* ---------------------------------------------------------------- footer */
1268
+ .mvp_footer{flex:none;align-items:center;gap:14px;color:var(--dsw-alias-label-tertiary);border-top:.5px solid var(--dsw-alias-border-l1);margin-top:12px;padding-top:8px;font-size:11px;line-height:16px;display:flex;flex-wrap:wrap}
1269
+ .mvp_footerItem{min-width:0;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
1171
1270
  `
1172
1271
 
1173
1272
 
@@ -1181,10 +1280,12 @@ window.__ModuleLoader__.load({
1181
1280
  'nav': '模型显示',
1182
1281
  'page.title': '模型显示',
1183
1282
  'page.description':
1184
- '关闭的模型不会出现在聊天输入框的模型下拉列表中;这里的改动立即生效,之后仍可随时重新打开。每个模型标注真实能力:适配器声明优先,其次读中转站的模型列表,再用内置目录补全;生图、生视频等生成类模型单独标记。',
1185
- 'page.summary': '已显示 {visible} / {total} 个模型',
1186
- 'page.hiddenSummary': '已关闭 {count} 个',
1187
- 'page.kindSummary': '{count} 个生图/生视频模型',
1283
+ '关闭的模型不会出现在聊天输入框的模型下拉列表中;这里的改动立即生效,之后仍可随时重新打开。每个模型标注真实能力:适配器声明优先,其次读中转站模型列表,最后用校验结果或模型名补全。',
1284
+ 'page.about': '说明',
1285
+ 'page.aboutTitle': '展开标签含义、来源与颜色说明',
1286
+ 'page.summary': '已显示 {visible} / {total}',
1287
+ 'page.hiddenSummary': '已关闭 {count}',
1288
+ 'page.kindSummary': '生图/生视频 {count}',
1188
1289
  'page.loading': '正在加载模型列表…',
1189
1290
  'page.empty': '暂无可用模型。',
1190
1291
  'page.filterEmpty': '当前筛选下没有模型。',
@@ -1192,31 +1293,33 @@ window.__ModuleLoader__.load({
1192
1293
  'page.saveFailed': '保存失败,请重试。',
1193
1294
  'page.refresh': '刷新',
1194
1295
  'page.refreshing': '刷新中…',
1296
+ 'page.refreshTitle': '重新读取适配器与中转站的能力信息(沿用缓存,10 分钟内的探测不会重发请求)',
1195
1297
  'page.reload': '重新获取能力',
1196
1298
  'page.reloading': '正在获取…',
1197
1299
  'page.reloadTitle':
1198
1300
  '清空本插件的能力缓存和正在进行的读取,重新询问适配器与所有中转站;不会改动任何模型开关。适配器没有公布的思考强度仍不会凭空出现。',
1199
1301
  'page.reloadUnavailable': 'Host 半不支持强制重新获取;重启 DSH 后可用。',
1200
- 'page.collapseGroups': '折叠全部分组',
1201
- 'page.expandGroups': '展开全部分组',
1302
+ 'page.collapseGroups': '折叠全部',
1303
+ 'page.expandGroups': '展开全部',
1202
1304
  'page.collapseGroupsTitle': '只保留分组标题,方便跳到其他分组',
1203
1305
  'page.expandGroupsTitle': '展开所有被折叠的分组',
1204
1306
  'page.showAll': '全部显示',
1205
1307
  'page.legend':
1206
- '标签说明:推理 = 可调推理等级;上下文 = 输入窗口;多模态 = 可输入图像。实线标签来自适配器、中转站或厂商文档,虚线标签来自内置目录或模型名推断。生图 / 生视频 / 嵌入等模型只标类型,不显示思考强度与文本标签。',
1308
+ '标签:推理 = 可调推理等级;上下文 = 输入窗口;多模态 = 可输入图像。实线来自适配器、中转站或厂商文档,虚线来自联网校验或模型名推断。生图 / 生视频 / 嵌入等只标类型,不显示思考强度与文本标签。',
1207
1309
  'page.metaFallback':
1208
1310
  '模型能力信息暂不可用:上下文与模态显示为未知,推理档位仍来自官方目录。',
1209
1311
  'page.hostStale':
1210
1312
  'Host 半仍是旧版本:颜色与开关已更新,但「放行图像」和官方校正要等 DSH 重启(或 profile 重组)后才会生效。',
1211
- 'page.providerFailures': '部分提供方暂时读不到模型:{names}',
1212
- 'page.probeSummary': '中转站探测:{ok} 个已读取模型列表,{caps} 个公布了能力字段',
1213
- 'page.probeSummaryNone': '中转站探测:暂无可读取的路由配置',
1214
- 'page.catalogNote': '内置目录:pi-ai {version} 快照({date}),共 {models} 个模型',
1215
- 'page.learnedNote': '学习快照:联网校验({date}),缓存 {models} 个模型的可验证事实',
1216
- 'usage.title': 'token 用量',
1313
+ 'page.providerFailures': '部分提供方读不到模型:{names}',
1314
+ 'page.probeFailures': '中转站读取失败:{names}',
1315
+ 'page.probeSummary': '探测:{ok} 个中转站可读',
1316
+ 'page.probeSummaryTitle': '中转站探测结果;悬停分组上的标记可看具体诊断',
1317
+ 'page.probeSummaryNone': '探测:未配置可探测的路由',
1318
+ 'page.learnedNote': '学习快照 {models} 条({date})',
1319
+ 'usage.title': 'token',
1217
1320
  'usage.computing': '统计中…',
1218
1321
  'usage.none': '—',
1219
- 'usage.range': '输入 {input} · 输出 {output}(合计 {total})',
1322
+ 'usage.detail': '输入 {input} · 输出 {output} · 缓存读 {cacheRead} · 缓存写 {cacheWrite}',
1220
1323
  'usage.today': '今天',
1221
1324
  'usage.todayTitle': '本地自然日(今天 0 点起)',
1222
1325
  'usage.week': '本周',
@@ -1225,47 +1328,62 @@ window.__ModuleLoader__.load({
1225
1328
  'usage.monthTitle': '本月 1 日 0 点起',
1226
1329
  'usage.total': '总计',
1227
1330
  'usage.totalTitle': '全部历史(更早的按会话创建日粗算)',
1228
- 'usage.exportUsage': '导出用量 CSV',
1331
+ 'usage.exportUsage': '用量 CSV',
1229
1332
  'usage.exportUsageTitle': '下载按天分桶、含每模型 token 分解的 CSV',
1230
- 'usage.exportModels': '导出模型清单 CSV',
1333
+ 'usage.exportModels': '模型 CSV',
1231
1334
  'usage.exportModelsTitle': '下载全部提供商模型的可见性与能力表',
1232
1335
  'usage.hostStale': '宿主半尚未提供用量统计,重启 DSH 即可启用。',
1233
- 'checker.title': '联网校验模型能力',
1336
+ 'checker.title': '联网校验',
1234
1337
  'checker.description':
1235
- '选定一个模型作为校验员:先把两个公开目录(models.dev / OpenRouter)拉下来做基准,再把其余模型分批交给它核对上下文窗口、输入模态、思考档位与类型;结果回填本页并(在开关打开时)写入对应提供商配置,同时存进本地快照 learned.json,之后同名模型直接复用缓存。校验请求会消耗所选模型的 API 额度,其 token 消耗会计入上方用量统计。',
1338
+ '选定一个校验员模型:先拉 models.dev / OpenRouter / Hugging Face 三个公开目录做基准,再把其余模型分批交给它核对。公开目录已经知道的能力会直接采用,校验员答不上时也不例外。校验请求会消耗所选模型的额度,其 token 计入上方统计。提示:思考型模型会先花掉输出预算,若批次普遍失败,换一个非思考模型或降低其思考档位。',
1236
1339
  'checker.provider': '校验员提供商',
1237
1340
  'checker.model': '校验员模型',
1238
1341
  'checker.start': '开始校验',
1239
1342
  'checker.startTitle': '在宿主侧后台跑一轮全模型校验',
1240
1343
  'checker.cancel': '取消',
1241
1344
  'checker.cancelTitle': '中止正在进行的校验',
1242
- 'checker.writeBack': '校验通过即写入提供商配置',
1345
+ 'checker.writeBack': '同时写入提供商配置',
1243
1346
  'checker.writeBackTitle':
1244
- '开启:判定到的 input / contextWindow / 推理档位直接写进各提供商该模型条目(learned.json 只留写不进去的字段);关闭:只更新本页与快照,需要时再用「编辑能力」手动写入。',
1347
+ '开启:判定到的 input / contextWindow / 推理档位直接写进各提供商该模型条目(learned.json 只留写不进去的字段);关闭:只更新本页与快照。',
1245
1348
  'checker.idle': '未运行',
1246
- 'checker.running': '进行中 {phase} · {done}/{total} · 已耗 {tokens}',
1247
- 'checker.finished': '已完成 {done}/{total} · 校验耗 token {tokens}',
1349
+ 'checker.running': '进行中{phase} · {done}/{total} · 已耗 {tokens}',
1350
+ 'checker.finished': '已完成 {done}/{total} · 耗 {tokens}',
1248
1351
  'checker.progress': '{done}/{total} 已核对 · 校验本身消耗 {tokens} token',
1249
- 'checker.errors': '{count} 个批次失败(悬停看详情)',
1250
- 'checker.pickNeeded': '请先选择校验员提供商与模型。',
1251
- 'checker.failed': '校验任务未能启动。',
1352
+ 'checker.errorBadge': '{count} 个批次失败',
1353
+ 'checker.errorTitle': '失败批次',
1354
+ 'checker.errorMore': '另有 {count} 条未显示',
1355
+ 'checker.concurrency': '并发线程',
1356
+ 'checker.concurrencyTitle': '同时发出的校验请求数。中转站限流时调低,网络快时调高(上限 8)。',
1357
+ 'checker.concurrencyValue': '{count} 路',
1358
+ 'checker.threads': '并发 {count} 路',
1359
+ 'checker.sourcesTitle': '公开目录',
1360
+ 'checker.source.ok': '已读取',
1361
+ 'checker.source.empty': '无匹配',
1362
+ 'checker.source.http': 'HTTP 错误',
1363
+ 'checker.source.failed': '请求失败',
1364
+ 'checker.source.bad-json': '非 JSON',
1365
+ 'checker.source.too-large': '响应过大',
1366
+ 'checker.source.pending': '等待中',
1367
+ 'checker.sourceModels': '{count} 条',
1368
+ 'checker.publicOnly': '其中 {count} 个模型的能力来自公开目录(校验员未作答)。',
1252
1369
  'checker.hostStale': '宿主半不支持联网校验,请重启 DSH 后使用。',
1253
1370
  'checker.phase.public': '(读取公开目录)',
1254
1371
  'checker.phase.checker': '(读取公开目录)',
1255
1372
  'checker.phase.verifying': '(分批校验)',
1256
1373
  'source.snapshot': '已校验',
1257
1374
  'page.applyInput': '让输入框按能力标注放行图像',
1375
+ 'page.applyInputShort': '多模态中转站模型自动补 image;关闭则只按 Provider 的 input 声明',
1258
1376
  'page.applyInputHint':
1259
1377
  '开启后,被标注为「多模态」的中转站对话模型会在发送请求时自动补上图像输入能力;关闭则完全按 Provider 配置里的 input 声明。只影响中转站(pi-ai)路由,不改任何配置文件;若某个上游实际拒绝图像,关闭此开关即可。',
1260
1378
  'page.kindLegend':
1261
- '颜色区分模型类型:生图 蓝 · 生视频 绿 · 嵌入 橙 · 音频 紫 · 重排 红 · 审核 玫红 · 翻译 灰。「官方校正」表示内置目录与厂商文档不一致时以文档为准。',
1262
- 'page.filterLabel': '筛选',
1263
- 'page.searchPlaceholder': '搜索模型名称或 ID',
1264
- 'page.capabilityLabel': '能力',
1379
+ '类型配色:生图 蓝 · 生视频 绿 · 嵌入 橙 · 音频 紫 · 重排 红 · 审核 玫红 · 翻译 灰。「官方校正」表示目录与厂商文档不一致时以文档为准。',
1380
+ 'page.searchPlaceholder': '搜索名称或 ID',
1265
1381
  'page.capability.multimodal': '多模态',
1382
+ 'page.capability.multimodalTitle': '只看可输入图像的模型',
1266
1383
  'page.capability.reasoning': '支持推理',
1384
+ 'page.capability.reasoningTitle': '只看公布了推理能力的模型',
1267
1385
  'page.contextLabel': '上下文至少',
1268
- 'page.context.any': '不限',
1386
+ 'page.context.any': '上下文不限',
1269
1387
  'page.context.128k': '128K+',
1270
1388
  'page.context.256k': '256K+',
1271
1389
  'page.context.512k': '512K+',
@@ -1277,15 +1395,15 @@ window.__ModuleLoader__.load({
1277
1395
  'page.filter.other': '其它',
1278
1396
  'group.showAll': '全组显示',
1279
1397
  'group.hideAll': '全组隐藏',
1280
- 'group.showFiltered': '显示筛选结果',
1281
- 'group.hideFiltered': '隐藏筛选结果',
1398
+ 'group.showFiltered': '显示结果',
1399
+ 'group.hideFiltered': '隐藏结果',
1282
1400
  'group.collapseNamed': '折叠 {name} 分组',
1283
1401
  'group.expandNamed': '展开 {name} 分组',
1284
- 'group.capabilities': '{chat} 对话 · {image} 生图 · {video} 生视频 · {other} 其它',
1285
- 'group.probeOk': '中转站 {models} 个模型 · {caps} 个带能力字段',
1286
- 'group.probeNoCaps': '中转站 {models} 个模型 · 未公布能力字段',
1287
- 'group.probeFailed': '中转站读取失败',
1288
- 'group.probeSkipped': '未探测中转站',
1402
+ 'group.probeOk': '{models} 个模型 · {caps} 带能力',
1403
+ 'group.probeNoCaps': '{models} 个模型 · 无能力字段',
1404
+ 'group.probePartial': '价格接口 {models} 个',
1405
+ 'group.probeFailed': '读取失败',
1406
+ 'group.probeSkipped': '未探测',
1289
1407
  'tag.reasoning': '支持推理',
1290
1408
  'tag.noReasoning': '不支持推理',
1291
1409
  'tag.reasoningUnknown': '推理未知',
@@ -1308,7 +1426,6 @@ window.__ModuleLoader__.load({
1308
1426
  'tag.kindVideoTitle': '视频生成模型(输出视频);类型来源:{source}',
1309
1427
  'tag.correction': '官方校正',
1310
1428
  'tag.correctionTitle': '{note}\n来源:{source}',
1311
- 'tag.resolution': '分辨率',
1312
1429
  'tag.resolutionTitle': '模型名标注了 {value} 输出档位',
1313
1430
  'tag.imageInput': '可改图',
1314
1431
  'tag.imageInputTitle': '支持图像输入(图生图 / 参考图)',
@@ -1391,10 +1508,12 @@ window.__ModuleLoader__.load({
1391
1508
  'nav': 'Model visibility',
1392
1509
  'page.title': 'Model visibility',
1393
1510
  'page.description':
1394
- 'Models turned off here disappear from the chat composer model list. Changes apply immediately and can be turned back on at any time. Every model shows its real capabilities: adapter declarations first, then the relay model list, then a built-in catalog; image, video and other generation models are labelled separately.',
1395
- 'page.summary': 'Showing {visible} of {total} models',
1511
+ 'Models turned off here disappear from the chat composer model list. Changes apply immediately and can be turned back on at any time. Every model shows its real capabilities: adapter declarations first, then the relay model list, then verification results or the model name.',
1512
+ 'page.about': 'About',
1513
+ 'page.aboutTitle': 'Show what the tags, sources and colours mean',
1514
+ 'page.summary': 'Showing {visible} of {total}',
1396
1515
  'page.hiddenSummary': '{count} hidden',
1397
- 'page.kindSummary': '{count} image/video models',
1516
+ 'page.kindSummary': '{count} image/video',
1398
1517
  'page.loading': 'Loading model list…',
1399
1518
  'page.empty': 'No models available.',
1400
1519
  'page.filterEmpty': 'No models match this filter.',
@@ -1402,80 +1521,99 @@ window.__ModuleLoader__.load({
1402
1521
  'page.saveFailed': 'Could not save this change. Try again.',
1403
1522
  'page.refresh': 'Refresh',
1404
1523
  'page.refreshing': 'Refreshing…',
1524
+ 'page.refreshTitle': 'Read adapter and relay capabilities again (honours caches; a probe younger than 10 minutes is not repeated)',
1405
1525
  'page.reload': 'Re-fetch capabilities',
1406
1526
  'page.reloading': 'Re-fetching…',
1407
1527
  'page.reloadTitle':
1408
1528
  'Drops this plugin’s capability caches and any in-flight read, then asks the adapter and every relay again. Model switches are untouched; effort levels the adapter does not publish still cannot appear.',
1409
1529
  'page.reloadUnavailable': 'The Host half does not support a hard re-fetch; restart DSH to enable it.',
1410
- 'page.collapseGroups': 'Collapse all groups',
1411
- 'page.expandGroups': 'Expand all groups',
1530
+ 'page.collapseGroups': 'Collapse all',
1531
+ 'page.expandGroups': 'Expand all',
1412
1532
  'page.collapseGroupsTitle': 'Keep only group headers so other groups are easy to reach',
1413
1533
  'page.expandGroupsTitle': 'Expand every collapsed group',
1414
1534
  'page.showAll': 'Show all',
1415
1535
  'page.legend':
1416
- 'Tags: Reasoning = adjustable levels; Context = input window; Multimodal = accepts images. Solid tags come from the adapter, the relay or vendor documentation; dashed tags come from the built-in catalog or the model name. Image, video, embedding and other generation models show only their kind, never effort or text tags.',
1536
+ 'Tags: Reasoning = adjustable levels; Context = input window; Multimodal = accepts images. Solid tags come from the adapter, the relay or vendor documentation; dashed tags come from online verification or the model name. Image, video, embedding and other models show only their kind.',
1417
1537
  'page.metaFallback':
1418
1538
  'Model capability details are unavailable right now: context and modalities show as unknown, while effort levels still come from the shipped catalog.',
1419
1539
  'page.hostStale':
1420
1540
  'The Host half is still the previous version: the colours and switch are live, but image admission and vendor corrections take effect after DSH restarts (or the profile recomposes).',
1421
1541
  'page.providerFailures': 'Some providers could not be read: {names}',
1422
- 'page.probeSummary': 'Relay probe: {ok} model lists read, {caps} published capability fields',
1423
- 'page.probeSummaryNone': 'Relay probe: no readable route configuration',
1424
- 'page.catalogNote': 'Built-in catalog: pi-ai {version} snapshot ({date}), {models} models',
1425
- 'page.learnedNote': 'Learned snapshot: online verification ({date}), {models} verified models',
1426
- 'usage.title': 'Token usage',
1542
+ 'page.probeFailures': 'Relay read failed: {names}',
1543
+ 'page.probeSummary': 'Probe: {ok} relays readable',
1544
+ 'page.probeSummaryTitle': 'Relay probe result; hover a group badge for its diagnostics',
1545
+ 'page.probeSummaryNone': 'Probe: no route configured to probe',
1546
+ 'page.learnedNote': 'Learned {models} facts ({date})',
1547
+ 'usage.title': 'tokens',
1427
1548
  'usage.computing': 'scanning…',
1428
1549
  'usage.none': '—',
1429
- 'usage.range': 'in {input} · out {output} ({total} total)',
1550
+ 'usage.detail': 'input {input} · output {output} · cache read {cacheRead} · cache write {cacheWrite}',
1430
1551
  'usage.today': 'Today',
1552
+ 'usage.week': 'Week',
1553
+ 'usage.month': 'Month',
1554
+ 'usage.total': 'Total',
1431
1555
  'usage.todayTitle': 'Local calendar day',
1432
- 'usage.week': 'This week',
1433
1556
  'usage.weekTitle': 'Since Monday 00:00 local',
1434
- 'usage.month': 'This month',
1435
1557
  'usage.monthTitle': 'Since the 1st local',
1436
- 'usage.total': 'Total',
1437
1558
  'usage.totalTitle': 'All history (older months attributed by session start day)',
1438
- 'usage.exportUsage': 'Export usage CSV',
1559
+ 'usage.exportUsage': 'Usage CSV',
1439
1560
  'usage.exportUsageTitle': 'Download a day-bucketed CSV with per-model breakdown',
1440
- 'usage.exportModels': 'Export model list CSV',
1561
+ 'usage.exportModels': 'Models CSV',
1441
1562
  'usage.exportModelsTitle': 'Download visibility and capability table of every provider model',
1442
1563
  'usage.hostStale': 'The Host half does not provide usage stats yet; restart DSH to enable it.',
1443
- 'checker.title': 'Verify model capabilities online',
1564
+ 'checker.title': 'Online verification',
1444
1565
  'checker.description':
1445
- 'Pick one model as the checker: two public catalogs (models.dev / OpenRouter) are fetched first, then the remaining models are verified in batches — context window, input modalities, effort ladder and kind. Confirmed facts fill this page and, with the switch on, are written into the provider entries; they are also stored in the local learned.json snapshot so a same-named model on another route reuses the cache. Verification spends the checker route quota and its tokens count in the usage strip above.',
1566
+ 'Pick one model as the checker: three public catalogs (models.dev / OpenRouter / Hugging Face) are fetched first, then the remaining models are verified in batches. Whatever the catalogs already know is applied directly, even when the checker has no answer. A sweep spends the checker route quota and its tokens count in the strip above. Note: a thinking model spends the output budget before it answers, so if batches keep failing pick a non-thinking checker or lower its effort.',
1446
1567
  'checker.provider': 'Checker provider',
1447
1568
  'checker.model': 'Checker model',
1448
- 'checker.start': 'Verify all',
1569
+ 'checker.start': 'Start',
1449
1570
  'checker.startTitle': 'Run a background verification sweep in the Host',
1450
1571
  'checker.cancel': 'Cancel',
1451
1572
  'checker.cancelTitle': 'Abort the running sweep',
1452
- 'checker.writeBack': 'Write confirmed facts into provider settings',
1573
+ 'checker.writeBack': 'Also write into provider settings',
1453
1574
  'checker.writeBackTitle':
1454
- 'On: the judged input / contextWindow / efforts are written into each provider entry (learned.json keeps only what cannot be mapped). Off: only this page and the snapshot update; use Edit capabilities to write manually.',
1575
+ 'On: the judged input / contextWindow / efforts are written into each provider entry (learned.json keeps only what cannot be mapped). Off: only this page and the snapshot update.',
1455
1576
  'checker.idle': 'not run yet',
1456
- 'checker.running': 'running {phase} · {done}/{total} · {tokens} spent',
1457
- 'checker.finished': 'finished {done}/{total} · checker spent {tokens} tokens',
1577
+ 'checker.running': 'running{phase} · {done}/{total} · {tokens} spent',
1578
+ 'checker.finished': 'finished {done}/{total} · {tokens} spent',
1458
1579
  'checker.progress': '{done}/{total} verified · checker spent {tokens} tokens',
1459
- 'checker.errors': '{count} failed batches (hover for details)',
1460
- 'checker.pickNeeded': 'Pick a checker provider and model first.',
1461
- 'checker.failed': 'The verify sweep could not start.',
1580
+ 'checker.errorBadge': '{count} failed batches',
1581
+ 'checker.errorTitle': 'Failed batches',
1582
+ 'checker.errorMore': '{count} more not shown',
1583
+ 'checker.concurrency': 'Threads',
1584
+ 'checker.concurrencyTitle':
1585
+ 'How many checker requests may be in flight at once. Lower it if the relay rate-limits you, raise it on a fast link (max 8).',
1586
+ 'checker.concurrencyValue': '{count}',
1587
+ 'checker.threads': '{count} in parallel',
1588
+ 'checker.sourcesTitle': 'Public catalogs',
1589
+ 'checker.source.ok': 'read',
1590
+ 'checker.source.empty': 'no match',
1591
+ 'checker.source.http': 'HTTP error',
1592
+ 'checker.source.failed': 'request failed',
1593
+ 'checker.source.bad-json': 'not JSON',
1594
+ 'checker.source.too-large': 'response too large',
1595
+ 'checker.source.pending': 'pending',
1596
+ 'checker.sourceModels': '{count}',
1597
+ 'checker.publicOnly': '{count} models were resolved from a public catalog alone (the checker had no answer).',
1462
1598
  'checker.hostStale': 'The Host half does not support online verification yet; restart DSH.',
1463
- 'checker.phase.public': '(fetching public catalogs)',
1464
- 'checker.phase.checker': '(fetching public catalogs)',
1465
- 'checker.phase.verifying': '(verifying in batches)',
1599
+ 'checker.phase.public': ' (fetching public catalogs)',
1600
+ 'checker.phase.checker': ' (fetching public catalogs)',
1601
+ 'checker.phase.verifying': ' (verifying in batches)',
1466
1602
  'source.snapshot': 'verified',
1467
1603
  'page.applyInput': 'Let the composer admit images shown as supported',
1604
+ 'page.applyInputShort':
1605
+ 'Adds image input to multimodal relay models; off means the Provider input declaration only',
1468
1606
  'page.applyInputHint':
1469
1607
  'When on, relay chat models labelled Multimodal advertise image input to the request path; when off, only the Provider input declaration counts. Relay (pi-ai) routes only — no configuration file is modified. Turn it off if a particular upstream still rejects images.',
1470
1608
  'page.kindLegend':
1471
- 'Colours separate model kinds: image blue · video green · embedding orange · audio purple · rerank red · moderation rose · translation grey. “Official correction” means the vendor documentation overrides a wrong catalog row.',
1472
- 'page.filterLabel': 'Filter',
1473
- 'page.searchPlaceholder': 'Search model name or ID',
1474
- 'page.capabilityLabel': 'Capabilities',
1609
+ 'Kind colours: image blue · video green · embedding orange · audio purple · rerank red · moderation rose · translation grey. “Official correction” means vendor documentation overrides a wrong catalog row.',
1610
+ 'page.searchPlaceholder': 'Search name or ID',
1475
1611
  'page.capability.multimodal': 'Multimodal',
1612
+ 'page.capability.multimodalTitle': 'Only models that accept image input',
1476
1613
  'page.capability.reasoning': 'Reasoning',
1614
+ 'page.capability.reasoningTitle': 'Only models that publish reasoning support',
1477
1615
  'page.contextLabel': 'Context at least',
1478
- 'page.context.any': 'Any',
1616
+ 'page.context.any': 'Any context',
1479
1617
  'page.context.128k': '128K+',
1480
1618
  'page.context.256k': '256K+',
1481
1619
  'page.context.512k': '512K+',
@@ -1487,15 +1625,15 @@ window.__ModuleLoader__.load({
1487
1625
  'page.filter.other': 'Other',
1488
1626
  'group.showAll': 'Show all',
1489
1627
  'group.hideAll': 'Hide all',
1490
- 'group.showFiltered': 'Show filtered',
1491
- 'group.hideFiltered': 'Hide filtered',
1628
+ 'group.showFiltered': 'Show results',
1629
+ 'group.hideFiltered': 'Hide results',
1492
1630
  'group.collapseNamed': 'Collapse the {name} group',
1493
1631
  'group.expandNamed': 'Expand the {name} group',
1494
- 'group.capabilities': '{chat} chat · {image} image · {video} video · {other} other',
1495
- 'group.probeOk': 'relay: {models} models · {caps} with capability fields',
1496
- 'group.probeNoCaps': 'relay: {models} models · no capability fields',
1497
- 'group.probeFailed': 'relay read failed',
1498
- 'group.probeSkipped': 'relay not probed',
1632
+ 'group.probeOk': '{models} models · {caps} with caps',
1633
+ 'group.probeNoCaps': '{models} models · no cap fields',
1634
+ 'group.probePartial': 'pricing {models} models',
1635
+ 'group.probeFailed': 'read failed',
1636
+ 'group.probeSkipped': 'not probed',
1499
1637
  'tag.reasoning': 'Reasoning',
1500
1638
  'tag.noReasoning': 'No reasoning',
1501
1639
  'tag.reasoningUnknown': 'Reasoning unknown',
@@ -1518,7 +1656,6 @@ window.__ModuleLoader__.load({
1518
1656
  'tag.kindVideoTitle': 'Video generation model (outputs video); kind source: {source}',
1519
1657
  'tag.correction': 'Official correction',
1520
1658
  'tag.correctionTitle': '{note}\nSource: {source}',
1521
- 'tag.resolution': 'Resolution',
1522
1659
  'tag.resolutionTitle': 'The model name advertises a {value} output tier',
1523
1660
  'tag.imageInput': 'Image edit',
1524
1661
  'tag.imageInputTitle': 'Accepts image input (image-to-image / reference images)',
@@ -1704,9 +1841,16 @@ window.__ModuleLoader__.load({
1704
1841
  return sources && typeof sources[field] === 'string' ? sources[field] : 'unknown'
1705
1842
  }
1706
1843
 
1707
- /** Catalog and name heuristics are completed values, not declarations. */
1844
+ /**
1845
+ * Sources whose values are completed or inferred rather than declared.
1846
+ *
1847
+ * `snapshot` is the learned store: facts a checker model or a public catalog
1848
+ * reported. Only the adapter, the settings document, the relay's own listing
1849
+ * and a vendor correction count as declarations, so a verified row must stay
1850
+ * dashed — otherwise an LLM guess would look as trustworthy as a declaration.
1851
+ */
1708
1852
  function isInferredSource(source) {
1709
- return source === 'catalog' || source === 'inferred'
1853
+ return source === 'snapshot' || source === 'inferred'
1710
1854
  }
1711
1855
 
1712
1856
  /** Kind with a safe fallback for records from an older Host. */
@@ -2125,6 +2269,8 @@ window.__ModuleLoader__.load({
2125
2269
  * @module client-src/pure
2126
2270
  */
2127
2271
 
2272
+ const { bucketOf, kindOf } = require("format")
2273
+
2128
2274
  /** Number of one bucket's token sum, guarding partial buckets. */
2129
2275
  function bucketTotalOf(buckets) {
2130
2276
  if (!Array.isArray(buckets)) return 0
@@ -2199,11 +2345,9 @@ window.__ModuleLoader__.load({
2199
2345
  }
2200
2346
 
2201
2347
  function bucket(model) {
2202
- const kind = model && typeof model.kind === 'string' && model.kind !== '' ? model.kind : 'chat'
2203
- if (kind === 'image') return 'image'
2204
- if (kind === 'video') return 'video'
2205
- if (kind === 'chat') return 'chat'
2206
- return 'other'
2348
+ // One implementation lives in `format.mjs` as `bucketOf`; duplicating the
2349
+ // four branches here meant the filter and the tag colours could drift apart.
2350
+ return bucketOf(kindOf(model))
2207
2351
  }
2208
2352
 
2209
2353
  /** Counts per kind bucket for the whole list. */
@@ -2224,7 +2368,11 @@ window.__ModuleLoader__.load({
2224
2368
  return routes
2225
2369
  }
2226
2370
 
2227
- /** Compact label + tooltip for one provider probe diagnostic. */
2371
+ /**
2372
+ * Compact label + tooltip for one provider probe diagnostic.
2373
+ * @returns `{ label, title, failed? }` or null; `failed` marks a route the page
2374
+ * should warn about instead of quietly annotating.
2375
+ */
2228
2376
  function probeOf(state, group, t) {
2229
2377
  const list = Array.isArray(state.providers) ? state.providers : []
2230
2378
  const diagnostic = list.find((item) => item && item.id === group.id)
@@ -2232,6 +2380,8 @@ window.__ModuleLoader__.load({
2232
2380
  const details = [
2233
2381
  diagnostic.host ? 'host: ' + diagnostic.host : null,
2234
2382
  diagnostic.status ? 'status: ' + diagnostic.status : null,
2383
+ diagnostic.models ? 'models: ' + diagnostic.models : null,
2384
+ diagnostic.capabilities ? 'capabilities: ' + diagnostic.capabilities : null,
2235
2385
  diagnostic.message ? 'message: ' + diagnostic.message : null,
2236
2386
  diagnostic.pricing && diagnostic.pricing !== 'skipped' ? 'pricing: ' + diagnostic.pricing : null,
2237
2387
  ]
@@ -2249,8 +2399,19 @@ window.__ModuleLoader__.load({
2249
2399
  title: details,
2250
2400
  }
2251
2401
  }
2402
+ // The listing failed but the pricing endpoint still enumerated the route.
2403
+ if (diagnostic.status === 'partial') {
2404
+ return {
2405
+ label: t('group.probePartial', { models: diagnostic.models ?? 0 }),
2406
+ title: details,
2407
+ }
2408
+ }
2252
2409
  if (diagnostic.status === 'failed' || diagnostic.status === 'unauthorized') {
2253
- return { label: t('group.probeFailed'), title: details }
2410
+ return {
2411
+ label: t('group.probeFailed'),
2412
+ title: details,
2413
+ failed: true,
2414
+ }
2254
2415
  }
2255
2416
  return { label: t('group.probeSkipped'), title: details }
2256
2417
  }
@@ -2272,6 +2433,7 @@ window.__ModuleLoader__.load({
2272
2433
  const React = require('react')
2273
2434
  const h = React.createElement
2274
2435
 
2436
+ const { FIELD } = require("constants")
2275
2437
  const { keyOf, kindOf, isGenerationKind, providerLabel, countModels, readCollapsedGroups, persistCollapsedGroups, overrideKeyOf } = require("format")
2276
2438
  const { computeFilteredGroups, countKinds, probeOf } = require("pure")
2277
2439
  const { capabilityTags, tagClassOf } = require("tags")
@@ -2280,6 +2442,33 @@ window.__ModuleLoader__.load({
2280
2442
  const { UsageToolbar } = require("usage-view")
2281
2443
  const { VerifyPanel } = require("checker-view")
2282
2444
 
2445
+ /** Browser-local key for the "about this page" disclosure. */
2446
+ const ABOUT_KEY = 'dsh-model-visibility:about-open:v1'
2447
+
2448
+ function readAboutOpen() {
2449
+ try {
2450
+ return window.localStorage.getItem(ABOUT_KEY) === '1'
2451
+ } catch (_error) {
2452
+ return false
2453
+ }
2454
+ }
2455
+
2456
+ function persistAboutOpen(open) {
2457
+ try {
2458
+ window.localStorage.setItem(ABOUT_KEY, open ? '1' : '0')
2459
+ } catch (_error) {
2460
+ // Storage is optional for a preference like this.
2461
+ }
2462
+ }
2463
+
2464
+ /**
2465
+ * The settings page.
2466
+ *
2467
+ * Layout, in the order a user needs it: a one-line summary with the two
2468
+ * primary actions, the token strip, the filter bar, the models, then the
2469
+ * diagnostics — and behind a disclosure the long explanation that used to
2470
+ * occupy a third of the viewport before the first model row.
2471
+ */
2283
2472
  function makeSection(form, catalog, overrides, usage, verify) {
2284
2473
  return function ModelVisibilitySection({ t }) {
2285
2474
  const { hidden, snapshot } = useHidden(form)
@@ -2290,6 +2479,7 @@ window.__ModuleLoader__.load({
2290
2479
  const [refreshing, setRefreshing] = React.useState(false)
2291
2480
  const [reloading, setReloading] = React.useState(false)
2292
2481
  const [collapsed, setCollapsed] = React.useState(readCollapsedGroups)
2482
+ const [aboutOpen, setAboutOpen] = React.useState(readAboutOpen)
2293
2483
  const [editing, setEditing] = React.useState(null)
2294
2484
  const [providerNote, setProviderNote] = React.useState(null)
2295
2485
 
@@ -2339,16 +2529,18 @@ window.__ModuleLoader__.load({
2339
2529
  if (disabled) return
2340
2530
  setBusy(true)
2341
2531
  setError(null)
2342
- Promise.resolve(form.set(field, value)).then(
2343
- (accepted) => {
2344
- if (accepted === false) setError(t('page.saveFailed'))
2345
- },
2346
- () => {
2347
- setError(t('page.saveFailed'))
2348
- },
2349
- ).finally(() => {
2350
- setBusy(false)
2351
- })
2532
+ Promise.resolve(form.set(field, value))
2533
+ .then(
2534
+ (accepted) => {
2535
+ if (accepted === false) setError(t('page.saveFailed'))
2536
+ },
2537
+ () => {
2538
+ setError(t('page.saveFailed'))
2539
+ },
2540
+ )
2541
+ .finally(() => {
2542
+ setBusy(false)
2543
+ })
2352
2544
  }
2353
2545
 
2354
2546
  const commit = (next) => writeField(FIELD, Array.from(next))
@@ -2389,7 +2581,7 @@ window.__ModuleLoader__.load({
2389
2581
  // A hard reload may supersede a soft refresh that is still running;
2390
2582
  // catalog.load() gives it a new run and discards the older result.
2391
2583
  const reloadCatalog = () => {
2392
- if (busy || reloading) return
2584
+ if (busy || reloading || refreshing) return
2393
2585
  runCatalogAction(() => catalog.reload(), setReloading)
2394
2586
  }
2395
2587
 
@@ -2412,24 +2604,31 @@ window.__ModuleLoader__.load({
2412
2604
  })
2413
2605
  }
2414
2606
 
2607
+ const toggleAbout = () => {
2608
+ setAboutOpen((previous) => {
2609
+ persistAboutOpen(!previous)
2610
+ return !previous
2611
+ })
2612
+ }
2613
+
2614
+ /* ------------------------------------------------------------- groups */
2615
+
2415
2616
  const rows = []
2416
2617
  for (const group of filteredGroups) {
2417
2618
  const allGroupModels =
2418
2619
  (groups.find((candidate) => candidate.id === group.id) || group).models || []
2419
2620
  const models = group.models || []
2420
- const shown = allGroupModels.filter(
2421
- (model) => !hidden.has(keyOf(group.id, model.id)),
2422
- ).length
2621
+ const shown = allGroupModels.filter((model) => !hidden.has(keyOf(group.id, model.id))).length
2423
2622
  const allShown = models.every((model) => !hidden.has(keyOf(group.id, model.id)))
2424
2623
  const kinds = countKinds(allGroupModels)
2425
2624
  const kindParts = []
2426
- if (kinds.chat > 0) kindParts.push(['chat', t('page.filter.chat') + ' ' + kinds.chat])
2427
- if (kinds.image > 0) kindParts.push(['image', t('page.filter.image') + ' ' + kinds.image])
2428
- if (kinds.video > 0) kindParts.push(['video', t('page.filter.video') + ' ' + kinds.video])
2429
- if (kinds.other > 0) kindParts.push(['other', t('page.filter.other') + ' ' + kinds.other])
2625
+ if (kinds.chat > 0) kindParts.push('chat')
2626
+ if (kinds.image > 0) kindParts.push('image')
2627
+ if (kinds.video > 0) kindParts.push('video')
2628
+ if (kinds.other > 0) kindParts.push('other')
2430
2629
  const kindNodes = []
2431
- for (const [index, [kindKey, label]] of kindParts.entries()) {
2432
- if (index > 0) kindNodes.push(' · ')
2630
+ for (const [index, kindKey] of kindParts.entries()) {
2631
+ if (index > 0) kindNodes.push(h('span', { key: 'sep-' + kindKey, className: 'mvp_sep' }, '·'))
2433
2632
  kindNodes.push(
2434
2633
  h(
2435
2634
  'span',
@@ -2438,8 +2637,9 @@ window.__ModuleLoader__.load({
2438
2637
  className:
2439
2638
  'mvp_kindCount' +
2440
2639
  (kindKey === 'chat' ? '' : ' mvp_kindCount--' + kindKey),
2640
+ title: t('page.filter.' + kindKey) + ' ' + kinds[kindKey],
2441
2641
  },
2442
- label,
2642
+ kinds[kindKey],
2443
2643
  ),
2444
2644
  )
2445
2645
  }
@@ -2457,34 +2657,37 @@ window.__ModuleLoader__.load({
2457
2657
  'div',
2458
2658
  { className: 'mvp_groupHead' },
2459
2659
  h(
2460
- 'div',
2461
- { className: 'mvp_groupTitle' },
2462
- h(
2463
- 'button',
2464
- {
2465
- type: 'button',
2466
- className: 'mvp_collapse',
2467
- 'aria-expanded': !collapsedNow,
2468
- 'aria-label': collapseLabel,
2469
- title: collapseLabel,
2470
- onClick: () => toggleGroupCollapsed(group.id),
2471
- },
2472
- collapsedNow ? '▸' : '▾',
2473
- ),
2660
+ 'button',
2661
+ {
2662
+ type: 'button',
2663
+ className: 'mvp_groupTitle',
2664
+ 'aria-expanded': !collapsedNow,
2665
+ 'aria-label': collapseLabel,
2666
+ title: collapseLabel,
2667
+ onClick: () => toggleGroupCollapsed(group.id),
2668
+ },
2669
+ h('span', { className: 'mvp_chevron' }, collapsedNow ? '▸' : '▾'),
2474
2670
  h('span', { className: 'mvp_groupName' }, groupName),
2475
2671
  h('span', { className: 'mvp_groupCount' }, shown + '/' + allGroupModels.length),
2476
2672
  kindParts.length > 0
2477
2673
  ? h('span', { className: 'mvp_groupCap' }, kindNodes)
2478
2674
  : null,
2479
2675
  probe !== null
2480
- ? h('span', { className: 'mvp_groupProbe', title: probe.title || undefined }, probe.label)
2676
+ ? h(
2677
+ 'span',
2678
+ {
2679
+ className: 'mvp_groupProbe' + (probe.failed === true ? ' mvp_groupProbe--warn' : ''),
2680
+ title: probe.title || undefined,
2681
+ },
2682
+ probe.label,
2683
+ )
2481
2684
  : null,
2482
2685
  ),
2483
2686
  h(
2484
2687
  'button',
2485
2688
  {
2486
2689
  type: 'button',
2487
- className: 'mvp_button',
2690
+ className: 'mvp_button mvp_button--quiet',
2488
2691
  disabled: disabled,
2489
2692
  onClick: () => setGroup(group, models, !allShown),
2490
2693
  },
@@ -2516,15 +2719,20 @@ window.__ModuleLoader__.load({
2516
2719
  className:
2517
2720
  'mvp_row' +
2518
2721
  (generation ? ' mvp_row--gen' : '') +
2722
+ (shownModel ? '' : ' mvp_row--off') +
2519
2723
  kindTint,
2520
2724
  },
2521
2725
  h(
2522
2726
  'div',
2523
2727
  { className: 'mvp_rowText' },
2524
- h('div', { className: 'mvp_rowTitle' }, model.name),
2525
- model.id === model.name
2526
- ? null
2527
- : h('div', { className: 'mvp_rowMeta' }, model.id),
2728
+ h(
2729
+ 'div',
2730
+ { className: 'mvp_rowHead' },
2731
+ h('span', { className: 'mvp_rowTitle' }, model.name),
2732
+ model.id === model.name
2733
+ ? null
2734
+ : h('span', { className: 'mvp_rowMeta' }, model.id),
2735
+ ),
2528
2736
  h(
2529
2737
  'div',
2530
2738
  { className: 'mvp_tags' },
@@ -2549,7 +2757,7 @@ window.__ModuleLoader__.load({
2549
2757
  'button',
2550
2758
  {
2551
2759
  type: 'button',
2552
- className: 'mvp_button mvp_editButton',
2760
+ className: 'mvp_button mvp_button--quiet mvp_editButton',
2553
2761
  'aria-pressed': editing === rowKey,
2554
2762
  'aria-label': t('row.edit') + ': ' + model.name,
2555
2763
  title: t('row.editTitle'),
@@ -2597,9 +2805,13 @@ window.__ModuleLoader__.load({
2597
2805
  )
2598
2806
  }
2599
2807
 
2808
+ /* ------------------------------------------------------------- status */
2809
+
2600
2810
  const loading = state.status === 'loading' || state.status === 'idle'
2601
2811
  const providerList = Array.isArray(state.providers) ? state.providers : []
2602
- const probeOk = providerList.filter((provider) => provider && provider.status === 'ok').length
2812
+ const probeOk = providerList.filter(
2813
+ (provider) => provider && (provider.status === 'ok' || provider.status === 'partial'),
2814
+ ).length
2603
2815
  const probeCaps = providerList.reduce(
2604
2816
  (count, provider) => count + (Number(provider && provider.capabilities) || 0),
2605
2817
  0,
@@ -2610,10 +2822,7 @@ window.__ModuleLoader__.load({
2610
2822
  ? t('page.probeSummary', { ok: probeOk, caps: probeCaps })
2611
2823
  : t('page.probeSummaryNone')
2612
2824
  : null
2613
- // A page refresh can deliver this bundle while the Host half is still
2614
- // the previous version; the switch would look live but do nothing.
2615
2825
  const hostStale = state.source === 'host' && !hostFeatures.includes('applyInput')
2616
- // A hard reload is only meaningful when the Host half understands it.
2617
2826
  const canReload = state.source === 'host' && hostFeatures.includes('reload')
2618
2827
  const anyCollapsed = groups.some((group) => collapsed.has(group.id))
2619
2828
  const learnedDate =
@@ -2638,54 +2847,52 @@ window.__ModuleLoader__.load({
2638
2847
  ]
2639
2848
  const hasActiveFilters =
2640
2849
  filter !== 'all' || normalizedQuery !== '' || multimodalOnly || reasoningOnly || minContext > 0
2850
+ // A relay that answered with a diagnostic is worth surfacing next to the
2851
+ // group, but only failed routes belong in the page-level warning list.
2852
+ const failedRoutes = providerList.filter(
2853
+ (provider) => provider && (provider.status === 'failed' || provider.status === 'unauthorized'),
2854
+ )
2855
+ const summaryText =
2856
+ total === 0
2857
+ ? loading
2858
+ ? t('page.loading')
2859
+ : t('page.empty')
2860
+ : t('page.summary', { visible: total - hiddenCount, total })
2861
+
2862
+ /* ------------------------------------------------------------- render */
2641
2863
 
2642
2864
  return h(
2643
2865
  'div',
2644
2866
  { className: 'mvp_page' },
2645
- h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
2867
+ // Title + summary + the two actions that are worth a permanent slot.
2646
2868
  h(
2647
2869
  'div',
2648
2870
  { className: 'mvp_head' },
2649
- h('h2', { className: 'mvp_title' }, t('page.title')),
2650
- h('p', { className: 'mvp_desc' }, t('page.description')),
2651
- ),
2652
- h(
2653
- 'div',
2654
- { className: 'mvp_toolbar' },
2655
2871
  h(
2656
2872
  'div',
2657
- { className: 'mvp_summary' },
2658
- total === 0
2659
- ? loading
2660
- ? t('page.loading')
2661
- : t('page.empty')
2662
- : t('page.summary', { visible: total - hiddenCount, total }) +
2663
- (hiddenCount > 0 ? ' · ' + t('page.hiddenSummary', { count: hiddenCount }) : '') +
2664
- (generationCount > 0 ? ' · ' + t('page.kindSummary', { count: generationCount }) : ''),
2873
+ { className: 'mvp_headMain' },
2874
+ h('h2', { className: 'mvp_title' }, t('page.title')),
2875
+ h('span', { className: 'mvp_summary' }, summaryText),
2876
+ hiddenCount > 0
2877
+ ? h('span', { className: 'mvp_summary mvp_summary--dim' }, t('page.hiddenSummary', { count: hiddenCount }))
2878
+ : null,
2879
+ generationCount > 0
2880
+ ? h('span', { className: 'mvp_summary mvp_summary--dim' }, t('page.kindSummary', { count: generationCount }))
2881
+ : null,
2665
2882
  ),
2666
2883
  h(
2667
2884
  'div',
2668
- { className: 'mvp_actions' },
2885
+ { className: 'mvp_headActions' },
2669
2886
  h(
2670
2887
  'button',
2671
2888
  {
2672
2889
  type: 'button',
2673
- className: 'mvp_button',
2674
- disabled: disabled || hiddenCount === 0,
2675
- onClick: () => commit(new Set()),
2676
- },
2677
- t('page.showAll'),
2678
- ),
2679
- h(
2680
- 'button',
2681
- {
2682
- type: 'button',
2683
- className: 'mvp_button',
2684
- disabled: groups.length === 0,
2685
- title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
2686
- onClick: toggleAllCollapsed,
2890
+ className: 'mvp_button mvp_button--quiet',
2891
+ 'aria-pressed': aboutOpen,
2892
+ title: t('page.aboutTitle'),
2893
+ onClick: toggleAbout,
2687
2894
  },
2688
- t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
2895
+ t('page.about') + (aboutOpen ? ' ▴' : ' ▾'),
2689
2896
  ),
2690
2897
  h(
2691
2898
  'button',
@@ -2693,6 +2900,7 @@ window.__ModuleLoader__.load({
2693
2900
  type: 'button',
2694
2901
  className: 'mvp_button',
2695
2902
  disabled: busy || refreshing || reloading,
2903
+ title: t('page.refreshTitle'),
2696
2904
  onClick: refreshCatalog,
2697
2905
  },
2698
2906
  t(refreshing ? 'page.refreshing' : 'page.refresh'),
@@ -2710,63 +2918,77 @@ window.__ModuleLoader__.load({
2710
2918
  ),
2711
2919
  ),
2712
2920
  ),
2921
+ aboutOpen
2922
+ ? h(
2923
+ 'div',
2924
+ { className: 'mvp_about' },
2925
+ h('p', { className: 'mvp_aboutP' }, t('page.description')),
2926
+ h('p', { className: 'mvp_aboutP' }, t('page.legend')),
2927
+ h('p', { className: 'mvp_aboutP' }, t('page.kindLegend')),
2928
+ h('p', { className: 'mvp_aboutP' }, t('page.manualNote')),
2929
+ )
2930
+ : null,
2931
+ h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
2713
2932
  h(
2714
2933
  'div',
2715
2934
  { className: 'mvp_filters' },
2716
- h('label', { className: 'mvp_search' },
2717
- h('span', { className: 'mvp_filterLabel' }, t('page.searchPlaceholder')),
2718
- h('input', {
2719
- className: 'mvp_searchInput',
2720
- type: 'search',
2721
- value: query,
2722
- placeholder: t('page.searchPlaceholder'),
2723
- 'aria-label': t('page.searchPlaceholder'),
2724
- onInput: (event) => setQuery(event.target.value),
2725
- }),
2935
+ h('input', {
2936
+ className: 'mvp_searchInput',
2937
+ type: 'search',
2938
+ value: query,
2939
+ placeholder: t('page.searchPlaceholder'),
2940
+ 'aria-label': t('page.searchPlaceholder'),
2941
+ onInput: (event) => setQuery(event.target.value),
2942
+ }),
2943
+ h(
2944
+ 'div',
2945
+ { className: 'mvp_filterGroup' },
2946
+ filterOptions.map(([value, label, count]) =>
2947
+ h(
2948
+ 'button',
2949
+ {
2950
+ key: value,
2951
+ type: 'button',
2952
+ className: 'mvp_filter',
2953
+ 'aria-pressed': filter === value,
2954
+ onClick: () => setFilter(value),
2955
+ },
2956
+ label + ' ' + count,
2957
+ ),
2958
+ ),
2726
2959
  ),
2727
- h('span', { className: 'mvp_filterLabel' }, t('page.filterLabel')),
2728
- filterOptions.map(([value, label, count]) =>
2960
+ h(
2961
+ 'div',
2962
+ { className: 'mvp_filterGroup' },
2729
2963
  h(
2730
2964
  'button',
2731
2965
  {
2732
- key: value,
2733
2966
  type: 'button',
2734
2967
  className: 'mvp_filter',
2735
- 'aria-pressed': filter === value,
2736
- onClick: () => setFilter(value),
2968
+ 'aria-pressed': multimodalOnly,
2969
+ title: t('page.capability.multimodalTitle'),
2970
+ onClick: () => setMultimodalOnly(!multimodalOnly),
2737
2971
  },
2738
- label + ' ' + count,
2972
+ t('page.capability.multimodal'),
2973
+ ),
2974
+ h(
2975
+ 'button',
2976
+ {
2977
+ type: 'button',
2978
+ className: 'mvp_filter',
2979
+ 'aria-pressed': reasoningOnly,
2980
+ title: t('page.capability.reasoningTitle'),
2981
+ onClick: () => setReasoningOnly(!reasoningOnly),
2982
+ },
2983
+ t('page.capability.reasoning'),
2739
2984
  ),
2740
- ),
2741
- h('span', { className: 'mvp_filterLabel' }, t('page.capabilityLabel')),
2742
- h(
2743
- 'button',
2744
- {
2745
- type: 'button',
2746
- className: 'mvp_filter',
2747
- 'aria-pressed': multimodalOnly,
2748
- onClick: () => setMultimodalOnly(!multimodalOnly),
2749
- },
2750
- t('page.capability.multimodal'),
2751
- ),
2752
- h(
2753
- 'button',
2754
- {
2755
- type: 'button',
2756
- className: 'mvp_filter',
2757
- 'aria-pressed': reasoningOnly,
2758
- onClick: () => setReasoningOnly(!reasoningOnly),
2759
- },
2760
- t('page.capability.reasoning'),
2761
- ),
2762
- h('label', { className: 'mvp_context' },
2763
- h('span', { className: 'mvp_filterLabel' }, t('page.contextLabel')),
2764
2985
  h(
2765
2986
  'select',
2766
2987
  {
2767
2988
  className: 'mvp_contextSelect',
2768
2989
  value: String(minContext),
2769
2990
  'aria-label': t('page.contextLabel'),
2991
+ title: t('page.contextLabel'),
2770
2992
  onChange: (event) => setMinContext(Number(event.target.value)),
2771
2993
  },
2772
2994
  [
@@ -2778,6 +3000,31 @@ window.__ModuleLoader__.load({
2778
3000
  ].map(([value, label]) => h('option', { key: value, value: String(value) }, t(label))),
2779
3001
  ),
2780
3002
  ),
3003
+ h(
3004
+ 'div',
3005
+ { className: 'mvp_filterGroup mvp_filterGroup--push' },
3006
+ h(
3007
+ 'button',
3008
+ {
3009
+ type: 'button',
3010
+ className: 'mvp_button mvp_button--quiet',
3011
+ disabled: disabled || hiddenCount === 0,
3012
+ onClick: () => commit(new Set()),
3013
+ },
3014
+ t('page.showAll'),
3015
+ ),
3016
+ h(
3017
+ 'button',
3018
+ {
3019
+ type: 'button',
3020
+ className: 'mvp_button mvp_button--quiet',
3021
+ disabled: groups.length === 0,
3022
+ title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
3023
+ onClick: toggleAllCollapsed,
3024
+ },
3025
+ t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
3026
+ ),
3027
+ ),
2781
3028
  ),
2782
3029
  h(
2783
3030
  'div',
@@ -2790,6 +3037,7 @@ window.__ModuleLoader__.load({
2790
3037
  className: 'mvp_switch',
2791
3038
  'aria-checked': applyInput,
2792
3039
  'aria-label': t('page.applyInput'),
3040
+ 'aria-describedby': 'mvp-apply-hint',
2793
3041
  disabled: disabled,
2794
3042
  onClick: () => commitApply(!applyInput),
2795
3043
  },
@@ -2798,29 +3046,33 @@ window.__ModuleLoader__.load({
2798
3046
  h(
2799
3047
  'div',
2800
3048
  { className: 'mvp_applyText' },
2801
- h('div', { className: 'mvp_applyTitle' }, t('page.applyInput')),
2802
- h('div', { className: 'mvp_applyHint' }, t('page.applyInputHint')),
3049
+ h('span', { className: 'mvp_applyTitle' }, t('page.applyInput')),
3050
+ h('span', { className: 'mvp_applyHint', id: 'mvp-apply-hint' }, t('page.applyInputShort')),
2803
3051
  ),
2804
3052
  ),
2805
- h(
2806
- VerifyPanel,
2807
- { verify, catalog: state, t, canUse: canVerify },
2808
- ),
2809
- h('div', { className: 'mvp_legend' }, t('page.legend')),
2810
- h('div', { className: 'mvp_legend' }, t('page.kindLegend')),
2811
- h('div', { className: 'mvp_legend' }, t('page.manualNote')),
2812
- overrideState.available === false
2813
- ? h('div', { className: 'mvp_fallback' }, t('override.unavailable'))
3053
+ failedRoutes.length > 0
3054
+ ? h(
3055
+ 'div',
3056
+ {
3057
+ className: 'mvp_banner mvp_banner--warn',
3058
+ title: failedRoutes
3059
+ .map((route) => (route.name || route.id) + ': ' + (route.message || route.status))
3060
+ .join('\n'),
3061
+ },
3062
+ t('page.probeFailures', {
3063
+ names: failedRoutes.map((route) => route.name || route.id).join(' · '),
3064
+ }),
3065
+ )
2814
3066
  : null,
3067
+ hostStale ? h('div', { className: 'mvp_banner' }, t('page.hostStale')) : null,
2815
3068
  state.detailsError !== null && state.source === 'catalog'
2816
- ? h('div', { className: 'mvp_fallback' }, t('page.metaFallback'))
3069
+ ? h('div', { className: 'mvp_banner' }, t('page.metaFallback'))
2817
3070
  : null,
2818
- hostStale ? h('div', { className: 'mvp_fallback' }, t('page.hostStale')) : null,
2819
3071
  state.failures.length > 0
2820
3072
  ? h(
2821
3073
  'div',
2822
3074
  {
2823
- className: 'mvp_fallback',
3075
+ className: 'mvp_banner mvp_banner--warn',
2824
3076
  title: state.failures
2825
3077
  .map((failure) => (failure.name || failure.id) + ': ' + failure.message)
2826
3078
  .join('\n'),
@@ -2832,20 +3084,34 @@ window.__ModuleLoader__.load({
2832
3084
  }),
2833
3085
  )
2834
3086
  : null,
2835
- error !== null ? h('div', { className: 'mvp_error', role: 'alert' }, error) : null,
3087
+ overrideState.available === false
3088
+ ? h('div', { className: 'mvp_banner' }, t('override.unavailable'))
3089
+ : null,
3090
+ error !== null ? h('div', { className: 'mvp_banner mvp_banner--error', role: 'alert' }, error) : null,
2836
3091
  state.error !== null
2837
- ? h('div', { className: 'mvp_error', role: 'alert' }, t('page.error', { message: state.error }))
3092
+ ? h(
3093
+ 'div',
3094
+ { className: 'mvp_banner mvp_banner--error', role: 'alert' },
3095
+ t('page.error', { message: state.error }),
3096
+ )
2838
3097
  : null,
2839
- rows.length > 0 ? rows : null,
3098
+ h('div', { className: 'mvp_groups' }, rows),
2840
3099
  total > 0 && hasActiveFilters && rows.length === 0
2841
3100
  ? h('div', { className: 'mvp_note' }, t('page.filterEmpty'))
2842
3101
  : null,
2843
3102
  rows.length === 0 && !loading && state.error === null && total === 0
2844
- ? h('div', { className: 'mvp_error' }, t('page.empty'))
3103
+ ? h('div', { className: 'mvp_note' }, t('page.empty'))
2845
3104
  : null,
2846
- probeNote !== null ? h('div', { className: 'mvp_note' }, probeNote) : null,
3105
+ h(
3106
+ 'div',
3107
+ { className: 'mvp_footer' },
3108
+ probeNote !== null
3109
+ ? h('span', { className: 'mvp_footerItem', title: t('page.probeSummaryTitle') }, probeNote)
3110
+ : null,
3111
+ catalogNote !== null ? h('span', { className: 'mvp_footerItem' }, catalogNote) : null,
3112
+ ),
2847
3113
  providerNote !== null ? h('div', { className: 'mvp_note' }, providerNote) : null,
2848
- catalogNote !== null ? h('div', { className: 'mvp_note' }, catalogNote) : null,
3114
+ h(VerifyPanel, { verify, catalog: state, t, canUse: canVerify }),
2849
3115
  )
2850
3116
  }
2851
3117
  }
@@ -3060,13 +3326,11 @@ window.__ModuleLoader__.load({
3060
3326
  // -------------------------------------------------------- usage-view
3061
3327
  define("usage-view", function (require, module, exports) {
3062
3328
  /**
3063
- * Token-usage strip with the one-click exports.
3329
+ * Token-usage strip with the two CSV exports.
3064
3330
  *
3065
- * The strip renders the ledger's today/week/month/total buckets and links the
3066
- * two CSV endpoints. It is an inner piece of the settings section: the section
3067
- * supplies the translator and the usage store instance.
3068
- *
3069
- * @module client-src/usage-view
3331
+ * One line, four numbers, two icon-sized buttons: the strip is context, not
3332
+ * the point of the page, so it must not push the model list down. Exact
3333
+ * numbers live in the tooltip; the visible figure is the total.
3070
3334
  */
3071
3335
 
3072
3336
  const React = require('react')
@@ -3076,15 +3340,22 @@ window.__ModuleLoader__.load({
3076
3340
  const { formatTokens } = require("pure")
3077
3341
  const { useSource } = require("hooks")
3078
3342
 
3079
- /** Render one [in, out] window as "输入 x · 输出 y". */
3080
- function rangeText(range, t) {
3081
- if (!Array.isArray(range) || range.every((value) => value === 0)) return t('usage.none')
3343
+ /** Total of one bucket tuple (input + output + cache read + cache write). */
3344
+ function totalOf(range) {
3345
+ if (!Array.isArray(range)) return 0
3346
+ return range.reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0)
3347
+ }
3348
+
3349
+ /** Detail line for one window: input / output / cache split. */
3350
+ function detailOf(range, t) {
3351
+ if (!Array.isArray(range) || totalOf(range) === 0) return t('usage.none')
3082
3352
  const input = (range[0] ?? 0) + (range[2] ?? 0) + (range[3] ?? 0)
3083
3353
  const output = range[1] ?? 0
3084
- return t('usage.range', {
3354
+ return t('usage.detail', {
3085
3355
  input: formatTokens(input),
3086
3356
  output: formatTokens(output),
3087
- total: formatTokens(input + output),
3357
+ cacheRead: formatTokens(range[2] ?? 0),
3358
+ cacheWrite: formatTokens(range[3] ?? 0),
3088
3359
  })
3089
3360
  }
3090
3361
 
@@ -3093,6 +3364,7 @@ window.__ModuleLoader__.load({
3093
3364
  const anchor = document.createElement('a')
3094
3365
  anchor.href = href
3095
3366
  anchor.rel = 'noopener'
3367
+ anchor.download = ''
3096
3368
  document.body.appendChild(anchor)
3097
3369
  anchor.click()
3098
3370
  anchor.remove()
@@ -3103,71 +3375,69 @@ window.__ModuleLoader__.load({
3103
3375
  const supported = features.length === 0 || features.includes('usage')
3104
3376
  const ranges = state.snapshot?.ranges
3105
3377
  const computing = state.snapshot?.computing === true
3378
+
3379
+ if (!supported) {
3380
+ return h('div', { className: 'mvp_usage mvp_usage--off' }, t('usage.hostStale'))
3381
+ }
3382
+
3383
+ const windows = [
3384
+ ['today', 'usage.today', 'usage.todayTitle'],
3385
+ ['week', 'usage.week', 'usage.weekTitle'],
3386
+ ['month', 'usage.month', 'usage.monthTitle'],
3387
+ ['total', 'usage.total', 'usage.totalTitle'],
3388
+ ]
3389
+
3106
3390
  return h(
3107
3391
  'div',
3108
- { className: 'mvp_usage' + (supported ? '' : ' mvp_usage--off') },
3392
+ { className: 'mvp_usage' },
3109
3393
  h(
3110
- 'div',
3111
- { className: 'mvp_usageHead' },
3112
- h('span', { className: 'mvp_usageTitle' }, t('usage.title')),
3394
+ 'span',
3395
+ { className: 'mvp_usageLabel' },
3396
+ t('usage.title'),
3113
3397
  computing ? h('span', { className: 'mvp_usageComputing' }, t('usage.computing')) : null,
3114
- h(
3115
- 'span',
3116
- { className: 'mvp_usageActions' },
3117
- h(
3118
- 'button',
3119
- {
3120
- type: 'button',
3121
- className: 'mvp_button',
3122
- title: t('usage.exportUsageTitle'),
3123
- onClick: () => download(`${API}/export?type=usage`),
3124
- },
3125
- t('usage.exportUsage'),
3126
- ),
3398
+ ),
3399
+ h(
3400
+ 'span',
3401
+ { className: 'mvp_usageRanges' },
3402
+ windows.map(([key, labelKey, titleKey]) =>
3127
3403
  h(
3128
- 'button',
3404
+ 'span',
3129
3405
  {
3130
- type: 'button',
3131
- className: 'mvp_button',
3132
- title: t('usage.exportModelsTitle'),
3133
- onClick: () => download(`${API}/export?type=models`),
3406
+ key,
3407
+ className: 'mvp_stat' + (key === 'total' ? ' mvp_stat--total' : ''),
3408
+ title: t(titleKey) + '\n' + detailOf(ranges?.[key], t),
3134
3409
  },
3135
- t('usage.exportModels'),
3410
+ h('span', { className: 'mvp_statLabel' }, t(labelKey)),
3411
+ h('span', { className: 'mvp_statValue' }, formatTokens(totalOf(ranges?.[key]))),
3136
3412
  ),
3137
3413
  ),
3138
3414
  ),
3139
- supported
3140
- ? h(
3141
- 'div',
3142
- { className: 'mvp_usageRanges' },
3143
- h(
3144
- 'span',
3145
- { className: 'mvp_stat', title: t('usage.todayTitle') },
3146
- h('span', { className: 'mvp_statLabel' }, t('usage.today')),
3147
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.today, t)),
3148
- ),
3149
- h(
3150
- 'span',
3151
- { className: 'mvp_stat', title: t('usage.weekTitle') },
3152
- h('span', { className: 'mvp_statLabel' }, t('usage.week')),
3153
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.week, t)),
3154
- ),
3155
- h(
3156
- 'span',
3157
- { className: 'mvp_stat', title: t('usage.monthTitle') },
3158
- h('span', { className: 'mvp_statLabel' }, t('usage.month')),
3159
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.month, t)),
3160
- ),
3161
- h(
3162
- 'span',
3163
- { className: 'mvp_stat mvp_stat--total', title: t('usage.totalTitle') },
3164
- h('span', { className: 'mvp_statLabel' }, t('usage.total')),
3165
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.total, t)),
3166
- ),
3167
- )
3168
- : h('div', { className: 'mvp_usageHint' }, t('usage.hostStale')),
3415
+ h(
3416
+ 'span',
3417
+ { className: 'mvp_usageActions' },
3418
+ h(
3419
+ 'button',
3420
+ {
3421
+ type: 'button',
3422
+ className: 'mvp_button mvp_button--quiet',
3423
+ title: t('usage.exportUsageTitle'),
3424
+ onClick: () => download(`${API}/export?type=usage`),
3425
+ },
3426
+ t('usage.exportUsage'),
3427
+ ),
3428
+ h(
3429
+ 'button',
3430
+ {
3431
+ type: 'button',
3432
+ className: 'mvp_button mvp_button--quiet',
3433
+ title: t('usage.exportModelsTitle'),
3434
+ onClick: () => download(`${API}/export?type=models`),
3435
+ },
3436
+ t('usage.exportModels'),
3437
+ ),
3438
+ ),
3169
3439
  state.error !== null
3170
- ? h('div', { className: 'mvp_usageHint mvp_usageHint--warn' }, state.error)
3440
+ ? h('span', { className: 'mvp_usageHint mvp_usageHint--warn' }, state.error)
3171
3441
  : null,
3172
3442
  )
3173
3443
  }