@tzyc/dsh-model-visibility 1.10.2 → 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
@@ -111,7 +111,10 @@ window.__ModuleLoader__.load({
111
111
  groups: Array.isArray(value.groups) ? value.groups : [],
112
112
  failures: Array.isArray(value.failures) ? value.failures : [],
113
113
  providers: Array.isArray(value.providers) ? value.providers : [],
114
- catalog: value.catalog && typeof value.catalog === 'object' ? value.catalog : null,
114
+ // The section reads `state.learned` (the learned-snapshot meta with
115
+ // its feature flags); this store is its only source, so the field
116
+ // name must stay in lock-step with lib/model-info.js's response.
117
+ learned: value.learned && typeof value.learned === 'object' ? value.learned : null,
115
118
  error: null,
116
119
  detailsError: null,
117
120
  source: 'host',
@@ -128,7 +131,7 @@ window.__ModuleLoader__.load({
128
131
  groups: value.groups,
129
132
  failures: value.failures,
130
133
  providers: [],
131
- catalog: null,
134
+ learned: null,
132
135
  error: null,
133
136
  detailsError: detail,
134
137
  source: 'catalog',
@@ -309,6 +312,7 @@ window.__ModuleLoader__.load({
309
312
  let state = { status: 'idle', snapshot: null, error: null }
310
313
  let inflight = null
311
314
  let timer = null
315
+ let timerMs = null
312
316
 
313
317
  const publish = (next) => {
314
318
  state = next
@@ -340,16 +344,33 @@ window.__ModuleLoader__.load({
340
344
  return run
341
345
  }
342
346
 
343
- /** 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
344
353
  const POLL_MS = 30_000
345
- const startTimer = () => {
346
- if (timer !== null) return
347
- 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()
348
368
  }
349
369
  const stopTimer = () => {
350
370
  if (timer !== null) {
351
371
  clearInterval(timer)
352
372
  timer = null
373
+ timerMs = null
353
374
  }
354
375
  }
355
376
 
@@ -357,7 +378,11 @@ window.__ModuleLoader__.load({
357
378
  subscribe(fn) {
358
379
  const first = listeners.size === 0
359
380
  listeners.add(fn)
360
- 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
+ }
361
386
  return () => {
362
387
  listeners.delete(fn)
363
388
  if (listeners.size === 0) stopTimer()
@@ -367,12 +392,21 @@ window.__ModuleLoader__.load({
367
392
  return state
368
393
  },
369
394
  load() {
370
- return read()
395
+ return read().then(
396
+ (value) => {
397
+ rearm()
398
+ return value
399
+ },
400
+ (error) => {
401
+ rearm()
402
+ throw error
403
+ },
404
+ )
371
405
  },
372
406
  refresh() {
373
407
  // A POST rescan is never needed: the Host rescan throttle refreshes on
374
408
  // reads anyway. A plain read is enough here.
375
- return read()
409
+ return this.load()
376
410
  },
377
411
  }
378
412
  }
@@ -387,6 +421,8 @@ window.__ModuleLoader__.load({
387
421
  let state = { status: 'idle', snapshot: null, error: null }
388
422
  let inflight = null
389
423
  let timer = null
424
+ /** Cadence the armed timer currently runs at, so rearm can compare. */
425
+ let timerMs = null
390
426
 
391
427
  const publish = (next) => {
392
428
  state = next
@@ -448,16 +484,30 @@ window.__ModuleLoader__.load({
448
484
 
449
485
  const SWEEP_POLL_MS = 2_000
450
486
  const IDLE_POLL_MS = 60_000
451
- const startTimer = () => {
452
- 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
453
500
  timer = setInterval(() => {
454
- void read()
455
- }, 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()
456
505
  }
457
506
  const stopTimer = () => {
458
507
  if (timer !== null) {
459
508
  clearInterval(timer)
460
509
  timer = null
510
+ timerMs = null
461
511
  }
462
512
  }
463
513
 
@@ -466,8 +516,8 @@ window.__ModuleLoader__.load({
466
516
  const first = listeners.size === 0
467
517
  listeners.add(fn)
468
518
  if (first) {
469
- void read()
470
- startTimer()
519
+ void read().then(rearm, rearm)
520
+ rearm()
471
521
  }
472
522
  return () => {
473
523
  listeners.delete(fn)
@@ -478,13 +528,40 @@ window.__ModuleLoader__.load({
478
528
  return state
479
529
  },
480
530
  load() {
481
- return read()
531
+ return read().then(
532
+ (value) => {
533
+ rearm()
534
+ return value
535
+ },
536
+ (error) => {
537
+ rearm()
538
+ throw error
539
+ },
540
+ )
482
541
  },
483
542
  start(payload) {
484
- 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
+ )
485
553
  },
486
554
  cancel() {
487
- 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
+ )
488
565
  },
489
566
  }
490
567
  }
@@ -501,12 +578,13 @@ window.__ModuleLoader__.load({
501
578
  /**
502
579
  * The online capability verification panel.
503
580
  *
504
- * The user picks a checker route (provider + model) and a write-back switch,
505
- * runs the sweep, and the panel polls the Host for progress and the checker's
506
- * token spend. The last route choice is kept in browser storage only: the
507
- * 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.
508
585
  *
509
- * @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.
510
588
  */
511
589
 
512
590
  const React = require('react')
@@ -516,6 +594,10 @@ window.__ModuleLoader__.load({
516
594
  const { useSource } = require("hooks")
517
595
 
518
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]
519
601
 
520
602
  function readPick() {
521
603
  try {
@@ -525,10 +607,14 @@ window.__ModuleLoader__.load({
525
607
  provider: typeof parsed.provider === 'string' ? parsed.provider : '',
526
608
  model: typeof parsed.model === 'string' ? parsed.model : '',
527
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,
528
614
  }
529
- : { provider: '', model: '', writeBack: true }
615
+ : { provider: '', model: '', writeBack: true, concurrency: 2 }
530
616
  } catch (_error) {
531
- return { provider: '', model: '', writeBack: true }
617
+ return { provider: '', model: '', writeBack: true, concurrency: 2 }
532
618
  }
533
619
  }
534
620
 
@@ -559,10 +645,12 @@ window.__ModuleLoader__.load({
559
645
  .find((group) => group.id === pick.provider)?.models ?? []
560
646
 
561
647
  if (!canUse) {
562
- return h('div', { className: 'mvp_fallback' }, t('checker.hostStale'))
648
+ return h('div', { className: 'mvp_banner' }, t('checker.hostStale'))
563
649
  }
564
650
 
565
- 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
566
654
 
567
655
  const commit = (next) => {
568
656
  setPick(next)
@@ -572,11 +660,11 @@ window.__ModuleLoader__.load({
572
660
  const run = () => {
573
661
  setMessage(null)
574
662
  verify
575
- .start({ provider: pick.provider, model: pick.model, writeBack: pick.writeBack })
576
- .then((result) => {
577
- if (result !== undefined && result.ok === false) {
578
- setMessage(result.message ?? t('checker.failed'))
579
- }
663
+ .start({
664
+ provider: pick.provider,
665
+ model: pick.model,
666
+ writeBack: pick.writeBack,
667
+ concurrency: pick.concurrency,
580
668
  })
581
669
  .catch((error) => setMessage(String(error?.message ?? error)))
582
670
  }
@@ -585,6 +673,24 @@ window.__ModuleLoader__.load({
585
673
  verify.cancel().catch((error) => setMessage(String(error?.message ?? error)))
586
674
  }
587
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
+
588
694
  return h(
589
695
  'div',
590
696
  { className: 'mvp_checker' + (open ? '' : ' mvp_checker--closed') },
@@ -596,26 +702,16 @@ window.__ModuleLoader__.load({
596
702
  'aria-expanded': open,
597
703
  onClick: () => setOpen(!open),
598
704
  },
705
+ h('span', { className: 'mvp_chevron' }, open ? '▾' : '▸'),
599
706
  h('span', { className: 'mvp_checkerTitle' }, t('checker.title')),
600
- h(
601
- 'span',
602
- { className: 'mvp_checkerSummary' },
603
- running
604
- ? t('checker.running', {
605
- done: progress?.done ?? 0,
606
- total: progress?.total ?? 0,
607
- phase: t(`checker.phase.${progress?.phase ?? 'checker'}`),
608
- })
609
- : finished
610
- ? t('checker.finished', { done: progress?.done ?? 0, total: progress?.total ?? 0, tokens: tokensSpent })
611
- : t('checker.idle'),
612
- ),
707
+ h('span', { className: 'mvp_checkerSummary' }, summary),
708
+ errors.length > 0 ? h('span', { className: 'mvp_badge' }, t('checker.errorBadge', { count: errors.length })) : null,
613
709
  ),
614
710
  open
615
711
  ? h(
616
712
  'div',
617
713
  { className: 'mvp_checkerBody' },
618
- h('div', { className: 'mvp_checkerHint' }, t('checker.description')),
714
+ h('p', { className: 'mvp_checkerHint' }, t('checker.description')),
619
715
  h(
620
716
  'div',
621
717
  { className: 'mvp_checkerForm' },
@@ -654,67 +750,141 @@ window.__ModuleLoader__.load({
654
750
  ),
655
751
  ),
656
752
  ),
657
- ),
658
- h(
659
- 'div',
660
- { className: 'mvp_checkerActions' },
661
753
  h(
662
- 'button',
663
- {
664
- type: 'button',
665
- role: 'switch',
666
- className: 'mvp_switch',
667
- 'aria-checked': pick.writeBack,
668
- title: t('checker.writeBackTitle'),
669
- disabled: running,
670
- onClick: () => commit({ ...pick, writeBack: !pick.writeBack }),
671
- },
672
- 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
+ ),
673
771
  ),
674
772
  h(
675
- 'button',
676
- {
677
- type: 'button',
678
- className: 'mvp_button',
679
- title: t('checker.writeBackTitle'),
680
- onClick: () => commit({ ...pick, writeBack: !pick.writeBack }),
681
- },
682
- 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')),
683
782
  ),
684
783
  h(
685
784
  'button',
686
785
  {
687
786
  type: 'button',
688
787
  className: running ? 'mvp_button' : 'mvp_button mvp_button--accent',
689
- disabled: running ? false : pick.provider === '' || pick.model === '',
788
+ disabled: !running && (pick.provider === '' || pick.model === ''),
690
789
  title: running ? t('checker.cancelTitle') : t('checker.startTitle'),
691
790
  onClick: running ? cancel : run,
692
791
  },
693
792
  running ? t('checker.cancel') : t('checker.start'),
694
793
  ),
695
794
  ),
696
- progress !== null && (running || finished)
795
+ running || finished
697
796
  ? h(
698
797
  'div',
699
798
  { className: 'mvp_checkerProgress' },
700
- t('checker.progress', {
701
- done: progress?.done ?? 0,
702
- total: progress?.total ?? 0,
703
- tokens: tokensSpent,
704
- }),
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
+ ),
705
828
  )
706
829
  : null,
707
- (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
708
834
  ? h(
709
835
  'div',
710
- {
711
- className: 'mvp_usageHint mvp_usageHint--warn',
712
- title: progress.errors.join('\n'),
713
- },
714
- 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
+ ),
715
856
  )
716
857
  : null,
717
- 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,
718
888
  )
719
889
  : null,
720
890
  )
@@ -919,47 +1089,116 @@ window.__ModuleLoader__.load({
919
1089
  // -------------------------------------------------------- css
920
1090
  define("css", function (require, module, exports) {
921
1091
  const CSS = `
1092
+ /* ---------------------------------------------------------------- shell */
922
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}
923
- .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}
924
1098
  .mvp_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
925
- .mvp_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}
926
- .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}
927
- .mvp_summary{color:var(--dsw-alias-label-secondary);flex:1;min-width:0;font-size:12px;line-height:18px}
928
- .mvp_actions{flex:none;align-items:center;justify-content:flex-end;gap:8px;display:flex;flex-wrap:wrap}
929
- .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)}
930
1110
  .mvp_button--accent{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent)}
931
1111
  .mvp_button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
932
1112
  .mvp_button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
933
- .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}
934
1155
  .mvp_group{flex:none;flex-direction:column;display:flex}
935
- .mvp_groupHead{justify-content:space-between;align-items:center;gap:12px;padding:14px 0 6px;display:flex}
936
- .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}
937
- .mvp_groupName{min-width:0}
938
- .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}
939
- .mvp_collapse:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
940
- .mvp_collapse:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
941
- .mvp_groupCount{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400;line-height:18px}
942
- .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}
943
- .mvp_row--gen{background:transparent;background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, transparent)}
944
- .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)}
945
- .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)}
946
- .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)}
947
- .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)}
948
- .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)}
949
- .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)}
950
1182
  .mvp_row--kind-translation{box-shadow:inset 2px 0 0 var(--dsw-alias-border-l2)}
951
- .mvp_rowText{flex-direction:column;gap:2px;min-width:0;display:flex}
952
- .mvp_rowTitle{color:var(--dsw-alias-label-primary);font-size:14px;line-height:22px}
953
- .mvp_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}
954
- .mvp_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px;padding:6px 0}
955
- .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}
956
- .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)}
957
1193
  .mvp_switch:disabled{opacity:.45;cursor:default}
958
1194
  .mvp_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
959
- .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}
960
- .mvp_switch[aria-checked="true"] .mvp_thumb{background:var(--dsw-alias-label-primary);transform:translateX(16px)}
961
- .mvp_tags{flex-wrap:wrap;align-items:center;gap:4px;margin-top:4px;display:flex}
962
- .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}
963
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)}
964
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)}
965
1204
  .mvp_tag--kind{font-weight:500}
@@ -973,198 +1212,61 @@ window.__ModuleLoader__.load({
973
1212
  .mvp_tag--kind-unknown{color:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-border-l2)}
974
1213
  .mvp_tag--inferred{border-style:dashed}
975
1214
  .mvp_tag--muted{color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l1)}
976
- .mvp_groupCap{color:var(--dsw-alias-label-tertiary);font-size:11px;font-weight:400;line-height:16px}
977
- .mvp_kindCount--image{color:var(--dsw-alias-state-business-primary)}
978
- .mvp_kindCount--video{color:var(--dsw-alias-state-success-primary)}
979
- .mvp_kindCount--other{color:var(--dsw-alias-state-warn-primary)}
980
- .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)}
981
- .mvp_applyText{flex-direction:column;gap:2px;min-width:0;display:flex}
982
- .mvp_applyTitle{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;line-height:18px}
983
- .mvp_applyHint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
984
- .mvp_applyRow .mvp_switch{margin-top:1px}
985
- .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}
986
- .mvp_filters{align-items:center;gap:6px;padding:8px 0 2px;display:flex;flex-wrap:wrap}
987
- .mvp_filterLabel{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
988
- .mvp_search{align-items:center;gap:6px;display:flex;min-width:220px;flex:1 1 220px}
989
- .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}
990
- .mvp_searchInput:focus-visible,.mvp_contextSelect:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
991
- .mvp_context{align-items:center;gap:6px;display:flex}
992
- .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}
993
- .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}
994
- .mvp_filter:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
995
- .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)}
996
- .mvp_filter:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
997
- .mvp_note{color:var(--dsw-alias-label-tertiary);flex:none;padding:6px 0 0;font-size:10px;line-height:15px}
998
- .mvp_legend{color:var(--dsw-alias-label-tertiary);flex:none;padding:8px 0 2px;font-size:11px;line-height:16px}
999
- .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}
1000
- .mvp_rowActions{flex:none;align-items:center;gap:8px;display:flex}
1001
- .mvp_editButton{height:24px;padding:0 8px;font-size:11px;line-height:18px}
1002
- .mvp_editButton[aria-pressed="true"]{color:var(--dsw-alias-brand-primary);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent)}
1003
- .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}
1004
- .mvp_editorHead{flex-direction:column;gap:2px;display:flex}
1005
- .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}
1006
1220
  .mvp_editorSub{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}
1007
- .mvp_editorGrid{flex-wrap:wrap;align-items:flex-end;gap:10px 16px;display:flex}
1008
- .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}
1009
1223
  .mvp_field--wide{flex:1 1 100%}
1010
- .mvp_fieldLabel{color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}
1011
- .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}
1012
- .mvp_number{width:180px}
1013
- .mvp_select:focus-visible,.mvp_number:focus-visible,.mvp_chip:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1014
- .mvp_chips{flex-wrap:wrap;align-items:center;gap:6px;display:flex}
1015
- .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}
1016
- .mvp_chip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}
1017
- .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)}
1018
1229
  .mvp_chip:disabled{opacity:.45;cursor:default}
1019
- .mvp_editorFooter{justify-content:space-between;align-items:center;gap:12px;display:flex;flex-wrap:wrap}
1020
- .mvp_editorNote{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:15px;flex:1 1 220px;min-width:0}
1021
- .mvp_check{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
1022
- .mvp_check input{accent-color:var(--dsw-alias-brand-primary, #4c8dff);margin:0}
1023
- .mvp_editorActions{flex:none;align-items:center;gap:6px;display:flex;flex-wrap:wrap}
1024
- /* ---------------------------------------------------- usage & checker */
1025
-
1026
- .mvp_usage {
1027
- margin: 0 0 12px;
1028
- padding: 10px 12px;
1029
- border: 1px solid var(--dsw-alias-border, transparent);
1030
- border-radius: 10px;
1031
- background: transparent;
1032
- background: color-mix(in srgb, currentColor 4%, transparent);
1033
- }
1034
-
1035
- .mvp_usage--off {
1036
- opacity: 0.75;
1037
- }
1038
-
1039
- .mvp_usageHead {
1040
- display: flex;
1041
- align-items: center;
1042
- gap: 10px;
1043
- }
1044
-
1045
- .mvp_usageTitle {
1046
- font-weight: 600;
1047
- font-size: 12px;
1048
- }
1049
-
1050
- .mvp_usageComputing {
1051
- color: var(--dsw-alias-text-tertiary, currentColor);
1052
- font-size: 11px;
1053
- }
1054
-
1055
- .mvp_usageActions {
1056
- display: flex;
1057
- gap: 8px;
1058
- margin-left: auto;
1059
- }
1060
-
1061
- .mvp_usageRanges {
1062
- display: flex;
1063
- gap: 18px;
1064
- flex-wrap: wrap;
1065
- margin-top: 8px;
1066
- }
1067
-
1068
- .mvp_stat {
1069
- display: flex;
1070
- flex-direction: column;
1071
- gap: 2px;
1072
- }
1073
-
1074
- .mvp_statLabel {
1075
- font-size: 11px;
1076
- color: var(--dsw-alias-text-tertiary, currentColor);
1077
- }
1078
-
1079
- .mvp_statValue {
1080
- font-size: 12px;
1081
- font-variant-numeric: tabular-nums;
1082
- }
1083
-
1084
- .mvp_stat--total .mvp_statValue {
1085
- font-weight: 600;
1086
- }
1087
-
1088
- .mvp_usageHint {
1089
- margin-top: 8px;
1090
- font-size: 11px;
1091
- color: var(--dsw-alias-text-tertiary, currentColor);
1092
- }
1093
-
1094
- .mvp_usageHint--warn {
1095
- color: var(--dsw-alias-danger, #d0605e);
1096
- }
1097
-
1098
- .mvp_checker {
1099
- margin: 0 0 12px;
1100
- padding: 10px 12px;
1101
- border: 1px solid var(--dsw-alias-border, transparent);
1102
- border-radius: 10px;
1103
- }
1104
-
1105
- .mvp_checkerHead {
1106
- display: flex;
1107
- width: 100%;
1108
- align-items: center;
1109
- gap: 10px;
1110
- padding: 0;
1111
- border: 0;
1112
- background: transparent;
1113
- color: inherit;
1114
- text-align: left;
1115
- cursor: pointer;
1116
- }
1117
-
1118
- .mvp_checkerTitle {
1119
- font-weight: 600;
1120
- font-size: 12px;
1121
- }
1122
-
1123
- .mvp_checkerSummary {
1124
- margin-left: auto;
1125
- font-size: 11px;
1126
- color: var(--dsw-alias-text-tertiary, currentColor);
1127
- }
1128
-
1129
- .mvp_checker--closed.mvp_checker {
1130
- padding: 8px 12px;
1131
- }
1132
-
1133
- .mvp_checkerBody {
1134
- margin-top: 10px;
1135
- display: flex;
1136
- flex-direction: column;
1137
- gap: 10px;
1138
- }
1139
-
1140
- .mvp_checkerHint {
1141
- font-size: 11px;
1142
- color: var(--dsw-alias-text-tertiary, currentColor);
1143
- }
1144
-
1145
- .mvp_checkerForm {
1146
- display: grid;
1147
- grid-template-columns: 1fr 1fr;
1148
- gap: 10px;
1149
- max-width: 560px;
1150
- }
1151
-
1152
- .mvp_checkerActions {
1153
- display: flex;
1154
- align-items: center;
1155
- gap: 10px;
1156
- }
1157
-
1158
- .mvp_checkerSwitchText {
1159
- font-size: 12px;
1160
- }
1161
-
1162
- .mvp_checkerProgress {
1163
- font-size: 11px;
1164
- font-variant-numeric: tabular-nums;
1165
- color: var(--dsw-alias-text-secondary, currentColor);
1166
- }
1167
-
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}
1168
1270
  `
1169
1271
 
1170
1272
 
@@ -1178,10 +1280,12 @@ window.__ModuleLoader__.load({
1178
1280
  'nav': '模型显示',
1179
1281
  'page.title': '模型显示',
1180
1282
  'page.description':
1181
- '关闭的模型不会出现在聊天输入框的模型下拉列表中;这里的改动立即生效,之后仍可随时重新打开。每个模型标注真实能力:适配器声明优先,其次读中转站的模型列表,再用内置目录补全;生图、生视频等生成类模型单独标记。',
1182
- 'page.summary': '已显示 {visible} / {total} 个模型',
1183
- 'page.hiddenSummary': '已关闭 {count} 个',
1184
- 'page.kindSummary': '{count} 个生图/生视频模型',
1283
+ '关闭的模型不会出现在聊天输入框的模型下拉列表中;这里的改动立即生效,之后仍可随时重新打开。每个模型标注真实能力:适配器声明优先,其次读中转站模型列表,最后用校验结果或模型名补全。',
1284
+ 'page.about': '说明',
1285
+ 'page.aboutTitle': '展开标签含义、来源与颜色说明',
1286
+ 'page.summary': '已显示 {visible} / {total}',
1287
+ 'page.hiddenSummary': '已关闭 {count}',
1288
+ 'page.kindSummary': '生图/生视频 {count}',
1185
1289
  'page.loading': '正在加载模型列表…',
1186
1290
  'page.empty': '暂无可用模型。',
1187
1291
  'page.filterEmpty': '当前筛选下没有模型。',
@@ -1189,31 +1293,33 @@ window.__ModuleLoader__.load({
1189
1293
  'page.saveFailed': '保存失败,请重试。',
1190
1294
  'page.refresh': '刷新',
1191
1295
  'page.refreshing': '刷新中…',
1296
+ 'page.refreshTitle': '重新读取适配器与中转站的能力信息(沿用缓存,10 分钟内的探测不会重发请求)',
1192
1297
  'page.reload': '重新获取能力',
1193
1298
  'page.reloading': '正在获取…',
1194
1299
  'page.reloadTitle':
1195
1300
  '清空本插件的能力缓存和正在进行的读取,重新询问适配器与所有中转站;不会改动任何模型开关。适配器没有公布的思考强度仍不会凭空出现。',
1196
1301
  'page.reloadUnavailable': 'Host 半不支持强制重新获取;重启 DSH 后可用。',
1197
- 'page.collapseGroups': '折叠全部分组',
1198
- 'page.expandGroups': '展开全部分组',
1302
+ 'page.collapseGroups': '折叠全部',
1303
+ 'page.expandGroups': '展开全部',
1199
1304
  'page.collapseGroupsTitle': '只保留分组标题,方便跳到其他分组',
1200
1305
  'page.expandGroupsTitle': '展开所有被折叠的分组',
1201
1306
  'page.showAll': '全部显示',
1202
1307
  'page.legend':
1203
- '标签说明:推理 = 可调推理等级;上下文 = 输入窗口;多模态 = 可输入图像。实线标签来自适配器、中转站或厂商文档,虚线标签来自内置目录或模型名推断。生图 / 生视频 / 嵌入等模型只标类型,不显示思考强度与文本标签。',
1308
+ '标签:推理 = 可调推理等级;上下文 = 输入窗口;多模态 = 可输入图像。实线来自适配器、中转站或厂商文档,虚线来自联网校验或模型名推断。生图 / 生视频 / 嵌入等只标类型,不显示思考强度与文本标签。',
1204
1309
  'page.metaFallback':
1205
1310
  '模型能力信息暂不可用:上下文与模态显示为未知,推理档位仍来自官方目录。',
1206
1311
  'page.hostStale':
1207
1312
  'Host 半仍是旧版本:颜色与开关已更新,但「放行图像」和官方校正要等 DSH 重启(或 profile 重组)后才会生效。',
1208
- 'page.providerFailures': '部分提供方暂时读不到模型:{names}',
1209
- 'page.probeSummary': '中转站探测:{ok} 个已读取模型列表,{caps} 个公布了能力字段',
1210
- 'page.probeSummaryNone': '中转站探测:暂无可读取的路由配置',
1211
- 'page.catalogNote': '内置目录:pi-ai {version} 快照({date}),共 {models} 个模型',
1212
- 'page.learnedNote': '学习快照:联网校验({date}),缓存 {models} 个模型的可验证事实',
1213
- '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',
1214
1320
  'usage.computing': '统计中…',
1215
1321
  'usage.none': '—',
1216
- 'usage.range': '输入 {input} · 输出 {output}(合计 {total})',
1322
+ 'usage.detail': '输入 {input} · 输出 {output} · 缓存读 {cacheRead} · 缓存写 {cacheWrite}',
1217
1323
  'usage.today': '今天',
1218
1324
  'usage.todayTitle': '本地自然日(今天 0 点起)',
1219
1325
  'usage.week': '本周',
@@ -1222,47 +1328,62 @@ window.__ModuleLoader__.load({
1222
1328
  'usage.monthTitle': '本月 1 日 0 点起',
1223
1329
  'usage.total': '总计',
1224
1330
  'usage.totalTitle': '全部历史(更早的按会话创建日粗算)',
1225
- 'usage.exportUsage': '导出用量 CSV',
1331
+ 'usage.exportUsage': '用量 CSV',
1226
1332
  'usage.exportUsageTitle': '下载按天分桶、含每模型 token 分解的 CSV',
1227
- 'usage.exportModels': '导出模型清单 CSV',
1333
+ 'usage.exportModels': '模型 CSV',
1228
1334
  'usage.exportModelsTitle': '下载全部提供商模型的可见性与能力表',
1229
1335
  'usage.hostStale': '宿主半尚未提供用量统计,重启 DSH 即可启用。',
1230
- 'checker.title': '联网校验模型能力',
1336
+ 'checker.title': '联网校验',
1231
1337
  'checker.description':
1232
- '选定一个模型作为校验员:先把两个公开目录(models.dev / OpenRouter)拉下来做基准,再把其余模型分批交给它核对上下文窗口、输入模态、思考档位与类型;结果回填本页并(在开关打开时)写入对应提供商配置,同时存进本地快照 learned.json,之后同名模型直接复用缓存。校验请求会消耗所选模型的 API 额度,其 token 消耗会计入上方用量统计。',
1338
+ '选定一个校验员模型:先拉 models.dev / OpenRouter / Hugging Face 三个公开目录做基准,再把其余模型分批交给它核对。公开目录已经知道的能力会直接采用,校验员答不上时也不例外。校验请求会消耗所选模型的额度,其 token 计入上方统计。提示:思考型模型会先花掉输出预算,若批次普遍失败,换一个非思考模型或降低其思考档位。',
1233
1339
  'checker.provider': '校验员提供商',
1234
1340
  'checker.model': '校验员模型',
1235
1341
  'checker.start': '开始校验',
1236
1342
  'checker.startTitle': '在宿主侧后台跑一轮全模型校验',
1237
1343
  'checker.cancel': '取消',
1238
1344
  'checker.cancelTitle': '中止正在进行的校验',
1239
- 'checker.writeBack': '校验通过即写入提供商配置',
1345
+ 'checker.writeBack': '同时写入提供商配置',
1240
1346
  'checker.writeBackTitle':
1241
- '开启:判定到的 input / contextWindow / 推理档位直接写进各提供商该模型条目(learned.json 只留写不进去的字段);关闭:只更新本页与快照,需要时再用「编辑能力」手动写入。',
1347
+ '开启:判定到的 input / contextWindow / 推理档位直接写进各提供商该模型条目(learned.json 只留写不进去的字段);关闭:只更新本页与快照。',
1242
1348
  'checker.idle': '未运行',
1243
- 'checker.running': '进行中 {phase} · {done}/{total} · 已耗 {tokens}',
1244
- 'checker.finished': '已完成 {done}/{total} · 校验耗 token {tokens}',
1349
+ 'checker.running': '进行中{phase} · {done}/{total} · 已耗 {tokens}',
1350
+ 'checker.finished': '已完成 {done}/{total} · 耗 {tokens}',
1245
1351
  'checker.progress': '{done}/{total} 已核对 · 校验本身消耗 {tokens} token',
1246
- 'checker.errors': '{count} 个批次失败(悬停看详情)',
1247
- 'checker.pickNeeded': '请先选择校验员提供商与模型。',
1248
- '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} 个模型的能力来自公开目录(校验员未作答)。',
1249
1369
  'checker.hostStale': '宿主半不支持联网校验,请重启 DSH 后使用。',
1250
1370
  'checker.phase.public': '(读取公开目录)',
1251
1371
  'checker.phase.checker': '(读取公开目录)',
1252
1372
  'checker.phase.verifying': '(分批校验)',
1253
1373
  'source.snapshot': '已校验',
1254
1374
  'page.applyInput': '让输入框按能力标注放行图像',
1375
+ 'page.applyInputShort': '多模态中转站模型自动补 image;关闭则只按 Provider 的 input 声明',
1255
1376
  'page.applyInputHint':
1256
1377
  '开启后,被标注为「多模态」的中转站对话模型会在发送请求时自动补上图像输入能力;关闭则完全按 Provider 配置里的 input 声明。只影响中转站(pi-ai)路由,不改任何配置文件;若某个上游实际拒绝图像,关闭此开关即可。',
1257
1378
  'page.kindLegend':
1258
- '颜色区分模型类型:生图 蓝 · 生视频 绿 · 嵌入 橙 · 音频 紫 · 重排 红 · 审核 玫红 · 翻译 灰。「官方校正」表示内置目录与厂商文档不一致时以文档为准。',
1259
- 'page.filterLabel': '筛选',
1260
- 'page.searchPlaceholder': '搜索模型名称或 ID',
1261
- 'page.capabilityLabel': '能力',
1379
+ '类型配色:生图 蓝 · 生视频 绿 · 嵌入 橙 · 音频 紫 · 重排 红 · 审核 玫红 · 翻译 灰。「官方校正」表示目录与厂商文档不一致时以文档为准。',
1380
+ 'page.searchPlaceholder': '搜索名称或 ID',
1262
1381
  'page.capability.multimodal': '多模态',
1382
+ 'page.capability.multimodalTitle': '只看可输入图像的模型',
1263
1383
  'page.capability.reasoning': '支持推理',
1384
+ 'page.capability.reasoningTitle': '只看公布了推理能力的模型',
1264
1385
  'page.contextLabel': '上下文至少',
1265
- 'page.context.any': '不限',
1386
+ 'page.context.any': '上下文不限',
1266
1387
  'page.context.128k': '128K+',
1267
1388
  'page.context.256k': '256K+',
1268
1389
  'page.context.512k': '512K+',
@@ -1274,15 +1395,15 @@ window.__ModuleLoader__.load({
1274
1395
  'page.filter.other': '其它',
1275
1396
  'group.showAll': '全组显示',
1276
1397
  'group.hideAll': '全组隐藏',
1277
- 'group.showFiltered': '显示筛选结果',
1278
- 'group.hideFiltered': '隐藏筛选结果',
1398
+ 'group.showFiltered': '显示结果',
1399
+ 'group.hideFiltered': '隐藏结果',
1279
1400
  'group.collapseNamed': '折叠 {name} 分组',
1280
1401
  'group.expandNamed': '展开 {name} 分组',
1281
- 'group.capabilities': '{chat} 对话 · {image} 生图 · {video} 生视频 · {other} 其它',
1282
- 'group.probeOk': '中转站 {models} 个模型 · {caps} 个带能力字段',
1283
- 'group.probeNoCaps': '中转站 {models} 个模型 · 未公布能力字段',
1284
- 'group.probeFailed': '中转站读取失败',
1285
- 'group.probeSkipped': '未探测中转站',
1402
+ 'group.probeOk': '{models} 个模型 · {caps} 带能力',
1403
+ 'group.probeNoCaps': '{models} 个模型 · 无能力字段',
1404
+ 'group.probePartial': '价格接口 {models} 个',
1405
+ 'group.probeFailed': '读取失败',
1406
+ 'group.probeSkipped': '未探测',
1286
1407
  'tag.reasoning': '支持推理',
1287
1408
  'tag.noReasoning': '不支持推理',
1288
1409
  'tag.reasoningUnknown': '推理未知',
@@ -1305,7 +1426,6 @@ window.__ModuleLoader__.load({
1305
1426
  'tag.kindVideoTitle': '视频生成模型(输出视频);类型来源:{source}',
1306
1427
  'tag.correction': '官方校正',
1307
1428
  'tag.correctionTitle': '{note}\n来源:{source}',
1308
- 'tag.resolution': '分辨率',
1309
1429
  'tag.resolutionTitle': '模型名标注了 {value} 输出档位',
1310
1430
  'tag.imageInput': '可改图',
1311
1431
  'tag.imageInputTitle': '支持图像输入(图生图 / 参考图)',
@@ -1388,10 +1508,12 @@ window.__ModuleLoader__.load({
1388
1508
  'nav': 'Model visibility',
1389
1509
  'page.title': 'Model visibility',
1390
1510
  'page.description':
1391
- '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.',
1392
- '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}',
1393
1515
  'page.hiddenSummary': '{count} hidden',
1394
- 'page.kindSummary': '{count} image/video models',
1516
+ 'page.kindSummary': '{count} image/video',
1395
1517
  'page.loading': 'Loading model list…',
1396
1518
  'page.empty': 'No models available.',
1397
1519
  'page.filterEmpty': 'No models match this filter.',
@@ -1399,80 +1521,99 @@ window.__ModuleLoader__.load({
1399
1521
  'page.saveFailed': 'Could not save this change. Try again.',
1400
1522
  'page.refresh': 'Refresh',
1401
1523
  'page.refreshing': 'Refreshing…',
1524
+ 'page.refreshTitle': 'Read adapter and relay capabilities again (honours caches; a probe younger than 10 minutes is not repeated)',
1402
1525
  'page.reload': 'Re-fetch capabilities',
1403
1526
  'page.reloading': 'Re-fetching…',
1404
1527
  'page.reloadTitle':
1405
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.',
1406
1529
  'page.reloadUnavailable': 'The Host half does not support a hard re-fetch; restart DSH to enable it.',
1407
- 'page.collapseGroups': 'Collapse all groups',
1408
- 'page.expandGroups': 'Expand all groups',
1530
+ 'page.collapseGroups': 'Collapse all',
1531
+ 'page.expandGroups': 'Expand all',
1409
1532
  'page.collapseGroupsTitle': 'Keep only group headers so other groups are easy to reach',
1410
1533
  'page.expandGroupsTitle': 'Expand every collapsed group',
1411
1534
  'page.showAll': 'Show all',
1412
1535
  'page.legend':
1413
- '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.',
1414
1537
  'page.metaFallback':
1415
1538
  'Model capability details are unavailable right now: context and modalities show as unknown, while effort levels still come from the shipped catalog.',
1416
1539
  'page.hostStale':
1417
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).',
1418
1541
  'page.providerFailures': 'Some providers could not be read: {names}',
1419
- 'page.probeSummary': 'Relay probe: {ok} model lists read, {caps} published capability fields',
1420
- 'page.probeSummaryNone': 'Relay probe: no readable route configuration',
1421
- 'page.catalogNote': 'Built-in catalog: pi-ai {version} snapshot ({date}), {models} models',
1422
- 'page.learnedNote': 'Learned snapshot: online verification ({date}), {models} verified models',
1423
- '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',
1424
1548
  'usage.computing': 'scanning…',
1425
1549
  'usage.none': '—',
1426
- 'usage.range': 'in {input} · out {output} ({total} total)',
1550
+ 'usage.detail': 'input {input} · output {output} · cache read {cacheRead} · cache write {cacheWrite}',
1427
1551
  'usage.today': 'Today',
1552
+ 'usage.week': 'Week',
1553
+ 'usage.month': 'Month',
1554
+ 'usage.total': 'Total',
1428
1555
  'usage.todayTitle': 'Local calendar day',
1429
- 'usage.week': 'This week',
1430
1556
  'usage.weekTitle': 'Since Monday 00:00 local',
1431
- 'usage.month': 'This month',
1432
1557
  'usage.monthTitle': 'Since the 1st local',
1433
- 'usage.total': 'Total',
1434
1558
  'usage.totalTitle': 'All history (older months attributed by session start day)',
1435
- 'usage.exportUsage': 'Export usage CSV',
1559
+ 'usage.exportUsage': 'Usage CSV',
1436
1560
  'usage.exportUsageTitle': 'Download a day-bucketed CSV with per-model breakdown',
1437
- 'usage.exportModels': 'Export model list CSV',
1561
+ 'usage.exportModels': 'Models CSV',
1438
1562
  'usage.exportModelsTitle': 'Download visibility and capability table of every provider model',
1439
1563
  'usage.hostStale': 'The Host half does not provide usage stats yet; restart DSH to enable it.',
1440
- 'checker.title': 'Verify model capabilities online',
1564
+ 'checker.title': 'Online verification',
1441
1565
  'checker.description':
1442
- '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.',
1443
1567
  'checker.provider': 'Checker provider',
1444
1568
  'checker.model': 'Checker model',
1445
- 'checker.start': 'Verify all',
1569
+ 'checker.start': 'Start',
1446
1570
  'checker.startTitle': 'Run a background verification sweep in the Host',
1447
1571
  'checker.cancel': 'Cancel',
1448
1572
  'checker.cancelTitle': 'Abort the running sweep',
1449
- 'checker.writeBack': 'Write confirmed facts into provider settings',
1573
+ 'checker.writeBack': 'Also write into provider settings',
1450
1574
  'checker.writeBackTitle':
1451
- '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.',
1452
1576
  'checker.idle': 'not run yet',
1453
- 'checker.running': 'running {phase} · {done}/{total} · {tokens} spent',
1454
- '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',
1455
1579
  'checker.progress': '{done}/{total} verified · checker spent {tokens} tokens',
1456
- 'checker.errors': '{count} failed batches (hover for details)',
1457
- 'checker.pickNeeded': 'Pick a checker provider and model first.',
1458
- '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).',
1459
1598
  'checker.hostStale': 'The Host half does not support online verification yet; restart DSH.',
1460
- 'checker.phase.public': '(fetching public catalogs)',
1461
- 'checker.phase.checker': '(fetching public catalogs)',
1462
- '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)',
1463
1602
  'source.snapshot': 'verified',
1464
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',
1465
1606
  'page.applyInputHint':
1466
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.',
1467
1608
  'page.kindLegend':
1468
- '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.',
1469
- 'page.filterLabel': 'Filter',
1470
- 'page.searchPlaceholder': 'Search model name or ID',
1471
- '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',
1472
1611
  'page.capability.multimodal': 'Multimodal',
1612
+ 'page.capability.multimodalTitle': 'Only models that accept image input',
1473
1613
  'page.capability.reasoning': 'Reasoning',
1614
+ 'page.capability.reasoningTitle': 'Only models that publish reasoning support',
1474
1615
  'page.contextLabel': 'Context at least',
1475
- 'page.context.any': 'Any',
1616
+ 'page.context.any': 'Any context',
1476
1617
  'page.context.128k': '128K+',
1477
1618
  'page.context.256k': '256K+',
1478
1619
  'page.context.512k': '512K+',
@@ -1484,15 +1625,15 @@ window.__ModuleLoader__.load({
1484
1625
  'page.filter.other': 'Other',
1485
1626
  'group.showAll': 'Show all',
1486
1627
  'group.hideAll': 'Hide all',
1487
- 'group.showFiltered': 'Show filtered',
1488
- 'group.hideFiltered': 'Hide filtered',
1628
+ 'group.showFiltered': 'Show results',
1629
+ 'group.hideFiltered': 'Hide results',
1489
1630
  'group.collapseNamed': 'Collapse the {name} group',
1490
1631
  'group.expandNamed': 'Expand the {name} group',
1491
- 'group.capabilities': '{chat} chat · {image} image · {video} video · {other} other',
1492
- 'group.probeOk': 'relay: {models} models · {caps} with capability fields',
1493
- 'group.probeNoCaps': 'relay: {models} models · no capability fields',
1494
- 'group.probeFailed': 'relay read failed',
1495
- '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',
1496
1637
  'tag.reasoning': 'Reasoning',
1497
1638
  'tag.noReasoning': 'No reasoning',
1498
1639
  'tag.reasoningUnknown': 'Reasoning unknown',
@@ -1515,7 +1656,6 @@ window.__ModuleLoader__.load({
1515
1656
  'tag.kindVideoTitle': 'Video generation model (outputs video); kind source: {source}',
1516
1657
  'tag.correction': 'Official correction',
1517
1658
  'tag.correctionTitle': '{note}\nSource: {source}',
1518
- 'tag.resolution': 'Resolution',
1519
1659
  'tag.resolutionTitle': 'The model name advertises a {value} output tier',
1520
1660
  'tag.imageInput': 'Image edit',
1521
1661
  'tag.imageInputTitle': 'Accepts image input (image-to-image / reference images)',
@@ -1701,9 +1841,16 @@ window.__ModuleLoader__.load({
1701
1841
  return sources && typeof sources[field] === 'string' ? sources[field] : 'unknown'
1702
1842
  }
1703
1843
 
1704
- /** 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
+ */
1705
1852
  function isInferredSource(source) {
1706
- return source === 'catalog' || source === 'inferred'
1853
+ return source === 'snapshot' || source === 'inferred'
1707
1854
  }
1708
1855
 
1709
1856
  /** Kind with a safe fallback for records from an older Host. */
@@ -2122,6 +2269,8 @@ window.__ModuleLoader__.load({
2122
2269
  * @module client-src/pure
2123
2270
  */
2124
2271
 
2272
+ const { bucketOf, kindOf } = require("format")
2273
+
2125
2274
  /** Number of one bucket's token sum, guarding partial buckets. */
2126
2275
  function bucketTotalOf(buckets) {
2127
2276
  if (!Array.isArray(buckets)) return 0
@@ -2196,11 +2345,9 @@ window.__ModuleLoader__.load({
2196
2345
  }
2197
2346
 
2198
2347
  function bucket(model) {
2199
- const kind = model && typeof model.kind === 'string' && model.kind !== '' ? model.kind : 'chat'
2200
- if (kind === 'image') return 'image'
2201
- if (kind === 'video') return 'video'
2202
- if (kind === 'chat') return 'chat'
2203
- 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))
2204
2351
  }
2205
2352
 
2206
2353
  /** Counts per kind bucket for the whole list. */
@@ -2221,7 +2368,11 @@ window.__ModuleLoader__.load({
2221
2368
  return routes
2222
2369
  }
2223
2370
 
2224
- /** 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
+ */
2225
2376
  function probeOf(state, group, t) {
2226
2377
  const list = Array.isArray(state.providers) ? state.providers : []
2227
2378
  const diagnostic = list.find((item) => item && item.id === group.id)
@@ -2229,6 +2380,8 @@ window.__ModuleLoader__.load({
2229
2380
  const details = [
2230
2381
  diagnostic.host ? 'host: ' + diagnostic.host : null,
2231
2382
  diagnostic.status ? 'status: ' + diagnostic.status : null,
2383
+ diagnostic.models ? 'models: ' + diagnostic.models : null,
2384
+ diagnostic.capabilities ? 'capabilities: ' + diagnostic.capabilities : null,
2232
2385
  diagnostic.message ? 'message: ' + diagnostic.message : null,
2233
2386
  diagnostic.pricing && diagnostic.pricing !== 'skipped' ? 'pricing: ' + diagnostic.pricing : null,
2234
2387
  ]
@@ -2246,8 +2399,19 @@ window.__ModuleLoader__.load({
2246
2399
  title: details,
2247
2400
  }
2248
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
+ }
2249
2409
  if (diagnostic.status === 'failed' || diagnostic.status === 'unauthorized') {
2250
- return { label: t('group.probeFailed'), title: details }
2410
+ return {
2411
+ label: t('group.probeFailed'),
2412
+ title: details,
2413
+ failed: true,
2414
+ }
2251
2415
  }
2252
2416
  return { label: t('group.probeSkipped'), title: details }
2253
2417
  }
@@ -2269,6 +2433,7 @@ window.__ModuleLoader__.load({
2269
2433
  const React = require('react')
2270
2434
  const h = React.createElement
2271
2435
 
2436
+ const { FIELD } = require("constants")
2272
2437
  const { keyOf, kindOf, isGenerationKind, providerLabel, countModels, readCollapsedGroups, persistCollapsedGroups, overrideKeyOf } = require("format")
2273
2438
  const { computeFilteredGroups, countKinds, probeOf } = require("pure")
2274
2439
  const { capabilityTags, tagClassOf } = require("tags")
@@ -2277,6 +2442,33 @@ window.__ModuleLoader__.load({
2277
2442
  const { UsageToolbar } = require("usage-view")
2278
2443
  const { VerifyPanel } = require("checker-view")
2279
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
+ */
2280
2472
  function makeSection(form, catalog, overrides, usage, verify) {
2281
2473
  return function ModelVisibilitySection({ t }) {
2282
2474
  const { hidden, snapshot } = useHidden(form)
@@ -2287,6 +2479,7 @@ window.__ModuleLoader__.load({
2287
2479
  const [refreshing, setRefreshing] = React.useState(false)
2288
2480
  const [reloading, setReloading] = React.useState(false)
2289
2481
  const [collapsed, setCollapsed] = React.useState(readCollapsedGroups)
2482
+ const [aboutOpen, setAboutOpen] = React.useState(readAboutOpen)
2290
2483
  const [editing, setEditing] = React.useState(null)
2291
2484
  const [providerNote, setProviderNote] = React.useState(null)
2292
2485
 
@@ -2336,16 +2529,18 @@ window.__ModuleLoader__.load({
2336
2529
  if (disabled) return
2337
2530
  setBusy(true)
2338
2531
  setError(null)
2339
- Promise.resolve(form.set(field, value)).then(
2340
- (accepted) => {
2341
- if (accepted === false) setError(t('page.saveFailed'))
2342
- },
2343
- () => {
2344
- setError(t('page.saveFailed'))
2345
- },
2346
- ).finally(() => {
2347
- setBusy(false)
2348
- })
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
+ })
2349
2544
  }
2350
2545
 
2351
2546
  const commit = (next) => writeField(FIELD, Array.from(next))
@@ -2386,7 +2581,7 @@ window.__ModuleLoader__.load({
2386
2581
  // A hard reload may supersede a soft refresh that is still running;
2387
2582
  // catalog.load() gives it a new run and discards the older result.
2388
2583
  const reloadCatalog = () => {
2389
- if (busy || reloading) return
2584
+ if (busy || reloading || refreshing) return
2390
2585
  runCatalogAction(() => catalog.reload(), setReloading)
2391
2586
  }
2392
2587
 
@@ -2409,24 +2604,31 @@ window.__ModuleLoader__.load({
2409
2604
  })
2410
2605
  }
2411
2606
 
2607
+ const toggleAbout = () => {
2608
+ setAboutOpen((previous) => {
2609
+ persistAboutOpen(!previous)
2610
+ return !previous
2611
+ })
2612
+ }
2613
+
2614
+ /* ------------------------------------------------------------- groups */
2615
+
2412
2616
  const rows = []
2413
2617
  for (const group of filteredGroups) {
2414
2618
  const allGroupModels =
2415
2619
  (groups.find((candidate) => candidate.id === group.id) || group).models || []
2416
2620
  const models = group.models || []
2417
- const shown = allGroupModels.filter(
2418
- (model) => !hidden.has(keyOf(group.id, model.id)),
2419
- ).length
2621
+ const shown = allGroupModels.filter((model) => !hidden.has(keyOf(group.id, model.id))).length
2420
2622
  const allShown = models.every((model) => !hidden.has(keyOf(group.id, model.id)))
2421
2623
  const kinds = countKinds(allGroupModels)
2422
2624
  const kindParts = []
2423
- if (kinds.chat > 0) kindParts.push(['chat', t('page.filter.chat') + ' ' + kinds.chat])
2424
- if (kinds.image > 0) kindParts.push(['image', t('page.filter.image') + ' ' + kinds.image])
2425
- if (kinds.video > 0) kindParts.push(['video', t('page.filter.video') + ' ' + kinds.video])
2426
- 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')
2427
2629
  const kindNodes = []
2428
- for (const [index, [kindKey, label]] of kindParts.entries()) {
2429
- 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' }, '·'))
2430
2632
  kindNodes.push(
2431
2633
  h(
2432
2634
  'span',
@@ -2435,8 +2637,9 @@ window.__ModuleLoader__.load({
2435
2637
  className:
2436
2638
  'mvp_kindCount' +
2437
2639
  (kindKey === 'chat' ? '' : ' mvp_kindCount--' + kindKey),
2640
+ title: t('page.filter.' + kindKey) + ' ' + kinds[kindKey],
2438
2641
  },
2439
- label,
2642
+ kinds[kindKey],
2440
2643
  ),
2441
2644
  )
2442
2645
  }
@@ -2454,34 +2657,37 @@ window.__ModuleLoader__.load({
2454
2657
  'div',
2455
2658
  { className: 'mvp_groupHead' },
2456
2659
  h(
2457
- 'div',
2458
- { className: 'mvp_groupTitle' },
2459
- h(
2460
- 'button',
2461
- {
2462
- type: 'button',
2463
- className: 'mvp_collapse',
2464
- 'aria-expanded': !collapsedNow,
2465
- 'aria-label': collapseLabel,
2466
- title: collapseLabel,
2467
- onClick: () => toggleGroupCollapsed(group.id),
2468
- },
2469
- collapsedNow ? '▸' : '▾',
2470
- ),
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 ? '▸' : '▾'),
2471
2670
  h('span', { className: 'mvp_groupName' }, groupName),
2472
2671
  h('span', { className: 'mvp_groupCount' }, shown + '/' + allGroupModels.length),
2473
2672
  kindParts.length > 0
2474
2673
  ? h('span', { className: 'mvp_groupCap' }, kindNodes)
2475
2674
  : null,
2476
2675
  probe !== null
2477
- ? 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
+ )
2478
2684
  : null,
2479
2685
  ),
2480
2686
  h(
2481
2687
  'button',
2482
2688
  {
2483
2689
  type: 'button',
2484
- className: 'mvp_button',
2690
+ className: 'mvp_button mvp_button--quiet',
2485
2691
  disabled: disabled,
2486
2692
  onClick: () => setGroup(group, models, !allShown),
2487
2693
  },
@@ -2513,15 +2719,20 @@ window.__ModuleLoader__.load({
2513
2719
  className:
2514
2720
  'mvp_row' +
2515
2721
  (generation ? ' mvp_row--gen' : '') +
2722
+ (shownModel ? '' : ' mvp_row--off') +
2516
2723
  kindTint,
2517
2724
  },
2518
2725
  h(
2519
2726
  'div',
2520
2727
  { className: 'mvp_rowText' },
2521
- h('div', { className: 'mvp_rowTitle' }, model.name),
2522
- model.id === model.name
2523
- ? null
2524
- : 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
+ ),
2525
2736
  h(
2526
2737
  'div',
2527
2738
  { className: 'mvp_tags' },
@@ -2546,7 +2757,7 @@ window.__ModuleLoader__.load({
2546
2757
  'button',
2547
2758
  {
2548
2759
  type: 'button',
2549
- className: 'mvp_button mvp_editButton',
2760
+ className: 'mvp_button mvp_button--quiet mvp_editButton',
2550
2761
  'aria-pressed': editing === rowKey,
2551
2762
  'aria-label': t('row.edit') + ': ' + model.name,
2552
2763
  title: t('row.editTitle'),
@@ -2594,9 +2805,13 @@ window.__ModuleLoader__.load({
2594
2805
  )
2595
2806
  }
2596
2807
 
2808
+ /* ------------------------------------------------------------- status */
2809
+
2597
2810
  const loading = state.status === 'loading' || state.status === 'idle'
2598
2811
  const providerList = Array.isArray(state.providers) ? state.providers : []
2599
- 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
2600
2815
  const probeCaps = providerList.reduce(
2601
2816
  (count, provider) => count + (Number(provider && provider.capabilities) || 0),
2602
2817
  0,
@@ -2607,10 +2822,7 @@ window.__ModuleLoader__.load({
2607
2822
  ? t('page.probeSummary', { ok: probeOk, caps: probeCaps })
2608
2823
  : t('page.probeSummaryNone')
2609
2824
  : null
2610
- // A page refresh can deliver this bundle while the Host half is still
2611
- // the previous version; the switch would look live but do nothing.
2612
2825
  const hostStale = state.source === 'host' && !hostFeatures.includes('applyInput')
2613
- // A hard reload is only meaningful when the Host half understands it.
2614
2826
  const canReload = state.source === 'host' && hostFeatures.includes('reload')
2615
2827
  const anyCollapsed = groups.some((group) => collapsed.has(group.id))
2616
2828
  const learnedDate =
@@ -2635,54 +2847,52 @@ window.__ModuleLoader__.load({
2635
2847
  ]
2636
2848
  const hasActiveFilters =
2637
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 */
2638
2863
 
2639
2864
  return h(
2640
2865
  'div',
2641
2866
  { className: 'mvp_page' },
2642
- h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
2867
+ // Title + summary + the two actions that are worth a permanent slot.
2643
2868
  h(
2644
2869
  'div',
2645
2870
  { className: 'mvp_head' },
2646
- h('h2', { className: 'mvp_title' }, t('page.title')),
2647
- h('p', { className: 'mvp_desc' }, t('page.description')),
2648
- ),
2649
- h(
2650
- 'div',
2651
- { className: 'mvp_toolbar' },
2652
2871
  h(
2653
2872
  'div',
2654
- { className: 'mvp_summary' },
2655
- total === 0
2656
- ? loading
2657
- ? t('page.loading')
2658
- : t('page.empty')
2659
- : t('page.summary', { visible: total - hiddenCount, total }) +
2660
- (hiddenCount > 0 ? ' · ' + t('page.hiddenSummary', { count: hiddenCount }) : '') +
2661
- (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,
2662
2882
  ),
2663
2883
  h(
2664
2884
  'div',
2665
- { className: 'mvp_actions' },
2885
+ { className: 'mvp_headActions' },
2666
2886
  h(
2667
2887
  'button',
2668
2888
  {
2669
2889
  type: 'button',
2670
- className: 'mvp_button',
2671
- disabled: disabled || hiddenCount === 0,
2672
- onClick: () => commit(new Set()),
2673
- },
2674
- t('page.showAll'),
2675
- ),
2676
- h(
2677
- 'button',
2678
- {
2679
- type: 'button',
2680
- className: 'mvp_button',
2681
- disabled: groups.length === 0,
2682
- title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
2683
- onClick: toggleAllCollapsed,
2890
+ className: 'mvp_button mvp_button--quiet',
2891
+ 'aria-pressed': aboutOpen,
2892
+ title: t('page.aboutTitle'),
2893
+ onClick: toggleAbout,
2684
2894
  },
2685
- t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
2895
+ t('page.about') + (aboutOpen ? ' ▴' : ' ▾'),
2686
2896
  ),
2687
2897
  h(
2688
2898
  'button',
@@ -2690,6 +2900,7 @@ window.__ModuleLoader__.load({
2690
2900
  type: 'button',
2691
2901
  className: 'mvp_button',
2692
2902
  disabled: busy || refreshing || reloading,
2903
+ title: t('page.refreshTitle'),
2693
2904
  onClick: refreshCatalog,
2694
2905
  },
2695
2906
  t(refreshing ? 'page.refreshing' : 'page.refresh'),
@@ -2707,63 +2918,77 @@ window.__ModuleLoader__.load({
2707
2918
  ),
2708
2919
  ),
2709
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 : [] }),
2710
2932
  h(
2711
2933
  'div',
2712
2934
  { className: 'mvp_filters' },
2713
- h('label', { className: 'mvp_search' },
2714
- h('span', { className: 'mvp_filterLabel' }, t('page.searchPlaceholder')),
2715
- h('input', {
2716
- className: 'mvp_searchInput',
2717
- type: 'search',
2718
- value: query,
2719
- placeholder: t('page.searchPlaceholder'),
2720
- 'aria-label': t('page.searchPlaceholder'),
2721
- onInput: (event) => setQuery(event.target.value),
2722
- }),
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
+ ),
2723
2959
  ),
2724
- h('span', { className: 'mvp_filterLabel' }, t('page.filterLabel')),
2725
- filterOptions.map(([value, label, count]) =>
2960
+ h(
2961
+ 'div',
2962
+ { className: 'mvp_filterGroup' },
2726
2963
  h(
2727
2964
  'button',
2728
2965
  {
2729
- key: value,
2730
2966
  type: 'button',
2731
2967
  className: 'mvp_filter',
2732
- 'aria-pressed': filter === value,
2733
- onClick: () => setFilter(value),
2968
+ 'aria-pressed': multimodalOnly,
2969
+ title: t('page.capability.multimodalTitle'),
2970
+ onClick: () => setMultimodalOnly(!multimodalOnly),
2734
2971
  },
2735
- 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'),
2736
2984
  ),
2737
- ),
2738
- h('span', { className: 'mvp_filterLabel' }, t('page.capabilityLabel')),
2739
- h(
2740
- 'button',
2741
- {
2742
- type: 'button',
2743
- className: 'mvp_filter',
2744
- 'aria-pressed': multimodalOnly,
2745
- onClick: () => setMultimodalOnly(!multimodalOnly),
2746
- },
2747
- t('page.capability.multimodal'),
2748
- ),
2749
- h(
2750
- 'button',
2751
- {
2752
- type: 'button',
2753
- className: 'mvp_filter',
2754
- 'aria-pressed': reasoningOnly,
2755
- onClick: () => setReasoningOnly(!reasoningOnly),
2756
- },
2757
- t('page.capability.reasoning'),
2758
- ),
2759
- h('label', { className: 'mvp_context' },
2760
- h('span', { className: 'mvp_filterLabel' }, t('page.contextLabel')),
2761
2985
  h(
2762
2986
  'select',
2763
2987
  {
2764
2988
  className: 'mvp_contextSelect',
2765
2989
  value: String(minContext),
2766
2990
  'aria-label': t('page.contextLabel'),
2991
+ title: t('page.contextLabel'),
2767
2992
  onChange: (event) => setMinContext(Number(event.target.value)),
2768
2993
  },
2769
2994
  [
@@ -2775,6 +3000,31 @@ window.__ModuleLoader__.load({
2775
3000
  ].map(([value, label]) => h('option', { key: value, value: String(value) }, t(label))),
2776
3001
  ),
2777
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
+ ),
2778
3028
  ),
2779
3029
  h(
2780
3030
  'div',
@@ -2787,6 +3037,7 @@ window.__ModuleLoader__.load({
2787
3037
  className: 'mvp_switch',
2788
3038
  'aria-checked': applyInput,
2789
3039
  'aria-label': t('page.applyInput'),
3040
+ 'aria-describedby': 'mvp-apply-hint',
2790
3041
  disabled: disabled,
2791
3042
  onClick: () => commitApply(!applyInput),
2792
3043
  },
@@ -2795,29 +3046,33 @@ window.__ModuleLoader__.load({
2795
3046
  h(
2796
3047
  'div',
2797
3048
  { className: 'mvp_applyText' },
2798
- h('div', { className: 'mvp_applyTitle' }, t('page.applyInput')),
2799
- 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')),
2800
3051
  ),
2801
3052
  ),
2802
- h(
2803
- VerifyPanel,
2804
- { verify, catalog: state, t, canUse: canVerify },
2805
- ),
2806
- h('div', { className: 'mvp_legend' }, t('page.legend')),
2807
- h('div', { className: 'mvp_legend' }, t('page.kindLegend')),
2808
- h('div', { className: 'mvp_legend' }, t('page.manualNote')),
2809
- overrideState.available === false
2810
- ? 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
+ )
2811
3066
  : null,
3067
+ hostStale ? h('div', { className: 'mvp_banner' }, t('page.hostStale')) : null,
2812
3068
  state.detailsError !== null && state.source === 'catalog'
2813
- ? h('div', { className: 'mvp_fallback' }, t('page.metaFallback'))
3069
+ ? h('div', { className: 'mvp_banner' }, t('page.metaFallback'))
2814
3070
  : null,
2815
- hostStale ? h('div', { className: 'mvp_fallback' }, t('page.hostStale')) : null,
2816
3071
  state.failures.length > 0
2817
3072
  ? h(
2818
3073
  'div',
2819
3074
  {
2820
- className: 'mvp_fallback',
3075
+ className: 'mvp_banner mvp_banner--warn',
2821
3076
  title: state.failures
2822
3077
  .map((failure) => (failure.name || failure.id) + ': ' + failure.message)
2823
3078
  .join('\n'),
@@ -2829,20 +3084,34 @@ window.__ModuleLoader__.load({
2829
3084
  }),
2830
3085
  )
2831
3086
  : null,
2832
- 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,
2833
3091
  state.error !== null
2834
- ? 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
+ )
2835
3097
  : null,
2836
- rows.length > 0 ? rows : null,
3098
+ h('div', { className: 'mvp_groups' }, rows),
2837
3099
  total > 0 && hasActiveFilters && rows.length === 0
2838
3100
  ? h('div', { className: 'mvp_note' }, t('page.filterEmpty'))
2839
3101
  : null,
2840
3102
  rows.length === 0 && !loading && state.error === null && total === 0
2841
- ? h('div', { className: 'mvp_error' }, t('page.empty'))
3103
+ ? h('div', { className: 'mvp_note' }, t('page.empty'))
2842
3104
  : null,
2843
- 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
+ ),
2844
3113
  providerNote !== null ? h('div', { className: 'mvp_note' }, providerNote) : null,
2845
- catalogNote !== null ? h('div', { className: 'mvp_note' }, catalogNote) : null,
3114
+ h(VerifyPanel, { verify, catalog: state, t, canUse: canVerify }),
2846
3115
  )
2847
3116
  }
2848
3117
  }
@@ -3057,13 +3326,11 @@ window.__ModuleLoader__.load({
3057
3326
  // -------------------------------------------------------- usage-view
3058
3327
  define("usage-view", function (require, module, exports) {
3059
3328
  /**
3060
- * Token-usage strip with the one-click exports.
3329
+ * Token-usage strip with the two CSV exports.
3061
3330
  *
3062
- * The strip renders the ledger's today/week/month/total buckets and links the
3063
- * two CSV endpoints. It is an inner piece of the settings section: the section
3064
- * supplies the translator and the usage store instance.
3065
- *
3066
- * @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.
3067
3334
  */
3068
3335
 
3069
3336
  const React = require('react')
@@ -3073,15 +3340,22 @@ window.__ModuleLoader__.load({
3073
3340
  const { formatTokens } = require("pure")
3074
3341
  const { useSource } = require("hooks")
3075
3342
 
3076
- /** Render one [in, out] window as "输入 x · 输出 y". */
3077
- function rangeText(range, t) {
3078
- 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')
3079
3352
  const input = (range[0] ?? 0) + (range[2] ?? 0) + (range[3] ?? 0)
3080
3353
  const output = range[1] ?? 0
3081
- return t('usage.range', {
3354
+ return t('usage.detail', {
3082
3355
  input: formatTokens(input),
3083
3356
  output: formatTokens(output),
3084
- total: formatTokens(input + output),
3357
+ cacheRead: formatTokens(range[2] ?? 0),
3358
+ cacheWrite: formatTokens(range[3] ?? 0),
3085
3359
  })
3086
3360
  }
3087
3361
 
@@ -3090,6 +3364,7 @@ window.__ModuleLoader__.load({
3090
3364
  const anchor = document.createElement('a')
3091
3365
  anchor.href = href
3092
3366
  anchor.rel = 'noopener'
3367
+ anchor.download = ''
3093
3368
  document.body.appendChild(anchor)
3094
3369
  anchor.click()
3095
3370
  anchor.remove()
@@ -3100,71 +3375,69 @@ window.__ModuleLoader__.load({
3100
3375
  const supported = features.length === 0 || features.includes('usage')
3101
3376
  const ranges = state.snapshot?.ranges
3102
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
+
3103
3390
  return h(
3104
3391
  'div',
3105
- { className: 'mvp_usage' + (supported ? '' : ' mvp_usage--off') },
3392
+ { className: 'mvp_usage' },
3106
3393
  h(
3107
- 'div',
3108
- { className: 'mvp_usageHead' },
3109
- h('span', { className: 'mvp_usageTitle' }, t('usage.title')),
3394
+ 'span',
3395
+ { className: 'mvp_usageLabel' },
3396
+ t('usage.title'),
3110
3397
  computing ? h('span', { className: 'mvp_usageComputing' }, t('usage.computing')) : null,
3111
- h(
3112
- 'span',
3113
- { className: 'mvp_usageActions' },
3114
- h(
3115
- 'button',
3116
- {
3117
- type: 'button',
3118
- className: 'mvp_button',
3119
- title: t('usage.exportUsageTitle'),
3120
- onClick: () => download(`${API}/export?type=usage`),
3121
- },
3122
- t('usage.exportUsage'),
3123
- ),
3398
+ ),
3399
+ h(
3400
+ 'span',
3401
+ { className: 'mvp_usageRanges' },
3402
+ windows.map(([key, labelKey, titleKey]) =>
3124
3403
  h(
3125
- 'button',
3404
+ 'span',
3126
3405
  {
3127
- type: 'button',
3128
- className: 'mvp_button',
3129
- title: t('usage.exportModelsTitle'),
3130
- 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),
3131
3409
  },
3132
- t('usage.exportModels'),
3410
+ h('span', { className: 'mvp_statLabel' }, t(labelKey)),
3411
+ h('span', { className: 'mvp_statValue' }, formatTokens(totalOf(ranges?.[key]))),
3133
3412
  ),
3134
3413
  ),
3135
3414
  ),
3136
- supported
3137
- ? h(
3138
- 'div',
3139
- { className: 'mvp_usageRanges' },
3140
- h(
3141
- 'span',
3142
- { className: 'mvp_stat', title: t('usage.todayTitle') },
3143
- h('span', { className: 'mvp_statLabel' }, t('usage.today')),
3144
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.today, t)),
3145
- ),
3146
- h(
3147
- 'span',
3148
- { className: 'mvp_stat', title: t('usage.weekTitle') },
3149
- h('span', { className: 'mvp_statLabel' }, t('usage.week')),
3150
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.week, t)),
3151
- ),
3152
- h(
3153
- 'span',
3154
- { className: 'mvp_stat', title: t('usage.monthTitle') },
3155
- h('span', { className: 'mvp_statLabel' }, t('usage.month')),
3156
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.month, t)),
3157
- ),
3158
- h(
3159
- 'span',
3160
- { className: 'mvp_stat mvp_stat--total', title: t('usage.totalTitle') },
3161
- h('span', { className: 'mvp_statLabel' }, t('usage.total')),
3162
- h('span', { className: 'mvp_statValue' }, rangeText(ranges?.total, t)),
3163
- ),
3164
- )
3165
- : 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
+ ),
3166
3439
  state.error !== null
3167
- ? h('div', { className: 'mvp_usageHint mvp_usageHint--warn' }, state.error)
3440
+ ? h('span', { className: 'mvp_usageHint mvp_usageHint--warn' }, state.error)
3168
3441
  : null,
3169
3442
  )
3170
3443
  }