@tzyc/dsh-model-visibility 1.9.0 → 1.10.0

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,581 @@
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 overrideEntries = new Map()
36
+ for (const entry of overrideState.entries) {
37
+ overrideEntries.set(overrideKeyOf(entry), entry)
38
+ }
39
+
40
+ const writable = snapshot.writable !== false
41
+ const groups = state.groups
42
+ const total = countModels(groups)
43
+ const hiddenCount = groups.reduce(
44
+ (count, group) =>
45
+ count +
46
+ (group.models || []).filter((model) => hidden.has(keyOf(group.id, model.id))).length,
47
+ 0,
48
+ )
49
+ const allKinds = countKinds(groups.flatMap((group) => group.models || []))
50
+ const generationCount = allKinds.image + allKinds.video
51
+ const [filter, setFilter] = React.useState('all')
52
+ const [query, setQuery] = React.useState('')
53
+ const [multimodalOnly, setMultimodalOnly] = React.useState(false)
54
+ const [reasoningOnly, setReasoningOnly] = React.useState(false)
55
+ const [minContext, setMinContext] = React.useState(0)
56
+ const normalizedQuery = query.trim().toLowerCase()
57
+ const filteredGroups = computeFilteredGroups(groups, {
58
+ filter,
59
+ query: normalizedQuery,
60
+ multimodalOnly,
61
+ reasoningOnly,
62
+ minContext,
63
+ })
64
+ const disabled = busy || !writable
65
+
66
+ const writeField = (field, value) => {
67
+ if (disabled) return
68
+ setBusy(true)
69
+ setError(null)
70
+ Promise.resolve(form.set(field, value)).then(
71
+ (accepted) => {
72
+ if (accepted === false) setError(t('page.saveFailed'))
73
+ },
74
+ () => {
75
+ setError(t('page.saveFailed'))
76
+ },
77
+ ).finally(() => {
78
+ setBusy(false)
79
+ })
80
+ }
81
+
82
+ const commit = (next) => writeField(FIELD, Array.from(next))
83
+ const applyInput = !(snapshot.value && snapshot.value.applyInput === false)
84
+ const commitApply = (next) => writeField('applyInput', next)
85
+
86
+ const toggleModel = (group, model) => {
87
+ const next = new Set(hidden)
88
+ const key = keyOf(group.id, model.id)
89
+ if (next.has(key)) next.delete(key)
90
+ else next.add(key)
91
+ commit(next)
92
+ }
93
+
94
+ const setGroup = (group, subset, visible) => {
95
+ const next = new Set(hidden)
96
+ for (const model of subset) {
97
+ const key = keyOf(group.id, model.id)
98
+ if (visible) next.delete(key)
99
+ else next.add(key)
100
+ }
101
+ commit(next)
102
+ }
103
+
104
+ const runCatalogAction = (action, setActive) => {
105
+ setActive(true)
106
+ Promise.resolve()
107
+ .then(action)
108
+ .catch(() => {})
109
+ .finally(() => setActive(false))
110
+ }
111
+
112
+ const refreshCatalog = () => {
113
+ if (refreshing || reloading) return
114
+ runCatalogAction(() => catalog.refresh(), setRefreshing)
115
+ }
116
+
117
+ // A hard reload may supersede a soft refresh that is still running;
118
+ // catalog.load() gives it a new run and discards the older result.
119
+ const reloadCatalog = () => {
120
+ if (busy || reloading) return
121
+ runCatalogAction(() => catalog.reload(), setReloading)
122
+ }
123
+
124
+ const toggleGroupCollapsed = (id) => {
125
+ setCollapsed((previous) => {
126
+ const next = new Set(previous)
127
+ if (next.has(id)) next.delete(id)
128
+ else next.add(id)
129
+ persistCollapsedGroups(next)
130
+ return next
131
+ })
132
+ }
133
+
134
+ const toggleAllCollapsed = () => {
135
+ setCollapsed((previous) => {
136
+ const collapsedNow = groups.some((group) => previous.has(group.id))
137
+ const next = collapsedNow ? new Set() : new Set(groups.map((group) => group.id))
138
+ persistCollapsedGroups(next)
139
+ return next
140
+ })
141
+ }
142
+
143
+ const rows = []
144
+ for (const group of filteredGroups) {
145
+ const allGroupModels =
146
+ (groups.find((candidate) => candidate.id === group.id) || group).models || []
147
+ const models = group.models || []
148
+ const shown = allGroupModels.filter(
149
+ (model) => !hidden.has(keyOf(group.id, model.id)),
150
+ ).length
151
+ const allShown = models.every((model) => !hidden.has(keyOf(group.id, model.id)))
152
+ const kinds = countKinds(allGroupModels)
153
+ const kindParts = []
154
+ if (kinds.chat > 0) kindParts.push(['chat', t('page.filter.chat') + ' ' + kinds.chat])
155
+ if (kinds.image > 0) kindParts.push(['image', t('page.filter.image') + ' ' + kinds.image])
156
+ if (kinds.video > 0) kindParts.push(['video', t('page.filter.video') + ' ' + kinds.video])
157
+ if (kinds.other > 0) kindParts.push(['other', t('page.filter.other') + ' ' + kinds.other])
158
+ const kindNodes = []
159
+ for (const [index, [kindKey, label]] of kindParts.entries()) {
160
+ if (index > 0) kindNodes.push(' · ')
161
+ kindNodes.push(
162
+ h(
163
+ 'span',
164
+ {
165
+ key: kindKey,
166
+ className:
167
+ 'mvp_kindCount' +
168
+ (kindKey === 'chat' ? '' : ' mvp_kindCount--' + kindKey),
169
+ },
170
+ label,
171
+ ),
172
+ )
173
+ }
174
+ const groupName = providerLabel(group, t)
175
+ const collapsedNow = collapsed.has(group.id)
176
+ const collapseLabel = t(collapsedNow ? 'group.expandNamed' : 'group.collapseNamed', {
177
+ name: groupName,
178
+ })
179
+ const probe = probeOf(state, group, t)
180
+ rows.push(
181
+ h(
182
+ 'div',
183
+ { key: group.id, className: 'mvp_group' },
184
+ h(
185
+ 'div',
186
+ { className: 'mvp_groupHead' },
187
+ h(
188
+ 'div',
189
+ { className: 'mvp_groupTitle' },
190
+ h(
191
+ 'button',
192
+ {
193
+ type: 'button',
194
+ className: 'mvp_collapse',
195
+ 'aria-expanded': !collapsedNow,
196
+ 'aria-label': collapseLabel,
197
+ title: collapseLabel,
198
+ onClick: () => toggleGroupCollapsed(group.id),
199
+ },
200
+ collapsedNow ? '▸' : '▾',
201
+ ),
202
+ h('span', { className: 'mvp_groupName' }, groupName),
203
+ h('span', { className: 'mvp_groupCount' }, shown + '/' + allGroupModels.length),
204
+ kindParts.length > 0
205
+ ? h('span', { className: 'mvp_groupCap' }, kindNodes)
206
+ : null,
207
+ probe !== null
208
+ ? h('span', { className: 'mvp_groupProbe', title: probe.title || undefined }, probe.label)
209
+ : null,
210
+ ),
211
+ h(
212
+ 'button',
213
+ {
214
+ type: 'button',
215
+ className: 'mvp_button',
216
+ disabled: disabled,
217
+ onClick: () => setGroup(group, models, !allShown),
218
+ },
219
+ allShown
220
+ ? filter === 'all'
221
+ ? t('group.hideAll')
222
+ : t('group.hideFiltered')
223
+ : filter === 'all'
224
+ ? t('group.showAll')
225
+ : t('group.showFiltered'),
226
+ ),
227
+ ),
228
+ collapsedNow
229
+ ? null
230
+ : models.map((model) => {
231
+ const rowKey = keyOf(group.id, model.id)
232
+ const shownModel = !hidden.has(rowKey)
233
+ const tags = capabilityTags(model, t)
234
+ const kind = kindOf(model)
235
+ const generation = isGenerationKind(kind)
236
+ const kindTint =
237
+ kind !== 'chat' &&
238
+ ['image', 'video', 'embedding', 'rerank', 'audio', 'classifier', 'translation'].includes(kind)
239
+ ? ' mvp_row--kind-' + kind
240
+ : ''
241
+ const row = h(
242
+ 'div',
243
+ {
244
+ className:
245
+ 'mvp_row' +
246
+ (generation ? ' mvp_row--gen' : '') +
247
+ kindTint,
248
+ },
249
+ h(
250
+ 'div',
251
+ { className: 'mvp_rowText' },
252
+ h('div', { className: 'mvp_rowTitle' }, model.name),
253
+ model.id === model.name
254
+ ? null
255
+ : h('div', { className: 'mvp_rowMeta' }, model.id),
256
+ h(
257
+ 'div',
258
+ { className: 'mvp_tags' },
259
+ tags.map((tag) =>
260
+ h(
261
+ 'span',
262
+ {
263
+ key: tag.key,
264
+ className: tagClassOf(tag),
265
+ title: tag.title || undefined,
266
+ },
267
+ tag.label,
268
+ ),
269
+ ),
270
+ ),
271
+ ),
272
+ h(
273
+ 'div',
274
+ { className: 'mvp_rowActions' },
275
+ canEdit
276
+ ? h(
277
+ 'button',
278
+ {
279
+ type: 'button',
280
+ className: 'mvp_button mvp_editButton',
281
+ 'aria-pressed': editing === rowKey,
282
+ 'aria-label': t('row.edit') + ': ' + model.name,
283
+ title: t('row.editTitle'),
284
+ disabled: busy,
285
+ onClick: () => setEditing(editing === rowKey ? null : rowKey),
286
+ },
287
+ t('row.edit'),
288
+ )
289
+ : null,
290
+ h(
291
+ 'button',
292
+ {
293
+ type: 'button',
294
+ role: 'switch',
295
+ className: 'mvp_switch',
296
+ 'aria-checked': shownModel,
297
+ 'aria-label': model.name,
298
+ disabled: disabled,
299
+ onClick: () => toggleModel(group, model),
300
+ },
301
+ h('span', { className: 'mvp_thumb' }),
302
+ ),
303
+ ),
304
+ )
305
+ const editor =
306
+ canEdit && editing === rowKey
307
+ ? h(ModelOverrideEditor, {
308
+ key: 'editor',
309
+ entry:
310
+ overrideEntries.get(rowKey) || { provider: group.id, model: model.id },
311
+ name: model.name,
312
+ t,
313
+ overrides,
314
+ onClose: () => setEditing(null),
315
+ onSaved: (result) => {
316
+ setEditing(null)
317
+ setProviderNote(providerWriteNote(result && result.provider, t))
318
+ refreshCatalog()
319
+ },
320
+ })
321
+ : null
322
+ return h(React.Fragment, { key: model.id }, row, editor)
323
+ }),
324
+ ),
325
+ )
326
+ }
327
+
328
+ const loading = state.status === 'loading' || state.status === 'idle'
329
+ const providerList = Array.isArray(state.providers) ? state.providers : []
330
+ const probeOk = providerList.filter((provider) => provider && provider.status === 'ok').length
331
+ const probeCaps = providerList.reduce(
332
+ (count, provider) => count + (Number(provider && provider.capabilities) || 0),
333
+ 0,
334
+ )
335
+ const probeNote =
336
+ state.source === 'host'
337
+ ? providerList.length > 0
338
+ ? t('page.probeSummary', { ok: probeOk, caps: probeCaps })
339
+ : t('page.probeSummaryNone')
340
+ : null
341
+ // A page refresh can deliver this bundle while the Host half is still
342
+ // the previous version; the switch would look live but do nothing.
343
+ const hostStale = state.source === 'host' && !hostFeatures.includes('applyInput')
344
+ // A hard reload is only meaningful when the Host half understands it.
345
+ const canReload = state.source === 'host' && hostFeatures.includes('reload')
346
+ const anyCollapsed = groups.some((group) => collapsed.has(group.id))
347
+ const learnedDate =
348
+ catalogMeta && Number.isFinite(catalogMeta.updatedAt)
349
+ ? catalogMeta.updatedAt > 0
350
+ ? new Date(catalogMeta.updatedAt).toISOString().slice(0, 10)
351
+ : null
352
+ : null
353
+ const catalogNote =
354
+ catalogMeta !== null
355
+ ? t('page.learnedNote', {
356
+ models: catalogMeta.models ?? 0,
357
+ date: learnedDate || '?',
358
+ })
359
+ : null
360
+ const filterOptions = [
361
+ ['all', t('page.filter.all'), total],
362
+ ['chat', t('page.filter.chat'), allKinds.chat],
363
+ ['image', t('page.filter.image'), allKinds.image],
364
+ ['video', t('page.filter.video'), allKinds.video],
365
+ ['other', t('page.filter.other'), allKinds.other],
366
+ ]
367
+ const hasActiveFilters =
368
+ filter !== 'all' || normalizedQuery !== '' || multimodalOnly || reasoningOnly || minContext > 0
369
+
370
+ return h(
371
+ 'div',
372
+ { className: 'mvp_page' },
373
+ h(UsageToolbar, { usage, t, features: state.source === 'host' ? hostFeatures : [] }),
374
+ h(
375
+ 'div',
376
+ { className: 'mvp_head' },
377
+ h('h2', { className: 'mvp_title' }, t('page.title')),
378
+ h('p', { className: 'mvp_desc' }, t('page.description')),
379
+ ),
380
+ h(
381
+ 'div',
382
+ { className: 'mvp_toolbar' },
383
+ h(
384
+ 'div',
385
+ { className: 'mvp_summary' },
386
+ total === 0
387
+ ? loading
388
+ ? t('page.loading')
389
+ : t('page.empty')
390
+ : t('page.summary', { visible: total - hiddenCount, total }) +
391
+ (hiddenCount > 0 ? ' · ' + t('page.hiddenSummary', { count: hiddenCount }) : '') +
392
+ (generationCount > 0 ? ' · ' + t('page.kindSummary', { count: generationCount }) : ''),
393
+ ),
394
+ h(
395
+ 'div',
396
+ { className: 'mvp_actions' },
397
+ h(
398
+ 'button',
399
+ {
400
+ type: 'button',
401
+ className: 'mvp_button',
402
+ disabled: disabled || hiddenCount === 0,
403
+ onClick: () => commit(new Set()),
404
+ },
405
+ t('page.showAll'),
406
+ ),
407
+ h(
408
+ 'button',
409
+ {
410
+ type: 'button',
411
+ className: 'mvp_button',
412
+ disabled: groups.length === 0,
413
+ title: t(anyCollapsed ? 'page.expandGroupsTitle' : 'page.collapseGroupsTitle'),
414
+ onClick: toggleAllCollapsed,
415
+ },
416
+ t(anyCollapsed ? 'page.expandGroups' : 'page.collapseGroups'),
417
+ ),
418
+ h(
419
+ 'button',
420
+ {
421
+ type: 'button',
422
+ className: 'mvp_button',
423
+ disabled: busy || refreshing || reloading,
424
+ onClick: refreshCatalog,
425
+ },
426
+ t(refreshing ? 'page.refreshing' : 'page.refresh'),
427
+ ),
428
+ h(
429
+ 'button',
430
+ {
431
+ type: 'button',
432
+ className: 'mvp_button mvp_button--accent',
433
+ disabled: !canReload || busy || reloading,
434
+ title: t(canReload ? 'page.reloadTitle' : 'page.reloadUnavailable'),
435
+ onClick: reloadCatalog,
436
+ },
437
+ t(reloading ? 'page.reloading' : 'page.reload'),
438
+ ),
439
+ ),
440
+ ),
441
+ h(
442
+ 'div',
443
+ { className: 'mvp_filters' },
444
+ h('label', { className: 'mvp_search' },
445
+ h('span', { className: 'mvp_filterLabel' }, t('page.searchPlaceholder')),
446
+ h('input', {
447
+ className: 'mvp_searchInput',
448
+ type: 'search',
449
+ value: query,
450
+ placeholder: t('page.searchPlaceholder'),
451
+ 'aria-label': t('page.searchPlaceholder'),
452
+ onInput: (event) => setQuery(event.target.value),
453
+ }),
454
+ ),
455
+ h('span', { className: 'mvp_filterLabel' }, t('page.filterLabel')),
456
+ filterOptions.map(([value, label, count]) =>
457
+ h(
458
+ 'button',
459
+ {
460
+ key: value,
461
+ type: 'button',
462
+ className: 'mvp_filter',
463
+ 'aria-pressed': filter === value,
464
+ onClick: () => setFilter(value),
465
+ },
466
+ label + ' ' + count,
467
+ ),
468
+ ),
469
+ h('span', { className: 'mvp_filterLabel' }, t('page.capabilityLabel')),
470
+ h(
471
+ 'button',
472
+ {
473
+ type: 'button',
474
+ className: 'mvp_filter',
475
+ 'aria-pressed': multimodalOnly,
476
+ onClick: () => setMultimodalOnly(!multimodalOnly),
477
+ },
478
+ t('page.capability.multimodal'),
479
+ ),
480
+ h(
481
+ 'button',
482
+ {
483
+ type: 'button',
484
+ className: 'mvp_filter',
485
+ 'aria-pressed': reasoningOnly,
486
+ onClick: () => setReasoningOnly(!reasoningOnly),
487
+ },
488
+ t('page.capability.reasoning'),
489
+ ),
490
+ h('label', { className: 'mvp_context' },
491
+ h('span', { className: 'mvp_filterLabel' }, t('page.contextLabel')),
492
+ h(
493
+ 'select',
494
+ {
495
+ className: 'mvp_contextSelect',
496
+ value: String(minContext),
497
+ 'aria-label': t('page.contextLabel'),
498
+ onChange: (event) => setMinContext(Number(event.target.value)),
499
+ },
500
+ [
501
+ [0, 'page.context.any'],
502
+ [128000, 'page.context.128k'],
503
+ [256000, 'page.context.256k'],
504
+ [512000, 'page.context.512k'],
505
+ [1000000, 'page.context.1m'],
506
+ ].map(([value, label]) => h('option', { key: value, value: String(value) }, t(label))),
507
+ ),
508
+ ),
509
+ ),
510
+ h(
511
+ 'div',
512
+ { className: 'mvp_applyRow' },
513
+ h(
514
+ 'button',
515
+ {
516
+ type: 'button',
517
+ role: 'switch',
518
+ className: 'mvp_switch',
519
+ 'aria-checked': applyInput,
520
+ 'aria-label': t('page.applyInput'),
521
+ disabled: disabled,
522
+ onClick: () => commitApply(!applyInput),
523
+ },
524
+ h('span', { className: 'mvp_thumb' }),
525
+ ),
526
+ h(
527
+ 'div',
528
+ { className: 'mvp_applyText' },
529
+ h('div', { className: 'mvp_applyTitle' }, t('page.applyInput')),
530
+ h('div', { className: 'mvp_applyHint' }, t('page.applyInputHint')),
531
+ ),
532
+ ),
533
+ h(
534
+ VerifyPanel,
535
+ { verify, catalog: state, t, canUse: canVerify },
536
+ ),
537
+ h('div', { className: 'mvp_legend' }, t('page.legend')),
538
+ h('div', { className: 'mvp_legend' }, t('page.kindLegend')),
539
+ h('div', { className: 'mvp_legend' }, t('page.manualNote')),
540
+ overrideState.available === false
541
+ ? h('div', { className: 'mvp_fallback' }, t('override.unavailable'))
542
+ : null,
543
+ state.detailsError !== null && state.source === 'catalog'
544
+ ? h('div', { className: 'mvp_fallback' }, t('page.metaFallback'))
545
+ : null,
546
+ hostStale ? h('div', { className: 'mvp_fallback' }, t('page.hostStale')) : null,
547
+ state.failures.length > 0
548
+ ? h(
549
+ 'div',
550
+ {
551
+ className: 'mvp_fallback',
552
+ title: state.failures
553
+ .map((failure) => (failure.name || failure.id) + ': ' + failure.message)
554
+ .join('\n'),
555
+ },
556
+ t('page.providerFailures', {
557
+ names: state.failures
558
+ .map((failure) => failure.name || failure.id)
559
+ .join(' · '),
560
+ }),
561
+ )
562
+ : null,
563
+ error !== null ? h('div', { className: 'mvp_error', role: 'alert' }, error) : null,
564
+ state.error !== null
565
+ ? h('div', { className: 'mvp_error', role: 'alert' }, t('page.error', { message: state.error }))
566
+ : null,
567
+ rows.length > 0 ? rows : null,
568
+ total > 0 && hasActiveFilters && rows.length === 0
569
+ ? h('div', { className: 'mvp_note' }, t('page.filterEmpty'))
570
+ : null,
571
+ rows.length === 0 && !loading && state.error === null && total === 0
572
+ ? h('div', { className: 'mvp_error' }, t('page.empty'))
573
+ : null,
574
+ probeNote !== null ? h('div', { className: 'mvp_note' }, probeNote) : null,
575
+ providerNote !== null ? h('div', { className: 'mvp_note' }, providerNote) : null,
576
+ catalogNote !== null ? h('div', { className: 'mvp_note' }, catalogNote) : null,
577
+ )
578
+ }
579
+ }
580
+
581
+ export { makeSection }