@goodandready/dsh-voice 0.8.24 โ†’ 0.8.26

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,520 +0,0 @@
1
- // ------------------------------------------------------- settings voice section
2
- function VoiceSection(props) {
3
- const t = (props && props.t) || moduleT
4
- const ctx = props.ctx
5
- // On a non-localhost page the kernel disables settings entirely: the
6
- // shared document is not readable, every section gets "unavailable" and
7
- // writes are dropped. The server does not share that restriction, and
8
- // dsh-lanmode rebuilds the same mechanics on the same calls. Prefer its
9
- // service when installed, otherwise the kernel one (fine on loopback).
10
- const scope = ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).bind({ namespace: NS })
11
- const [snap, setSnap] = React.useState(null)
12
- const [draft, setDraft] = React.useState(null)
13
- const [saved, setSaved] = React.useState(false)
14
- const [err, setErr] = React.useState('')
15
-
16
- // Hotkey picker: not a name field โ€” press the key you want.
17
- const [catching, setCatching] = React.useState(false)
18
- React.useEffect(() => {
19
- if (!catching) return undefined
20
- const onKey = (event) => {
21
- event.preventDefault()
22
- event.stopPropagation()
23
- if (event.key === 'Escape') { setCatching(false); return }
24
- const chosen = keyFromEvent(event)
25
- setDraft((d) => Object.assign({}, d || {}, { hotkey: chosen }))
26
- setCatching(false)
27
- }
28
- document.addEventListener('keydown', onKey, true)
29
- return () => document.removeEventListener('keydown', onKey, true)
30
- }, [catching])
31
-
32
- React.useEffect(() => {
33
- let alive = true
34
- const render = () => { if (alive) setSnap(scope.getSnapshot()) }
35
- render()
36
- const off = scope.subscribe(render)
37
- return () => { alive = false; off() }
38
- }, [])
39
-
40
- const [devices, setDevices] = React.useState([])
41
- React.useEffect(() => {
42
- if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
43
- navigator.mediaDevices.enumerateDevices()
44
- .then((list) => setDevices(list.filter((d) => d.kind === 'audioinput')))
45
- .catch(() => {})
46
- }, [])
47
-
48
- const ready = !!snap && snap.status === 'ready'
49
- const value = ready && snap.value ? snap.value : {}
50
- const writable = ready && snap.writable !== false
51
-
52
- React.useEffect(() => {
53
- if (ready) return undefined
54
- let tries = 0
55
- const timer = setInterval(() => {
56
- if (tries >= 15) { clearInterval(timer); return }
57
- tries += 1
58
- try { ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).describe().load() } catch (e) { /* service not up yet */ }
59
- }, 1000)
60
- return () => clearInterval(timer)
61
- }, [ready])
62
-
63
- const deepClone = (obj) => {
64
- if (typeof structuredClone === 'function') {
65
- try { return structuredClone(obj) } catch (e) { /* fallback */ }
66
- }
67
- try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
68
- }
69
-
70
- React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
71
-
72
- React.useEffect(() => {
73
- if (!ready) return
74
- voice.settings = Object.assign({}, voice.settings, {
75
- vadSilenceMs: Number(value && value.dictation && value.dictation.vadSilenceMs) || 700,
76
- autoSendMs: Number(value && value.message && value.message.autoSendMs) || 4000,
77
- beep: !!(snap && snap.beep),
78
- micDeviceId: String((snap && snap.micDeviceId) || ''),
79
- historyLimit: Number(snap && snap.historyLimit),
80
- voiceCommands: !!(snap && snap.voiceCommands),
81
- sendDelayMs: Number(value && value.dictation && value.dictation.sendDelayMs) || 0,
82
- stream: !!(value && value.dictation && value.dictation.stream),
83
- streamChunkMs: Number(value && value.dictation && value.dictation.streamChunkMs) || 1200,
84
- vadAdapt: Number(value && value.dictation && value.dictation.vadAdapt) || 0,
85
- wakeWord: String((snap && snap.wakeWord) || ''),
86
- bargeIn: !!(snap && snap.bargeIn),
87
- polishSend: !!(value && value.message && value.message.polishSend),
88
- sessionCommands: !!(value && value.message && value.message.sessionCommands),
89
- polishBaseUrl: String((snap && snap.polishBaseUrl) || ''),
90
- noiseSuppression: value.noiseSuppression !== false,
91
- contextGlossary: value.contextGlossary !== false,
92
- })
93
- }, [ready, value, snap])
94
-
95
- const [statusData, setStatusData] = React.useState(null)
96
- const [statusLatency, setStatusLatency] = React.useState(null)
97
- const fetchStatus = React.useCallback(() => {
98
- const start = performance.now()
99
- const ctrl = new AbortController()
100
- const tId = setTimeout(() => ctrl.abort(), 5000)
101
- fetch('/dsh-voice/status', { cache: 'no-store', signal: ctrl.signal })
102
- .then((r) => r.json())
103
- .then((data) => {
104
- clearTimeout(tId)
105
- setStatusLatency(Math.round(performance.now() - start))
106
- if (data && data.ok) {
107
- setStatusData(data)
108
- }
109
- })
110
- .catch(() => {
111
- clearTimeout(tId)
112
- setStatusLatency(null)
113
- })
114
- }, [])
115
-
116
- React.useEffect(() => {
117
- fetchStatus()
118
- }, [fetchStatus])
119
-
120
- if (!ready) {
121
- const waiting = !snap || snap.status === 'loading'
122
- return React.createElement('div', { className: 'cb-page' },
123
- React.createElement('div', { className: 'dvs-wait' }, waiting
124
- ? t('loadingSettings')
125
- : t('notReady1')
126
- + t('notReady2')),
127
- )
128
- }
129
-
130
- const setIn = (mode, key, v) => setDraft((d) => {
131
- const next = deepClone(d || {})
132
- next[mode] = next[mode] || {}
133
- next[mode][key] = v
134
- return next
135
- })
136
- const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
137
-
138
- const save = async () => {
139
- setErr(''); setSaved(false)
140
- if (!draft) return
141
-
142
- const failed = []
143
- for (const k of Object.keys(draft)) {
144
- try {
145
- await scope.set(k, draft[k])
146
- } catch (e) {
147
- failed.push(k + ': ' + String(e && e.message ? e.message : e))
148
- }
149
- }
150
-
151
- Object.assign(voice.settings, {
152
- vadSilenceMs: Number(draft.dictation && draft.dictation.vadSilenceMs) || 700,
153
- autoSendMs: Number(draft.message && draft.message.autoSendMs) || 4000,
154
- beep: !!draft.beep,
155
- micDeviceId: String(draft.micDeviceId || ''),
156
- historyLimit: Number(draft.historyLimit),
157
- voiceCommands: !!draft.voiceCommands,
158
- sendDelayMs: Number(draft.dictation && draft.dictation.sendDelayMs) || 0,
159
- stream: !!(draft.dictation && draft.dictation.stream),
160
- streamChunkMs: Number(draft.dictation && draft.dictation.streamChunkMs) || 1200,
161
- vadAdapt: Number(draft.dictation && draft.dictation.vadAdapt) || 0,
162
- noiseSuppression: draft.noiseSuppression !== false,
163
- contextGlossary: draft.contextGlossary !== false,
164
- visualizerStyle: draft.visualizerStyle || 'liquid-wave',
165
- })
166
- try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
167
-
168
- if (failed.length) { setErr(t('saveFailed') + ' ' + failed.join('; ')); return }
169
- setSaved(true); setTimeout(() => setSaved(false), 2000)
170
- fetchStatus()
171
- }
172
-
173
- const modeVal = (mode, key, fallback) => {
174
- const m = draft && draft[mode]
175
- return m && m[key] !== undefined ? m[key] : fallback
176
- }
177
-
178
- const chainOptions = BUILTIN.concat(
179
- (draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
180
- .map((c) => String(c && c.key || '').trim())
181
- .filter((k) => k && BUILTIN.indexOf(k) < 0),
182
- )
183
-
184
- const hotkeyField = () => React.createElement('div', { className: 'cb-field' },
185
- React.createElement('label', null, t('hotkey')),
186
- React.createElement('div', { className: 'cb-row' },
187
- React.createElement('button', {
188
- type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
189
- onClick: () => setCatching(true),
190
- }, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
191
- React.createElement('button', {
192
- type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
193
- disabled: !writable, onClick: () => setDraft((d) => Object.assign({}, d || {}, { hotkey: '' })),
194
- }, 'ร—'),
195
- ),
196
- React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
197
- )
198
-
199
- const micField = () => React.createElement('div', { className: 'cb-field' },
200
- React.createElement('label', null, t('mic')),
201
- React.createElement('select', {
202
- value: String((snap && snap.micDeviceId) || ''), disabled: !writable,
203
- onChange: (e) => setTop('micDeviceId', e.target.value),
204
- },
205
- React.createElement('option', { value: '' }, t('micDefault')),
206
- devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
207
- d.label || d.deviceId.slice(0, 12))),
208
- ),
209
- )
210
-
211
- const langField = (mode) => React.createElement('div', { className: 'cb-field' },
212
- React.createElement('label', null, t('language')),
213
- React.createElement('select', {
214
- value: modeVal(mode, 'language', 'ru'), disabled: !writable,
215
- onChange: (e) => setIn(mode, 'language', e.target.value),
216
- }, LANGS.map((o) => React.createElement('option', { key: o, value: o }, o))),
217
- )
218
-
219
- const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
220
- React.createElement('label', null, label),
221
- React.createElement('input', {
222
- type: 'number', value: modeVal(mode, key, ''), disabled: !writable,
223
- onChange: (e) => setIn(mode, key, Number(e.target.value)),
224
- }),
225
- React.createElement('span', { className: 'dvs-sub' }, hint),
226
- )
227
-
228
- const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
229
- React.createElement('label', null, label),
230
- React.createElement('input', {
231
- type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
232
- onChange: (e) => setTop(key, e.target.value),
233
- }),
234
- React.createElement('span', { className: 'dvs-sub' }, hint),
235
- )
236
-
237
- const statsData = (statusData && statusData.providerStats) || {}
238
- const hostOnline = statusLatency !== null
239
- const whisperRunning = !!(statusData && statusData.whisperRunning)
240
- const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
241
-
242
- return React.createElement('div', { className: 'cb-page' },
243
- // 1. Connection & Engine Status Card
244
- React.createElement('div', { className: 'cb-section-card' },
245
- React.createElement('div', { className: 'cb-section-title' },
246
- React.createElement('span', null, t('statusTitle')),
247
- React.createElement('button', {
248
- type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
249
- onClick: fetchStatus,
250
- }, '๐Ÿ”„ ' + t('refreshStats')),
251
- ),
252
- React.createElement('div', { className: 'cb-section-desc' }, t('statusDesc')),
253
- React.createElement('div', { className: 'cb-row' },
254
- hostOnline
255
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'โ— ' + t('badgeHostOnline').replace('{ms}', statusLatency))
256
- : React.createElement('span', { className: 'cb-badge cb-badge-bad' }, 'โ— ' + t('badgeHostOffline')),
257
- whisperRunning
258
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'โ— ' + t('badgeWhisperActive'))
259
- : React.createElement('span', { className: 'cb-badge cb-badge-warn' }, 'โ—‹ ' + t('badgeWhisperInactive')),
260
- React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'โœ“ ' + t('badgeProviders').replace('{count}', providerCount)),
261
- ),
262
- ),
263
-
264
- // 2. Dictation Mode Card
265
- React.createElement('div', { className: 'cb-section-card' },
266
- React.createElement('div', { className: 'cb-section-title' },
267
- React.createElement('span', null, '๐ŸŽ™๏ธ ' + t('dictationBtn')),
268
- ),
269
- React.createElement('div', { className: 'cb-section-desc' }, t('dictationHint')),
270
- React.createElement(ChainEditor, {
271
- value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
272
- options: chainOptions,
273
- onChange: (v) => setIn('dictation', 'chain', v),
274
- }),
275
- React.createElement('div', { className: 'cb-grid-2' },
276
- langField('dictation'),
277
- numField('dictation', 'vadSilenceMs', t('pauseMs'), t('pauseHint')),
278
- numField('dictation', 'sendDelayMs', t('sendDelay'), t('sendDelayHint')),
279
- numField('dictation', 'streamChunkMs', t('streamChunkMs'), t('streamHint')),
280
- ),
281
- React.createElement('div', { className: 'cb-row' },
282
- React.createElement('label', { className: 'cb-check-label' },
283
- React.createElement('input', {
284
- type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.polish), disabled: !writable,
285
- onChange: (e) => setIn('dictation', 'polish', e.target.checked),
286
- }),
287
- t('polish'),
288
- ),
289
- React.createElement('label', { className: 'cb-check-label' },
290
- React.createElement('input', {
291
- type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.stream), disabled: !writable,
292
- onChange: (e) => setIn('dictation', 'stream', e.target.checked),
293
- }),
294
- t('stream'),
295
- ),
296
- ),
297
- React.createElement('div', { className: 'cb-field' },
298
- React.createElement('label', null, t('vadAdapt')),
299
- React.createElement('input', {
300
- type: 'range', min: 0, max: 1, step: 0.1,
301
- value: Number(draft && draft.dictation && draft.dictation.vadAdapt) || 0, disabled: !writable,
302
- onChange: (e) => setIn('dictation', 'vadAdapt', Number(e.target.value)),
303
- }),
304
- React.createElement('span', { className: 'dvs-sub' }, t('vadAdaptHint')),
305
- ),
306
- textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
307
- ),
308
-
309
- // 3. Voice Message Card
310
- React.createElement('div', { className: 'cb-section-card' },
311
- React.createElement('div', { className: 'cb-section-title' },
312
- React.createElement('span', null, '๐Ÿ’ฌ ' + t('messageTitle')),
313
- ),
314
- React.createElement('div', { className: 'cb-section-desc' }, t('messageHint')),
315
- React.createElement(ChainEditor, {
316
- value: draft && draft.message ? draft.message.chain : [], writable: writable,
317
- options: chainOptions,
318
- onChange: (v) => setIn('message', 'chain', v),
319
- }),
320
- React.createElement('div', { className: 'cb-grid-2' },
321
- langField('message'),
322
- numField('message', 'autoSendMs', t('undoMs'), t('undoHint')),
323
- ),
324
- React.createElement('div', { className: 'cb-row' },
325
- React.createElement('label', { className: 'cb-check-label' },
326
- React.createElement('input', {
327
- type: 'checkbox', checked: !!(draft && draft.message && draft.message.polish), disabled: !writable,
328
- onChange: (e) => setIn('message', 'polish', e.target.checked),
329
- }),
330
- t('polish'),
331
- ),
332
- React.createElement('label', { className: 'cb-check-label' },
333
- React.createElement('input', {
334
- type: 'checkbox', checked: !!(draft && draft.message && draft.message.polishSend), disabled: !writable,
335
- onChange: (e) => setIn('message', 'polishSend', e.target.checked),
336
- }),
337
- t('polishSend'),
338
- ),
339
- React.createElement('label', { className: 'cb-check-label' },
340
- React.createElement('input', {
341
- type: 'checkbox', checked: !!(draft && draft.message && draft.message.sessionCommands), disabled: !writable,
342
- onChange: (e) => setIn('message', 'sessionCommands', e.target.checked),
343
- }),
344
- t('sessionCommands'),
345
- ),
346
- ),
347
- ),
348
-
349
- // 4. Custom Providers Card
350
- React.createElement('div', { className: 'cb-section-card' },
351
- React.createElement('div', { className: 'cb-section-title' },
352
- React.createElement('span', null, 'โš™๏ธ ' + t('customTitle')),
353
- ),
354
- React.createElement('div', { className: 'cb-section-desc' }, t('customHint')),
355
- React.createElement(CustomEditor, {
356
- value: draft && draft.customProviders ? draft.customProviders : [], writable: writable,
357
- options: chainOptions,
358
- onChange: (v) => setTop('customProviders', v),
359
- }),
360
- ),
361
-
362
- // 5. Hardware & Local Engines Card
363
- React.createElement('div', { className: 'cb-section-card' },
364
- React.createElement('div', { className: 'cb-section-title' },
365
- React.createElement('span', null, '๐Ÿ”ง ' + t('hardwareTitle')),
366
- ),
367
- React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
368
- hotkeyField(),
369
- micField(),
370
- React.createElement('div', { className: 'cb-grid-2' },
371
- textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
372
- textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
373
- textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
374
- textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
375
- textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
376
- textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
377
- textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
378
- textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
379
- textField('polishModel', t('polishModel'), ''),
380
- textField('polishKeyEnv', t('polishKeyEnv'), ''),
381
- ),
382
- React.createElement('div', { className: 'cb-row' },
383
- React.createElement('label', { className: 'cb-check-label' },
384
- React.createElement('input', {
385
- type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
386
- onChange: (e) => setTop('autoStart', e.target.checked),
387
- }),
388
- t('whisperAutostart'),
389
- ),
390
- React.createElement('label', { className: 'cb-check-label' },
391
- React.createElement('input', {
392
- type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
393
- onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
394
- }),
395
- t('sensevoiceAutostart'),
396
- ),
397
- React.createElement('label', { className: 'cb-check-label' },
398
- React.createElement('input', {
399
- type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
400
- onChange: (e) => setTop('localOnly', e.target.checked),
401
- }),
402
- t('localOnly'),
403
- ),
404
- React.createElement('label', { className: 'cb-check-label' },
405
- React.createElement('input', {
406
- type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
407
- onChange: (e) => setTop('beep', e.target.checked),
408
- }),
409
- t('beep'),
410
- ),
411
- React.createElement('label', { className: 'cb-check-label' },
412
- React.createElement('input', {
413
- type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
414
- onChange: (e) => setTop('noiseSuppression', e.target.checked),
415
- }),
416
- t('noiseSuppression'),
417
- ),
418
- React.createElement('label', { className: 'cb-check-label' },
419
- React.createElement('input', {
420
- type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
421
- onChange: (e) => setTop('contextGlossary', e.target.checked),
422
- }),
423
- t('contextGlossary'),
424
- ),
425
- React.createElement('label', { className: 'cb-check-label' },
426
- React.createElement('input', {
427
- type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
428
- onChange: (e) => setTop('voiceCommands', e.target.checked),
429
- }),
430
- t('voiceCommandsLabel'),
431
- ),
432
- React.createElement('label', { className: 'cb-check-label' },
433
- React.createElement('input', {
434
- type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
435
- onChange: (e) => setTop('normalizeTranscript', e.target.checked),
436
- }),
437
- t('normalizeTranscript'),
438
- ),
439
- ),
440
- React.createElement('div', { className: 'cb-field' },
441
- React.createElement('label', null, t('visualizerStyle')),
442
- React.createElement('select', {
443
- value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
444
- onChange: (e) => setTop('visualizerStyle', e.target.value),
445
- },
446
- React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
447
- React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
448
- React.createElement('option', { value: 'bars' }, t('visBars')),
449
- React.createElement('option', { value: 'off' }, t('visOff')),
450
- ),
451
- ),
452
- React.createElement('div', { className: 'cb-field' },
453
- React.createElement('label', null, t('vocabulary')),
454
- React.createElement('textarea', {
455
- rows: 3, disabled: !writable,
456
- value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
457
- onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
458
- }),
459
- ),
460
- ),
461
-
462
- // 6. Provider Telemetry & Latency Dashboard
463
- React.createElement('div', { className: 'cb-section-card' },
464
- React.createElement('div', { className: 'cb-section-title' },
465
- React.createElement('span', null, '๐Ÿ“Š ' + t('providerDashboard')),
466
- React.createElement('button', {
467
- type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
468
- onClick: fetchStatus,
469
- }, '๐Ÿ”„ ' + t('refreshStats')),
470
- ),
471
- React.createElement('div', { className: 'dvo-dash' },
472
- React.createElement('div', { className: 'dvo-dash-grid' },
473
- Object.keys(statsData).length === 0
474
- ? React.createElement('div', { className: 'dvs-sub' }, t('idle'))
475
- : Object.entries(statsData).map(([key, item]) => {
476
- const avg = item.avgTookMs || 0
477
- const hasErrors = item.failures > 0
478
- let badgeClass = 'dvo-badge-idle'
479
- let badgeText = t('idle')
480
- if (item.attempts > 0) {
481
- if (hasErrors && item.successes === 0) {
482
- badgeClass = 'dvo-badge-err'
483
- badgeText = t('error')
484
- } else if (avg > 0 && avg < 400) {
485
- badgeClass = 'dvo-badge-fast'
486
- badgeText = avg + 'ms ยท ' + t('fast')
487
- } else if (avg >= 400 && avg <= 1500) {
488
- badgeClass = 'dvo-badge-norm'
489
- badgeText = avg + 'ms ยท ' + t('normal')
490
- } else {
491
- badgeClass = 'dvo-badge-slow'
492
- badgeText = avg + 'ms ยท ' + t('slow')
493
- }
494
- }
495
- const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
496
- return React.createElement('div', { key, className: 'dvo-dash-item', title: item.lastError ? `Error: ${item.lastError}` : '' },
497
- React.createElement('div', { className: 'dvo-dash-name' }, key),
498
- React.createElement('div', { className: 'dvo-dash-row' },
499
- React.createElement('span', { className: 'dvo-badge ' + badgeClass }, badgeText),
500
- React.createElement('span', null, rate + '%'),
501
- ),
502
- React.createElement('div', { className: 'dvo-dash-row' },
503
- React.createElement('span', null, `${item.successes}/${item.attempts}`),
504
- React.createElement('span', null, t('successRate')),
505
- ),
506
- )
507
- }),
508
- ),
509
- ),
510
- ),
511
-
512
- // Action bar (Save)
513
- React.createElement('div', { className: 'cb-row' },
514
- React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
515
- saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
516
- err ? React.createElement('span', { className: 'dvs-bad' }, err) : null,
517
- ),
518
- )
519
- }
520
-
@@ -1,38 +0,0 @@
1
- // Card in Settings โ†’ Plugins โ†’ Plugin settings (#18)
2
- function PluginCard(props) {
3
- const [open, setOpen] = React.useState(false)
4
- const tt = (props && props.t) || t
5
- const title = tt('title')
6
- return React.createElement(ErrorBoundary, null,
7
- React.createElement('li',
8
- { className: open ? 'dvo-pcard dvo-pcardOpen' : 'dvo-pcard' },
9
- React.createElement('button',
10
- {
11
- type: 'button', className: 'dvo-phead',
12
- 'aria-expanded': open ? 'true' : 'false',
13
- 'aria-label': (open ? tt('collapse') : tt('expand')) + ': ' + title,
14
- onClick: () => setOpen((v) => !v),
15
- },
16
- React.createElement('span', { className: 'dvo-pheadtext' },
17
- React.createElement('span', { className: 'dvo-ptitle' }, title),
18
- React.createElement('span', { className: 'dvo-pdesc' }, tt('cardHint')),
19
- ),
20
- React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
21
- ),
22
- open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
23
- ),
24
- )
25
- }
26
-
27
- function registerSettings(ctx) {
28
- ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
29
- {
30
- name: 'settings.plugin.item',
31
- key: NS,
32
- locale: NS,
33
- inject: () => ({ ctx: ctx }),
34
- },
35
- PluginCard,
36
- ))
37
- }
38
-
@@ -1,23 +0,0 @@
1
- exports.inject = ['timer', 'slots', 'settingsScope', 'locale']
2
- exports.apply = function apply(ctx) {
3
- // English is the source language. Other languages come from the
4
- // separate translation plugin at runtime. Re-registering the same
5
- // namespace+language throws, so tolerate a pre-registered dictionary.
6
- const addLocale = (locale, dictionary) => {
7
- try {
8
- return ctx.locale.register(NS, locale, dictionary)
9
- } catch (alreadyTaken) {
10
- return () => {}
11
- }
12
- }
13
- ctx.effect(() => {
14
- const undo = [addLocale('en', en)]
15
- return () => { for (const off of undo) off() }
16
- }, 'dsh-voice: locale dictionaries')
17
- moduleT = ctx.locale.bind(NS)
18
- registerComposer(ctx)
19
- registerSettings(ctx)
20
- }
21
- return module.exports
22
- },
23
- })