@tzyc/dsh-model-visibility 1.9.1 → 1.10.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.
@@ -0,0 +1,582 @@
1
+ const React = require('react')
2
+ const h = React.createElement
3
+
4
+ import { keyOf, kindOf, isGenerationKind, providerLabel, countModels, readCollapsedGroups, persistCollapsedGroups, overrideKeyOf } from './format.mjs'
5
+ import { computeFilteredGroups, countKinds, probeOf } from './pure.mjs'
6
+ import { capabilityTags, tagClassOf } from './tags.mjs'
7
+ import { ModelOverrideEditor, providerWriteNote } from './override-editor.mjs'
8
+ import { useHidden, useSource } from './hooks.mjs'
9
+ import { UsageToolbar } from './usage-view.mjs'
10
+ import { VerifyPanel } from './checker-view.mjs'
11
+
12
+ function makeSection(form, catalog, overrides, usage, verify) {
13
+ return function ModelVisibilitySection({ t }) {
14
+ const { hidden, snapshot } = useHidden(form)
15
+ const state = useSource(catalog)
16
+ const overrideState = useSource(overrides)
17
+ const [busy, setBusy] = React.useState(false)
18
+ const [error, setError] = React.useState(null)
19
+ const [refreshing, setRefreshing] = React.useState(false)
20
+ const [reloading, setReloading] = React.useState(false)
21
+ const [collapsed, setCollapsed] = React.useState(readCollapsedGroups)
22
+ const [editing, setEditing] = React.useState(null)
23
+ const [providerNote, setProviderNote] = React.useState(null)
24
+
25
+ React.useEffect(() => {
26
+ catalog.load()
27
+ overrides.load()
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, [])
30
+
31
+ const catalogMeta = state.learned && typeof state.learned === 'object' ? state.learned : null
32
+ const hostFeatures =
33
+ catalogMeta && Array.isArray(catalogMeta.features) ? catalogMeta.features : []
34
+ const canEdit = state.source === 'host' && hostFeatures.includes('overrides')
35
+ const canVerify = state.source === 'host' && hostFeatures.includes('verify')
36
+ const overrideEntries = new Map()
37
+ for (const entry of overrideState.entries) {
38
+ overrideEntries.set(overrideKeyOf(entry), entry)
39
+ }
40
+
41
+ const writable = snapshot.writable !== false
42
+ const groups = state.groups
43
+ const total = countModels(groups)
44
+ const hiddenCount = groups.reduce(
45
+ (count, group) =>
46
+ count +
47
+ (group.models || []).filter((model) => hidden.has(keyOf(group.id, model.id))).length,
48
+ 0,
49
+ )
50
+ const allKinds = countKinds(groups.flatMap((group) => group.models || []))
51
+ const generationCount = allKinds.image + allKinds.video
52
+ const [filter, setFilter] = React.useState('all')
53
+ const [query, setQuery] = React.useState('')
54
+ const [multimodalOnly, setMultimodalOnly] = React.useState(false)
55
+ const [reasoningOnly, setReasoningOnly] = React.useState(false)
56
+ const [minContext, setMinContext] = React.useState(0)
57
+ const normalizedQuery = query.trim().toLowerCase()
58
+ const filteredGroups = computeFilteredGroups(groups, {
59
+ filter,
60
+ query: normalizedQuery,
61
+ multimodalOnly,
62
+ reasoningOnly,
63
+ minContext,
64
+ })
65
+ const disabled = busy || !writable
66
+
67
+ const writeField = (field, value) => {
68
+ if (disabled) return
69
+ setBusy(true)
70
+ 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
+ })
81
+ }
82
+
83
+ const commit = (next) => writeField(FIELD, Array.from(next))
84
+ const applyInput = !(snapshot.value && snapshot.value.applyInput === false)
85
+ const commitApply = (next) => writeField('applyInput', next)
86
+
87
+ const toggleModel = (group, model) => {
88
+ const next = new Set(hidden)
89
+ const key = keyOf(group.id, model.id)
90
+ if (next.has(key)) next.delete(key)
91
+ else next.add(key)
92
+ commit(next)
93
+ }
94
+
95
+ const setGroup = (group, subset, visible) => {
96
+ const next = new Set(hidden)
97
+ for (const model of subset) {
98
+ const key = keyOf(group.id, model.id)
99
+ if (visible) next.delete(key)
100
+ else next.add(key)
101
+ }
102
+ commit(next)
103
+ }
104
+
105
+ const runCatalogAction = (action, setActive) => {
106
+ setActive(true)
107
+ Promise.resolve()
108
+ .then(action)
109
+ .catch(() => {})
110
+ .finally(() => setActive(false))
111
+ }
112
+
113
+ const refreshCatalog = () => {
114
+ if (refreshing || reloading) return
115
+ runCatalogAction(() => catalog.refresh(), setRefreshing)
116
+ }
117
+
118
+ // A hard reload may supersede a soft refresh that is still running;
119
+ // catalog.load() gives it a new run and discards the older result.
120
+ const reloadCatalog = () => {
121
+ if (busy || reloading) return
122
+ runCatalogAction(() => catalog.reload(), setReloading)
123
+ }
124
+
125
+ const toggleGroupCollapsed = (id) => {
126
+ setCollapsed((previous) => {
127
+ const next = new Set(previous)
128
+ if (next.has(id)) next.delete(id)
129
+ else next.add(id)
130
+ persistCollapsedGroups(next)
131
+ return next
132
+ })
133
+ }
134
+
135
+ const toggleAllCollapsed = () => {
136
+ setCollapsed((previous) => {
137
+ const collapsedNow = groups.some((group) => previous.has(group.id))
138
+ const next = collapsedNow ? new Set() : new Set(groups.map((group) => group.id))
139
+ persistCollapsedGroups(next)
140
+ return next
141
+ })
142
+ }
143
+
144
+ const rows = []
145
+ for (const group of filteredGroups) {
146
+ const allGroupModels =
147
+ (groups.find((candidate) => candidate.id === group.id) || group).models || []
148
+ const models = group.models || []
149
+ const shown = allGroupModels.filter(
150
+ (model) => !hidden.has(keyOf(group.id, model.id)),
151
+ ).length
152
+ const allShown = models.every((model) => !hidden.has(keyOf(group.id, model.id)))
153
+ const kinds = countKinds(allGroupModels)
154
+ 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])
159
+ const kindNodes = []
160
+ for (const [index, [kindKey, label]] of kindParts.entries()) {
161
+ if (index > 0) kindNodes.push(' · ')
162
+ kindNodes.push(
163
+ h(
164
+ 'span',
165
+ {
166
+ key: kindKey,
167
+ className:
168
+ 'mvp_kindCount' +
169
+ (kindKey === 'chat' ? '' : ' mvp_kindCount--' + kindKey),
170
+ },
171
+ label,
172
+ ),
173
+ )
174
+ }
175
+ const groupName = providerLabel(group, t)
176
+ const collapsedNow = collapsed.has(group.id)
177
+ const collapseLabel = t(collapsedNow ? 'group.expandNamed' : 'group.collapseNamed', {
178
+ name: groupName,
179
+ })
180
+ const probe = probeOf(state, group, t)
181
+ rows.push(
182
+ h(
183
+ 'div',
184
+ { key: group.id, className: 'mvp_group' },
185
+ h(
186
+ 'div',
187
+ { className: 'mvp_groupHead' },
188
+ 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
+ ),
203
+ h('span', { className: 'mvp_groupName' }, groupName),
204
+ h('span', { className: 'mvp_groupCount' }, shown + '/' + allGroupModels.length),
205
+ kindParts.length > 0
206
+ ? h('span', { className: 'mvp_groupCap' }, kindNodes)
207
+ : null,
208
+ probe !== null
209
+ ? h('span', { className: 'mvp_groupProbe', title: probe.title || undefined }, probe.label)
210
+ : null,
211
+ ),
212
+ h(
213
+ 'button',
214
+ {
215
+ type: 'button',
216
+ className: 'mvp_button',
217
+ disabled: disabled,
218
+ onClick: () => setGroup(group, models, !allShown),
219
+ },
220
+ allShown
221
+ ? filter === 'all'
222
+ ? t('group.hideAll')
223
+ : t('group.hideFiltered')
224
+ : filter === 'all'
225
+ ? t('group.showAll')
226
+ : t('group.showFiltered'),
227
+ ),
228
+ ),
229
+ collapsedNow
230
+ ? null
231
+ : models.map((model) => {
232
+ const rowKey = keyOf(group.id, model.id)
233
+ const shownModel = !hidden.has(rowKey)
234
+ const tags = capabilityTags(model, t)
235
+ const kind = kindOf(model)
236
+ const generation = isGenerationKind(kind)
237
+ const kindTint =
238
+ kind !== 'chat' &&
239
+ ['image', 'video', 'embedding', 'rerank', 'audio', 'classifier', 'translation'].includes(kind)
240
+ ? ' mvp_row--kind-' + kind
241
+ : ''
242
+ const row = h(
243
+ 'div',
244
+ {
245
+ className:
246
+ 'mvp_row' +
247
+ (generation ? ' mvp_row--gen' : '') +
248
+ kindTint,
249
+ },
250
+ h(
251
+ 'div',
252
+ { 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),
257
+ h(
258
+ 'div',
259
+ { className: 'mvp_tags' },
260
+ tags.map((tag) =>
261
+ h(
262
+ 'span',
263
+ {
264
+ key: tag.key,
265
+ className: tagClassOf(tag),
266
+ title: tag.title || undefined,
267
+ },
268
+ tag.label,
269
+ ),
270
+ ),
271
+ ),
272
+ ),
273
+ h(
274
+ 'div',
275
+ { className: 'mvp_rowActions' },
276
+ canEdit
277
+ ? h(
278
+ 'button',
279
+ {
280
+ type: 'button',
281
+ className: 'mvp_button mvp_editButton',
282
+ 'aria-pressed': editing === rowKey,
283
+ 'aria-label': t('row.edit') + ': ' + model.name,
284
+ title: t('row.editTitle'),
285
+ disabled: busy,
286
+ onClick: () => setEditing(editing === rowKey ? null : rowKey),
287
+ },
288
+ t('row.edit'),
289
+ )
290
+ : null,
291
+ h(
292
+ 'button',
293
+ {
294
+ type: 'button',
295
+ role: 'switch',
296
+ className: 'mvp_switch',
297
+ 'aria-checked': shownModel,
298
+ 'aria-label': model.name,
299
+ disabled: disabled,
300
+ onClick: () => toggleModel(group, model),
301
+ },
302
+ h('span', { className: 'mvp_thumb' }),
303
+ ),
304
+ ),
305
+ )
306
+ const editor =
307
+ canEdit && editing === rowKey
308
+ ? h(ModelOverrideEditor, {
309
+ key: 'editor',
310
+ entry:
311
+ overrideEntries.get(rowKey) || { provider: group.id, model: model.id },
312
+ name: model.name,
313
+ t,
314
+ overrides,
315
+ onClose: () => setEditing(null),
316
+ onSaved: (result) => {
317
+ setEditing(null)
318
+ setProviderNote(providerWriteNote(result && result.provider, t))
319
+ refreshCatalog()
320
+ },
321
+ })
322
+ : null
323
+ return h(React.Fragment, { key: model.id }, row, editor)
324
+ }),
325
+ ),
326
+ )
327
+ }
328
+
329
+ const loading = state.status === 'loading' || state.status === 'idle'
330
+ const providerList = Array.isArray(state.providers) ? state.providers : []
331
+ const probeOk = providerList.filter((provider) => provider && provider.status === 'ok').length
332
+ const probeCaps = providerList.reduce(
333
+ (count, provider) => count + (Number(provider && provider.capabilities) || 0),
334
+ 0,
335
+ )
336
+ const probeNote =
337
+ state.source === 'host'
338
+ ? providerList.length > 0
339
+ ? t('page.probeSummary', { ok: probeOk, caps: probeCaps })
340
+ : t('page.probeSummaryNone')
341
+ : 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
+ const hostStale = state.source === 'host' && !hostFeatures.includes('applyInput')
345
+ // A hard reload is only meaningful when the Host half understands it.
346
+ const canReload = state.source === 'host' && hostFeatures.includes('reload')
347
+ const anyCollapsed = groups.some((group) => collapsed.has(group.id))
348
+ const learnedDate =
349
+ catalogMeta && Number.isFinite(catalogMeta.updatedAt)
350
+ ? catalogMeta.updatedAt > 0
351
+ ? new Date(catalogMeta.updatedAt).toISOString().slice(0, 10)
352
+ : null
353
+ : null
354
+ const catalogNote =
355
+ catalogMeta !== null
356
+ ? t('page.learnedNote', {
357
+ models: catalogMeta.models ?? 0,
358
+ date: learnedDate || '?',
359
+ })
360
+ : null
361
+ const filterOptions = [
362
+ ['all', t('page.filter.all'), total],
363
+ ['chat', t('page.filter.chat'), allKinds.chat],
364
+ ['image', t('page.filter.image'), allKinds.image],
365
+ ['video', t('page.filter.video'), allKinds.video],
366
+ ['other', t('page.filter.other'), allKinds.other],
367
+ ]
368
+ const hasActiveFilters =
369
+ filter !== 'all' || normalizedQuery !== '' || multimodalOnly || reasoningOnly || minContext > 0
370
+
371
+ return h(
372
+ 'div',
373
+ { className: 'mvp_page' },
374
+ h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
375
+ h(
376
+ 'div',
377
+ { 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
+ h(
385
+ '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 }) : ''),
394
+ ),
395
+ h(
396
+ 'div',
397
+ { className: 'mvp_actions' },
398
+ h(
399
+ 'button',
400
+ {
401
+ type: 'button',
402
+ className: 'mvp_button',
403
+ disabled: disabled || hiddenCount === 0,
404
+ onClick: () => commit(new Set()),
405
+ },
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'),
418
+ ),
419
+ h(
420
+ 'button',
421
+ {
422
+ type: 'button',
423
+ className: 'mvp_button',
424
+ disabled: busy || refreshing || reloading,
425
+ onClick: refreshCatalog,
426
+ },
427
+ t(refreshing ? 'page.refreshing' : 'page.refresh'),
428
+ ),
429
+ h(
430
+ 'button',
431
+ {
432
+ type: 'button',
433
+ className: 'mvp_button mvp_button--accent',
434
+ disabled: !canReload || busy || reloading,
435
+ title: t(canReload ? 'page.reloadTitle' : 'page.reloadUnavailable'),
436
+ onClick: reloadCatalog,
437
+ },
438
+ t(reloading ? 'page.reloading' : 'page.reload'),
439
+ ),
440
+ ),
441
+ ),
442
+ h(
443
+ 'div',
444
+ { 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
+ }),
455
+ ),
456
+ h('span', { className: 'mvp_filterLabel' }, t('page.filterLabel')),
457
+ filterOptions.map(([value, label, count]) =>
458
+ h(
459
+ 'button',
460
+ {
461
+ key: value,
462
+ type: 'button',
463
+ className: 'mvp_filter',
464
+ 'aria-pressed': filter === value,
465
+ onClick: () => setFilter(value),
466
+ },
467
+ label + ' ' + count,
468
+ ),
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
+ h(
494
+ 'select',
495
+ {
496
+ className: 'mvp_contextSelect',
497
+ value: String(minContext),
498
+ 'aria-label': t('page.contextLabel'),
499
+ onChange: (event) => setMinContext(Number(event.target.value)),
500
+ },
501
+ [
502
+ [0, 'page.context.any'],
503
+ [128000, 'page.context.128k'],
504
+ [256000, 'page.context.256k'],
505
+ [512000, 'page.context.512k'],
506
+ [1000000, 'page.context.1m'],
507
+ ].map(([value, label]) => h('option', { key: value, value: String(value) }, t(label))),
508
+ ),
509
+ ),
510
+ ),
511
+ h(
512
+ 'div',
513
+ { className: 'mvp_applyRow' },
514
+ h(
515
+ 'button',
516
+ {
517
+ type: 'button',
518
+ role: 'switch',
519
+ className: 'mvp_switch',
520
+ 'aria-checked': applyInput,
521
+ 'aria-label': t('page.applyInput'),
522
+ disabled: disabled,
523
+ onClick: () => commitApply(!applyInput),
524
+ },
525
+ h('span', { className: 'mvp_thumb' }),
526
+ ),
527
+ h(
528
+ 'div',
529
+ { className: 'mvp_applyText' },
530
+ h('div', { className: 'mvp_applyTitle' }, t('page.applyInput')),
531
+ h('div', { className: 'mvp_applyHint' }, t('page.applyInputHint')),
532
+ ),
533
+ ),
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'))
543
+ : null,
544
+ state.detailsError !== null && state.source === 'catalog'
545
+ ? h('div', { className: 'mvp_fallback' }, t('page.metaFallback'))
546
+ : null,
547
+ hostStale ? h('div', { className: 'mvp_fallback' }, t('page.hostStale')) : null,
548
+ state.failures.length > 0
549
+ ? h(
550
+ 'div',
551
+ {
552
+ className: 'mvp_fallback',
553
+ title: state.failures
554
+ .map((failure) => (failure.name || failure.id) + ': ' + failure.message)
555
+ .join('\n'),
556
+ },
557
+ t('page.providerFailures', {
558
+ names: state.failures
559
+ .map((failure) => failure.name || failure.id)
560
+ .join(' · '),
561
+ }),
562
+ )
563
+ : null,
564
+ error !== null ? h('div', { className: 'mvp_error', role: 'alert' }, error) : null,
565
+ state.error !== null
566
+ ? h('div', { className: 'mvp_error', role: 'alert' }, t('page.error', { message: state.error }))
567
+ : null,
568
+ rows.length > 0 ? rows : null,
569
+ total > 0 && hasActiveFilters && rows.length === 0
570
+ ? h('div', { className: 'mvp_note' }, t('page.filterEmpty'))
571
+ : null,
572
+ rows.length === 0 && !loading && state.error === null && total === 0
573
+ ? h('div', { className: 'mvp_error' }, t('page.empty'))
574
+ : null,
575
+ probeNote !== null ? h('div', { className: 'mvp_note' }, probeNote) : null,
576
+ providerNote !== null ? h('div', { className: 'mvp_note' }, providerNote) : null,
577
+ catalogNote !== null ? h('div', { className: 'mvp_note' }, catalogNote) : null,
578
+ )
579
+ }
580
+ }
581
+
582
+ export { makeSection }