@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.
@@ -1,6 +1,7 @@
1
1
  const React = require('react')
2
2
  const h = React.createElement
3
3
 
4
+ import { FIELD } from './constants.mjs'
4
5
  import { keyOf, kindOf, isGenerationKind, providerLabel, countModels, readCollapsedGroups, persistCollapsedGroups, overrideKeyOf } from './format.mjs'
5
6
  import { computeFilteredGroups, countKinds, probeOf } from './pure.mjs'
6
7
  import { capabilityTags, tagClassOf } from './tags.mjs'
@@ -9,6 +10,33 @@ import { useHidden, useSource } from './hooks.mjs'
9
10
  import { UsageToolbar } from './usage-view.mjs'
10
11
  import { VerifyPanel } from './checker-view.mjs'
11
12
 
13
+ /** Browser-local key for the "about this page" disclosure. */
14
+ const ABOUT_KEY = 'dsh-model-visibility:about-open:v1'
15
+
16
+ function readAboutOpen() {
17
+ try {
18
+ return window.localStorage.getItem(ABOUT_KEY) === '1'
19
+ } catch (_error) {
20
+ return false
21
+ }
22
+ }
23
+
24
+ function persistAboutOpen(open) {
25
+ try {
26
+ window.localStorage.setItem(ABOUT_KEY, open ? '1' : '0')
27
+ } catch (_error) {
28
+ // Storage is optional for a preference like this.
29
+ }
30
+ }
31
+
32
+ /**
33
+ * The settings page.
34
+ *
35
+ * Layout, in the order a user needs it: a one-line summary with the two
36
+ * primary actions, the token strip, the filter bar, the models, then the
37
+ * diagnostics — and behind a disclosure the long explanation that used to
38
+ * occupy a third of the viewport before the first model row.
39
+ */
12
40
  function makeSection(form, catalog, overrides, usage, verify) {
13
41
  return function ModelVisibilitySection({ t }) {
14
42
  const { hidden, snapshot } = useHidden(form)
@@ -19,6 +47,7 @@ function makeSection(form, catalog, overrides, usage, verify) {
19
47
  const [refreshing, setRefreshing] = React.useState(false)
20
48
  const [reloading, setReloading] = React.useState(false)
21
49
  const [collapsed, setCollapsed] = React.useState(readCollapsedGroups)
50
+ const [aboutOpen, setAboutOpen] = React.useState(readAboutOpen)
22
51
  const [editing, setEditing] = React.useState(null)
23
52
  const [providerNote, setProviderNote] = React.useState(null)
24
53
 
@@ -68,16 +97,18 @@ function makeSection(form, catalog, overrides, usage, verify) {
68
97
  if (disabled) return
69
98
  setBusy(true)
70
99
  setError(null)
71
- Promise.resolve(form.set(field, value)).then(
72
- (accepted) => {
73
- if (accepted === false) setError(t('page.saveFailed'))
74
- },
75
- () => {
76
- setError(t('page.saveFailed'))
77
- },
78
- ).finally(() => {
79
- setBusy(false)
80
- })
100
+ Promise.resolve(form.set(field, value))
101
+ .then(
102
+ (accepted) => {
103
+ if (accepted === false) setError(t('page.saveFailed'))
104
+ },
105
+ () => {
106
+ setError(t('page.saveFailed'))
107
+ },
108
+ )
109
+ .finally(() => {
110
+ setBusy(false)
111
+ })
81
112
  }
82
113
 
83
114
  const commit = (next) => writeField(FIELD, Array.from(next))
@@ -118,7 +149,7 @@ function makeSection(form, catalog, overrides, usage, verify) {
118
149
  // A hard reload may supersede a soft refresh that is still running;
119
150
  // catalog.load() gives it a new run and discards the older result.
120
151
  const reloadCatalog = () => {
121
- if (busy || reloading) return
152
+ if (busy || reloading || refreshing) return
122
153
  runCatalogAction(() => catalog.reload(), setReloading)
123
154
  }
124
155
 
@@ -141,24 +172,31 @@ function makeSection(form, catalog, overrides, usage, verify) {
141
172
  })
142
173
  }
143
174
 
175
+ const toggleAbout = () => {
176
+ setAboutOpen((previous) => {
177
+ persistAboutOpen(!previous)
178
+ return !previous
179
+ })
180
+ }
181
+
182
+ /* ------------------------------------------------------------- groups */
183
+
144
184
  const rows = []
145
185
  for (const group of filteredGroups) {
146
186
  const allGroupModels =
147
187
  (groups.find((candidate) => candidate.id === group.id) || group).models || []
148
188
  const models = group.models || []
149
- const shown = allGroupModels.filter(
150
- (model) => !hidden.has(keyOf(group.id, model.id)),
151
- ).length
189
+ const shown = allGroupModels.filter((model) => !hidden.has(keyOf(group.id, model.id))).length
152
190
  const allShown = models.every((model) => !hidden.has(keyOf(group.id, model.id)))
153
191
  const kinds = countKinds(allGroupModels)
154
192
  const kindParts = []
155
- if (kinds.chat > 0) kindParts.push(['chat', t('page.filter.chat') + ' ' + kinds.chat])
156
- if (kinds.image > 0) kindParts.push(['image', t('page.filter.image') + ' ' + kinds.image])
157
- if (kinds.video > 0) kindParts.push(['video', t('page.filter.video') + ' ' + kinds.video])
158
- if (kinds.other > 0) kindParts.push(['other', t('page.filter.other') + ' ' + kinds.other])
193
+ if (kinds.chat > 0) kindParts.push('chat')
194
+ if (kinds.image > 0) kindParts.push('image')
195
+ if (kinds.video > 0) kindParts.push('video')
196
+ if (kinds.other > 0) kindParts.push('other')
159
197
  const kindNodes = []
160
- for (const [index, [kindKey, label]] of kindParts.entries()) {
161
- if (index > 0) kindNodes.push(' · ')
198
+ for (const [index, kindKey] of kindParts.entries()) {
199
+ if (index > 0) kindNodes.push(h('span', { key: 'sep-' + kindKey, className: 'mvp_sep' }, '·'))
162
200
  kindNodes.push(
163
201
  h(
164
202
  'span',
@@ -167,8 +205,9 @@ function makeSection(form, catalog, overrides, usage, verify) {
167
205
  className:
168
206
  'mvp_kindCount' +
169
207
  (kindKey === 'chat' ? '' : ' mvp_kindCount--' + kindKey),
208
+ title: t('page.filter.' + kindKey) + ' ' + kinds[kindKey],
170
209
  },
171
- label,
210
+ kinds[kindKey],
172
211
  ),
173
212
  )
174
213
  }
@@ -186,34 +225,37 @@ function makeSection(form, catalog, overrides, usage, verify) {
186
225
  'div',
187
226
  { className: 'mvp_groupHead' },
188
227
  h(
189
- 'div',
190
- { className: 'mvp_groupTitle' },
191
- h(
192
- 'button',
193
- {
194
- type: 'button',
195
- className: 'mvp_collapse',
196
- 'aria-expanded': !collapsedNow,
197
- 'aria-label': collapseLabel,
198
- title: collapseLabel,
199
- onClick: () => toggleGroupCollapsed(group.id),
200
- },
201
- collapsedNow ? '▸' : '▾',
202
- ),
228
+ 'button',
229
+ {
230
+ type: 'button',
231
+ className: 'mvp_groupTitle',
232
+ 'aria-expanded': !collapsedNow,
233
+ 'aria-label': collapseLabel,
234
+ title: collapseLabel,
235
+ onClick: () => toggleGroupCollapsed(group.id),
236
+ },
237
+ h('span', { className: 'mvp_chevron' }, collapsedNow ? '▸' : '▾'),
203
238
  h('span', { className: 'mvp_groupName' }, groupName),
204
239
  h('span', { className: 'mvp_groupCount' }, shown + '/' + allGroupModels.length),
205
240
  kindParts.length > 0
206
241
  ? h('span', { className: 'mvp_groupCap' }, kindNodes)
207
242
  : null,
208
243
  probe !== null
209
- ? h('span', { className: 'mvp_groupProbe', title: probe.title || undefined }, probe.label)
244
+ ? h(
245
+ 'span',
246
+ {
247
+ className: 'mvp_groupProbe' + (probe.failed === true ? ' mvp_groupProbe--warn' : ''),
248
+ title: probe.title || undefined,
249
+ },
250
+ probe.label,
251
+ )
210
252
  : null,
211
253
  ),
212
254
  h(
213
255
  'button',
214
256
  {
215
257
  type: 'button',
216
- className: 'mvp_button',
258
+ className: 'mvp_button mvp_button--quiet',
217
259
  disabled: disabled,
218
260
  onClick: () => setGroup(group, models, !allShown),
219
261
  },
@@ -245,15 +287,20 @@ function makeSection(form, catalog, overrides, usage, verify) {
245
287
  className:
246
288
  'mvp_row' +
247
289
  (generation ? ' mvp_row--gen' : '') +
290
+ (shownModel ? '' : ' mvp_row--off') +
248
291
  kindTint,
249
292
  },
250
293
  h(
251
294
  'div',
252
295
  { className: 'mvp_rowText' },
253
- h('div', { className: 'mvp_rowTitle' }, model.name),
254
- model.id === model.name
255
- ? null
256
- : h('div', { className: 'mvp_rowMeta' }, model.id),
296
+ h(
297
+ 'div',
298
+ { className: 'mvp_rowHead' },
299
+ h('span', { className: 'mvp_rowTitle' }, model.name),
300
+ model.id === model.name
301
+ ? null
302
+ : h('span', { className: 'mvp_rowMeta' }, model.id),
303
+ ),
257
304
  h(
258
305
  'div',
259
306
  { className: 'mvp_tags' },
@@ -278,7 +325,7 @@ function makeSection(form, catalog, overrides, usage, verify) {
278
325
  'button',
279
326
  {
280
327
  type: 'button',
281
- className: 'mvp_button mvp_editButton',
328
+ className: 'mvp_button mvp_button--quiet mvp_editButton',
282
329
  'aria-pressed': editing === rowKey,
283
330
  'aria-label': t('row.edit') + ': ' + model.name,
284
331
  title: t('row.editTitle'),
@@ -326,9 +373,13 @@ function makeSection(form, catalog, overrides, usage, verify) {
326
373
  )
327
374
  }
328
375
 
376
+ /* ------------------------------------------------------------- status */
377
+
329
378
  const loading = state.status === 'loading' || state.status === 'idle'
330
379
  const providerList = Array.isArray(state.providers) ? state.providers : []
331
- const probeOk = providerList.filter((provider) => provider && provider.status === 'ok').length
380
+ const probeOk = providerList.filter(
381
+ (provider) => provider && (provider.status === 'ok' || provider.status === 'partial'),
382
+ ).length
332
383
  const probeCaps = providerList.reduce(
333
384
  (count, provider) => count + (Number(provider && provider.capabilities) || 0),
334
385
  0,
@@ -339,10 +390,7 @@ function makeSection(form, catalog, overrides, usage, verify) {
339
390
  ? t('page.probeSummary', { ok: probeOk, caps: probeCaps })
340
391
  : t('page.probeSummaryNone')
341
392
  : null
342
- // A page refresh can deliver this bundle while the Host half is still
343
- // the previous version; the switch would look live but do nothing.
344
393
  const hostStale = state.source === 'host' && !hostFeatures.includes('applyInput')
345
- // A hard reload is only meaningful when the Host half understands it.
346
394
  const canReload = state.source === 'host' && hostFeatures.includes('reload')
347
395
  const anyCollapsed = groups.some((group) => collapsed.has(group.id))
348
396
  const learnedDate =
@@ -367,54 +415,52 @@ function makeSection(form, catalog, overrides, usage, verify) {
367
415
  ]
368
416
  const hasActiveFilters =
369
417
  filter !== 'all' || normalizedQuery !== '' || multimodalOnly || reasoningOnly || minContext > 0
418
+ // A relay that answered with a diagnostic is worth surfacing next to the
419
+ // group, but only failed routes belong in the page-level warning list.
420
+ const failedRoutes = providerList.filter(
421
+ (provider) => provider && (provider.status === 'failed' || provider.status === 'unauthorized'),
422
+ )
423
+ const summaryText =
424
+ total === 0
425
+ ? loading
426
+ ? t('page.loading')
427
+ : t('page.empty')
428
+ : t('page.summary', { visible: total - hiddenCount, total })
429
+
430
+ /* ------------------------------------------------------------- render */
370
431
 
371
432
  return h(
372
433
  'div',
373
434
  { className: 'mvp_page' },
374
- h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
435
+ // Title + summary + the two actions that are worth a permanent slot.
375
436
  h(
376
437
  'div',
377
438
  { className: 'mvp_head' },
378
- h('h2', { className: 'mvp_title' }, t('page.title')),
379
- h('p', { className: 'mvp_desc' }, t('page.description')),
380
- ),
381
- h(
382
- 'div',
383
- { className: 'mvp_toolbar' },
384
439
  h(
385
440
  'div',
386
- { className: 'mvp_summary' },
387
- total === 0
388
- ? loading
389
- ? t('page.loading')
390
- : t('page.empty')
391
- : t('page.summary', { visible: total - hiddenCount, total }) +
392
- (hiddenCount > 0 ? ' · ' + t('page.hiddenSummary', { count: hiddenCount }) : '') +
393
- (generationCount > 0 ? ' · ' + t('page.kindSummary', { count: generationCount }) : ''),
441
+ { className: 'mvp_headMain' },
442
+ h('h2', { className: 'mvp_title' }, t('page.title')),
443
+ h('span', { className: 'mvp_summary' }, summaryText),
444
+ hiddenCount > 0
445
+ ? h('span', { className: 'mvp_summary mvp_summary--dim' }, t('page.hiddenSummary', { count: hiddenCount }))
446
+ : null,
447
+ generationCount > 0
448
+ ? h('span', { className: 'mvp_summary mvp_summary--dim' }, t('page.kindSummary', { count: generationCount }))
449
+ : null,
394
450
  ),
395
451
  h(
396
452
  'div',
397
- { className: 'mvp_actions' },
453
+ { className: 'mvp_headActions' },
398
454
  h(
399
455
  'button',
400
456
  {
401
457
  type: 'button',
402
- className: 'mvp_button',
403
- disabled: disabled || hiddenCount === 0,
404
- onClick: () => commit(new Set()),
458
+ className: 'mvp_button mvp_button--quiet',
459
+ 'aria-pressed': aboutOpen,
460
+ title: t('page.aboutTitle'),
461
+ onClick: toggleAbout,
405
462
  },
406
- t('page.showAll'),
407
- ),
408
- h(
409
- 'button',
410
- {
411
- type: 'button',
412
- className: 'mvp_button',
413
- disabled: groups.length === 0,
414
- title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
415
- onClick: toggleAllCollapsed,
416
- },
417
- t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
463
+ t('page.about') + (aboutOpen ? ' ▴' : ' ▾'),
418
464
  ),
419
465
  h(
420
466
  'button',
@@ -422,6 +468,7 @@ function makeSection(form, catalog, overrides, usage, verify) {
422
468
  type: 'button',
423
469
  className: 'mvp_button',
424
470
  disabled: busy || refreshing || reloading,
471
+ title: t('page.refreshTitle'),
425
472
  onClick: refreshCatalog,
426
473
  },
427
474
  t(refreshing ? 'page.refreshing' : 'page.refresh'),
@@ -439,63 +486,77 @@ function makeSection(form, catalog, overrides, usage, verify) {
439
486
  ),
440
487
  ),
441
488
  ),
489
+ aboutOpen
490
+ ? h(
491
+ 'div',
492
+ { className: 'mvp_about' },
493
+ h('p', { className: 'mvp_aboutP' }, t('page.description')),
494
+ h('p', { className: 'mvp_aboutP' }, t('page.legend')),
495
+ h('p', { className: 'mvp_aboutP' }, t('page.kindLegend')),
496
+ h('p', { className: 'mvp_aboutP' }, t('page.manualNote')),
497
+ )
498
+ : null,
499
+ h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
442
500
  h(
443
501
  'div',
444
502
  { className: 'mvp_filters' },
445
- h('label', { className: 'mvp_search' },
446
- h('span', { className: 'mvp_filterLabel' }, t('page.searchPlaceholder')),
447
- h('input', {
448
- className: 'mvp_searchInput',
449
- type: 'search',
450
- value: query,
451
- placeholder: t('page.searchPlaceholder'),
452
- 'aria-label': t('page.searchPlaceholder'),
453
- onInput: (event) => setQuery(event.target.value),
454
- }),
503
+ h('input', {
504
+ className: 'mvp_searchInput',
505
+ type: 'search',
506
+ value: query,
507
+ placeholder: t('page.searchPlaceholder'),
508
+ 'aria-label': t('page.searchPlaceholder'),
509
+ onInput: (event) => setQuery(event.target.value),
510
+ }),
511
+ h(
512
+ 'div',
513
+ { className: 'mvp_filterGroup' },
514
+ filterOptions.map(([value, label, count]) =>
515
+ h(
516
+ 'button',
517
+ {
518
+ key: value,
519
+ type: 'button',
520
+ className: 'mvp_filter',
521
+ 'aria-pressed': filter === value,
522
+ onClick: () => setFilter(value),
523
+ },
524
+ label + ' ' + count,
525
+ ),
526
+ ),
455
527
  ),
456
- h('span', { className: 'mvp_filterLabel' }, t('page.filterLabel')),
457
- filterOptions.map(([value, label, count]) =>
528
+ h(
529
+ 'div',
530
+ { className: 'mvp_filterGroup' },
458
531
  h(
459
532
  'button',
460
533
  {
461
- key: value,
462
534
  type: 'button',
463
535
  className: 'mvp_filter',
464
- 'aria-pressed': filter === value,
465
- onClick: () => setFilter(value),
536
+ 'aria-pressed': multimodalOnly,
537
+ title: t('page.capability.multimodalTitle'),
538
+ onClick: () => setMultimodalOnly(!multimodalOnly),
466
539
  },
467
- label + ' ' + count,
540
+ t('page.capability.multimodal'),
541
+ ),
542
+ h(
543
+ 'button',
544
+ {
545
+ type: 'button',
546
+ className: 'mvp_filter',
547
+ 'aria-pressed': reasoningOnly,
548
+ title: t('page.capability.reasoningTitle'),
549
+ onClick: () => setReasoningOnly(!reasoningOnly),
550
+ },
551
+ t('page.capability.reasoning'),
468
552
  ),
469
- ),
470
- h('span', { className: 'mvp_filterLabel' }, t('page.capabilityLabel')),
471
- h(
472
- 'button',
473
- {
474
- type: 'button',
475
- className: 'mvp_filter',
476
- 'aria-pressed': multimodalOnly,
477
- onClick: () => setMultimodalOnly(!multimodalOnly),
478
- },
479
- t('page.capability.multimodal'),
480
- ),
481
- h(
482
- 'button',
483
- {
484
- type: 'button',
485
- className: 'mvp_filter',
486
- 'aria-pressed': reasoningOnly,
487
- onClick: () => setReasoningOnly(!reasoningOnly),
488
- },
489
- t('page.capability.reasoning'),
490
- ),
491
- h('label', { className: 'mvp_context' },
492
- h('span', { className: 'mvp_filterLabel' }, t('page.contextLabel')),
493
553
  h(
494
554
  'select',
495
555
  {
496
556
  className: 'mvp_contextSelect',
497
557
  value: String(minContext),
498
558
  'aria-label': t('page.contextLabel'),
559
+ title: t('page.contextLabel'),
499
560
  onChange: (event) => setMinContext(Number(event.target.value)),
500
561
  },
501
562
  [
@@ -507,6 +568,31 @@ function makeSection(form, catalog, overrides, usage, verify) {
507
568
  ].map(([value, label]) => h('option', { key: value, value: String(value) }, t(label))),
508
569
  ),
509
570
  ),
571
+ h(
572
+ 'div',
573
+ { className: 'mvp_filterGroup mvp_filterGroup--push' },
574
+ h(
575
+ 'button',
576
+ {
577
+ type: 'button',
578
+ className: 'mvp_button mvp_button--quiet',
579
+ disabled: disabled || hiddenCount === 0,
580
+ onClick: () => commit(new Set()),
581
+ },
582
+ t('page.showAll'),
583
+ ),
584
+ h(
585
+ 'button',
586
+ {
587
+ type: 'button',
588
+ className: 'mvp_button mvp_button--quiet',
589
+ disabled: groups.length === 0,
590
+ title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
591
+ onClick: toggleAllCollapsed,
592
+ },
593
+ t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
594
+ ),
595
+ ),
510
596
  ),
511
597
  h(
512
598
  'div',
@@ -519,6 +605,7 @@ function makeSection(form, catalog, overrides, usage, verify) {
519
605
  className: 'mvp_switch',
520
606
  'aria-checked': applyInput,
521
607
  'aria-label': t('page.applyInput'),
608
+ 'aria-describedby': 'mvp-apply-hint',
522
609
  disabled: disabled,
523
610
  onClick: () => commitApply(!applyInput),
524
611
  },
@@ -527,29 +614,33 @@ function makeSection(form, catalog, overrides, usage, verify) {
527
614
  h(
528
615
  'div',
529
616
  { className: 'mvp_applyText' },
530
- h('div', { className: 'mvp_applyTitle' }, t('page.applyInput')),
531
- h('div', { className: 'mvp_applyHint' }, t('page.applyInputHint')),
617
+ h('span', { className: 'mvp_applyTitle' }, t('page.applyInput')),
618
+ h('span', { className: 'mvp_applyHint', id: 'mvp-apply-hint' }, t('page.applyInputShort')),
532
619
  ),
533
620
  ),
534
- h(
535
- VerifyPanel,
536
- { verify, catalog: state, t, canUse: canVerify },
537
- ),
538
- h('div', { className: 'mvp_legend' }, t('page.legend')),
539
- h('div', { className: 'mvp_legend' }, t('page.kindLegend')),
540
- h('div', { className: 'mvp_legend' }, t('page.manualNote')),
541
- overrideState.available === false
542
- ? h('div', { className: 'mvp_fallback' }, t('override.unavailable'))
621
+ failedRoutes.length > 0
622
+ ? h(
623
+ 'div',
624
+ {
625
+ className: 'mvp_banner mvp_banner--warn',
626
+ title: failedRoutes
627
+ .map((route) => (route.name || route.id) + ': ' + (route.message || route.status))
628
+ .join('\n'),
629
+ },
630
+ t('page.probeFailures', {
631
+ names: failedRoutes.map((route) => route.name || route.id).join(' · '),
632
+ }),
633
+ )
543
634
  : null,
635
+ hostStale ? h('div', { className: 'mvp_banner' }, t('page.hostStale')) : null,
544
636
  state.detailsError !== null && state.source === 'catalog'
545
- ? h('div', { className: 'mvp_fallback' }, t('page.metaFallback'))
637
+ ? h('div', { className: 'mvp_banner' }, t('page.metaFallback'))
546
638
  : null,
547
- hostStale ? h('div', { className: 'mvp_fallback' }, t('page.hostStale')) : null,
548
639
  state.failures.length > 0
549
640
  ? h(
550
641
  'div',
551
642
  {
552
- className: 'mvp_fallback',
643
+ className: 'mvp_banner mvp_banner--warn',
553
644
  title: state.failures
554
645
  .map((failure) => (failure.name || failure.id) + ': ' + failure.message)
555
646
  .join('\n'),
@@ -561,20 +652,34 @@ function makeSection(form, catalog, overrides, usage, verify) {
561
652
  }),
562
653
  )
563
654
  : null,
564
- error !== null ? h('div', { className: 'mvp_error', role: 'alert' }, error) : null,
655
+ overrideState.available === false
656
+ ? h('div', { className: 'mvp_banner' }, t('override.unavailable'))
657
+ : null,
658
+ error !== null ? h('div', { className: 'mvp_banner mvp_banner--error', role: 'alert' }, error) : null,
565
659
  state.error !== null
566
- ? h('div', { className: 'mvp_error', role: 'alert' }, t('page.error', { message: state.error }))
660
+ ? h(
661
+ 'div',
662
+ { className: 'mvp_banner mvp_banner--error', role: 'alert' },
663
+ t('page.error', { message: state.error }),
664
+ )
567
665
  : null,
568
- rows.length > 0 ? rows : null,
666
+ h('div', { className: 'mvp_groups' }, rows),
569
667
  total > 0 && hasActiveFilters && rows.length === 0
570
668
  ? h('div', { className: 'mvp_note' }, t('page.filterEmpty'))
571
669
  : null,
572
670
  rows.length === 0 && !loading && state.error === null && total === 0
573
- ? h('div', { className: 'mvp_error' }, t('page.empty'))
671
+ ? h('div', { className: 'mvp_note' }, t('page.empty'))
574
672
  : null,
575
- probeNote !== null ? h('div', { className: 'mvp_note' }, probeNote) : null,
673
+ h(
674
+ 'div',
675
+ { className: 'mvp_footer' },
676
+ probeNote !== null
677
+ ? h('span', { className: 'mvp_footerItem', title: t('page.probeSummaryTitle') }, probeNote)
678
+ : null,
679
+ catalogNote !== null ? h('span', { className: 'mvp_footerItem' }, catalogNote) : null,
680
+ ),
576
681
  providerNote !== null ? h('div', { className: 'mvp_note' }, providerNote) : null,
577
- catalogNote !== null ? h('div', { className: 'mvp_note' }, catalogNote) : null,
682
+ h(VerifyPanel, { verify, catalog: state, t, canUse: canVerify }),
578
683
  )
579
684
  }
580
685
  }