@goodandready/dsh-voice 0.8.19 → 0.8.20
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/70-settings.js +347 -222
- package/lib/client.js +361 -222
- package/lib/index.js +1 -1
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -163,6 +163,20 @@ window.__ModuleLoader__.load({
|
|
|
163
163
|
'visDynamicOrb': 'Dynamic Orb',
|
|
164
164
|
'visBars': 'Classic Bars',
|
|
165
165
|
'visOff': 'Off',
|
|
166
|
+
'statusTitle': 'Connection & Engine Status',
|
|
167
|
+
'statusDesc': 'Current runtime connectivity to voice backends and recognition services.',
|
|
168
|
+
'badgeHostOnline': 'Host online ({ms} ms)',
|
|
169
|
+
'badgeHostOffline': 'Host unreachable',
|
|
170
|
+
'badgeWhisperActive': 'Whisper active',
|
|
171
|
+
'badgeWhisperInactive': 'Whisper offline',
|
|
172
|
+
'badgeSenseVoiceActive': 'SenseVoice active',
|
|
173
|
+
'badgeSenseVoiceInactive': 'SenseVoice offline',
|
|
174
|
+
'badgeProviders': '{count} providers ready',
|
|
175
|
+
'refreshStats': 'Refresh Stats',
|
|
176
|
+
'hardwareTitle': 'Hardware & Local Engines',
|
|
177
|
+
'hardwareDesc': 'Microphone device, local whisper.cpp and SenseVoice runtime settings.',
|
|
178
|
+
'uiError': 'Voice UI Error:',
|
|
179
|
+
'retry': 'Retry',
|
|
166
180
|
}
|
|
167
181
|
|
|
168
182
|
// Strings are also needed outside components — in recording handlers and
|
|
@@ -1345,19 +1359,40 @@ window.__ModuleLoader__.load({
|
|
|
1345
1359
|
const LANGS = ['auto', 'ru', 'en', 'uk', 'de']
|
|
1346
1360
|
|
|
1347
1361
|
const SET_CSS =
|
|
1348
|
-
'.
|
|
1362
|
+
'.cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
1363
|
+
'.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}' +
|
|
1364
|
+
'.cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
1365
|
+
'.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
1366
|
+
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
1367
|
+
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
1368
|
+
'.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}' +
|
|
1369
|
+
'.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)}' +
|
|
1370
|
+
'.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)}' +
|
|
1371
|
+
'.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)}' +
|
|
1372
|
+
'.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}' +
|
|
1373
|
+
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
1374
|
+
'.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}' +
|
|
1375
|
+
'.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))}' +
|
|
1376
|
+
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
1377
|
+
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
1378
|
+
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}' +
|
|
1379
|
+
'.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}' +
|
|
1380
|
+
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
1381
|
+
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
1382
|
+
'.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}' +
|
|
1383
|
+
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
1384
|
+
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
1385
|
+
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
1386
|
+
'.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}' +
|
|
1387
|
+
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
1349
1388
|
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
1350
1389
|
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
1351
1390
|
'.dvs-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
1352
1391
|
'.dvs-row{display:flex;gap:8px;align-items:center}' +
|
|
1353
|
-
'.dvs-row select,.dvs-row input{background:var(--dsw-alias-bg-layer-
|
|
1392
|
+
'.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}' +
|
|
1354
1393
|
'.dvs-row .dvs-model{flex:1}' +
|
|
1355
|
-
'.dvs-
|
|
1356
|
-
'.dvs-
|
|
1357
|
-
'.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}' +
|
|
1358
|
-
'.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)}' +
|
|
1359
|
-
'.dvs-card{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px}' +
|
|
1360
|
-
'.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}' +
|
|
1394
|
+
'.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}' +
|
|
1395
|
+
'.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}' +
|
|
1361
1396
|
'.dvs-wait{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5;max-width:520px}' +
|
|
1362
1397
|
'.dvs-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
1363
1398
|
'.dvs-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
|
|
@@ -1368,7 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
1368
1403
|
'.dvo-pdesc{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
1369
1404
|
'.dvo-pchev{flex:none;display:flex;color:var(--dsw-alias-label-secondary);transition:transform .15s ease}' +
|
|
1370
1405
|
'.dvo-pcardOpen .dvo-pchev{transform:rotate(180deg)}' +
|
|
1371
|
-
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:
|
|
1406
|
+
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}'
|
|
1372
1407
|
const setCssId = 'dsh-voice/settings.module.css'
|
|
1373
1408
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + setCssId + '"]')) {
|
|
1374
1409
|
const tag = document.createElement('style')
|
|
@@ -1467,6 +1502,46 @@ window.__ModuleLoader__.load({
|
|
|
1467
1502
|
)
|
|
1468
1503
|
}
|
|
1469
1504
|
|
|
1505
|
+
function createErrorBoundary() {
|
|
1506
|
+
if (!React || typeof React.Component !== 'function') {
|
|
1507
|
+
return function NoopBoundary(props) { return props?.children || null }
|
|
1508
|
+
}
|
|
1509
|
+
return class ErrorBoundary extends React.Component {
|
|
1510
|
+
constructor(props) {
|
|
1511
|
+
super(props)
|
|
1512
|
+
this.state = { hasError: false, error: null }
|
|
1513
|
+
}
|
|
1514
|
+
static getDerivedStateFromError(error) {
|
|
1515
|
+
return { hasError: true, error }
|
|
1516
|
+
}
|
|
1517
|
+
componentDidCatch(error, errorInfo) {
|
|
1518
|
+
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
1519
|
+
}
|
|
1520
|
+
render() {
|
|
1521
|
+
if (this.state.hasError) {
|
|
1522
|
+
return React.createElement(
|
|
1523
|
+
'div',
|
|
1524
|
+
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
1525
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
1526
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
1527
|
+
React.createElement(
|
|
1528
|
+
'button',
|
|
1529
|
+
{
|
|
1530
|
+
type: 'button',
|
|
1531
|
+
className: 'cb-btn',
|
|
1532
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
1533
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
1534
|
+
},
|
|
1535
|
+
t('retry'),
|
|
1536
|
+
),
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
return this.props?.children || null
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
const ErrorBoundary = createErrorBoundary()
|
|
1544
|
+
|
|
1470
1545
|
function VoiceSection(props) {
|
|
1471
1546
|
const t = (props && props.t) || moduleT
|
|
1472
1547
|
const ctx = props.ctx
|
|
@@ -1481,12 +1556,7 @@ window.__ModuleLoader__.load({
|
|
|
1481
1556
|
const [saved, setSaved] = React.useState(false)
|
|
1482
1557
|
const [err, setErr] = React.useState('')
|
|
1483
1558
|
|
|
1484
|
-
// Hotkey picker: not a name field — press the key you want.
|
|
1485
|
-
// not need to know codes like KeyR.
|
|
1486
|
-
//
|
|
1487
|
-
// Hooks are declared here, above every return: declaring them later
|
|
1488
|
-
// would leave fewer hooks on the not-ready branch and React would
|
|
1489
|
-
// unmount the section with an error.
|
|
1559
|
+
// Hotkey picker: not a name field — press the key you want.
|
|
1490
1560
|
const [catching, setCatching] = React.useState(false)
|
|
1491
1561
|
React.useEffect(() => {
|
|
1492
1562
|
if (!catching) return undefined
|
|
@@ -1518,22 +1588,10 @@ window.__ModuleLoader__.load({
|
|
|
1518
1588
|
.catch(() => {})
|
|
1519
1589
|
}, [])
|
|
1520
1590
|
|
|
1521
|
-
// The snapshot carries a status, and status matters more than value.
|
|
1522
|
-
// loading — no host answer yet;
|
|
1523
|
-
// unavailable — host answered but does not know this namespace yet
|
|
1524
|
-
// (page opened in the first seconds while plugins register);
|
|
1525
|
-
// ready — values are present.
|
|
1526
|
-
// At unavailable, writable comes from the document and stays true, so
|
|
1527
|
-
// without a status check the card draws an empty but seemingly working
|
|
1528
|
-
// form — that is exactly how this bug looked.
|
|
1529
1591
|
const ready = !!snap && snap.status === 'ready'
|
|
1530
1592
|
const value = ready && snap.value ? snap.value : {}
|
|
1531
1593
|
const writable = ready && snap.writable !== false
|
|
1532
1594
|
|
|
1533
|
-
// The browser settings mirror reloads only on value writes and
|
|
1534
|
-
// reconnect; a new namespace is not such a signal. It will not fix
|
|
1535
|
-
// itself — ask it to reload until ready. The mirror is shared, so this
|
|
1536
|
-
// also repairs other settings sections.
|
|
1537
1595
|
React.useEffect(() => {
|
|
1538
1596
|
if (ready) return undefined
|
|
1539
1597
|
let tries = 0
|
|
@@ -1545,11 +1603,8 @@ window.__ModuleLoader__.load({
|
|
|
1545
1603
|
return () => clearInterval(timer)
|
|
1546
1604
|
}, [ready])
|
|
1547
1605
|
|
|
1548
|
-
// Seed the draft only from a ready snapshot. Previously it was filled
|
|
1549
|
-
// from the first snapshot and an empty draft froze forever: later
|
|
1550
|
-
// snapshots did not reseed it and the card stayed empty until reload.
|
|
1551
1606
|
React.useEffect(() => { if (ready && draft === null) setDraft(JSON.parse(JSON.stringify(value))) }, [ready, draft, value])
|
|
1552
|
-
|
|
1607
|
+
|
|
1553
1608
|
React.useEffect(() => {
|
|
1554
1609
|
if (!ready) return
|
|
1555
1610
|
voice.settings = Object.assign({}, voice.settings, {
|
|
@@ -1573,23 +1628,34 @@ window.__ModuleLoader__.load({
|
|
|
1573
1628
|
})
|
|
1574
1629
|
}, [ready, value, snap])
|
|
1575
1630
|
|
|
1576
|
-
const [
|
|
1577
|
-
React.
|
|
1578
|
-
|
|
1579
|
-
|
|
1631
|
+
const [statusData, setStatusData] = React.useState(null)
|
|
1632
|
+
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
1633
|
+
const fetchStatus = React.useCallback(() => {
|
|
1634
|
+
const start = performance.now()
|
|
1635
|
+
const ctrl = new AbortController()
|
|
1636
|
+
const tId = setTimeout(() => ctrl.abort(), 5000)
|
|
1637
|
+
fetch('/dsh-voice/status', { cache: 'no-store', signal: ctrl.signal })
|
|
1580
1638
|
.then((r) => r.json())
|
|
1581
1639
|
.then((data) => {
|
|
1582
|
-
|
|
1583
|
-
|
|
1640
|
+
clearTimeout(tId)
|
|
1641
|
+
setStatusLatency(Math.round(performance.now() - start))
|
|
1642
|
+
if (data && data.ok) {
|
|
1643
|
+
setStatusData(data)
|
|
1584
1644
|
}
|
|
1585
1645
|
})
|
|
1586
|
-
.catch(() => {
|
|
1587
|
-
|
|
1646
|
+
.catch(() => {
|
|
1647
|
+
clearTimeout(tId)
|
|
1648
|
+
setStatusLatency(null)
|
|
1649
|
+
})
|
|
1588
1650
|
}, [])
|
|
1589
1651
|
|
|
1652
|
+
React.useEffect(() => {
|
|
1653
|
+
fetchStatus()
|
|
1654
|
+
}, [fetchStatus])
|
|
1655
|
+
|
|
1590
1656
|
if (!ready) {
|
|
1591
1657
|
const waiting = !snap || snap.status === 'loading'
|
|
1592
|
-
return React.createElement('div', { className: '
|
|
1658
|
+
return React.createElement('div', { className: 'cb-page' },
|
|
1593
1659
|
React.createElement('div', { className: 'dvs-wait' }, waiting
|
|
1594
1660
|
? t('loadingSettings')
|
|
1595
1661
|
: t('notReady1')
|
|
@@ -1609,10 +1675,6 @@ window.__ModuleLoader__.load({
|
|
|
1609
1675
|
setErr(''); setSaved(false)
|
|
1610
1676
|
if (!draft) return
|
|
1611
1677
|
|
|
1612
|
-
// Fields are written one by one. Previously the first failure broke
|
|
1613
|
-
// the loop: later fields were never saved, the composer never got the
|
|
1614
|
-
// hotkey reload signal, and the button looked like "nothing happens".
|
|
1615
|
-
// Now every field is attempted and failures are collected by name.
|
|
1616
1678
|
const failed = []
|
|
1617
1679
|
for (const k of Object.keys(draft)) {
|
|
1618
1680
|
try {
|
|
@@ -1637,12 +1699,11 @@ window.__ModuleLoader__.load({
|
|
|
1637
1699
|
contextGlossary: draft.contextGlossary !== false,
|
|
1638
1700
|
visualizerStyle: draft.visualizerStyle || 'liquid-wave',
|
|
1639
1701
|
})
|
|
1640
|
-
// The composer owns the hotkey handler — tell it to reload, otherwise
|
|
1641
|
-
// a new key only works after a full page reload.
|
|
1642
1702
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
1643
1703
|
|
|
1644
1704
|
if (failed.length) { setErr(t('saveFailed') + ' ' + failed.join('; ')); return }
|
|
1645
1705
|
setSaved(true); setTimeout(() => setSaved(false), 2000)
|
|
1706
|
+
fetchStatus()
|
|
1646
1707
|
}
|
|
1647
1708
|
|
|
1648
1709
|
const modeVal = (mode, key, fallback) => {
|
|
@@ -1650,174 +1711,270 @@ window.__ModuleLoader__.load({
|
|
|
1650
1711
|
return m && m[key] !== undefined ? m[key] : fallback
|
|
1651
1712
|
}
|
|
1652
1713
|
|
|
1653
|
-
// Custom provider names come from the draft so a just-added entry
|
|
1654
|
-
// appears in chain selects immediately.
|
|
1655
1714
|
const chainOptions = BUILTIN.concat(
|
|
1656
1715
|
(draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
|
|
1657
1716
|
.map((c) => String(c && c.key || '').trim())
|
|
1658
1717
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
1659
1718
|
)
|
|
1660
1719
|
|
|
1661
|
-
const hotkeyField = () => React.createElement('
|
|
1662
|
-
t('hotkey'),
|
|
1663
|
-
React.createElement('div', { className: '
|
|
1720
|
+
const hotkeyField = () => React.createElement('div', { className: 'cb-field' },
|
|
1721
|
+
React.createElement('label', null, t('hotkey')),
|
|
1722
|
+
React.createElement('div', { className: 'cb-row' },
|
|
1664
1723
|
React.createElement('button', {
|
|
1665
|
-
type: 'button', className: '
|
|
1724
|
+
type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable,
|
|
1666
1725
|
onClick: () => setCatching(true),
|
|
1667
1726
|
}, catching ? t('pressKey') : keyLabel(draft && draft.hotkey)),
|
|
1668
1727
|
React.createElement('button', {
|
|
1669
|
-
type: 'button', className: '
|
|
1728
|
+
type: 'button', className: 'cb-btn cb-btn-mini', title: t('clearKey'),
|
|
1670
1729
|
disabled: !writable, onClick: () => setDraft((d) => Object.assign({}, d || {}, { hotkey: '' })),
|
|
1671
1730
|
}, '×'),
|
|
1672
1731
|
),
|
|
1673
|
-
React.createElement('span', { className: 'dvs-sub' },
|
|
1674
|
-
|
|
1675
|
-
+ t('hotkeyHint2')))
|
|
1732
|
+
React.createElement('span', { className: 'dvs-sub' }, t('hotkeyHint1') + t('hotkeyHint2')),
|
|
1733
|
+
)
|
|
1676
1734
|
|
|
1677
|
-
const micField = () => React.createElement('
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1735
|
+
const micField = () => React.createElement('div', { className: 'cb-field' },
|
|
1736
|
+
React.createElement('label', null, t('mic')),
|
|
1737
|
+
React.createElement('select', {
|
|
1738
|
+
value: String((snap && snap.micDeviceId) || ''), disabled: !writable,
|
|
1739
|
+
onChange: (e) => setTop('micDeviceId', e.target.value),
|
|
1740
|
+
},
|
|
1741
|
+
React.createElement('option', { value: '' }, t('micDefault')),
|
|
1742
|
+
devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId },
|
|
1743
|
+
d.label || d.deviceId.slice(0, 12))),
|
|
1744
|
+
),
|
|
1745
|
+
)
|
|
1746
|
+
|
|
1747
|
+
const langField = (mode) => React.createElement('div', { className: 'cb-field' },
|
|
1748
|
+
React.createElement('label', null, t('language')),
|
|
1686
1749
|
React.createElement('select', {
|
|
1687
1750
|
value: modeVal(mode, 'language', 'ru'), disabled: !writable,
|
|
1688
1751
|
onChange: (e) => setIn(mode, 'language', e.target.value),
|
|
1689
|
-
}, LANGS.map((o) => React.createElement('option', { key: o, value: o }, o)))
|
|
1752
|
+
}, LANGS.map((o) => React.createElement('option', { key: o, value: o }, o))),
|
|
1753
|
+
)
|
|
1690
1754
|
|
|
1691
|
-
const numField = (mode, key, label, hint) => React.createElement('
|
|
1755
|
+
const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
1756
|
+
React.createElement('label', null, label),
|
|
1692
1757
|
React.createElement('input', {
|
|
1693
1758
|
type: 'number', value: modeVal(mode, key, ''), disabled: !writable,
|
|
1694
1759
|
onChange: (e) => setIn(mode, key, Number(e.target.value)),
|
|
1695
1760
|
}),
|
|
1696
|
-
React.createElement('span', { className: 'dvs-sub' }, hint)
|
|
1761
|
+
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
1762
|
+
)
|
|
1697
1763
|
|
|
1698
|
-
const textField = (key, label, hint) => React.createElement('
|
|
1764
|
+
const textField = (key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
1765
|
+
React.createElement('label', null, label),
|
|
1699
1766
|
React.createElement('input', {
|
|
1700
|
-
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
1767
|
+
type: 'text', value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
1701
1768
|
onChange: (e) => setTop(key, e.target.value),
|
|
1702
1769
|
}),
|
|
1703
|
-
React.createElement('span', { className: 'dvs-sub' }, hint)
|
|
1770
|
+
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
1771
|
+
)
|
|
1772
|
+
|
|
1773
|
+
const statsData = (statusData && statusData.providerStats) || {}
|
|
1774
|
+
const hostOnline = statusLatency !== null
|
|
1775
|
+
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
1776
|
+
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
1777
|
+
|
|
1778
|
+
return React.createElement('div', { className: 'cb-page' },
|
|
1779
|
+
// 1. Connection & Engine Status Card
|
|
1780
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
1781
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
1782
|
+
React.createElement('span', null, t('statusTitle')),
|
|
1783
|
+
React.createElement('button', {
|
|
1784
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
1785
|
+
onClick: fetchStatus,
|
|
1786
|
+
}, '🔄 ' + t('refreshStats')),
|
|
1787
|
+
),
|
|
1788
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('statusDesc')),
|
|
1789
|
+
React.createElement('div', { className: 'cb-row' },
|
|
1790
|
+
hostOnline
|
|
1791
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '● ' + t('badgeHostOnline').replace('{ms}', statusLatency))
|
|
1792
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-bad' }, '● ' + t('badgeHostOffline')),
|
|
1793
|
+
whisperRunning
|
|
1794
|
+
? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '● ' + t('badgeWhisperActive'))
|
|
1795
|
+
: React.createElement('span', { className: 'cb-badge cb-badge-warn' }, '○ ' + t('badgeWhisperInactive')),
|
|
1796
|
+
React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '✓ ' + t('badgeProviders').replace('{count}', providerCount)),
|
|
1797
|
+
),
|
|
1798
|
+
),
|
|
1704
1799
|
|
|
1705
|
-
|
|
1706
|
-
React.createElement('div', { className: '
|
|
1707
|
-
React.createElement('div', { className: '
|
|
1708
|
-
|
|
1800
|
+
// 2. Dictation Mode Card
|
|
1801
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
1802
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
1803
|
+
React.createElement('span', null, '🎙️ ' + t('dictationBtn')),
|
|
1804
|
+
),
|
|
1805
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('dictationHint')),
|
|
1709
1806
|
React.createElement(ChainEditor, {
|
|
1710
1807
|
value: draft && draft.dictation ? draft.dictation.chain : [], writable: writable,
|
|
1711
1808
|
options: chainOptions,
|
|
1712
1809
|
onChange: (v) => setIn('dictation', 'chain', v),
|
|
1713
1810
|
}),
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1811
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
1812
|
+
langField('dictation'),
|
|
1813
|
+
numField('dictation', 'vadSilenceMs', t('pauseMs'), t('pauseHint')),
|
|
1814
|
+
numField('dictation', 'sendDelayMs', t('sendDelay'), t('sendDelayHint')),
|
|
1815
|
+
numField('dictation', 'streamChunkMs', t('streamChunkMs'), t('streamHint')),
|
|
1816
|
+
),
|
|
1817
|
+
React.createElement('div', { className: 'cb-row' },
|
|
1818
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1819
|
+
React.createElement('input', {
|
|
1820
|
+
type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.polish), disabled: !writable,
|
|
1821
|
+
onChange: (e) => setIn('dictation', 'polish', e.target.checked),
|
|
1822
|
+
}),
|
|
1823
|
+
t('polish'),
|
|
1824
|
+
),
|
|
1825
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1826
|
+
React.createElement('input', {
|
|
1827
|
+
type: 'checkbox', checked: !!(draft && draft.dictation && draft.dictation.stream), disabled: !writable,
|
|
1828
|
+
onChange: (e) => setIn('dictation', 'stream', e.target.checked),
|
|
1829
|
+
}),
|
|
1830
|
+
t('stream'),
|
|
1831
|
+
),
|
|
1832
|
+
),
|
|
1833
|
+
React.createElement('div', { className: 'cb-field' },
|
|
1834
|
+
React.createElement('label', null, t('vadAdapt')),
|
|
1735
1835
|
React.createElement('input', {
|
|
1736
1836
|
type: 'range', min: 0, max: 1, step: 0.1,
|
|
1737
1837
|
value: Number(draft && draft.dictation && draft.dictation.vadAdapt) || 0, disabled: !writable,
|
|
1738
1838
|
onChange: (e) => setIn('dictation', 'vadAdapt', Number(e.target.value)),
|
|
1739
|
-
})
|
|
1839
|
+
}),
|
|
1840
|
+
React.createElement('span', { className: 'dvs-sub' }, t('vadAdaptHint')),
|
|
1841
|
+
),
|
|
1842
|
+
textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
|
|
1740
1843
|
),
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1844
|
+
|
|
1845
|
+
// 3. Voice Message Card
|
|
1846
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
1847
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
1848
|
+
React.createElement('span', null, '💬 ' + t('messageTitle')),
|
|
1849
|
+
),
|
|
1850
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('messageHint')),
|
|
1744
1851
|
React.createElement(ChainEditor, {
|
|
1745
1852
|
value: draft && draft.message ? draft.message.chain : [], writable: writable,
|
|
1746
1853
|
options: chainOptions,
|
|
1747
1854
|
onChange: (v) => setIn('message', 'chain', v),
|
|
1748
1855
|
}),
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1856
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
1857
|
+
langField('message'),
|
|
1858
|
+
numField('message', 'autoSendMs', t('undoMs'), t('undoHint')),
|
|
1859
|
+
),
|
|
1860
|
+
React.createElement('div', { className: 'cb-row' },
|
|
1861
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1862
|
+
React.createElement('input', {
|
|
1863
|
+
type: 'checkbox', checked: !!(draft && draft.message && draft.message.polish), disabled: !writable,
|
|
1864
|
+
onChange: (e) => setIn('message', 'polish', e.target.checked),
|
|
1865
|
+
}),
|
|
1866
|
+
t('polish'),
|
|
1867
|
+
),
|
|
1868
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1869
|
+
React.createElement('input', {
|
|
1870
|
+
type: 'checkbox', checked: !!(draft && draft.message && draft.message.polishSend), disabled: !writable,
|
|
1871
|
+
onChange: (e) => setIn('message', 'polishSend', e.target.checked),
|
|
1872
|
+
}),
|
|
1873
|
+
t('polishSend'),
|
|
1874
|
+
),
|
|
1875
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1876
|
+
React.createElement('input', {
|
|
1877
|
+
type: 'checkbox', checked: !!(draft && draft.message && draft.message.sessionCommands), disabled: !writable,
|
|
1878
|
+
onChange: (e) => setIn('message', 'sessionCommands', e.target.checked),
|
|
1879
|
+
}),
|
|
1880
|
+
t('sessionCommands'),
|
|
1881
|
+
),
|
|
1882
|
+
),
|
|
1766
1883
|
),
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1884
|
+
|
|
1885
|
+
// 4. Custom Providers Card
|
|
1886
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
1887
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
1888
|
+
React.createElement('span', null, '⚙️ ' + t('customTitle')),
|
|
1889
|
+
),
|
|
1890
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('customHint')),
|
|
1771
1891
|
React.createElement(CustomEditor, {
|
|
1772
1892
|
value: draft && draft.customProviders ? draft.customProviders : [], writable: writable,
|
|
1893
|
+
options: chainOptions,
|
|
1773
1894
|
onChange: (v) => setTop('customProviders', v),
|
|
1774
1895
|
}),
|
|
1775
1896
|
),
|
|
1776
|
-
|
|
1777
|
-
|
|
1897
|
+
|
|
1898
|
+
// 5. Hardware & Local Engines Card
|
|
1899
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
1900
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
1901
|
+
React.createElement('span', null, '🔧 ' + t('hardwareTitle')),
|
|
1902
|
+
),
|
|
1903
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('hardwareDesc')),
|
|
1778
1904
|
hotkeyField(),
|
|
1779
|
-
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
1780
|
-
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
1781
|
-
textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
|
|
1782
|
-
textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
|
|
1783
|
-
React.createElement('label', { className: 'dvs-field' }, t('whisperAutostart'),
|
|
1784
|
-
React.createElement('input', {
|
|
1785
|
-
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
1786
|
-
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
1787
|
-
})),
|
|
1788
|
-
React.createElement('label', { className: 'dvs-field', title: t('normalizeTranscriptHint') },
|
|
1789
|
-
t('normalizeTranscript'),
|
|
1790
|
-
React.createElement('input', {
|
|
1791
|
-
type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
|
|
1792
|
-
onChange: (e) => setTop('normalizeTranscript', e.target.checked),
|
|
1793
|
-
})),
|
|
1794
|
-
React.createElement('label', { className: 'dvs-field' }, t('beep'),
|
|
1795
|
-
React.createElement('input', {
|
|
1796
|
-
type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
|
|
1797
|
-
onChange: (e) => setTop('beep', e.target.checked),
|
|
1798
|
-
})),
|
|
1799
|
-
React.createElement('label', { className: 'dvs-field', title: t('localOnlyHint') }, t('localOnly'),
|
|
1800
|
-
React.createElement('input', {
|
|
1801
|
-
type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
|
|
1802
|
-
onChange: (e) => setTop('localOnly', e.target.checked),
|
|
1803
|
-
})),
|
|
1804
|
-
React.createElement('label', { className: 'dvs-field' }, t('voiceCommandsLabel'),
|
|
1805
|
-
React.createElement('input', {
|
|
1806
|
-
type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
|
|
1807
|
-
onChange: (e) => setTop('voiceCommands', e.target.checked),
|
|
1808
|
-
})),
|
|
1809
1905
|
micField(),
|
|
1810
|
-
React.createElement('
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1906
|
+
React.createElement('div', { className: 'cb-grid-2' },
|
|
1907
|
+
textField('whisperUrl', t('whisperEndpoint'), t('whisperEndpointHint')),
|
|
1908
|
+
textField('whisperBin', t('whisperBin'), t('whisperBinHint')),
|
|
1909
|
+
textField('whisperModel', t('whisperModel'), t('whisperModelHint')),
|
|
1910
|
+
textField('deepgramBaseUrl', t('deepgramEndpoint'), t('deepgramEndpointHint')),
|
|
1911
|
+
textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
|
|
1912
|
+
textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
|
|
1913
|
+
textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
|
|
1914
|
+
textField('polishBaseUrl', t('polishBaseUrl'), t('polishBaseUrlHint')),
|
|
1915
|
+
textField('polishModel', t('polishModel'), ''),
|
|
1916
|
+
textField('polishKeyEnv', t('polishKeyEnv'), ''),
|
|
1917
|
+
),
|
|
1918
|
+
React.createElement('div', { className: 'cb-row' },
|
|
1919
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1920
|
+
React.createElement('input', {
|
|
1921
|
+
type: 'checkbox', checked: !!(draft && draft.autoStart), disabled: !writable,
|
|
1922
|
+
onChange: (e) => setTop('autoStart', e.target.checked),
|
|
1923
|
+
}),
|
|
1924
|
+
t('whisperAutostart'),
|
|
1925
|
+
),
|
|
1926
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1927
|
+
React.createElement('input', {
|
|
1928
|
+
type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
|
|
1929
|
+
onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
|
|
1930
|
+
}),
|
|
1931
|
+
t('sensevoiceAutostart'),
|
|
1932
|
+
),
|
|
1933
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1934
|
+
React.createElement('input', {
|
|
1935
|
+
type: 'checkbox', checked: !!(draft && draft.localOnly), disabled: !writable,
|
|
1936
|
+
onChange: (e) => setTop('localOnly', e.target.checked),
|
|
1937
|
+
}),
|
|
1938
|
+
t('localOnly'),
|
|
1939
|
+
),
|
|
1940
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1941
|
+
React.createElement('input', {
|
|
1942
|
+
type: 'checkbox', checked: !!(draft && draft.beep), disabled: !writable,
|
|
1943
|
+
onChange: (e) => setTop('beep', e.target.checked),
|
|
1944
|
+
}),
|
|
1945
|
+
t('beep'),
|
|
1946
|
+
),
|
|
1947
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1948
|
+
React.createElement('input', {
|
|
1949
|
+
type: 'checkbox', checked: draft ? draft.noiseSuppression !== false : true, disabled: !writable,
|
|
1950
|
+
onChange: (e) => setTop('noiseSuppression', e.target.checked),
|
|
1951
|
+
}),
|
|
1952
|
+
t('noiseSuppression'),
|
|
1953
|
+
),
|
|
1954
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1955
|
+
React.createElement('input', {
|
|
1956
|
+
type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
|
|
1957
|
+
onChange: (e) => setTop('contextGlossary', e.target.checked),
|
|
1958
|
+
}),
|
|
1959
|
+
t('contextGlossary'),
|
|
1960
|
+
),
|
|
1961
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1962
|
+
React.createElement('input', {
|
|
1963
|
+
type: 'checkbox', checked: !!(draft && draft.voiceCommands), disabled: !writable,
|
|
1964
|
+
onChange: (e) => setTop('voiceCommands', e.target.checked),
|
|
1965
|
+
}),
|
|
1966
|
+
t('voiceCommandsLabel'),
|
|
1967
|
+
),
|
|
1968
|
+
React.createElement('label', { className: 'cb-check-label' },
|
|
1969
|
+
React.createElement('input', {
|
|
1970
|
+
type: 'checkbox', checked: !!(draft && draft.normalizeTranscript), disabled: !writable,
|
|
1971
|
+
onChange: (e) => setTop('normalizeTranscript', e.target.checked),
|
|
1972
|
+
}),
|
|
1973
|
+
t('normalizeTranscript'),
|
|
1974
|
+
),
|
|
1975
|
+
),
|
|
1976
|
+
React.createElement('div', { className: 'cb-field' },
|
|
1977
|
+
React.createElement('label', null, t('visualizerStyle')),
|
|
1821
1978
|
React.createElement('select', {
|
|
1822
1979
|
value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
|
|
1823
1980
|
onChange: (e) => setTop('visualizerStyle', e.target.value),
|
|
@@ -1826,43 +1983,27 @@ window.__ModuleLoader__.load({
|
|
|
1826
1983
|
React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
|
|
1827
1984
|
React.createElement('option', { value: 'bars' }, t('visBars')),
|
|
1828
1985
|
React.createElement('option', { value: 'off' }, t('visOff')),
|
|
1829
|
-
)
|
|
1830
|
-
|
|
1986
|
+
),
|
|
1987
|
+
),
|
|
1988
|
+
React.createElement('div', { className: 'cb-field' },
|
|
1989
|
+
React.createElement('label', null, t('vocabulary')),
|
|
1831
1990
|
React.createElement('textarea', {
|
|
1832
1991
|
rows: 3, disabled: !writable,
|
|
1833
1992
|
value: Array.isArray(draft && draft.vocabulary) ? draft.vocabulary.join('\n') : '',
|
|
1834
1993
|
onChange: (e) => setTop('vocabulary', e.target.value.split('\n').map((x) => x.trim()).filter(Boolean)),
|
|
1835
|
-
})
|
|
1836
|
-
|
|
1837
|
-
React.createElement('input', {
|
|
1838
|
-
type: 'text', value: String((snap && snap.polishBaseUrl) || ''), disabled: !writable,
|
|
1839
|
-
onChange: (e) => setTop('polishBaseUrl', e.target.value),
|
|
1840
|
-
})),
|
|
1841
|
-
React.createElement('label', { className: 'dvs-field' }, t('polishModel'),
|
|
1842
|
-
React.createElement('input', {
|
|
1843
|
-
type: 'text', value: String((draft && draft.polishModel) || ''), disabled: !writable,
|
|
1844
|
-
onChange: (e) => setTop('polishModel', e.target.value),
|
|
1845
|
-
})),
|
|
1846
|
-
React.createElement('label', { className: 'dvs-field' }, t('polishKeyEnv'),
|
|
1847
|
-
React.createElement('input', {
|
|
1848
|
-
type: 'text', value: String((draft && draft.polishKeyEnv) || ''), disabled: !writable,
|
|
1849
|
-
onChange: (e) => setTop('polishKeyEnv', e.target.value),
|
|
1850
|
-
})),
|
|
1851
|
-
),
|
|
1852
|
-
React.createElement('div', { className: 'dvs-block' },
|
|
1853
|
-
React.createElement('div', { className: 'dvs-h' }, 'SenseVoice-ONNX / Sherpa-ONNX'),
|
|
1854
|
-
React.createElement('div', { className: 'dvs-sub' }, t('sensevoiceHint')),
|
|
1855
|
-
textField('sensevoiceUrl', t('sensevoiceEndpoint'), t('sensevoiceEndpointHint')),
|
|
1856
|
-
textField('sensevoiceBin', t('sensevoiceBin'), t('sensevoiceBinHint')),
|
|
1857
|
-
textField('sensevoiceModel', t('sensevoiceModel'), t('sensevoiceModelHint')),
|
|
1858
|
-
React.createElement('label', { className: 'dvs-field' }, t('sensevoiceAutostart'),
|
|
1859
|
-
React.createElement('input', {
|
|
1860
|
-
type: 'checkbox', checked: !!(draft && draft.sensevoiceAutostart), disabled: !writable,
|
|
1861
|
-
onChange: (e) => setTop('sensevoiceAutostart', e.target.checked),
|
|
1862
|
-
})),
|
|
1994
|
+
}),
|
|
1995
|
+
),
|
|
1863
1996
|
),
|
|
1864
|
-
|
|
1865
|
-
|
|
1997
|
+
|
|
1998
|
+
// 6. Provider Telemetry & Latency Dashboard
|
|
1999
|
+
React.createElement('div', { className: 'cb-section-card' },
|
|
2000
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
2001
|
+
React.createElement('span', null, '📊 ' + t('providerDashboard')),
|
|
2002
|
+
React.createElement('button', {
|
|
2003
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
2004
|
+
onClick: fetchStatus,
|
|
2005
|
+
}, '🔄 ' + t('refreshStats')),
|
|
2006
|
+
),
|
|
1866
2007
|
React.createElement('div', { className: 'dvo-dash' },
|
|
1867
2008
|
React.createElement('div', { className: 'dvo-dash-grid' },
|
|
1868
2009
|
Object.keys(statsData).length === 0
|
|
@@ -1899,53 +2040,51 @@ window.__ModuleLoader__.load({
|
|
|
1899
2040
|
React.createElement('span', null, t('successRate')),
|
|
1900
2041
|
),
|
|
1901
2042
|
)
|
|
1902
|
-
})
|
|
1903
|
-
)
|
|
1904
|
-
)
|
|
2043
|
+
}),
|
|
2044
|
+
),
|
|
2045
|
+
),
|
|
1905
2046
|
),
|
|
1906
|
-
|
|
1907
|
-
|
|
2047
|
+
|
|
2048
|
+
// Action bar (Save)
|
|
2049
|
+
React.createElement('div', { className: 'cb-row' },
|
|
2050
|
+
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
|
|
1908
2051
|
saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
|
|
1909
2052
|
err ? React.createElement('span', { className: 'dvs-bad' }, err) : null,
|
|
1910
2053
|
),
|
|
1911
2054
|
)
|
|
1912
2055
|
}
|
|
1913
2056
|
|
|
1914
|
-
// Card in Settings → Plugins → Plugin settings (#18)
|
|
1915
|
-
// draws the list shell, so title, hint and collapse are ours.
|
|
1916
|
-
// Body mounts on first expand; the settings snapshot arrives then.
|
|
2057
|
+
// Card in Settings → Plugins → Plugin settings (#18)
|
|
1917
2058
|
function PluginCard(props) {
|
|
1918
2059
|
const [open, setOpen] = React.useState(false)
|
|
1919
2060
|
const tt = (props && props.t) || t
|
|
1920
2061
|
const title = tt('title')
|
|
1921
|
-
return React.createElement(
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
React.createElement('span', { className: 'dvo-
|
|
1932
|
-
|
|
2062
|
+
return React.createElement(ErrorBoundary, null,
|
|
2063
|
+
React.createElement('li',
|
|
2064
|
+
{ className: open ? 'dvo-pcard dvo-pcardOpen' : 'dvo-pcard' },
|
|
2065
|
+
React.createElement('button',
|
|
2066
|
+
{
|
|
2067
|
+
type: 'button', className: 'dvo-phead',
|
|
2068
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
2069
|
+
'aria-label': (open ? tt('collapse') : tt('expand')) + ': ' + title,
|
|
2070
|
+
onClick: () => setOpen((v) => !v),
|
|
2071
|
+
},
|
|
2072
|
+
React.createElement('span', { className: 'dvo-pheadtext' },
|
|
2073
|
+
React.createElement('span', { className: 'dvo-ptitle' }, title),
|
|
2074
|
+
React.createElement('span', { className: 'dvo-pdesc' }, tt('cardHint')),
|
|
2075
|
+
),
|
|
2076
|
+
React.createElement('span', { className: 'dvo-pchev' }, chevronIcon()),
|
|
1933
2077
|
),
|
|
1934
|
-
React.createElement('
|
|
2078
|
+
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
1935
2079
|
),
|
|
1936
|
-
open ? React.createElement('div', { className: 'dvo-pbody' }, React.createElement(VoiceSection, props)) : null,
|
|
1937
2080
|
)
|
|
1938
2081
|
}
|
|
1939
2082
|
|
|
1940
2083
|
function registerSettings(ctx) {
|
|
1941
|
-
// Settings card under Settings → Plugins → Plugin settings (#18).
|
|
1942
|
-
// The registration key must equal NS (the settings namespace), otherwise
|
|
1943
|
-
// the tab silently skips the slot.
|
|
1944
2084
|
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
1945
2085
|
{
|
|
1946
2086
|
name: 'settings.plugin.item',
|
|
1947
2087
|
key: NS,
|
|
1948
|
-
// locale on the slot record is what makes the component receive props.t.
|
|
1949
2088
|
locale: NS,
|
|
1950
2089
|
inject: () => ({ ctx: ctx }),
|
|
1951
2090
|
},
|