@goodandready/dsh-voice 0.8.25 โ†’ 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,597 +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: !!(value && value.beep),
78
- micDeviceId: String((value && value.micDeviceId) || ''),
79
- historyLimit: Number(value && value.historyLimit) || 20,
80
- voiceCommands: !!(value && value.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((value && value.wakeWord) || ''),
86
- bargeIn: !!(value && value.bargeIn),
87
- polishSend: !!(value && value.message && value.message.polishSend),
88
- sessionCommands: !!(value && value.message && value.message.sessionCommands),
89
- polishBaseUrl: String((value && value.polishBaseUrl) || ''),
90
- noiseSuppression: value.noiseSuppression !== false,
91
- contextGlossary: value.contextGlossary !== false,
92
- visualizerStyle: (value && value.visualizerStyle) || 'liquid-wave',
93
- })
94
- }, [ready, value])
95
-
96
- // Interactive microphone tester
97
- const [testingMic, setTestingMic] = React.useState(false)
98
- const [testLevel, setTestLevel] = React.useState(0)
99
- const testRef = React.useRef(null)
100
-
101
- const toggleTestMic = async () => {
102
- if (testingMic) {
103
- if (testRef.current) {
104
- testRef.current.active = false
105
- try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) {}
106
- if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) {} }
107
- testRef.current = null
108
- }
109
- setTestingMic(false)
110
- setTestLevel(0)
111
- return
112
- }
113
- try {
114
- const devId = (draft && draft.micDeviceId) || (value && value.micDeviceId) || ''
115
- const audio = { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
116
- if (devId) audio.deviceId = { exact: devId }
117
- const stream = await navigator.mediaDevices.getUserMedia({ audio })
118
- const AC = typeof AudioContext !== 'undefined' ? AudioContext : (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
119
- if (!AC) return
120
- const audioCtx = new AC()
121
- if (audioCtx.state === 'suspended') await audioCtx.resume().catch(() => {})
122
- const src = audioCtx.createMediaStreamSource(stream)
123
- const analyser = audioCtx.createAnalyser()
124
- analyser.fftSize = 128
125
- src.connect(analyser)
126
- testRef.current = { stream, audioCtx, analyser, active: true }
127
- setTestingMic(true)
128
-
129
- const check = () => {
130
- if (!testRef.current || !testRef.current.active) return
131
- const data = new Uint8Array(analyser.frequencyBinCount)
132
- analyser.getByteFrequencyData(data)
133
- let sum = 0
134
- for (let i = 0; i < data.length; i++) sum += data[i]
135
- const lvl = Math.min(100, Math.round((sum / data.length / 255) * 220))
136
- setTestLevel(lvl)
137
- requestAnimationFrame(check)
138
- }
139
- requestAnimationFrame(check)
140
- } catch (e) {
141
- setErr(String(e && e.message ? e.message : e))
142
- }
143
- }
144
-
145
- React.useEffect(() => {
146
- return () => {
147
- if (testRef.current) {
148
- testRef.current.active = false
149
- try { testRef.current.stream.getTracks().forEach((t) => t.stop()) } catch (e) {}
150
- if (testRef.current.audioCtx) { try { testRef.current.audioCtx.close() } catch (e) {} }
151
- testRef.current = null
152
- }
153
- }
154
- }, [])
155
-
156
- const [statusData, setStatusData] = React.useState(null)
157
- const [statusLatency, setStatusLatency] = React.useState(null)
158
- const fetchStatus = React.useCallback(() => {
159
- const start = performance.now()
160
- const ctrl = new AbortController()
161
- const tId = setTimeout(() => ctrl.abort(), 5000)
162
- fetch('/dsh-voice/status', { cache: 'no-store', signal: ctrl.signal })
163
- .then((r) => r.json())
164
- .then((data) => {
165
- clearTimeout(tId)
166
- setStatusLatency(Math.round(performance.now() - start))
167
- if (data && data.ok) {
168
- setStatusData(data)
169
- }
170
- })
171
- .catch(() => {
172
- clearTimeout(tId)
173
- setStatusLatency(null)
174
- })
175
- }, [])
176
-
177
- React.useEffect(() => {
178
- fetchStatus()
179
- }, [fetchStatus])
180
-
181
- if (!ready) {
182
- const waiting = !snap || snap.status === 'loading'
183
- return React.createElement('div', { className: 'cb-page' },
184
- React.createElement('div', { className: 'dvs-wait' }, waiting
185
- ? t('loadingSettings')
186
- : t('notReady1')
187
- + t('notReady2')),
188
- )
189
- }
190
-
191
- const setIn = (mode, key, v) => setDraft((d) => {
192
- const next = deepClone(d || {})
193
- next[mode] = next[mode] || {}
194
- next[mode][key] = v
195
- return next
196
- })
197
- const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
198
-
199
- const save = async () => {
200
- setErr(''); setSaved(false)
201
- if (!draft) return
202
-
203
- const failed = []
204
- for (const k of Object.keys(draft)) {
205
- try {
206
- await scope.set(k, draft[k])
207
- } catch (e) {
208
- failed.push(k + ': ' + String(e && e.message ? e.message : e))
209
- }
210
- }
211
-
212
- Object.assign(voice.settings, {
213
- vadSilenceMs: Number(draft.dictation && draft.dictation.vadSilenceMs) || 700,
214
- autoSendMs: Number(draft.message && draft.message.autoSendMs) || 4000,
215
- beep: !!draft.beep,
216
- micDeviceId: String(draft.micDeviceId || ''),
217
- historyLimit: Number(draft.historyLimit),
218
- voiceCommands: !!draft.voiceCommands,
219
- sendDelayMs: Number(draft.dictation && draft.dictation.sendDelayMs) || 0,
220
- stream: !!(draft.dictation && draft.dictation.stream),
221
- streamChunkMs: Number(draft.dictation && draft.dictation.streamChunkMs) || 1200,
222
- vadAdapt: Number(draft.dictation && draft.dictation.vadAdapt) || 0,
223
- noiseSuppression: draft.noiseSuppression !== false,
224
- contextGlossary: draft.contextGlossary !== false,
225
- visualizerStyle: draft.visualizerStyle || 'liquid-wave',
226
- })
227
- try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
228
-
229
- if (failed.length) { setErr(t('saveFailed') + ' ' + failed.join('; ')); return }
230
- setSaved(true); setTimeout(() => setSaved(false), 2000)
231
- fetchStatus()
232
- }
233
-
234
- const modeVal = (mode, key, fallback) => {
235
- const m = draft && draft[mode]
236
- return m && m[key] !== undefined ? m[key] : fallback
237
- }
238
-
239
- const chainOptions = BUILTIN.concat(
240
- (draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
241
- .map((c) => String(c && c.key || '').trim())
242
- .filter((k) => k && BUILTIN.indexOf(k) < 0),
243
- )
244
-
245
- const hotkeyField = () => React.createElement('div', { className: 'cb-field' },
246
- React.createElement('label', null, t('hotkey')),
247
- React.createElement('div', { className: 'cb-row' },
248
- React.createElement('button', {
249
- type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
250
- onClick: () => setCatching(true),
251
- }, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
252
- React.createElement('button', {
253
- type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
254
- disabled: !writable, onClick: () => setDraft((d) => Object.assign({}, d || {}, { hotkey: '' })),
255
- }, 'ร—'),
256
- ),
257
- React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
258
- )
259
-
260
- const micField = () => React.createElement('div', { className: 'cb-field' },
261
- React.createElement('label', null, t('mic')),
262
- React.createElement('div', { className: 'cb-row' },
263
- React.createElement('select', {
264
- value: String((draft && draft.micDeviceId) ?? (value && value.micDeviceId) ?? ''), disabled: !writable || testingMic,
265
- onChange: (e) => setTop('micDeviceId', e.target.value),
266
- style: { flex: 1 },
267
- },
268
- React.createElement('option', { value: '' }, t('micDefault')),
269
- devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
270
- d.label || d.deviceId.slice(0, 12))),
271
- ),
272
- React.createElement('button', {
273
- type: 'button',
274
- className: 'cb-btn ' + (testingMic ? 'cb-btn-primary' : 'cb-btn-secondary'),
275
- onClick: toggleTestMic,
276
- style: { flex: 'none' },
277
- }, testingMic ? t('stopTest') : t('testMic')),
278
- ),
279
- testingMic ? React.createElement('div', { className: 'dvo-mic-test' },
280
- React.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary)' } }, t('micLevel') + ':'),
281
- React.createElement('div', { className: 'dvo-meter-bar' },
282
- React.createElement('div', { className: 'dvo-meter-fill', style: { width: `${testLevel}%` } }),
283
- ),
284
- React.createElement('span', { style: { fontSize: '11px', fontVariantNumeric: 'tabular-nums', width: '32px', textAlign: 'right' } }, `${testLevel}%`),
285
- ) : null,
286
- )
287
-
288
- const langField = (mode) => React.createElement('div', { className: 'cb-field' },
289
- React.createElement('label', null, t('language')),
290
- React.createElement('select', {
291
- value: modeVal(mode, 'language', 'ru'), disabled: !writable,
292
- onChange: (e) => setIn(mode, 'language', e.target.value),
293
- }, LANGS.map((o) => React.createElement('option', { key: o, value: o }, o))),
294
- )
295
-
296
- const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
297
- React.createElement('label', null, label),
298
- React.createElement('input', {
299
- type: 'number', value: modeVal(mode, key, ''), disabled: !writable,
300
- onChange: (e) => setIn(mode, key, Number(e.target.value)),
301
- }),
302
- React.createElement('span', { className: 'dvs-sub' }, hint),
303
- )
304
-
305
- const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
306
- React.createElement('label', null, label),
307
- React.createElement('input', {
308
- type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
309
- onChange: (e) => setTop(key, e.target.value),
310
- }),
311
- React.createElement('span', { className: 'dvs-sub' }, hint),
312
- )
313
-
314
- const statsData = (statusData && statusData.providerStats) || {}
315
- const hostOnline = statusLatency !== null
316
- const whisperRunning = !!(statusData && statusData.whisperRunning)
317
- const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
318
-
319
- return React.createElement('div', { className: 'cb-page' },
320
- // 1. Connection & Engine Status Card
321
- React.createElement('div', { className: 'cb-section-card' },
322
- React.createElement('div', { className: 'cb-section-title' },
323
- React.createElement('span', null, t('statusTitle')),
324
- React.createElement('button', {
325
- type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
326
- onClick: fetchStatus,
327
- }, '๐Ÿ”„ ' + t('refreshStats')),
328
- ),
329
- React.createElement('div', { className: 'cb-section-desc' }, t('statusDesc')),
330
- React.createElement('div', { className: 'cb-row' },
331
- hostOnline
332
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'โ— ' + t('badgeHostOnline').replace('{ms}', statusLatency))
333
- : React.createElement('span', { className: 'cb-badge cb-badge-bad' }, 'โ— ' + t('badgeHostOffline')),
334
- whisperRunning
335
- ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'โ— ' + t('badgeWhisperActive'))
336
- : React.createElement('span', { className: 'cb-badge cb-badge-warn' }, 'โ—‹ ' + t('badgeWhisperInactive')),
337
- React.createElement('span', { className: 'cb-badge cb-badge-ok' }, 'โœ“ ' + t('badgeProviders').replace('{count}', providerCount)),
338
- ),
339
- ),
340
-
341
- // 2. Dictation Mode Card
342
- React.createElement('div', { className: 'cb-section-card' },
343
- React.createElement('div', { className: 'cb-section-title' },
344
- React.createElement('span', null, '๐ŸŽ™๏ธ ' + t('dictationBtn')),
345
- ),
346
- React.createElement('div', { className: 'cb-section-desc' }, t('dictationHint')),
347
- React.createElement(ChainEditor, {
348
- value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
349
- options: chainOptions,
350
- onChange: (v) => setIn('dictation', 'chain', v),
351
- }),
352
- React.createElement('div', { className: 'cb-grid-2' },
353
- langField('dictation'),
354
- numField('dictation', 'vadSilenceMs', t('pauseMs'), t('pauseHint')),
355
- numField('dictation', 'sendDelayMs', t('sendDelay'), t('sendDelayHint')),
356
- numField('dictation', 'streamChunkMs', t('streamChunkMs'), t('streamHint')),
357
- ),
358
- React.createElement('div', { className: 'cb-row' },
359
- React.createElement('label', { className: 'cb-check-label' },
360
- React.createElement('input', {
361
- type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.polish), disabled: !writable,
362
- onChange: (e) => setIn('dictation', 'polish', e.target.checked),
363
- }),
364
- t('polish'),
365
- ),
366
- React.createElement('label', { className: 'cb-check-label' },
367
- React.createElement('input', {
368
- type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.stream), disabled: !writable,
369
- onChange: (e) => setIn('dictation', 'stream', e.target.checked),
370
- }),
371
- t('stream'),
372
- ),
373
- ),
374
- React.createElement('div', { className: 'cb-field' },
375
- React.createElement('label', null, t('vadAdapt')),
376
- React.createElement('input', {
377
- type: 'range', min: 0, max: 1, step: 0.1,
378
- value: Number(draft && draft.dictation && draft.dictation.vadAdapt) || 0, disabled: !writable,
379
- onChange: (e) => setIn('dictation', 'vadAdapt', Number(e.target.value)),
380
- }),
381
- React.createElement('span', { className: 'dvs-sub' }, t('vadAdaptHint')),
382
- ),
383
- textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
384
- ),
385
-
386
- // 3. Voice Message Card
387
- React.createElement('div', { className: 'cb-section-card' },
388
- React.createElement('div', { className: 'cb-section-title' },
389
- React.createElement('span', null, '๐Ÿ’ฌ ' + t('messageTitle')),
390
- ),
391
- React.createElement('div', { className: 'cb-section-desc' }, t('messageHint')),
392
- React.createElement(ChainEditor, {
393
- value: draft && draft.message ? draft.message.chain : [], writable: writable,
394
- options: chainOptions,
395
- onChange: (v) => setIn('message', 'chain', v),
396
- }),
397
- React.createElement('div', { className: 'cb-grid-2' },
398
- langField('message'),
399
- numField('message', 'autoSendMs', t('undoMs'), t('undoHint')),
400
- ),
401
- React.createElement('div', { className: 'cb-row' },
402
- React.createElement('label', { className: 'cb-check-label' },
403
- React.createElement('input', {
404
- type: 'checkbox', checked: !!(draft && draft.message && draft.message.polish), disabled: !writable,
405
- onChange: (e) => setIn('message', 'polish', e.target.checked),
406
- }),
407
- t('polish'),
408
- ),
409
- React.createElement('label', { className: 'cb-check-label' },
410
- React.createElement('input', {
411
- type: 'checkbox', checked: !!(draft && draft.message && draft.message.polishSend), disabled: !writable,
412
- onChange: (e) => setIn('message', 'polishSend', e.target.checked),
413
- }),
414
- t('polishSend'),
415
- ),
416
- React.createElement('label', { className: 'cb-check-label' },
417
- React.createElement('input', {
418
- type: 'checkbox', checked: !!(draft && draft.message && draft.message.sessionCommands), disabled: !writable,
419
- onChange: (e) => setIn('message', 'sessionCommands', e.target.checked),
420
- }),
421
- t('sessionCommands'),
422
- ),
423
- ),
424
- ),
425
-
426
- // 4. Custom Providers Card
427
- React.createElement('div', { className: 'cb-section-card' },
428
- React.createElement('div', { className: 'cb-section-title' },
429
- React.createElement('span', null, 'โš™๏ธ ' + t('customTitle')),
430
- ),
431
- React.createElement('div', { className: 'cb-section-desc' }, t('customHint')),
432
- React.createElement(CustomEditor, {
433
- value: draft && draft.customProviders ? draft.customProviders : [], writable: writable,
434
- options: chainOptions,
435
- onChange: (v) => setTop('customProviders', v),
436
- }),
437
- ),
438
-
439
- // 5. Hardware & Local Engines Card
440
- React.createElement('div', { className: 'cb-section-card' },
441
- React.createElement('div', { className: 'cb-section-title' },
442
- React.createElement('span', null, '๐Ÿ”ง ' + t('hardwareTitle')),
443
- ),
444
- React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
445
- hotkeyField(),
446
- micField(),
447
- React.createElement('div', { className: 'cb-grid-2' },
448
- textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
449
- textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
450
- textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
451
- textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
452
- textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
453
- textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
454
- textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
455
- textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
456
- textField('polishModel', t('polishModel'), ''),
457
- textField('polishKeyEnv', t('polishKeyEnv'), ''),
458
- ),
459
- React.createElement('div', { className: 'cb-row' },
460
- React.createElement('label', { className: 'cb-check-label' },
461
- React.createElement('input', {
462
- type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
463
- onChange: (e) => setTop('autoStart', e.target.checked),
464
- }),
465
- t('whisperAutostart'),
466
- ),
467
- React.createElement('label', { className: 'cb-check-label' },
468
- React.createElement('input', {
469
- type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
470
- onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
471
- }),
472
- t('sensevoiceAutostart'),
473
- ),
474
- React.createElement('label', { className: 'cb-check-label' },
475
- React.createElement('input', {
476
- type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
477
- onChange: (e) => setTop('localOnly', e.target.checked),
478
- }),
479
- t('localOnly'),
480
- ),
481
- React.createElement('label', { className: 'cb-check-label' },
482
- React.createElement('input', {
483
- type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
484
- onChange: (e) => setTop('beep', e.target.checked),
485
- }),
486
- t('beep'),
487
- ),
488
- React.createElement('label', { className: 'cb-check-label' },
489
- React.createElement('input', {
490
- type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
491
- onChange: (e) => setTop('noiseSuppression', e.target.checked),
492
- }),
493
- t('noiseSuppression'),
494
- ),
495
- React.createElement('label', { className: 'cb-check-label' },
496
- React.createElement('input', {
497
- type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
498
- onChange: (e) => setTop('contextGlossary', e.target.checked),
499
- }),
500
- t('contextGlossary'),
501
- ),
502
- React.createElement('label', { className: 'cb-check-label' },
503
- React.createElement('input', {
504
- type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
505
- onChange: (e) => setTop('voiceCommands', e.target.checked),
506
- }),
507
- t('voiceCommandsLabel'),
508
- ),
509
- React.createElement('label', { className: 'cb-check-label' },
510
- React.createElement('input', {
511
- type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
512
- onChange: (e) => setTop('normalizeTranscript', e.target.checked),
513
- }),
514
- t('normalizeTranscript'),
515
- ),
516
- ),
517
- React.createElement('div', { className: 'cb-field' },
518
- React.createElement('label', null, t('visualizerStyle')),
519
- React.createElement('select', {
520
- value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
521
- onChange: (e) => setTop('visualizerStyle', e.target.value),
522
- },
523
- React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
524
- React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
525
- React.createElement('option', { value: 'bars' }, t('visBars')),
526
- React.createElement('option', { value: 'off' }, t('visOff')),
527
- ),
528
- ),
529
- React.createElement('div', { className: 'cb-field' },
530
- React.createElement('label', null, t('vocabulary')),
531
- React.createElement('textarea', {
532
- rows: 3, disabled: !writable,
533
- value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
534
- onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
535
- }),
536
- ),
537
- ),
538
-
539
- // 6. Provider Telemetry & Latency Dashboard
540
- React.createElement('div', { className: 'cb-section-card' },
541
- React.createElement('div', { className: 'cb-section-title' },
542
- React.createElement('span', null, '๐Ÿ“Š ' + t('providerDashboard')),
543
- React.createElement('button', {
544
- type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
545
- onClick: fetchStatus,
546
- }, '๐Ÿ”„ ' + t('refreshStats')),
547
- ),
548
- React.createElement('div', { className: 'dvo-dash' },
549
- React.createElement('div', { className: 'dvo-dash-grid' },
550
- Object.keys(statsData).length === 0
551
- ? React.createElement('div', { className: 'dvs-sub' }, t('idle'))
552
- : Object.entries(statsData).map(([key, item]) => {
553
- const avg = item.avgTookMs || 0
554
- const hasErrors = item.failures > 0
555
- let badgeClass = 'dvo-badge-idle'
556
- let badgeText = t('idle')
557
- if (item.attempts > 0) {
558
- if (hasErrors && item.successes === 0) {
559
- badgeClass = 'dvo-badge-err'
560
- badgeText = t('error')
561
- } else if (avg > 0 && avg < 400) {
562
- badgeClass = 'dvo-badge-fast'
563
- badgeText = avg + 'ms ยท ' + t('fast')
564
- } else if (avg >= 400 && avg <= 1500) {
565
- badgeClass = 'dvo-badge-norm'
566
- badgeText = avg + 'ms ยท ' + t('normal')
567
- } else {
568
- badgeClass = 'dvo-badge-slow'
569
- badgeText = avg + 'ms ยท ' + t('slow')
570
- }
571
- }
572
- const rate = item.attempts > 0 ? Math.round((item.successes / item.attempts) * 100) : 100
573
- return React.createElement('div', { key, className: 'dvo-dash-item', title: item.lastError ? `Error: ${item.lastError}` : '' },
574
- React.createElement('div', { className: 'dvo-dash-name' }, key),
575
- React.createElement('div', { className: 'dvo-dash-row' },
576
- React.createElement('span', { className: 'dvo-badge ' + badgeClass }, badgeText),
577
- React.createElement('span', null, rate + '%'),
578
- ),
579
- React.createElement('div', { className: 'dvo-dash-row' },
580
- React.createElement('span', null, `${item.successes}/${item.attempts}`),
581
- React.createElement('span', null, t('successRate')),
582
- ),
583
- )
584
- }),
585
- ),
586
- ),
587
- ),
588
-
589
- // Action bar (Save)
590
- React.createElement('div', { className: 'cb-row' },
591
- React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
592
- saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
593
- err ? React.createElement('span', { className: 'dvs-bad' }, err) : null,
594
- ),
595
- )
596
- }
597
-
@@ -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
- })