@goodandready/dsh-tts 0.4.0 → 0.4.1
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/README.md +10 -0
- package/README.ru.md +10 -0
- package/README.zh.md +8 -0
- package/lib/client-src/00-open.js +85 -0
- package/lib/client-src/10-styles.js +54 -0
- package/lib/client-src/20-player.js +290 -0
- package/lib/client-src/30-locales.js +325 -0
- package/lib/client-src/40-editors.js +410 -0
- package/lib/client-src/50-settings.js +341 -0
- package/lib/client-src/60-card-dock.js +229 -0
- package/lib/client.js +691 -534
- package/lib/index.js +2 -0
- package/lib/routes.js +4 -1
- package/package.json +4 -2
- package/scripts/build-client.mjs +19 -0
package/lib/client.js
CHANGED
|
@@ -38,34 +38,96 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
f5: 'not bundled — offline neural runtime unavailable',
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
|
|
42
|
+
function createErrorBoundary() {
|
|
43
|
+
if (!React || typeof React.Component !== 'function') {
|
|
44
|
+
return function NoopBoundary(props) { return (props && props.children) || null }
|
|
45
|
+
}
|
|
46
|
+
return class ErrorBoundary extends React.Component {
|
|
47
|
+
constructor(props) {
|
|
48
|
+
super(props)
|
|
49
|
+
this.state = { hasError: false, error: null }
|
|
50
|
+
}
|
|
51
|
+
static getDerivedStateFromError(error) {
|
|
52
|
+
return { hasError: true, error }
|
|
53
|
+
}
|
|
54
|
+
componentDidCatch(error, errorInfo) {
|
|
55
|
+
console.error('[dsh-tts] React Error:', error, errorInfo)
|
|
56
|
+
}
|
|
57
|
+
render() {
|
|
58
|
+
if (this.state.hasError) {
|
|
59
|
+
return React.createElement(
|
|
60
|
+
'div',
|
|
61
|
+
{
|
|
62
|
+
className: 'dts-alert dts-alert-err',
|
|
63
|
+
style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
|
|
64
|
+
},
|
|
65
|
+
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ TTS UI Error:'),
|
|
66
|
+
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String((this.state.error && this.state.error.message) || this.state.error)),
|
|
67
|
+
React.createElement(
|
|
68
|
+
'button',
|
|
69
|
+
{
|
|
70
|
+
type: 'button',
|
|
71
|
+
className: 'dts-btn',
|
|
72
|
+
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
73
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
74
|
+
},
|
|
75
|
+
'Retry',
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return (this.props && this.props.children) || null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const ErrorBoundary = createErrorBoundary()
|
|
84
|
+
const SNAPSHOT_READY = Object.freeze({ status: 'ready', value: {} })
|
|
85
|
+
const SNAPSHOT_LOADING = Object.freeze({ status: 'loading', value: {} })
|
|
86
|
+
|
|
41
87
|
const SET_CSS =
|
|
42
|
-
'.dts-wrap{display:flex;flex-direction:column;gap:
|
|
43
|
-
'.dts-
|
|
44
|
-
'.dts-
|
|
45
|
-
|
|
88
|
+
'.dts-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:900px}' +
|
|
89
|
+
'.dts-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
|
|
90
|
+
'.dts-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px}' +
|
|
91
|
+
'.dts-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}' +
|
|
92
|
+
'.dts-block{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}' +
|
|
93
|
+
'.dts-h{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
94
|
+
'.dts-sub{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
|
|
46
95
|
'.dts-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
47
96
|
'.dts-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
48
97
|
'.dts-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
49
98
|
'.dts-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
50
|
-
'.dts-sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
51
99
|
'.dts-chev{margin-left:auto;color:var(--dsw-alias-label-secondary);flex:none;transition:transform .16s}' +
|
|
52
100
|
'.dts-chevOpen{transform:rotate(180deg)}' +
|
|
53
|
-
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding
|
|
54
|
-
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:
|
|
55
|
-
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-
|
|
56
|
-
'.dts-
|
|
57
|
-
'.dts-
|
|
58
|
-
|
|
59
|
-
'.dts-
|
|
101
|
+
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0}' +
|
|
102
|
+
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
103
|
+
'.dts-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}' +
|
|
104
|
+
'.dts-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
105
|
+
'.dts-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:10px;padding:14px 0 4px}' +
|
|
106
|
+
'.dts-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:6px 16px;font-size:13px;font-weight:500;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);transition:all .15s ease}' +
|
|
107
|
+
'.dts-save:hover:not(:disabled){opacity:0.88}' +
|
|
108
|
+
'.dts-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}' +
|
|
109
|
+
'.dts-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))}' +
|
|
110
|
+
'.dts-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}' +
|
|
111
|
+
'.dts-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.5)}' +
|
|
112
|
+
'.dts-entry{display:flex;flex-direction:column;gap:8px;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2)}' +
|
|
60
113
|
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
61
114
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
62
115
|
'.dts-row .dts-key{flex:1;min-width:180px}' +
|
|
63
|
-
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:
|
|
64
|
-
'.dts-
|
|
65
|
-
'.dts-
|
|
66
|
-
'.dts-
|
|
67
|
-
'.dts-badge-
|
|
68
|
-
'.dts-
|
|
116
|
+
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);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;font-size:13px;transition:all .15s ease}' +
|
|
117
|
+
'.dts-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2))}' +
|
|
118
|
+
'.dts-ok{font-size:12px;color:var(--dsw-alias-state-success-primary);font-weight:500}' +
|
|
119
|
+
'.dts-bad{font-size:12px;color:var(--dsw-alias-state-error-primary);font-weight:500}' +
|
|
120
|
+
'.dts-badge{font-size:11px;padding:3px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap;display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
121
|
+
'.dts-badge-on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
|
|
122
|
+
'.dts-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
|
|
123
|
+
'.dts-badge-bad{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
|
|
124
|
+
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0;text-decoration:underline}' +
|
|
125
|
+
'.dts-link:hover:not(:disabled){color:var(--dsw-alias-label-primary)}' +
|
|
126
|
+
'.dts-alert{padding:10px 14px;border-radius:8px;font-size:13px}' +
|
|
127
|
+
'.dts-alert-err{background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);border:1px solid rgba(239,68,68,0.2)}' +
|
|
128
|
+
'.dts-alert-warn{background:rgba(245,158,11,0.1);color:var(--dsw-alias-state-warning-primary);border:1px solid rgba(245,158,11,0.2)}' +
|
|
129
|
+
'.dts-alert-ok{background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);border:1px solid rgba(16,185,129,0.2)}' +
|
|
130
|
+
'.dts-grow{flex:1;min-width:100px}'
|
|
69
131
|
|
|
70
132
|
const setCssId = 'dsh-tts/settings.module.css'
|
|
71
133
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-tts"]')) {
|
|
@@ -75,7 +137,6 @@ window.__ModuleLoader__.load({
|
|
|
75
137
|
tag.dataset.pluginCss = setCssId
|
|
76
138
|
document.head.appendChild(tag)
|
|
77
139
|
}
|
|
78
|
-
|
|
79
140
|
// Player.
|
|
80
141
|
//
|
|
81
142
|
// Each incoming chunk used to replace the previous one immediately; while reading
|
|
@@ -366,77 +427,11 @@ window.__ModuleLoader__.load({
|
|
|
366
427
|
return (map && map[provider]) || { configured: false, writable: true, ref: '' }
|
|
367
428
|
}
|
|
368
429
|
|
|
369
|
-
function ChainEditor(props) {
|
|
370
|
-
// Translator comes from the card: the slot passes props.t only to it.
|
|
371
|
-
const t = props.t || ((key) => key)
|
|
372
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
373
|
-
const change = (i, patch) => {
|
|
374
|
-
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
375
|
-
props.onChange(next)
|
|
376
|
-
}
|
|
377
|
-
const move = (i, delta) => {
|
|
378
|
-
const j = i + delta
|
|
379
|
-
if (j < 0 || j >= rows.length) return
|
|
380
|
-
const next = rows.slice()
|
|
381
|
-
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
382
|
-
props.onChange(next)
|
|
383
|
-
}
|
|
384
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
385
|
-
const add = () => props.onChange(rows.concat([{ provider: 'espeak', model: '', voice: '' }]))
|
|
386
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
387
|
-
rows.map((row, i) => {
|
|
388
|
-
const cloud = !!CLOUD[row.provider]
|
|
389
|
-
const cred = credOf(props.credentials, row.provider)
|
|
390
|
-
const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
|
|
391
|
-
const badge = !cloud ? null
|
|
392
|
-
: (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
|
|
393
|
-
return React.createElement('div', { className: 'dts-entry', key: i },
|
|
394
|
-
React.createElement('div', { className: 'dts-row' },
|
|
395
|
-
React.createElement('select', {
|
|
396
|
-
className: 'dts-input',
|
|
397
|
-
value: row.provider, disabled: !props.writable,
|
|
398
|
-
onChange: (e) => change(i, { provider: e.target.value }),
|
|
399
|
-
}, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
400
|
-
React.createElement('input', {
|
|
401
|
-
className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
|
|
402
|
-
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
403
|
-
}),
|
|
404
|
-
React.createElement('input', {
|
|
405
|
-
className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
|
|
406
|
-
placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
|
|
407
|
-
}),
|
|
408
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
|
|
409
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
|
|
410
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
|
|
411
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
412
|
-
),
|
|
413
|
-
cloud ? React.createElement('div', { className: 'dts-row' },
|
|
414
|
-
React.createElement('input', {
|
|
415
|
-
className: 'dts-key dts-input', type: 'password', autoComplete: 'off',
|
|
416
|
-
value: draft, disabled: !props.writable || !cred.writable,
|
|
417
|
-
placeholder: cred.configured ? 'leave blank to keep' : 'paste API key',
|
|
418
|
-
onChange: (e) => props.onDraft(row.provider, e.target.value),
|
|
419
|
-
onBlur: (e) => props.onCommitKey(row.provider, e.target.value),
|
|
420
|
-
}),
|
|
421
|
-
React.createElement('span', { className: 'dts-badge' + (cred.configured ? ' dts-badge-on' : '') }, badge),
|
|
422
|
-
cred.configured && cred.writable ? React.createElement('button', {
|
|
423
|
-
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
424
|
-
onClick: () => props.onClearKey(row.provider),
|
|
425
|
-
}, t('clear')) : null,
|
|
426
|
-
) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
|
|
427
|
-
cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
|
|
428
|
-
)
|
|
429
|
-
}),
|
|
430
|
-
React.createElement('div', { className: 'dts-row' },
|
|
431
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
432
|
-
React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
|
|
433
|
-
),
|
|
434
|
-
)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Card strings live in the locale registry so a separate package can translate
|
|
438
|
-
// them without touching this plugin. English is the source language and fallback.
|
|
439
430
|
const en = {
|
|
431
|
+
'statusOnline': 'Host online',
|
|
432
|
+
'statusOffline': 'Host unreachable',
|
|
433
|
+
'cacheStats': 'Cache',
|
|
434
|
+
|
|
440
435
|
'general': 'General',
|
|
441
436
|
'loading': 'Loading\u2026',
|
|
442
437
|
'chainHintCard': 'Pick a provider, paste its API key, and Save (or leave the key field). The key is written to the host credentials store immediately. The browser never reads it back.',
|
|
@@ -470,6 +465,24 @@ window.__ModuleLoader__.load({
|
|
|
470
465
|
'edgeHint': 'Python CLI from the edge-tts package.',
|
|
471
466
|
'azureRegion': 'Azure region',
|
|
472
467
|
'azureHint': 'Required for the azure provider, e.g. eastus.',
|
|
468
|
+
'openaiBaseUrl': 'OpenAI base URL',
|
|
469
|
+
'openaiBaseUrlHint': 'OpenAI-compatible origin for the openai provider.',
|
|
470
|
+
'mimoBaseUrl': 'MiMo base URL',
|
|
471
|
+
'mimoBaseUrlHint': 'MiMo API root; synthesis uses its chat endpoint.',
|
|
472
|
+
'mimoFormat': 'MiMo format',
|
|
473
|
+
'mimoFormatHint': 'mp3 or wav.',
|
|
474
|
+
'minimaxBin': 'MiniMax CLI',
|
|
475
|
+
'minimaxBinHint': 'Looked up in PATH unless absolute; install and log in separately.',
|
|
476
|
+
'advancedTitle': 'Advanced',
|
|
477
|
+
'advancedHint': 'Limits and endpoint overrides. Provider API keys stay in the chain editor; credential slot names (*KeyEnv) are config-only.',
|
|
478
|
+
'maxChars': 'Max characters',
|
|
479
|
+
'maxCharsHint': 'Hard cap before long-reply handling; 0 disables.',
|
|
480
|
+
'sentenceChars': 'Sentence chunk size',
|
|
481
|
+
'sentenceCharsHint': 'Max characters per streaming sentence piece.',
|
|
482
|
+
'timeoutMs': 'Provider timeout (ms)',
|
|
483
|
+
'timeoutMsHint': 'Abort a provider attempt after this many milliseconds.',
|
|
484
|
+
'maxQueue': 'Max queue length',
|
|
485
|
+
'maxQueueHint': 'Drop oldest pending pieces beyond this count.',
|
|
473
486
|
'chainTitle': 'Provider chain',
|
|
474
487
|
'addProvider': 'Add provider',
|
|
475
488
|
'localProvider': 'Local / CLI provider — no API key',
|
|
@@ -577,6 +590,10 @@ window.__ModuleLoader__.load({
|
|
|
577
590
|
'dockStop': 'Stop speech',
|
|
578
591
|
}
|
|
579
592
|
const ru = {
|
|
593
|
+
'statusOnline': 'Хост на связи',
|
|
594
|
+
'statusOffline': 'Хост недоступен',
|
|
595
|
+
'cacheStats': 'Кэш',
|
|
596
|
+
|
|
580
597
|
'dockLabel': 'Чтение',
|
|
581
598
|
'dockPause': 'Пауза',
|
|
582
599
|
'dockResume': 'Продолжить чтение',
|
|
@@ -615,6 +632,24 @@ window.__ModuleLoader__.load({
|
|
|
615
632
|
'edgeHint': 'Консольная утилита из пакета edge-tts.',
|
|
616
633
|
'azureRegion': 'Azure: регион',
|
|
617
634
|
'azureHint': 'Нужен провайдеру azure, например eastus.',
|
|
635
|
+
'openaiBaseUrl': 'OpenAI base URL',
|
|
636
|
+
'openaiBaseUrlHint': 'OpenAI-совместимый origin для провайдера openai.',
|
|
637
|
+
'mimoBaseUrl': 'MiMo base URL',
|
|
638
|
+
'mimoBaseUrlHint': 'Корень API MiMo; синтез идёт через chat endpoint.',
|
|
639
|
+
'mimoFormat': 'Формат MiMo',
|
|
640
|
+
'mimoFormatHint': 'mp3 или wav.',
|
|
641
|
+
'minimaxBin': 'MiniMax CLI',
|
|
642
|
+
'minimaxBinHint': 'Ищется в PATH, если путь не абсолютный; вход в CLI — отдельно.',
|
|
643
|
+
'advancedTitle': 'Дополнительно',
|
|
644
|
+
'advancedHint': 'Лимиты и override endpoint. API-ключи — в редакторе цепочки; имена credential-слотов (*KeyEnv) — только в конфиге.',
|
|
645
|
+
'maxChars': 'Максимум символов',
|
|
646
|
+
'maxCharsHint': 'Жёсткий лимит до обработки длинных ответов; 0 — без лимита.',
|
|
647
|
+
'sentenceChars': 'Размер куска (символы)',
|
|
648
|
+
'sentenceCharsHint': 'Максимум символов в streaming-куске предложения.',
|
|
649
|
+
'timeoutMs': 'Таймаут провайдера (мс)',
|
|
650
|
+
'timeoutMsHint': 'Прервать попытку провайдера через столько миллисекунд.',
|
|
651
|
+
'maxQueue': 'Длина очереди',
|
|
652
|
+
'maxQueueHint': 'Отбрасывать самые старые куски сверх этого числа.',
|
|
618
653
|
'chainTitle': 'Цепочка провайдеров',
|
|
619
654
|
'addProvider': 'Добавить провайдера',
|
|
620
655
|
'localProvider': 'Локальный провайдер — ключ не нужен',
|
|
@@ -717,6 +752,77 @@ window.__ModuleLoader__.load({
|
|
|
717
752
|
'messengerTtsEnabledHint': 'Разрешить мосту мессенджеров озвучивать ответы голосовыми заметками.',
|
|
718
753
|
}
|
|
719
754
|
|
|
755
|
+
function ChainEditor(props) {
|
|
756
|
+
// Translator comes from the card: the slot passes props.t only to it.
|
|
757
|
+
const t = props.t || ((key) => key)
|
|
758
|
+
const rows = Array.isArray(props.value) ? props.value : []
|
|
759
|
+
const change = (i, patch) => {
|
|
760
|
+
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
761
|
+
props.onChange(next)
|
|
762
|
+
}
|
|
763
|
+
const move = (i, delta) => {
|
|
764
|
+
const j = i + delta
|
|
765
|
+
if (j < 0 || j >= rows.length) return
|
|
766
|
+
const next = rows.slice()
|
|
767
|
+
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
768
|
+
props.onChange(next)
|
|
769
|
+
}
|
|
770
|
+
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
771
|
+
const add = () => props.onChange(rows.concat([{ provider: 'espeak', model: '', voice: '' }]))
|
|
772
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
773
|
+
rows.map((row, i) => {
|
|
774
|
+
const cloud = !!CLOUD[row.provider]
|
|
775
|
+
const cred = credOf(props.credentials, row.provider)
|
|
776
|
+
const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
|
|
777
|
+
const badge = !cloud ? null
|
|
778
|
+
: (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
|
|
779
|
+
return React.createElement('div', { className: 'dts-entry', key: i },
|
|
780
|
+
React.createElement('div', { className: 'dts-row' },
|
|
781
|
+
React.createElement('select', {
|
|
782
|
+
className: 'dts-input',
|
|
783
|
+
value: row.provider, disabled: !props.writable,
|
|
784
|
+
onChange: (e) => change(i, { provider: e.target.value }),
|
|
785
|
+
}, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
786
|
+
React.createElement('input', {
|
|
787
|
+
className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
|
|
788
|
+
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
789
|
+
}),
|
|
790
|
+
React.createElement('input', {
|
|
791
|
+
className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
|
|
792
|
+
placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
|
|
793
|
+
}),
|
|
794
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
|
|
795
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
|
|
796
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
|
|
797
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
798
|
+
),
|
|
799
|
+
cloud ? React.createElement('div', { className: 'dts-row' },
|
|
800
|
+
React.createElement('input', {
|
|
801
|
+
className: 'dts-key dts-input', type: 'password', autoComplete: 'off',
|
|
802
|
+
value: draft, disabled: !props.writable || !cred.writable,
|
|
803
|
+
placeholder: cred.configured ? 'leave blank to keep' : 'paste API key',
|
|
804
|
+
onChange: (e) => props.onDraft(row.provider, e.target.value),
|
|
805
|
+
onBlur: (e) => props.onCommitKey(row.provider, e.target.value),
|
|
806
|
+
}),
|
|
807
|
+
React.createElement('span', { className: 'dts-badge' + (cred.configured ? ' dts-badge-on' : '') }, badge),
|
|
808
|
+
cred.configured && cred.writable ? React.createElement('button', {
|
|
809
|
+
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
810
|
+
onClick: () => props.onClearKey(row.provider),
|
|
811
|
+
}, t('clear')) : null,
|
|
812
|
+
) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
|
|
813
|
+
cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
|
|
814
|
+
)
|
|
815
|
+
}),
|
|
816
|
+
React.createElement('div', { className: 'dts-row' },
|
|
817
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
818
|
+
React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
|
|
819
|
+
),
|
|
820
|
+
)
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Card strings live in the locale registry so a separate package can translate
|
|
824
|
+
// them without touching this plugin. English is the source language and fallback.
|
|
825
|
+
|
|
720
826
|
function LocalEnginesEditor(props) {
|
|
721
827
|
const t = props.t || ((key) => key)
|
|
722
828
|
const [models, setModels] = React.useState({})
|
|
@@ -799,80 +905,366 @@ window.__ModuleLoader__.load({
|
|
|
799
905
|
)
|
|
800
906
|
}
|
|
801
907
|
|
|
802
|
-
function TtsSection(props) {
|
|
803
|
-
// Translator comes from the slot because its registration sets locale.
|
|
804
|
-
const t = (props && props.t) || ((key) => key)
|
|
805
|
-
const [draft, setDraft] = React.useState(null)
|
|
806
|
-
const [credentials, setCredentials] = React.useState({})
|
|
807
|
-
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
808
|
-
const [saved, setSaved] = React.useState(false)
|
|
809
|
-
const [err, setErr] = React.useState('')
|
|
810
|
-
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
811
|
-
const ctx = props && props.ctx
|
|
812
|
-
const scope = (ctx && ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope))
|
|
813
|
-
? ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).bind({ namespace: NS })
|
|
814
|
-
: null
|
|
815
|
-
const [snap, setSnap] = React.useState(null)
|
|
816
|
-
|
|
817
|
-
React.useEffect(() => {
|
|
818
|
-
if (!scope) return undefined
|
|
819
|
-
let alive = true
|
|
820
|
-
const render = () => { if (alive && scope.getSnapshot) setSnap(scope.getSnapshot()) }
|
|
821
|
-
render()
|
|
822
|
-
if (scope.subscribe) {
|
|
823
|
-
const off = scope.subscribe(render)
|
|
824
|
-
return () => { alive = false; if (typeof off === 'function') off() }
|
|
825
|
-
}
|
|
826
|
-
return () => { alive = false }
|
|
827
|
-
}, [scope])
|
|
828
908
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
909
|
+
const CLIENT_IT_TERMS = [
|
|
910
|
+
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
911
|
+
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
912
|
+
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
913
|
+
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
914
|
+
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
915
|
+
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
916
|
+
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
917
|
+
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
918
|
+
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
919
|
+
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
920
|
+
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
921
|
+
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
922
|
+
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
923
|
+
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
924
|
+
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
925
|
+
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
926
|
+
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
927
|
+
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
928
|
+
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
929
|
+
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
930
|
+
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
931
|
+
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
932
|
+
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
933
|
+
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
934
|
+
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
935
|
+
]
|
|
837
936
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
937
|
+
// Pronunciation dictionary editor: rules apply top-down.
|
|
938
|
+
function PronEditor(props) {
|
|
939
|
+
const t = props.t || ((key) => key)
|
|
940
|
+
const rows = Array.isArray(props.value) ? props.value : []
|
|
941
|
+
const change = (i, patch) => {
|
|
942
|
+
props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
943
|
+
}
|
|
944
|
+
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
945
|
+
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
946
|
+
const loadIt = () => {
|
|
947
|
+
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
948
|
+
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
949
|
+
props.onChange(rows.concat(toAdd))
|
|
843
950
|
}
|
|
844
951
|
|
|
845
|
-
React.
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
952
|
+
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
953
|
+
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
954
|
+
React.createElement('input', {
|
|
955
|
+
className: 'dts-input dts-grow', placeholder: t('pronFrom'),
|
|
956
|
+
value: rule.from || '', disabled: !props.writable,
|
|
957
|
+
onChange: (e) => change(i, { from: e.target.value }),
|
|
958
|
+
}),
|
|
959
|
+
React.createElement('input', {
|
|
960
|
+
className: 'dts-input dts-grow', placeholder: t('pronTo'),
|
|
961
|
+
value: rule.to || '', disabled: !props.writable,
|
|
962
|
+
onChange: (e) => change(i, { to: e.target.value }),
|
|
963
|
+
}),
|
|
964
|
+
React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
|
|
965
|
+
React.createElement('input', {
|
|
966
|
+
type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
|
|
967
|
+
onChange: (e) => change(i, { whole: e.target.checked }),
|
|
968
|
+
}),
|
|
969
|
+
),
|
|
970
|
+
React.createElement('input', {
|
|
971
|
+
className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
|
|
972
|
+
value: rule.lang || '', disabled: !props.writable,
|
|
973
|
+
onChange: (e) => change(i, { lang: e.target.value }),
|
|
974
|
+
}),
|
|
975
|
+
React.createElement('button', {
|
|
976
|
+
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
977
|
+
onClick: () => {
|
|
978
|
+
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
979
|
+
},
|
|
980
|
+
}, '\u25b6'),
|
|
981
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
982
|
+
)),
|
|
983
|
+
React.createElement('div', { className: 'dts-row' },
|
|
984
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
985
|
+
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
986
|
+
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
987
|
+
),
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function RolesEditor(props) {
|
|
992
|
+
const t = props.t || ((key) => key)
|
|
993
|
+
const value = props.value || {}
|
|
994
|
+
const [newRole, setNewRole] = React.useState('')
|
|
995
|
+
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
996
|
+
const removeRole = (r) => {
|
|
997
|
+
const next = Object.assign({}, value)
|
|
998
|
+
delete next[r]
|
|
999
|
+
props.onChange(next)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const standardKeys = ['reply', 'approval', 'error']
|
|
1003
|
+
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1004
|
+
const allKeys = [...standardKeys, ...customKeys]
|
|
1005
|
+
|
|
1006
|
+
const addCustom = () => {
|
|
1007
|
+
const name = newRole.trim().toLowerCase()
|
|
1008
|
+
if (!name || value[name]) return
|
|
1009
|
+
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1010
|
+
setNewRole('')
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1014
|
+
React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
|
|
1015
|
+
allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
|
|
1016
|
+
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
|
|
1017
|
+
React.createElement('select', {
|
|
1018
|
+
className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
|
|
1019
|
+
onChange: (e) => set(r, { provider: e.target.value }),
|
|
1020
|
+
},
|
|
1021
|
+
React.createElement('option', { value: '' }, '—'),
|
|
1022
|
+
PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
|
|
1023
|
+
),
|
|
1024
|
+
React.createElement('input', {
|
|
1025
|
+
className: 'dts-input dts-grow', placeholder: 'model',
|
|
1026
|
+
value: (value[r] && value[r].model) || '', disabled: !props.writable,
|
|
1027
|
+
onChange: (e) => set(r, { model: e.target.value }),
|
|
1028
|
+
}),
|
|
1029
|
+
React.createElement('input', {
|
|
1030
|
+
className: 'dts-input dts-grow', placeholder: 'voice',
|
|
1031
|
+
value: (value[r] && value[r].voice) || '', disabled: !props.writable,
|
|
1032
|
+
onChange: (e) => set(r, { voice: e.target.value }),
|
|
1033
|
+
}),
|
|
1034
|
+
React.createElement('input', {
|
|
1035
|
+
className: 'dts-input', style: { maxWidth: '90px' }, placeholder: t('chime'),
|
|
1036
|
+
value: (value[r] && value[r].chime) || '', disabled: !props.writable,
|
|
1037
|
+
onChange: (e) => set(r, { chime: e.target.value }),
|
|
1038
|
+
}),
|
|
1039
|
+
React.createElement('input', {
|
|
1040
|
+
className: 'dts-input dts-grow', placeholder: 'ssml',
|
|
1041
|
+
value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
|
|
1042
|
+
onChange: (e) => set(r, { ssmlStyle: e.target.value }),
|
|
1043
|
+
}),
|
|
1044
|
+
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => props.onPreview((value[r] && value[r].provider) || '', (value[r] && value[r].model) || '', (value[r] && value[r].voice) || '') }, '\u25b6'),
|
|
1045
|
+
!standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
|
|
1046
|
+
)),
|
|
1047
|
+
React.createElement('div', { className: 'dts-row' },
|
|
1048
|
+
React.createElement('input', {
|
|
1049
|
+
className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
|
|
1050
|
+
value: newRole, disabled: !props.writable,
|
|
1051
|
+
onChange: (e) => setNewRole(e.target.value),
|
|
1052
|
+
onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
|
|
1053
|
+
}),
|
|
1054
|
+
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
|
|
1055
|
+
),
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function VoiceDuplexEditor(props) {
|
|
1060
|
+
const t = props.t || ((key) => key)
|
|
1061
|
+
const installed = !!props.installed
|
|
1062
|
+
|
|
1063
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1064
|
+
React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
|
|
1065
|
+
React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
|
|
1066
|
+
!installed ? React.createElement('div', {
|
|
1067
|
+
style: {
|
|
1068
|
+
padding: '10px 14px',
|
|
1069
|
+
borderRadius: '8px',
|
|
1070
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1071
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1072
|
+
display: 'flex',
|
|
1073
|
+
flexDirection: 'column',
|
|
1074
|
+
gap: '4px',
|
|
1075
|
+
fontSize: '13px',
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1079
|
+
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1080
|
+
),
|
|
1081
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1082
|
+
t('voiceInstallHint') + ': '
|
|
1083
|
+
),
|
|
1084
|
+
React.createElement('code', {
|
|
1085
|
+
style: {
|
|
1086
|
+
fontFamily: 'monospace',
|
|
1087
|
+
fontSize: '12px',
|
|
1088
|
+
padding: '2px 6px',
|
|
1089
|
+
borderRadius: '4px',
|
|
1090
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1091
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1092
|
+
width: 'fit-content',
|
|
1093
|
+
},
|
|
1094
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1095
|
+
) : null,
|
|
1096
|
+
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1097
|
+
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1098
|
+
)
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function MessengerIntegrationEditor(props) {
|
|
1102
|
+
const t = props.t || ((key) => key)
|
|
1103
|
+
const installed = !!props.installed
|
|
1104
|
+
|
|
1105
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1106
|
+
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1107
|
+
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1108
|
+
!installed ? React.createElement('div', {
|
|
1109
|
+
style: {
|
|
1110
|
+
padding: '10px 14px',
|
|
1111
|
+
borderRadius: '8px',
|
|
1112
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1113
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1114
|
+
display: 'flex',
|
|
1115
|
+
flexDirection: 'column',
|
|
1116
|
+
gap: '4px',
|
|
1117
|
+
fontSize: '13px',
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1121
|
+
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1122
|
+
),
|
|
1123
|
+
React.createElement('span', { className: 'dts-sub' },
|
|
1124
|
+
t('messengerInstallHint') + ': '
|
|
1125
|
+
),
|
|
1126
|
+
React.createElement('code', {
|
|
1127
|
+
style: {
|
|
1128
|
+
fontFamily: 'monospace',
|
|
1129
|
+
fontSize: '12px',
|
|
1130
|
+
padding: '2px 6px',
|
|
1131
|
+
borderRadius: '4px',
|
|
1132
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1133
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
1134
|
+
width: 'fit-content',
|
|
1135
|
+
},
|
|
1136
|
+
}, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
|
|
1137
|
+
) : null,
|
|
1138
|
+
props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
|
|
1139
|
+
)
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Synthesis stats panel: live host counters.
|
|
1143
|
+
function StatsPanel(props) {
|
|
1144
|
+
const t = props.t || ((key) => key)
|
|
1145
|
+
const [data, setData] = React.useState(null)
|
|
1146
|
+
const loadStats = () => {
|
|
1147
|
+
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
|
|
1148
|
+
}
|
|
1149
|
+
React.useEffect(() => { loadStats() }, [])
|
|
1150
|
+
const reset = () => {
|
|
1151
|
+
fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
|
|
1152
|
+
}
|
|
1153
|
+
return React.createElement('div', { className: 'dts-block' },
|
|
1154
|
+
React.createElement('span', { className: 'dts-sub' }, data
|
|
1155
|
+
? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
|
|
1156
|
+
: t('loading')),
|
|
1157
|
+
data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
|
|
1158
|
+
p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
|
|
1159
|
+
React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Plugin-settings tab card follows the shared card pattern: li in the core list,
|
|
1164
|
+
// theme-reset header, body with a divider. Chevron is ours; core does not provide it.
|
|
1165
|
+
function TtsSection(props) {
|
|
1166
|
+
// Translator comes from the slot because its registration sets locale.
|
|
1167
|
+
const t = (props && props.t) || ((key) => key)
|
|
1168
|
+
const [draft, setDraft] = React.useState(null)
|
|
1169
|
+
const [credentials, setCredentials] = React.useState({})
|
|
1170
|
+
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
1171
|
+
const [saved, setSaved] = React.useState(false)
|
|
1172
|
+
const [err, setErr] = React.useState('')
|
|
1173
|
+
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
1174
|
+
const [telemetry, setTelemetry] = React.useState({ online: true, cacheHits: 0, sseOk: true, total: 0 })
|
|
1175
|
+
const ctx = props && props.ctx
|
|
1176
|
+
const scope = React.useMemo(() => {
|
|
1177
|
+
const s = (ctx && ctx.get && ctx.get('lanSettings')) || (ctx && ctx.settingsScope)
|
|
1178
|
+
if (!s || !s.bind) return null
|
|
1179
|
+
try {
|
|
1180
|
+
return s.bind({ namespace: NS })
|
|
1181
|
+
} catch (_) {
|
|
1182
|
+
return null
|
|
1183
|
+
}
|
|
1184
|
+
}, [ctx])
|
|
1185
|
+
|
|
1186
|
+
const subscribe = React.useMemo(() => {
|
|
1187
|
+
return (cb) => {
|
|
1188
|
+
if (!scope || !scope.subscribe) return () => {}
|
|
1189
|
+
try {
|
|
1190
|
+
return scope.subscribe(cb) || (() => {})
|
|
1191
|
+
} catch (_) {
|
|
1192
|
+
return () => {}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}, [scope])
|
|
1196
|
+
|
|
1197
|
+
const getSnapshot = React.useCallback(() => {
|
|
1198
|
+
if (!scope || !scope.getSnapshot) return SNAPSHOT_LOADING
|
|
1199
|
+
try {
|
|
1200
|
+
return scope.getSnapshot() || SNAPSHOT_LOADING
|
|
1201
|
+
} catch (_) {
|
|
1202
|
+
return SNAPSHOT_LOADING
|
|
1203
|
+
}
|
|
1204
|
+
}, [scope])
|
|
1205
|
+
|
|
1206
|
+
const snap = (React.useSyncExternalStore
|
|
1207
|
+
? React.useSyncExternalStore(subscribe, getSnapshot, React.useCallback(() => SNAPSHOT_LOADING, []))
|
|
1208
|
+
: null) || (scope && scope.getSnapshot ? scope.getSnapshot() : SNAPSHOT_LOADING)
|
|
1209
|
+
|
|
1210
|
+
// Snapshot status matters more than the value:
|
|
1211
|
+
// loading — host has not answered yet;
|
|
1212
|
+
// unavailable — host answered but the settings namespace is not ready;
|
|
1213
|
+
// ready — values are present.
|
|
1214
|
+
// On unavailable, writable defaults true; without a status check the card
|
|
1215
|
+
// looks like a working empty form.
|
|
1216
|
+
const ready = !scope || (!!snap && snap.status === 'ready')
|
|
1217
|
+
const writable = !scope ? true : (ready && snap.writable !== false)
|
|
1218
|
+
|
|
1219
|
+
const applyPayload = (data) => {
|
|
1220
|
+
const cfg = data && data.config ? data.config : {}
|
|
1221
|
+
setDraft(JSON.parse(JSON.stringify(cfg)))
|
|
1222
|
+
setCredentials(data && data.credentials ? data.credentials : {})
|
|
1223
|
+
player.enabled = !!cfg.speakReplies
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
React.useEffect(() => {
|
|
1227
|
+
let alive = true
|
|
1228
|
+
fetch('/dsh-tts/config', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
1229
|
+
if (!alive) return
|
|
1230
|
+
applyPayload(data)
|
|
1231
|
+
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
1232
|
+
|
|
1233
|
+
fetch('/dsh-tts/integrations', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
1234
|
+
if (!alive) return
|
|
1235
|
+
if (data && data.ok) setIntegrations(data)
|
|
1236
|
+
}).catch(() => {})
|
|
1237
|
+
|
|
1238
|
+
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((res) => res.json()).then((st) => {
|
|
1239
|
+
if (!alive || !st) return
|
|
1240
|
+
setTelemetry((prev) => Object.assign({}, prev, {
|
|
1241
|
+
cacheHits: st.cacheHits || 0,
|
|
1242
|
+
total: st.total || 0,
|
|
1243
|
+
online: true,
|
|
1244
|
+
}))
|
|
1245
|
+
}).catch(() => {
|
|
1246
|
+
if (alive) setTelemetry((prev) => Object.assign({}, prev, { online: false }))
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
return () => { alive = false }
|
|
1250
|
+
}, [])
|
|
1251
|
+
|
|
1252
|
+
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
1253
|
+
|
|
1254
|
+
const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
|
|
1255
|
+
const setDraftKey = (provider, value) => setKeyDrafts((d) => Object.assign({}, d, { [provider]: value }))
|
|
1256
|
+
|
|
1257
|
+
const commitKey = async (provider, raw) => {
|
|
1258
|
+
const value = String(raw != null ? raw : ((keyDrafts && keyDrafts[provider]) || '')).trim()
|
|
1259
|
+
if (!value) return
|
|
1260
|
+
setErr('')
|
|
1261
|
+
const res = await fetch('/dsh-tts/credential', {
|
|
1262
|
+
method: 'PUT',
|
|
1263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1264
|
+
body: JSON.stringify({ provider: provider, value: value }),
|
|
1265
|
+
})
|
|
1266
|
+
const data = await res.json().catch(() => ({}))
|
|
1267
|
+
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
876
1268
|
if (data && data.credentials) setCredentials(data.credentials)
|
|
877
1269
|
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
878
1270
|
}
|
|
@@ -966,389 +1358,151 @@ window.__ModuleLoader__.load({
|
|
|
966
1358
|
const textField = (key, label, hint) => React.createElement('label', { className: 'dts-field' }, label,
|
|
967
1359
|
React.createElement('input', {
|
|
968
1360
|
className: 'dts-input',
|
|
969
|
-
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
970
|
-
onChange: (e) => setTop(key, e.target.value),
|
|
971
|
-
}),
|
|
972
|
-
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
973
|
-
|
|
974
|
-
// The card header draws the title — do not duplicate a second title block.
|
|
975
|
-
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-h' }, t('title'))
|
|
976
|
-
return React.createElement('div', { className: 'dts-wrap' },
|
|
977
|
-
React.createElement('div', { className: 'dts-block' },
|
|
978
|
-
titleLine,
|
|
979
|
-
React.createElement('label', { className: 'dts-row' },
|
|
980
|
-
React.createElement('input', {
|
|
981
|
-
type: 'checkbox', checked: !!(draft && draft.speakReplies), disabled: !writable,
|
|
982
|
-
onChange: (e) => setTop('speakReplies', e.target.checked),
|
|
983
|
-
}),
|
|
984
|
-
React.createElement('span', null, t('speakReplies')),
|
|
985
|
-
),
|
|
986
|
-
React.createElement('div', { className: 'dts-sub' },
|
|
987
|
-
t('speakRepliesHint')),
|
|
988
|
-
),
|
|
989
|
-
React.createElement(LocalEnginesEditor, {
|
|
990
|
-
t: t, writable: writable, draft: draft,
|
|
991
|
-
boolField: boolField, setTop: setTop,
|
|
992
|
-
}),
|
|
993
|
-
React.createElement(VoiceDuplexEditor, {
|
|
994
|
-
t: t, writable: writable, draft: draft,
|
|
995
|
-
boolField: boolField, setTop: setTop,
|
|
996
|
-
installed: !!(integrations && integrations.voice && integrations.voice.installed),
|
|
997
|
-
}),
|
|
998
|
-
React.createElement(MessengerIntegrationEditor, {
|
|
999
|
-
t: t, writable: writable, draft: draft,
|
|
1000
|
-
boolField: boolField, setTop: setTop,
|
|
1001
|
-
installed: !!(integrations && integrations.messenger && integrations.messenger.installed),
|
|
1002
|
-
}),
|
|
1003
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1004
|
-
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
|
1005
|
-
boolField('enableItDictionary', t('enableItDictionary'), t('enableItDictionaryHint')),
|
|
1006
|
-
React.createElement(PronEditor, {
|
|
1007
|
-
t: t, writable: writable,
|
|
1008
|
-
value: draft && draft.pronunciation ? draft.pronunciation : [],
|
|
1009
|
-
onChange: (v) => setTop('pronunciation', v),
|
|
1010
|
-
onPreviewPhrase: onPreviewPhrase,
|
|
1011
|
-
}),
|
|
1012
|
-
),
|
|
1013
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1014
|
-
React.createElement('div', { className: 'dts-h' }, t('rolesTitle')),
|
|
1015
|
-
React.createElement(RolesEditor, {
|
|
1016
|
-
t: t, writable: writable, onPreview: onPreview,
|
|
1017
|
-
value: draft && draft.roles ? draft.roles : {},
|
|
1018
|
-
onChange: (v) => setTop('roles', v),
|
|
1019
|
-
}),
|
|
1020
|
-
),
|
|
1021
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1022
|
-
React.createElement('div', { className: 'dts-h' }, t('statsTitle')),
|
|
1023
|
-
React.createElement(StatsPanel, { t: t }),
|
|
1024
|
-
),
|
|
1025
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1026
|
-
React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
|
|
1027
|
-
React.createElement('div', { className: 'dts-sub' },
|
|
1028
|
-
t('chainHintCard')),
|
|
1029
|
-
React.createElement(ChainEditor, {
|
|
1030
|
-
t: t,
|
|
1031
|
-
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
1032
|
-
credentials: credentials, keyDrafts: keyDrafts,
|
|
1033
|
-
onChange: (v) => setTop('chain', v),
|
|
1034
|
-
onDraft: setDraftKey,
|
|
1035
|
-
onCommitKey: onCommitKey,
|
|
1036
|
-
onClearKey: clearKey,
|
|
1037
|
-
onPreview: onPreview,
|
|
1038
|
-
}),
|
|
1039
|
-
),
|
|
1040
|
-
React.createElement('div', { className: 'dts-block' },
|
|
1041
|
-
React.createElement('div', { className: 'dts-h' }, t('general')),
|
|
1042
|
-
boolField('speakAsItGoes', t('speakAsItGoes'),
|
|
1043
|
-
t('speakAsItGoesHint')),
|
|
1044
|
-
numberField('rate', t('rate'), t('rateHint'), 0.1),
|
|
1045
|
-
boolField('bargeIn', t('bargeIn'),
|
|
1046
|
-
t('bargeInHint')),
|
|
1047
|
-
boolField('announceApproval', t('announce'),
|
|
1048
|
-
t('announceHint')),
|
|
1049
|
-
boolField('skipCode', t('skipCode'),
|
|
1050
|
-
t('skipCodeHint')),
|
|
1051
|
-
boolField('cache', t('cache'),
|
|
1052
|
-
t('cacheHint')),
|
|
1053
|
-
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
1054
|
-
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1055
|
-
React.createElement('select', {
|
|
1056
|
-
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1057
|
-
onChange: (e) => setTop('longReply', e.target.value),
|
|
1058
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1059
|
-
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1060
|
-
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1061
|
-
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1062
|
-
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1063
|
-
React.createElement('select', {
|
|
1064
|
-
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1065
|
-
onChange: (e) => setTop('longReply', e.target.value),
|
|
1066
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1067
|
-
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1068
|
-
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1069
|
-
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1070
|
-
boolField('autoDetect', t('autoDetect'), t('autoDetectHint')),
|
|
1071
|
-
boolField('narrateQuotesOnly', t('narrateQuotesOnly'), t('narrateQuotesHint')),
|
|
1072
|
-
boolField('skipActions', t('skipActions'), t('skipActionsHint')),
|
|
1073
|
-
textField('removeRegex', t('removeRegex'), t('removeRegexHint')),
|
|
1074
|
-
textField('customBaseUrl', t('customBaseUrl'), t('customBaseUrlHint')),
|
|
1075
|
-
textField('customKeyEnv', t('customKeyEnv'), t('customKeyEnvHint')),
|
|
1076
|
-
textField('approvalText', t('approvalText'), t('approvalHint')),
|
|
1077
|
-
textField('questionText', t('questionText'), t('questionHint')),
|
|
1078
|
-
textField('chime', t('chime'), t('chimeHint')),
|
|
1079
|
-
textField('language', t('language'), t('languageHint')),
|
|
1080
|
-
textField('piperModel', t('piperModel'), t('piperModelHint')),
|
|
1081
|
-
textField('piperBin', t('piperBin'), t('binHint')),
|
|
1082
|
-
textField('espeakBin', t('espeakBin'), t('espeakHint')),
|
|
1083
|
-
textField('edgeBin', t('edgeBin'), t('edgeHint')),
|
|
1084
|
-
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
1085
|
-
),
|
|
1086
|
-
React.createElement('div', { className: 'dts-foot' },
|
|
1087
|
-
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
1088
|
-
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
|
|
1089
|
-
saved ? React.createElement('span', { className: 'dts-ok' }, t('saved')) : null,
|
|
1090
|
-
err ? React.createElement('span', { className: 'dts-bad' }, err) : null,
|
|
1091
|
-
),
|
|
1092
|
-
)
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
const CLIENT_IT_TERMS = [
|
|
1097
|
-
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
1098
|
-
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
1099
|
-
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1100
|
-
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1101
|
-
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
1102
|
-
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
1103
|
-
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
1104
|
-
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
1105
|
-
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
1106
|
-
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
1107
|
-
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
1108
|
-
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
1109
|
-
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
1110
|
-
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
1111
|
-
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
1112
|
-
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
1113
|
-
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
1114
|
-
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
1115
|
-
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
1116
|
-
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
1117
|
-
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
1118
|
-
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
1119
|
-
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
1120
|
-
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
1121
|
-
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1122
|
-
]
|
|
1123
|
-
|
|
1124
|
-
// Pronunciation dictionary editor: rules apply top-down.
|
|
1125
|
-
function PronEditor(props) {
|
|
1126
|
-
const t = props.t || ((key) => key)
|
|
1127
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
1128
|
-
const change = (i, patch) => {
|
|
1129
|
-
props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
1130
|
-
}
|
|
1131
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
1132
|
-
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
1133
|
-
const loadIt = () => {
|
|
1134
|
-
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
1135
|
-
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
1136
|
-
props.onChange(rows.concat(toAdd))
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
1140
|
-
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
1141
|
-
React.createElement('input', {
|
|
1142
|
-
className: 'dts-input dts-grow', placeholder: t('pronFrom'),
|
|
1143
|
-
value: rule.from || '', disabled: !props.writable,
|
|
1144
|
-
onChange: (e) => change(i, { from: e.target.value }),
|
|
1145
|
-
}),
|
|
1146
|
-
React.createElement('input', {
|
|
1147
|
-
className: 'dts-input dts-grow', placeholder: t('pronTo'),
|
|
1148
|
-
value: rule.to || '', disabled: !props.writable,
|
|
1149
|
-
onChange: (e) => change(i, { to: e.target.value }),
|
|
1150
|
-
}),
|
|
1151
|
-
React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
|
|
1152
|
-
React.createElement('input', {
|
|
1153
|
-
type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
|
|
1154
|
-
onChange: (e) => change(i, { whole: e.target.checked }),
|
|
1155
|
-
}),
|
|
1156
|
-
),
|
|
1157
|
-
React.createElement('input', {
|
|
1158
|
-
className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
|
|
1159
|
-
value: rule.lang || '', disabled: !props.writable,
|
|
1160
|
-
onChange: (e) => change(i, { lang: e.target.value }),
|
|
1161
|
-
}),
|
|
1162
|
-
React.createElement('button', {
|
|
1163
|
-
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
1164
|
-
onClick: () => {
|
|
1165
|
-
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
1166
|
-
},
|
|
1167
|
-
}, '\u25b6'),
|
|
1168
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
1169
|
-
)),
|
|
1170
|
-
React.createElement('div', { className: 'dts-row' },
|
|
1171
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
1172
|
-
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
1173
|
-
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
1174
|
-
),
|
|
1175
|
-
)
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
function RolesEditor(props) {
|
|
1179
|
-
const t = props.t || ((key) => key)
|
|
1180
|
-
const value = props.value || {}
|
|
1181
|
-
const [newRole, setNewRole] = React.useState('')
|
|
1182
|
-
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
1183
|
-
const removeRole = (r) => {
|
|
1184
|
-
const next = Object.assign({}, value)
|
|
1185
|
-
delete next[r]
|
|
1186
|
-
props.onChange(next)
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
const standardKeys = ['reply', 'approval', 'error']
|
|
1190
|
-
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1191
|
-
const allKeys = [...standardKeys, ...customKeys]
|
|
1192
|
-
|
|
1193
|
-
const addCustom = () => {
|
|
1194
|
-
const name = newRole.trim().toLowerCase()
|
|
1195
|
-
if (!name || value[name]) return
|
|
1196
|
-
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1197
|
-
setNewRole('')
|
|
1198
|
-
}
|
|
1361
|
+
value: draft && draft[key] !== undefined ? draft[key] : '', disabled: !writable,
|
|
1362
|
+
onChange: (e) => setTop(key, e.target.value),
|
|
1363
|
+
}),
|
|
1364
|
+
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
1199
1365
|
|
|
1200
|
-
|
|
1201
|
-
React.createElement('span', {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1366
|
+
const telemetryBadges = React.createElement('div', { className: 'dts-row', style: { marginBottom: '8px' } },
|
|
1367
|
+
React.createElement('span', {
|
|
1368
|
+
className: 'dts-badge ' + (telemetry.online ? 'dts-badge-on' : 'dts-badge-bad'),
|
|
1369
|
+
}, telemetry.online ? '🟢 ' + (t('statusOnline') || 'Host online') : '🔴 ' + (t('statusOffline') || 'Host offline')),
|
|
1370
|
+
React.createElement('span', {
|
|
1371
|
+
className: 'dts-badge dts-badge-on',
|
|
1372
|
+
}, '⚡ SSE Active'),
|
|
1373
|
+
React.createElement('span', {
|
|
1374
|
+
className: 'dts-badge',
|
|
1375
|
+
}, '💾 ' + (t('cacheStats') || 'Cache') + ': ' + telemetry.cacheHits + ' / ' + telemetry.total),
|
|
1376
|
+
)
|
|
1377
|
+
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-header' },
|
|
1378
|
+
React.createElement('div', { className: 'dts-page-title' }, '🔊 ' + t('title')),
|
|
1379
|
+
React.createElement('div', { className: 'dts-page-sub' }, t('subtitle')),
|
|
1380
|
+
telemetryBadges,
|
|
1381
|
+
)
|
|
1382
|
+
return React.createElement('div', { className: 'dts-wrap' },
|
|
1383
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1384
|
+
titleLine,
|
|
1385
|
+
React.createElement('label', { className: 'dts-row' },
|
|
1386
|
+
React.createElement('input', {
|
|
1387
|
+
type: 'checkbox', checked: !!(draft && draft.speakReplies), disabled: !writable,
|
|
1388
|
+
onChange: (e) => setTop('speakReplies', e.target.checked),
|
|
1389
|
+
}),
|
|
1390
|
+
React.createElement('span', null, t('speakReplies')),
|
|
1210
1391
|
),
|
|
1211
|
-
React.createElement('
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1392
|
+
React.createElement('div', { className: 'dts-sub' },
|
|
1393
|
+
t('speakRepliesHint')),
|
|
1394
|
+
),
|
|
1395
|
+
React.createElement(LocalEnginesEditor, {
|
|
1396
|
+
t: t, writable: writable, draft: draft,
|
|
1397
|
+
boolField: boolField, setTop: setTop,
|
|
1398
|
+
}),
|
|
1399
|
+
React.createElement(VoiceDuplexEditor, {
|
|
1400
|
+
t: t, writable: writable, draft: draft,
|
|
1401
|
+
boolField: boolField, setTop: setTop,
|
|
1402
|
+
installed: !!(integrations && integrations.voice && integrations.voice.installed),
|
|
1403
|
+
}),
|
|
1404
|
+
React.createElement(MessengerIntegrationEditor, {
|
|
1405
|
+
t: t, writable: writable, draft: draft,
|
|
1406
|
+
boolField: boolField, setTop: setTop,
|
|
1407
|
+
installed: !!(integrations && integrations.messenger && integrations.messenger.installed),
|
|
1408
|
+
}),
|
|
1409
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1410
|
+
React.createElement('div', { className: 'dts-h' }, t('pronTitle')),
|
|
1411
|
+
boolField('enableItDictionary', t('enableItDictionary'), t('enableItDictionaryHint')),
|
|
1412
|
+
React.createElement(PronEditor, {
|
|
1413
|
+
t: t, writable: writable,
|
|
1414
|
+
value: draft && draft.pronunciation ? draft.pronunciation : [],
|
|
1415
|
+
onChange: (v) => setTop('pronunciation', v),
|
|
1416
|
+
onPreviewPhrase: onPreviewPhrase,
|
|
1225
1417
|
}),
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1418
|
+
),
|
|
1419
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1420
|
+
React.createElement('div', { className: 'dts-h' }, t('rolesTitle')),
|
|
1421
|
+
React.createElement(RolesEditor, {
|
|
1422
|
+
t: t, writable: writable, onPreview: onPreview,
|
|
1423
|
+
value: draft && draft.roles ? draft.roles : {},
|
|
1424
|
+
onChange: (v) => setTop('roles', v),
|
|
1230
1425
|
}),
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1426
|
+
),
|
|
1427
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1428
|
+
React.createElement('div', { className: 'dts-h' }, t('statsTitle')),
|
|
1429
|
+
React.createElement(StatsPanel, { t: t }),
|
|
1430
|
+
),
|
|
1431
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1432
|
+
React.createElement('div', { className: 'dts-h' }, t('chainTitle')),
|
|
1433
|
+
React.createElement('div', { className: 'dts-sub' },
|
|
1434
|
+
t('chainHintCard')),
|
|
1435
|
+
React.createElement(ChainEditor, {
|
|
1436
|
+
t: t,
|
|
1437
|
+
value: draft && draft.chain ? draft.chain : [], writable: writable,
|
|
1438
|
+
credentials: credentials, keyDrafts: keyDrafts,
|
|
1439
|
+
onChange: (v) => setTop('chain', v),
|
|
1440
|
+
onDraft: setDraftKey,
|
|
1441
|
+
onCommitKey: onCommitKey,
|
|
1442
|
+
onClearKey: clearKey,
|
|
1443
|
+
onPreview: onPreview,
|
|
1240
1444
|
}),
|
|
1241
|
-
|
|
1445
|
+
),
|
|
1446
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1447
|
+
React.createElement('div', { className: 'dts-h' }, t('general')),
|
|
1448
|
+
boolField('speakAsItGoes', t('speakAsItGoes'),
|
|
1449
|
+
t('speakAsItGoesHint')),
|
|
1450
|
+
numberField('rate', t('rate'), t('rateHint'), 0.1),
|
|
1451
|
+
boolField('bargeIn', t('bargeIn'),
|
|
1452
|
+
t('bargeInHint')),
|
|
1453
|
+
boolField('announceApproval', t('announce'),
|
|
1454
|
+
t('announceHint')),
|
|
1455
|
+
boolField('skipCode', t('skipCode'),
|
|
1456
|
+
t('skipCodeHint')),
|
|
1457
|
+
boolField('cache', t('cache'),
|
|
1458
|
+
t('cacheHint')),
|
|
1459
|
+
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
1460
|
+
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1461
|
+
React.createElement('select', {
|
|
1462
|
+
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1463
|
+
onChange: (e) => setTop('longReply', e.target.value),
|
|
1464
|
+
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1465
|
+
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1466
|
+
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1467
|
+
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1468
|
+
boolField('autoDetect', t('autoDetect'), t('autoDetectHint')),
|
|
1469
|
+
boolField('narrateQuotesOnly', t('narrateQuotesOnly'), t('narrateQuotesHint')),
|
|
1470
|
+
boolField('skipActions', t('skipActions'), t('skipActionsHint')),
|
|
1471
|
+
textField('removeRegex', t('removeRegex'), t('removeRegexHint')),
|
|
1472
|
+
textField('customBaseUrl', t('customBaseUrl'), t('customBaseUrlHint')),
|
|
1473
|
+
textField('customKeyEnv', t('customKeyEnv'), t('customKeyEnvHint')),
|
|
1474
|
+
textField('approvalText', t('approvalText'), t('approvalHint')),
|
|
1475
|
+
textField('questionText', t('questionText'), t('questionHint')),
|
|
1476
|
+
textField('chime', t('chime'), t('chimeHint')),
|
|
1477
|
+
textField('language', t('language'), t('languageHint')),
|
|
1478
|
+
textField('piperModel', t('piperModel'), t('piperModelHint')),
|
|
1479
|
+
textField('piperBin', t('piperBin'), t('binHint')),
|
|
1480
|
+
textField('espeakBin', t('espeakBin'), t('espeakHint')),
|
|
1481
|
+
textField('edgeBin', t('edgeBin'), t('edgeHint')),
|
|
1482
|
+
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
1483
|
+
),
|
|
1484
|
+
React.createElement('div', { className: 'dts-block' },
|
|
1485
|
+
React.createElement('div', { className: 'dts-h' }, t('advancedTitle')),
|
|
1486
|
+
React.createElement('div', { className: 'dts-sub' }, t('advancedHint')),
|
|
1487
|
+
numberField('maxChars', t('maxChars'), t('maxCharsHint'), 100),
|
|
1488
|
+
numberField('sentenceChars', t('sentenceChars'), t('sentenceCharsHint'), 20),
|
|
1489
|
+
numberField('timeoutMs', t('timeoutMs'), t('timeoutMsHint'), 1000),
|
|
1490
|
+
numberField('maxQueue', t('maxQueue'), t('maxQueueHint'), 1),
|
|
1491
|
+
textField('openaiBaseUrl', t('openaiBaseUrl'), t('openaiBaseUrlHint')),
|
|
1492
|
+
textField('mimoBaseUrl', t('mimoBaseUrl'), t('mimoBaseUrlHint')),
|
|
1493
|
+
textField('mimoFormat', t('mimoFormat'), t('mimoFormatHint')),
|
|
1494
|
+
textField('minimaxBin', t('minimaxBin'), t('minimaxBinHint')),
|
|
1495
|
+
),
|
|
1496
|
+
React.createElement('div', { className: 'dts-foot' },
|
|
1497
|
+
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
1498
|
+
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
|
|
1499
|
+
saved ? React.createElement('span', { className: 'dts-ok' }, t('saved')) : null,
|
|
1500
|
+
err ? React.createElement('span', { className: 'dts-bad' }, err) : null,
|
|
1242
1501
|
),
|
|
1243
1502
|
)
|
|
1244
1503
|
}
|
|
1245
1504
|
|
|
1246
|
-
function VoiceDuplexEditor(props) {
|
|
1247
|
-
const t = props.t || ((key) => key)
|
|
1248
|
-
const installed = !!props.installed
|
|
1249
|
-
|
|
1250
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1251
|
-
React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
|
|
1252
|
-
React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
|
|
1253
|
-
!installed ? React.createElement('div', {
|
|
1254
|
-
style: {
|
|
1255
|
-
padding: '10px 14px',
|
|
1256
|
-
borderRadius: '8px',
|
|
1257
|
-
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1258
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1259
|
-
display: 'flex',
|
|
1260
|
-
flexDirection: 'column',
|
|
1261
|
-
gap: '4px',
|
|
1262
|
-
fontSize: '13px',
|
|
1263
|
-
},
|
|
1264
|
-
},
|
|
1265
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1266
|
-
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1267
|
-
),
|
|
1268
|
-
React.createElement('span', { className: 'dts-sub' },
|
|
1269
|
-
t('voiceInstallHint') + ': '
|
|
1270
|
-
),
|
|
1271
|
-
React.createElement('code', {
|
|
1272
|
-
style: {
|
|
1273
|
-
fontFamily: 'monospace',
|
|
1274
|
-
fontSize: '12px',
|
|
1275
|
-
padding: '2px 6px',
|
|
1276
|
-
borderRadius: '4px',
|
|
1277
|
-
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1278
|
-
color: 'var(--dsw-alias-label-primary)',
|
|
1279
|
-
width: 'fit-content',
|
|
1280
|
-
},
|
|
1281
|
-
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1282
|
-
) : null,
|
|
1283
|
-
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1284
|
-
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1285
|
-
)
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
function MessengerIntegrationEditor(props) {
|
|
1289
|
-
const t = props.t || ((key) => key)
|
|
1290
|
-
const installed = !!props.installed
|
|
1291
|
-
|
|
1292
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1293
|
-
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1294
|
-
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1295
|
-
!installed ? React.createElement('div', {
|
|
1296
|
-
style: {
|
|
1297
|
-
padding: '10px 14px',
|
|
1298
|
-
borderRadius: '8px',
|
|
1299
|
-
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1300
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1301
|
-
display: 'flex',
|
|
1302
|
-
flexDirection: 'column',
|
|
1303
|
-
gap: '4px',
|
|
1304
|
-
fontSize: '13px',
|
|
1305
|
-
},
|
|
1306
|
-
},
|
|
1307
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1308
|
-
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1309
|
-
),
|
|
1310
|
-
React.createElement('span', { className: 'dts-sub' },
|
|
1311
|
-
t('messengerInstallHint') + ': '
|
|
1312
|
-
),
|
|
1313
|
-
React.createElement('code', {
|
|
1314
|
-
style: {
|
|
1315
|
-
fontFamily: 'monospace',
|
|
1316
|
-
fontSize: '12px',
|
|
1317
|
-
padding: '2px 6px',
|
|
1318
|
-
borderRadius: '4px',
|
|
1319
|
-
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1320
|
-
color: 'var(--dsw-alias-label-primary)',
|
|
1321
|
-
width: 'fit-content',
|
|
1322
|
-
},
|
|
1323
|
-
}, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
|
|
1324
|
-
) : null,
|
|
1325
|
-
props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
|
|
1326
|
-
)
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// Synthesis stats panel: live host counters.
|
|
1330
|
-
function StatsPanel(props) {
|
|
1331
|
-
const t = props.t || ((key) => key)
|
|
1332
|
-
const [data, setData] = React.useState(null)
|
|
1333
|
-
const loadStats = () => {
|
|
1334
|
-
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
|
|
1335
|
-
}
|
|
1336
|
-
React.useEffect(() => { loadStats() }, [])
|
|
1337
|
-
const reset = () => {
|
|
1338
|
-
fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
|
|
1339
|
-
}
|
|
1340
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1341
|
-
React.createElement('span', { className: 'dts-sub' }, data
|
|
1342
|
-
? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
|
|
1343
|
-
: t('loading')),
|
|
1344
|
-
data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
|
|
1345
|
-
p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
|
|
1346
|
-
React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
|
|
1347
|
-
)
|
|
1348
|
-
}
|
|
1349
1505
|
|
|
1350
|
-
// Plugin-settings tab card follows the shared card pattern: li in the core list,
|
|
1351
|
-
// theme-reset header, body with a divider. Chevron is ours; core does not provide it.
|
|
1352
1506
|
function TtsCard(props) {
|
|
1353
1507
|
const t = (props && props.t) || ((key) => key)
|
|
1354
1508
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1376,7 +1530,9 @@ window.__ModuleLoader__.load({
|
|
|
1376
1530
|
),
|
|
1377
1531
|
),
|
|
1378
1532
|
open ? React.createElement('div', { className: 'dts-body' },
|
|
1379
|
-
React.createElement(
|
|
1533
|
+
React.createElement(ErrorBoundary, null,
|
|
1534
|
+
React.createElement(TtsSection, Object.assign({}, props, { compact: true, ctx: props && props.ctx })),
|
|
1535
|
+
),
|
|
1380
1536
|
) : null,
|
|
1381
1537
|
)
|
|
1382
1538
|
}
|
|
@@ -1509,7 +1665,7 @@ window.__ModuleLoader__.load({
|
|
|
1509
1665
|
label: () => t('title'),
|
|
1510
1666
|
inject: () => ({ ctx: ctx }),
|
|
1511
1667
|
},
|
|
1512
|
-
TtsSection,
|
|
1668
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(TtsSection, props)),
|
|
1513
1669
|
))
|
|
1514
1670
|
}
|
|
1515
1671
|
|
|
@@ -1575,3 +1731,4 @@ window.__ModuleLoader__.load({
|
|
|
1575
1731
|
|
|
1576
1732
|
// ModelManager UI placeholder - manual install buttons for Kokoro/F5
|
|
1577
1733
|
// Actual implementation would show download progress and status
|
|
1734
|
+
|