@goodandready/dsh-voice 0.8.19 → 0.8.21
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.
- package/lib/client-src/10-locale.js +14 -0
- package/lib/client-src/40-recording.js +3 -0
- package/lib/client-src/70-settings.js +347 -222
- package/lib/client.js +364 -222
- package/lib/index.js +8 -4
- package/lib/providers.js +16 -5
- package/package.json +1 -1
|
@@ -30,19 +30,40 @@
|
|
|
30
30
|
const LANGS = ['auto', 'ru', 'en', 'uk', 'de']
|
|
31
31
|
|
|
32
32
|
const SET_CSS =
|
|
33
|
-
'.
|
|
33
|
+
'.cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
34
|
+
'.cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
35
|
+
'.cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
36
|
+
'.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
37
|
+
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
38
|
+
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
39
|
+
'.cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
40
|
+
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
|
|
41
|
+
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
|
|
42
|
+
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
|
|
43
|
+
'.cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
44
|
+
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
45
|
+
'.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
46
|
+
'.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
47
|
+
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
48
|
+
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
49
|
+
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}' +
|
|
50
|
+
'.cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
51
|
+
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
52
|
+
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
53
|
+
'.cb-field select,.cb-field input[type="text"],.cb-field input[type="number"],.cb-field textarea{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
54
|
+
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
55
|
+
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
56
|
+
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
57
|
+
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
58
|
+
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
34
59
|
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
35
60
|
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
36
61
|
'.dvs-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
37
62
|
'.dvs-row{display:flex;gap:8px;align-items:center}' +
|
|
38
|
-
'.dvs-row select,.dvs-row input{background:var(--dsw-alias-bg-layer-
|
|
63
|
+
'.dvs-row select,.dvs-row input{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px}' +
|
|
39
64
|
'.dvs-row .dvs-model{flex:1}' +
|
|
40
|
-
'.dvs-
|
|
41
|
-
'.dvs-
|
|
42
|
-
'.dvs-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none}' +
|
|
43
|
-
'.dvs-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
|
|
44
|
-
'.dvs-card{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px}' +
|
|
45
|
-
'.dvs-card input,.dvs-card select{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
|
|
65
|
+
'.dvs-card{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px}' +
|
|
66
|
+
'.dvs-card input,.dvs-card select{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
|
|
46
67
|
'.dvs-wait{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5;max-width:520px}' +
|
|
47
68
|
'.dvs-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
48
69
|
'.dvs-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
|
|
@@ -53,7 +74,7 @@
|
|
|
53
74
|
'.dvo-pdesc{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
54
75
|
'.dvo-pchev{flex:none;display:flex;color:var(--dsw-alias-label-secondary);transition:transform .15s ease}' +
|
|
55
76
|
'.dvo-pcardOpen .dvo-pchev{transform:rotate(180deg)}' +
|
|
56
|
-
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:
|
|
77
|
+
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}'
|
|
57
78
|
const setCssId = 'dsh-voice/settings.module.css'
|
|
58
79
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + setCssId + '"]')) {
|
|
59
80
|
const tag = document.createElement('style')
|
|
@@ -152,6 +173,46 @@
|
|
|
152
173
|
)
|
|
153
174
|
}
|
|
154
175
|
|
|
176
|
+
function createErrorBoundary() {
|
|
177
|
+
if (!React || typeof React.Component !== 'function') {
|
|
178
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
179
|
+
}
|
|
180
|
+
return class ErrorBoundary extends React.Component {
|
|
181
|
+
constructor(props) {
|
|
182
|
+
super(props)
|
|
183
|
+
this.state = { hasError: false, error: null }
|
|
184
|
+
}
|
|
185
|
+
static getDerivedStateFromError(error) {
|
|
186
|
+
return { hasError: true, error }
|
|
187
|
+
}
|
|
188
|
+
componentDidCatch(error, errorInfo) {
|
|
189
|
+
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
190
|
+
}
|
|
191
|
+
render() {
|
|
192
|
+
if (this.state.hasError) {
|
|
193
|
+
return React.createElement(
|
|
194
|
+
'div',
|
|
195
|
+
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
196
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
197
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
198
|
+
React.createElement(
|
|
199
|
+
'button',
|
|
200
|
+
{
|
|
201
|
+
type: 'button',
|
|
202
|
+
className: 'cb-btn',
|
|
203
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
204
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
205
|
+
},
|
|
206
|
+
t('retry'),
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
return this.props?.children || null
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const ErrorBoundary = createErrorBoundary()
|
|
215
|
+
|
|
155
216
|
function VoiceSection(props) {
|
|
156
217
|
const t = (props && props.t) || moduleT
|
|
157
218
|
const ctx = props.ctx
|
|
@@ -166,12 +227,7 @@
|
|
|
166
227
|
const [saved, setSaved] = React.useState(false)
|
|
167
228
|
const [err, setErr] = React.useState('')
|
|
168
229
|
|
|
169
|
-
// Hotkey picker: not a name field — press the key you want.
|
|
170
|
-
// not need to know codes like KeyR.
|
|
171
|
-
//
|
|
172
|
-
// Hooks are declared here, above every return: declaring them later
|
|
173
|
-
// would leave fewer hooks on the not-ready branch and React would
|
|
174
|
-
// unmount the section with an error.
|
|
230
|
+
// Hotkey picker: not a name field — press the key you want.
|
|
175
231
|
const [catching, setCatching] = React.useState(false)
|
|
176
232
|
React.useEffect(() => {
|
|
177
233
|
if (!catching) return undefined
|
|
@@ -203,22 +259,10 @@
|
|
|
203
259
|
.catch(() => {})
|
|
204
260
|
}, [])
|
|
205
261
|
|
|
206
|
-
// The snapshot carries a status, and status matters more than value.
|
|
207
|
-
// loading — no host answer yet;
|
|
208
|
-
// unavailable — host answered but does not know this namespace yet
|
|
209
|
-
// (page opened in the first seconds while plugins register);
|
|
210
|
-
// ready — values are present.
|
|
211
|
-
// At unavailable, writable comes from the document and stays true, so
|
|
212
|
-
// without a status check the card draws an empty but seemingly working
|
|
213
|
-
// form — that is exactly how this bug looked.
|
|
214
262
|
const ready = !!snap && snap.status === 'ready'
|
|
215
263
|
const value = ready && snap.value ? snap.value : {}
|
|
216
264
|
const writable = ready && snap.writable !== false
|
|
217
265
|
|
|
218
|
-
// The browser settings mirror reloads only on value writes and
|
|
219
|
-
// reconnect; a new namespace is not such a signal. It will not fix
|
|
220
|
-
// itself — ask it to reload until ready. The mirror is shared, so this
|
|
221
|
-
// also repairs other settings sections.
|
|
222
266
|
React.useEffect(() => {
|
|
223
267
|
if (ready) return undefined
|
|
224
268
|
let tries = 0
|
|
@@ -230,11 +274,8 @@
|
|
|
230
274
|
return () => clearInterval(timer)
|
|
231
275
|
}, [ready])
|
|
232
276
|
|
|
233
|
-
// Seed the draft only from a ready snapshot. Previously it was filled
|
|
234
|
-
// from the first snapshot and an empty draft froze forever: later
|
|
235
|
-
// snapshots did not reseed it and the card stayed empty until reload.
|
|
236
277
|
React.useEffect(() => { if (ready && draft === null) setDraft(JSON.parse(JSON.stringify(value))) }, [ready, draft, value])
|
|
237
|
-
|
|
278
|
+
|
|
238
279
|
React.useEffect(() => {
|
|
239
280
|
if (!ready) return
|
|
240
281
|
voice.settings = Object.assign({}, voice.settings, {
|
|
@@ -258,23 +299,34 @@
|
|
|
258
299
|
})
|
|
259
300
|
}, [ready, value, snap])
|
|
260
301
|
|
|
261
|
-
const [
|
|
262
|
-
React.
|
|
263
|
-
|
|
264
|
-
|
|
302
|
+
const [statusData, setStatusData] = React.useState(null)
|
|
303
|
+
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
304
|
+
const fetchStatus = React.useCallback(() => {
|
|
305
|
+
const start = performance.now()
|
|
306
|
+
const ctrl = new AbortController()
|
|
307
|
+
const tId = setTimeout(() => ctrl.abort(), 5000)
|
|
308
|
+
fetch('/dsh-voice/status', { cache: 'no-store', signal: ctrl.signal })
|
|
265
309
|
.then((r) => r.json())
|
|
266
310
|
.then((data) => {
|
|
267
|
-
|
|
268
|
-
|
|
311
|
+
clearTimeout(tId)
|
|
312
|
+
setStatusLatency(Math.round(performance.now() - start))
|
|
313
|
+
if (data && data.ok) {
|
|
314
|
+
setStatusData(data)
|
|
269
315
|
}
|
|
270
316
|
})
|
|
271
|
-
.catch(() => {
|
|
272
|
-
|
|
317
|
+
.catch(() => {
|
|
318
|
+
clearTimeout(tId)
|
|
319
|
+
setStatusLatency(null)
|
|
320
|
+
})
|
|
273
321
|
}, [])
|
|
274
322
|
|
|
323
|
+
React.useEffect(() => {
|
|
324
|
+
fetchStatus()
|
|
325
|
+
}, [fetchStatus])
|
|
326
|
+
|
|
275
327
|
if (!ready) {
|
|
276
328
|
const waiting = !snap || snap.status === 'loading'
|
|
277
|
-
return React.createElement('div', { className: '
|
|
329
|
+
return React.createElement('div', { className: 'cb-page' },
|
|
278
330
|
React.createElement('div', { className: 'dvs-wait' }, waiting
|
|
279
331
|
? t('loadingSettings')
|
|
280
332
|
: t('notReady1')
|
|
@@ -294,10 +346,6 @@
|
|
|
294
346
|
setErr(''); setSaved(false)
|
|
295
347
|
if (!draft) return
|
|
296
348
|
|
|
297
|
-
// Fields are written one by one. Previously the first failure broke
|
|
298
|
-
// the loop: later fields were never saved, the composer never got the
|
|
299
|
-
// hotkey reload signal, and the button looked like "nothing happens".
|
|
300
|
-
// Now every field is attempted and failures are collected by name.
|
|
301
349
|
const failed = []
|
|
302
350
|
for (const k of Object.keys(draft)) {
|
|
303
351
|
try {
|
|
@@ -322,12 +370,11 @@
|
|
|
322
370
|
contextGlossary: draft.contextGlossary !== false,
|
|
323
371
|
visualizerStyle: draft.visualizerStyle || 'liquid-wave',
|
|
324
372
|
})
|
|
325
|
-
// The composer owns the hotkey handler — tell it to reload, otherwise
|
|
326
|
-
// a new key only works after a full page reload.
|
|
327
373
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
328
374
|
|
|
329
375
|
if (failed.length) { setErr(t('saveFailed') + ' ' + failed.join('; ')); return }
|
|
330
376
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
377
|
+
fetchStatus()
|
|
331
378
|
}
|
|
332
379
|
|
|
333
380
|
const modeVal = (mode, key, fallback) => {
|
|
@@ -335,174 +382,270 @@
|
|
|
335
382
|
return m && m[key] !== undefined ? m[key] : fallback
|
|
336
383
|
}
|
|
337
384
|
|
|
338
|
-
// Custom provider names come from the draft so a just-added entry
|
|
339
|
-
// appears in chain selects immediately.
|
|
340
385
|
const chainOptions = BUILTIN.concat(
|
|
341
386
|
(draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
|
|
342
387
|
.map((c) => String(c && c.key || '').trim())
|
|
343
388
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
344
389
|
)
|
|
345
390
|
|
|
346
|
-
const hotkeyField = () => React.createElement('
|
|
347
|
-
t('hotkey'),
|
|
348
|
-
React.createElement('div', { className: '
|
|
391
|
+
const hotkeyField = () => React.createElement('div', { className: 'cb-field' },
|
|
392
|
+
React.createElement('label', null, t('hotkey')),
|
|
393
|
+
React.createElement('div', { className: 'cb-row' },
|
|
349
394
|
React.createElement('button', {
|
|
350
|
-
type: 'button', className: '
|
|
395
|
+
type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
|
|
351
396
|
onClick: () => setCatching(true),
|
|
352
397
|
}, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
|
|
353
398
|
React.createElement('button', {
|
|
354
|
-
type: 'button', className: '
|
|
399
|
+
type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
|
|
355
400
|
disabled: !writable, onClick: () => setDraft((d) => Object.assign({}, d || {}, { hotkey: '' })),
|
|
356
401
|
}, '×'),
|
|
357
402
|
),
|
|
358
|
-
React.createElement('span', { className: 'dvs-sub' },
|
|
359
|
-
|
|
360
|
-
+ t('hotkeyHint2')))
|
|
403
|
+
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
404
|
+
)
|
|
361
405
|
|
|
362
|
-
const micField = () => React.createElement('
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
406
|
+
const micField = () => React.createElement('div', { className: 'cb-field' },
|
|
407
|
+
React.createElement('label', null, t('mic')),
|
|
408
|
+
React.createElement('select', {
|
|
409
|
+
value: String((snap && snap.micDeviceId) || ''), disabled: !writable,
|
|
410
|
+
onChange: (e) => setTop('micDeviceId', e.target.value),
|
|
411
|
+
},
|
|
412
|
+
React.createElement('option', { value: '' }, t('micDefault')),
|
|
413
|
+
devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
|
|
414
|
+
d.label || d.deviceId.slice(0, 12))),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
const langField = (mode) => React.createElement('div', { className: 'cb-field' },
|
|
419
|
+
React.createElement('label', null, t('language')),
|
|
371
420
|
React.createElement('select', {
|
|
372
421
|
value: modeVal(mode, 'language', 'ru'), disabled: !writable,
|
|
373
422
|
onChange: (e) => setIn(mode, 'language', e.target.value),
|
|
374
|
-
}, LANGS.map((o) => React.createElement('option', { key: o, value: o }, o)))
|
|
423
|
+
}, LANGS.map((o) => React.createElement('option', { key: o, value: o }, o))),
|
|
424
|
+
)
|
|
375
425
|
|
|
376
|
-
const numField = (mode, key, label, hint) => React.createElement('
|
|
426
|
+
const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
427
|
+
React.createElement('label', null, label),
|
|
377
428
|
React.createElement('input', {
|
|
378
429
|
type: 'number', value: modeVal(mode, key, ''), disabled: !writable,
|
|
379
430
|
onChange: (e) => setIn(mode, key, Number(e.target.value)),
|
|
380
431
|
}),
|
|
381
|
-
React.createElement('span', { className: 'dvs-sub' }, hint)
|
|
432
|
+
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
433
|
+
)
|
|
382
434
|
|
|
383
|
-
const textField = (key, label, hint) => React.createElement('
|
|
435
|
+
const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
436
|
+
React.createElement('label', null, label),
|
|
384
437
|
React.createElement('input', {
|
|
385
|
-
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
438
|
+
type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
386
439
|
onChange: (e) => setTop(key, e.target.value),
|
|
387
440
|
}),
|
|
388
|
-
React.createElement('span', { className: 'dvs-sub' }, hint)
|
|
441
|
+
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
442
|
+
)
|
|
389
443
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
444
|
+
const statsData = (statusData && statusData.providerStats) || {}
|
|
445
|
+
const hostOnline = statusLatency !== null
|
|
446
|
+
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
447
|
+
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
448
|
+
|
|
449
|
+
return React.createElement('div', { className: 'cb-page' },
|
|
450
|
+
// 1. Connection & Engine Status Card
|
|
451
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
452
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
453
|
+
React.createElement('span', null, t('statusTitle')),
|
|
454
|
+
React.createElement('button', {
|
|
455
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
456
|
+
onClick: fetchStatus,
|
|
457
|
+
}, '🔄 ' + t('refreshStats')),
|
|
458
|
+
),
|
|
459
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('statusDesc')),
|
|
460
|
+
React.createElement('div', { className: 'cb-row' },
|
|
461
|
+
hostOnline
|
|
462
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '● ' + t('badgeHostOnline').replace('{ms}', statusLatency))
|
|
463
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-bad' }, '● ' + t('badgeHostOffline')),
|
|
464
|
+
whisperRunning
|
|
465
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '● ' + t('badgeWhisperActive'))
|
|
466
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-warn' }, '○ ' + t('badgeWhisperInactive')),
|
|
467
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '✓ ' + t('badgeProviders').replace('{count}', providerCount)),
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
|
|
471
|
+
// 2. Dictation Mode Card
|
|
472
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
473
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
474
|
+
React.createElement('span', null, '🎙️ ' + t('dictationBtn')),
|
|
475
|
+
),
|
|
476
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('dictationHint')),
|
|
394
477
|
React.createElement(ChainEditor, {
|
|
395
478
|
value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
|
|
396
479
|
options: chainOptions,
|
|
397
480
|
onChange: (v) => setIn('dictation', 'chain', v),
|
|
398
481
|
}),
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
482
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
483
|
+
langField('dictation'),
|
|
484
|
+
numField('dictation', 'vadSilenceMs', t('pauseMs'), t('pauseHint')),
|
|
485
|
+
numField('dictation', 'sendDelayMs', t('sendDelay'), t('sendDelayHint')),
|
|
486
|
+
numField('dictation', 'streamChunkMs', t('streamChunkMs'), t('streamHint')),
|
|
487
|
+
),
|
|
488
|
+
React.createElement('div', { className: 'cb-row' },
|
|
489
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
490
|
+
React.createElement('input', {
|
|
491
|
+
type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.polish), disabled: !writable,
|
|
492
|
+
onChange: (e) => setIn('dictation', 'polish', e.target.checked),
|
|
493
|
+
}),
|
|
494
|
+
t('polish'),
|
|
495
|
+
),
|
|
496
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
497
|
+
React.createElement('input', {
|
|
498
|
+
type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.stream), disabled: !writable,
|
|
499
|
+
onChange: (e) => setIn('dictation', 'stream', e.target.checked),
|
|
500
|
+
}),
|
|
501
|
+
t('stream'),
|
|
502
|
+
),
|
|
503
|
+
),
|
|
504
|
+
React.createElement('div', { className: 'cb-field' },
|
|
505
|
+
React.createElement('label', null, t('vadAdapt')),
|
|
420
506
|
React.createElement('input', {
|
|
421
507
|
type: 'range', min: 0, max: 1, step: 0.1,
|
|
422
508
|
value: Number(draft && draft.dictation && draft.dictation.vadAdapt) || 0, disabled: !writable,
|
|
423
509
|
onChange: (e) => setIn('dictation', 'vadAdapt', Number(e.target.value)),
|
|
424
|
-
})
|
|
510
|
+
}),
|
|
511
|
+
React.createElement('span', { className: 'dvs-sub' }, t('vadAdaptHint')),
|
|
512
|
+
),
|
|
513
|
+
textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
|
|
425
514
|
),
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
515
|
+
|
|
516
|
+
// 3. Voice Message Card
|
|
517
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
518
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
519
|
+
React.createElement('span', null, '💬 ' + t('messageTitle')),
|
|
520
|
+
),
|
|
521
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('messageHint')),
|
|
429
522
|
React.createElement(ChainEditor, {
|
|
430
523
|
value: draft && draft.message ? draft.message.chain : [], writable: writable,
|
|
431
524
|
options: chainOptions,
|
|
432
525
|
onChange: (v) => setIn('message', 'chain', v),
|
|
433
526
|
}),
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
527
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
528
|
+
langField('message'),
|
|
529
|
+
numField('message', 'autoSendMs', t('undoMs'), t('undoHint')),
|
|
530
|
+
),
|
|
531
|
+
React.createElement('div', { className: 'cb-row' },
|
|
532
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
533
|
+
React.createElement('input', {
|
|
534
|
+
type: 'checkbox', checked: !!(draft && draft.message && draft.message.polish), disabled: !writable,
|
|
535
|
+
onChange: (e) => setIn('message', 'polish', e.target.checked),
|
|
536
|
+
}),
|
|
537
|
+
t('polish'),
|
|
538
|
+
),
|
|
539
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
540
|
+
React.createElement('input', {
|
|
541
|
+
type: 'checkbox', checked: !!(draft && draft.message && draft.message.polishSend), disabled: !writable,
|
|
542
|
+
onChange: (e) => setIn('message', 'polishSend', e.target.checked),
|
|
543
|
+
}),
|
|
544
|
+
t('polishSend'),
|
|
545
|
+
),
|
|
546
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
547
|
+
React.createElement('input', {
|
|
548
|
+
type: 'checkbox', checked: !!(draft && draft.message && draft.message.sessionCommands), disabled: !writable,
|
|
549
|
+
onChange: (e) => setIn('message', 'sessionCommands', e.target.checked),
|
|
550
|
+
}),
|
|
551
|
+
t('sessionCommands'),
|
|
552
|
+
),
|
|
553
|
+
),
|
|
451
554
|
),
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
555
|
+
|
|
556
|
+
// 4. Custom Providers Card
|
|
557
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
558
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
559
|
+
React.createElement('span', null, '⚙️ ' + t('customTitle')),
|
|
560
|
+
),
|
|
561
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('customHint')),
|
|
456
562
|
React.createElement(CustomEditor, {
|
|
457
563
|
value: draft && draft.customProviders ? draft.customProviders : [], writable: writable,
|
|
564
|
+
options: chainOptions,
|
|
458
565
|
onChange: (v) => setTop('customProviders', v),
|
|
459
566
|
}),
|
|
460
567
|
),
|
|
461
|
-
|
|
462
|
-
|
|
568
|
+
|
|
569
|
+
// 5. Hardware & Local Engines Card
|
|
570
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
571
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
572
|
+
React.createElement('span', null, '🔧 ' + t('hardwareTitle')),
|
|
573
|
+
),
|
|
574
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
|
|
463
575
|
hotkeyField(),
|
|
464
|
-
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
465
|
-
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
466
|
-
textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
|
|
467
|
-
textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
|
|
468
|
-
React.createElement('label', { className: 'dvs-field' }, t('whisperAutostart'),
|
|
469
|
-
React.createElement('input', {
|
|
470
|
-
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
471
|
-
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
472
|
-
})),
|
|
473
|
-
React.createElement('label', { className: 'dvs-field', title: t('normalizeTranscriptHint') },
|
|
474
|
-
t('normalizeTranscript'),
|
|
475
|
-
React.createElement('input', {
|
|
476
|
-
type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
|
|
477
|
-
onChange: (e) => setTop('normalizeTranscript', e.target.checked),
|
|
478
|
-
})),
|
|
479
|
-
React.createElement('label', { className: 'dvs-field' }, t('beep'),
|
|
480
|
-
React.createElement('input', {
|
|
481
|
-
type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
|
|
482
|
-
onChange: (e) => setTop('beep', e.target.checked),
|
|
483
|
-
})),
|
|
484
|
-
React.createElement('label', { className: 'dvs-field', title: t('localOnlyHint') }, t('localOnly'),
|
|
485
|
-
React.createElement('input', {
|
|
486
|
-
type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
|
|
487
|
-
onChange: (e) => setTop('localOnly', e.target.checked),
|
|
488
|
-
})),
|
|
489
|
-
React.createElement('label', { className: 'dvs-field' }, t('voiceCommandsLabel'),
|
|
490
|
-
React.createElement('input', {
|
|
491
|
-
type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
|
|
492
|
-
onChange: (e) => setTop('voiceCommands', e.target.checked),
|
|
493
|
-
})),
|
|
494
576
|
micField(),
|
|
495
|
-
React.createElement('
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
577
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
578
|
+
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
579
|
+
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
580
|
+
textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
|
|
581
|
+
textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
|
|
582
|
+
textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
|
|
583
|
+
textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
|
|
584
|
+
textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
|
|
585
|
+
textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
|
|
586
|
+
textField('polishModel', t('polishModel'), ''),
|
|
587
|
+
textField('polishKeyEnv', t('polishKeyEnv'), ''),
|
|
588
|
+
),
|
|
589
|
+
React.createElement('div', { className: 'cb-row' },
|
|
590
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
591
|
+
React.createElement('input', {
|
|
592
|
+
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
593
|
+
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
594
|
+
}),
|
|
595
|
+
t('whisperAutostart'),
|
|
596
|
+
),
|
|
597
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
598
|
+
React.createElement('input', {
|
|
599
|
+
type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
|
|
600
|
+
onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
|
|
601
|
+
}),
|
|
602
|
+
t('sensevoiceAutostart'),
|
|
603
|
+
),
|
|
604
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
605
|
+
React.createElement('input', {
|
|
606
|
+
type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
|
|
607
|
+
onChange: (e) => setTop('localOnly', e.target.checked),
|
|
608
|
+
}),
|
|
609
|
+
t('localOnly'),
|
|
610
|
+
),
|
|
611
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
612
|
+
React.createElement('input', {
|
|
613
|
+
type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
|
|
614
|
+
onChange: (e) => setTop('beep', e.target.checked),
|
|
615
|
+
}),
|
|
616
|
+
t('beep'),
|
|
617
|
+
),
|
|
618
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
619
|
+
React.createElement('input', {
|
|
620
|
+
type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
|
|
621
|
+
onChange: (e) => setTop('noiseSuppression', e.target.checked),
|
|
622
|
+
}),
|
|
623
|
+
t('noiseSuppression'),
|
|
624
|
+
),
|
|
625
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
626
|
+
React.createElement('input', {
|
|
627
|
+
type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
|
|
628
|
+
onChange: (e) => setTop('contextGlossary', e.target.checked),
|
|
629
|
+
}),
|
|
630
|
+
t('contextGlossary'),
|
|
631
|
+
),
|
|
632
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
633
|
+
React.createElement('input', {
|
|
634
|
+
type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
|
|
635
|
+
onChange: (e) => setTop('voiceCommands', e.target.checked),
|
|
636
|
+
}),
|
|
637
|
+
t('voiceCommandsLabel'),
|
|
638
|
+
),
|
|
639
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
640
|
+
React.createElement('input', {
|
|
641
|
+
type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
|
|
642
|
+
onChange: (e) => setTop('normalizeTranscript', e.target.checked),
|
|
643
|
+
}),
|
|
644
|
+
t('normalizeTranscript'),
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
React.createElement('div', { className: 'cb-field' },
|
|
648
|
+
React.createElement('label', null, t('visualizerStyle')),
|
|
506
649
|
React.createElement('select', {
|
|
507
650
|
value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
|
|
508
651
|
onChange: (e) => setTop('visualizerStyle', e.target.value),
|
|
@@ -511,43 +654,27 @@
|
|
|
511
654
|
React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
|
|
512
655
|
React.createElement('option', { value: 'bars' }, t('visBars')),
|
|
513
656
|
React.createElement('option', { value: 'off' }, t('visOff')),
|
|
514
|
-
)
|
|
515
|
-
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
React.createElement('div', { className: 'cb-field' },
|
|
660
|
+
React.createElement('label', null, t('vocabulary')),
|
|
516
661
|
React.createElement('textarea', {
|
|
517
662
|
rows: 3, disabled: !writable,
|
|
518
663
|
value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
|
|
519
664
|
onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
React.createElement('input', {
|
|
523
|
-
type: 'text', value: String((snap && snap.polishBaseUrl) || ''), disabled: !writable,
|
|
524
|
-
onChange: (e) => setTop('polishBaseUrl', e.target.value),
|
|
525
|
-
})),
|
|
526
|
-
React.createElement('label', { className: 'dvs-field' }, t('polishModel'),
|
|
527
|
-
React.createElement('input', {
|
|
528
|
-
type: 'text', value: String((draft && draft.polishModel) || ''), disabled: !writable,
|
|
529
|
-
onChange: (e) => setTop('polishModel', e.target.value),
|
|
530
|
-
})),
|
|
531
|
-
React.createElement('label', { className: 'dvs-field' }, t('polishKeyEnv'),
|
|
532
|
-
React.createElement('input', {
|
|
533
|
-
type: 'text', value: String((draft && draft.polishKeyEnv) || ''), disabled: !writable,
|
|
534
|
-
onChange: (e) => setTop('polishKeyEnv', e.target.value),
|
|
535
|
-
})),
|
|
536
|
-
),
|
|
537
|
-
React.createElement('div', { className: 'dvs-block' },
|
|
538
|
-
React.createElement('div', { className: 'dvs-h' }, 'SenseVoice-ONNX / Sherpa-ONNX'),
|
|
539
|
-
React.createElement('div', { className: 'dvs-sub' }, t('sensevoiceHint')),
|
|
540
|
-
textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
|
|
541
|
-
textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
|
|
542
|
-
textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
|
|
543
|
-
React.createElement('label', { className: 'dvs-field' }, t('sensevoiceAutostart'),
|
|
544
|
-
React.createElement('input', {
|
|
545
|
-
type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
|
|
546
|
-
onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
|
|
547
|
-
})),
|
|
665
|
+
}),
|
|
666
|
+
),
|
|
548
667
|
),
|
|
549
|
-
|
|
550
|
-
|
|
668
|
+
|
|
669
|
+
// 6. Provider Telemetry & Latency Dashboard
|
|
670
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
671
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
672
|
+
React.createElement('span', null, '📊 ' + t('providerDashboard')),
|
|
673
|
+
React.createElement('button', {
|
|
674
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
675
|
+
onClick: fetchStatus,
|
|
676
|
+
}, '🔄 ' + t('refreshStats')),
|
|
677
|
+
),
|
|
551
678
|
React.createElement('div', { className: 'dvo-dash' },
|
|
552
679
|
React.createElement('div', { className: 'dvo-dash-grid' },
|
|
553
680
|
Object.keys(statsData).length === 0
|
|
@@ -584,53 +711,51 @@
|
|
|
584
711
|
React.createElement('span', null, t('successRate')),
|
|
585
712
|
),
|
|
586
713
|
)
|
|
587
|
-
})
|
|
588
|
-
)
|
|
589
|
-
)
|
|
714
|
+
}),
|
|
715
|
+
),
|
|
716
|
+
),
|
|
590
717
|
),
|
|
591
|
-
|
|
592
|
-
|
|
718
|
+
|
|
719
|
+
// Action bar (Save)
|
|
720
|
+
React.createElement('div', { className: 'cb-row' },
|
|
721
|
+
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
|
|
593
722
|
saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
|
|
594
723
|
err ? React.createElement('span', { className: 'dvs-bad' }, err) : null,
|
|
595
724
|
),
|
|
596
725
|
)
|
|
597
726
|
}
|
|
598
727
|
|
|
599
|
-
// Card in Settings → Plugins → Plugin settings (#18)
|
|
600
|
-
// draws the list shell, so title, hint and collapse are ours.
|
|
601
|
-
// Body mounts on first expand; the settings snapshot arrives then.
|
|
728
|
+
// Card in Settings → Plugins → Plugin settings (#18)
|
|
602
729
|
function PluginCard(props) {
|
|
603
730
|
const [open, setOpen] = React.useState(false)
|
|
604
731
|
const tt = (props && props.t) || t
|
|
605
732
|
const title = tt('title')
|
|
606
|
-
return React.createElement(
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
React.createElement('span', { className: 'dvo-
|
|
617
|
-
|
|
733
|
+
return React.createElement(ErrorBoundary, null,
|
|
734
|
+
React.createElement('li',
|
|
735
|
+
{ className: open ? 'dvo-pcard dvo-pcardOpen' : 'dvo-pcard' },
|
|
736
|
+
React.createElement('button',
|
|
737
|
+
{
|
|
738
|
+
type: 'button', className: 'dvo-phead',
|
|
739
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
740
|
+
'aria-label': (open ? tt('collapse') : tt('expand')) + ': ' + title,
|
|
741
|
+
onClick: () => setOpen((v) => !v),
|
|
742
|
+
},
|
|
743
|
+
React.createElement('span', { className: 'dvo-pheadtext' },
|
|
744
|
+
React.createElement('span', { className: 'dvo-ptitle' }, title),
|
|
745
|
+
React.createElement('span', { className: 'dvo-pdesc' }, tt('cardHint')),
|
|
746
|
+
),
|
|
747
|
+
React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
|
|
618
748
|
),
|
|
619
|
-
React.createElement('
|
|
749
|
+
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
620
750
|
),
|
|
621
|
-
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
622
751
|
)
|
|
623
752
|
}
|
|
624
753
|
|
|
625
754
|
function registerSettings(ctx) {
|
|
626
|
-
// Settings card under Settings → Plugins → Plugin settings (#18).
|
|
627
|
-
// The registration key must equal NS (the settings namespace), otherwise
|
|
628
|
-
// the tab silently skips the slot.
|
|
629
755
|
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
630
756
|
{
|
|
631
757
|
name: 'settings.plugin.item',
|
|
632
758
|
key: NS,
|
|
633
|
-
// locale on the slot record is what makes the component receive props.t.
|
|
634
759
|
locale: NS,
|
|
635
760
|
inject: () => ({ ctx: ctx }),
|
|
636
761
|
},
|