@goodandready/dsh-tts 0.3.23 → 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 +22 -12
- package/README.ru.md +23 -10
- package/README.zh.md +18 -10
- package/lib/cache.js +4 -5
- 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 +651 -506
- package/lib/engines/f5.js +45 -33
- package/lib/engines/kokoro.js +21 -29
- package/lib/engines/types.js +26 -13
- package/lib/http-util.js +27 -0
- package/lib/index.js +43 -455
- package/lib/providers.js +29 -18
- package/lib/routes.js +403 -0
- package/lib/stream-hub.js +31 -0
- package/lib/text.js +19 -19
- package/package.json +8 -3
- package/scripts/build-client.mjs +19 -0
- package/scripts/f5_daemon.py +40 -0
package/lib/client.js
CHANGED
|
@@ -5,9 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports
|
|
6
6
|
const React = require('react')
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// поворачивает её классом.
|
|
8
|
+
// Chevron comes from core UI primitives; local SVG is a fallback for
|
|
9
|
+
// builds without the package. Icon always points down; open state rotates.
|
|
11
10
|
let ChevronIcon = null
|
|
12
11
|
try {
|
|
13
12
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
@@ -23,64 +22,126 @@ window.__ModuleLoader__.load({
|
|
|
23
22
|
const CLOUD = {
|
|
24
23
|
openai: 1, elevenlabs: 1, google: 1, azure: 1, groq: 1, deepgram: 1, openrouter: 1, custom: 1,
|
|
25
24
|
}
|
|
25
|
+
// Hints stay generic: concrete provider model IDs rot quickly.
|
|
26
26
|
const MODEL_HINT = {
|
|
27
|
-
openai: '
|
|
28
|
-
elevenlabs: '
|
|
29
|
-
google: '
|
|
30
|
-
azure: 'region voice
|
|
31
|
-
groq: '
|
|
32
|
-
deepgram: '
|
|
33
|
-
openrouter: '
|
|
34
|
-
edge: 'ru-RU-SvetlanaNeural',
|
|
27
|
+
openai: 'provider default',
|
|
28
|
+
elevenlabs: 'provider default',
|
|
29
|
+
google: 'provider default',
|
|
30
|
+
azure: 'region voice name',
|
|
31
|
+
groq: 'provider default',
|
|
32
|
+
deepgram: 'provider default (English-only voices)',
|
|
33
|
+
openrouter: 'provider/model id',
|
|
34
|
+
edge: 'BCP-47 voice, e.g. ru-RU-SvetlanaNeural',
|
|
35
35
|
piper: 'path to .onnx',
|
|
36
|
-
espeak: 'ru',
|
|
36
|
+
espeak: 'language code, e.g. ru',
|
|
37
|
+
kokoro: 'not bundled — offline neural runtime unavailable',
|
|
38
|
+
f5: 'not bundled — offline neural runtime unavailable',
|
|
37
39
|
}
|
|
38
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
|
+
|
|
39
87
|
const SET_CSS =
|
|
40
|
-
'.dts-wrap{display:flex;flex-direction:column;gap:
|
|
41
|
-
'.dts-
|
|
42
|
-
'.dts-
|
|
43
|
-
|
|
44
|
-
|
|
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}' +
|
|
45
95
|
'.dts-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
46
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}' +
|
|
47
97
|
'.dts-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
48
98
|
'.dts-title{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
49
|
-
'.dts-sub{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
50
99
|
'.dts-chev{margin-left:auto;color:var(--dsw-alias-label-secondary);flex:none;transition:transform .16s}' +
|
|
51
100
|
'.dts-chevOpen{transform:rotate(180deg)}' +
|
|
52
|
-
'.dts-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding
|
|
53
|
-
'.dts-field{display:flex;flex-direction:column;gap:6px;padding:
|
|
54
|
-
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-
|
|
55
|
-
'.dts-
|
|
56
|
-
'.dts-
|
|
57
|
-
|
|
58
|
-
'.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)}' +
|
|
59
113
|
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
60
114
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
61
115
|
'.dts-row .dts-key{flex:1;min-width:180px}' +
|
|
62
|
-
'.dts-mini{border:1px solid var(--dsw-alias-border-l2);background:
|
|
63
|
-
'.dts-
|
|
64
|
-
'.dts-
|
|
65
|
-
'.dts-
|
|
66
|
-
'.dts-badge-
|
|
67
|
-
'.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}'
|
|
68
131
|
|
|
69
132
|
const setCssId = 'dsh-tts/settings.module.css'
|
|
70
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin
|
|
133
|
+
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-tts"]')) {
|
|
71
134
|
const tag = document.createElement('style')
|
|
72
135
|
tag.textContent = SET_CSS
|
|
73
|
-
tag.setAttribute('data-plugin', 'dsh-tts')
|
|
136
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-tts')
|
|
74
137
|
tag.dataset.pluginCss = setCssId
|
|
75
138
|
document.head.appendChild(tag)
|
|
76
139
|
}
|
|
77
|
-
|
|
78
|
-
// Проигрыватель.
|
|
140
|
+
// Player.
|
|
79
141
|
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// закончилось предыдущее.
|
|
142
|
+
// Each incoming chunk used to replace the previous one immediately; while reading
|
|
143
|
+
// For full replies this was invisible; with speak-as-it-goes only the last
|
|
144
|
+
// chunk would be heard. Now items queue: the next starts after the previous ends.
|
|
84
145
|
const player = {
|
|
85
146
|
audio: null,
|
|
86
147
|
after: '',
|
|
@@ -111,8 +172,7 @@ window.__ModuleLoader__.load({
|
|
|
111
172
|
return player
|
|
112
173
|
}
|
|
113
174
|
|
|
114
|
-
//
|
|
115
|
-
// хранить и настраивать.
|
|
175
|
+
// Short chime is synthesized in-browser: no file to download, store, or configure.
|
|
116
176
|
function playChime(kind) {
|
|
117
177
|
return new Promise((resolve) => {
|
|
118
178
|
try {
|
|
@@ -139,8 +199,8 @@ window.__ModuleLoader__.load({
|
|
|
139
199
|
})
|
|
140
200
|
}
|
|
141
201
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
202
|
+
// Provider failed — fall back to the browser voice. Worse quality, but silence
|
|
203
|
+
// is worse: the user would not know the reply is ready.
|
|
144
204
|
function speakInBrowser(text) {
|
|
145
205
|
return new Promise((resolve) => {
|
|
146
206
|
try {
|
|
@@ -203,8 +263,7 @@ window.__ModuleLoader__.load({
|
|
|
203
263
|
audio.onended = done
|
|
204
264
|
audio.onerror = done
|
|
205
265
|
audio.play().catch(() => {
|
|
206
|
-
|
|
207
|
-
// поломкой, просто ничего не звучит.
|
|
266
|
+
// Autoplay without a user gesture is blocked — not a bug; just no sound.
|
|
208
267
|
done()
|
|
209
268
|
})
|
|
210
269
|
} catch (cannotDecode) {
|
|
@@ -272,11 +331,10 @@ window.__ModuleLoader__.load({
|
|
|
272
331
|
}
|
|
273
332
|
}
|
|
274
333
|
|
|
275
|
-
//
|
|
334
|
+
// User started talking — stop playback.
|
|
276
335
|
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// окно, а не чужую службу, ровно поэтому.
|
|
336
|
+
// The event may come from a voice-input plugin, but there is no hard dependency:
|
|
337
|
+
// without that plugin the event simply never fires. We listen on window on purpose.
|
|
280
338
|
function listenForVoice() {
|
|
281
339
|
if (typeof window === 'undefined') return () => {}
|
|
282
340
|
const onSpeaking = (event) => {
|
|
@@ -306,8 +364,8 @@ window.__ModuleLoader__.load({
|
|
|
306
364
|
const data = await res.json()
|
|
307
365
|
const items = data && Array.isArray(data.items) ? data.items : []
|
|
308
366
|
for (const item of items) {
|
|
309
|
-
|
|
310
|
-
|
|
367
|
+
// Queue slot reserved but synthesis still running — wait for the next poll
|
|
368
|
+
// or phrase order would break.
|
|
311
369
|
if (item.kind === 'reserved') break
|
|
312
370
|
player.after = item.id
|
|
313
371
|
if (seenIds.has(item.id)) continue
|
|
@@ -323,7 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
323
381
|
} catch (hostRestarting) { /* хост перезапускается */ }
|
|
324
382
|
}
|
|
325
383
|
|
|
326
|
-
//
|
|
384
|
+
// Recent utterances: last reply texts and favorites (localStorage).
|
|
327
385
|
function recentStore() {
|
|
328
386
|
const KEY = 'dsh-tts/recent'
|
|
329
387
|
let st
|
|
@@ -349,7 +407,7 @@ window.__ModuleLoader__.load({
|
|
|
349
407
|
}
|
|
350
408
|
const recent = recentStore()
|
|
351
409
|
|
|
352
|
-
//
|
|
410
|
+
// One-shot playback outside the queue: replay from recents.
|
|
353
411
|
function playStandalone(text) {
|
|
354
412
|
fetch('/dsh-tts/speak', {
|
|
355
413
|
method: 'POST',
|
|
@@ -369,78 +427,11 @@ window.__ModuleLoader__.load({
|
|
|
369
427
|
return (map && map[provider]) || { configured: false, writable: true, ref: '' }
|
|
370
428
|
}
|
|
371
429
|
|
|
372
|
-
function ChainEditor(props) {
|
|
373
|
-
// Переводчик приходит от карточки: слот отдаёт props.t только ей.
|
|
374
|
-
const t = props.t || ((key) => key)
|
|
375
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
376
|
-
const change = (i, patch) => {
|
|
377
|
-
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
378
|
-
props.onChange(next)
|
|
379
|
-
}
|
|
380
|
-
const move = (i, delta) => {
|
|
381
|
-
const j = i + delta
|
|
382
|
-
if (j < 0 || j >= rows.length) return
|
|
383
|
-
const next = rows.slice()
|
|
384
|
-
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
385
|
-
props.onChange(next)
|
|
386
|
-
}
|
|
387
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
388
|
-
const add = () => props.onChange(rows.concat([{ provider: 'espeak', model: '', voice: '' }]))
|
|
389
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
390
|
-
rows.map((row, i) => {
|
|
391
|
-
const cloud = !!CLOUD[row.provider]
|
|
392
|
-
const cred = credOf(props.credentials, row.provider)
|
|
393
|
-
const draft = (props.keyDrafts && props.keyDrafts[row.provider]) || ''
|
|
394
|
-
const badge = !cloud ? null
|
|
395
|
-
: (!cred.writable ? t('fromEnv') : (cred.configured ? t('configured') : t('notSet')))
|
|
396
|
-
return React.createElement('div', { className: 'dts-entry', key: i },
|
|
397
|
-
React.createElement('div', { className: 'dts-row' },
|
|
398
|
-
React.createElement('select', {
|
|
399
|
-
className: 'dts-input',
|
|
400
|
-
value: row.provider, disabled: !props.writable,
|
|
401
|
-
onChange: (e) => change(i, { provider: e.target.value }),
|
|
402
|
-
}, PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
403
|
-
React.createElement('input', {
|
|
404
|
-
className: 'dts-model dts-input', value: row.model || '', disabled: !props.writable,
|
|
405
|
-
placeholder: MODEL_HINT[row.provider] || '', onChange: (e) => change(i, { model: e.target.value }),
|
|
406
|
-
}),
|
|
407
|
-
React.createElement('input', {
|
|
408
|
-
className: 'dts-model dts-input', value: row.voice || '', disabled: !props.writable,
|
|
409
|
-
placeholder: 'voice', onChange: (e) => change(i, { voice: e.target.value }),
|
|
410
|
-
}),
|
|
411
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '\u2191'),
|
|
412
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '\u2193'),
|
|
413
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('preview'), disabled: !props.writable, onClick: () => props.onPreview(row.provider, row.model, row.voice) }, '\u25b6'),
|
|
414
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
415
|
-
),
|
|
416
|
-
cloud ? React.createElement('div', { className: 'dts-row' },
|
|
417
|
-
React.createElement('input', {
|
|
418
|
-
className: 'dts-key dts-input', type: 'password', autoComplete: 'off',
|
|
419
|
-
value: draft, disabled: !props.writable || !cred.writable,
|
|
420
|
-
placeholder: cred.configured ? 'leave blank to keep' : 'paste API key',
|
|
421
|
-
onChange: (e) => props.onDraft(row.provider, e.target.value),
|
|
422
|
-
onBlur: (e) => props.onCommitKey(row.provider, e.target.value),
|
|
423
|
-
}),
|
|
424
|
-
React.createElement('span', { className: 'dts-badge' + (cred.configured ? ' dts-badge-on' : '') }, badge),
|
|
425
|
-
cred.configured && cred.writable ? React.createElement('button', {
|
|
426
|
-
type: 'button', className: 'dts-link', disabled: !props.writable,
|
|
427
|
-
onClick: () => props.onClearKey(row.provider),
|
|
428
|
-
}, t('clear')) : null,
|
|
429
|
-
) : React.createElement('span', { className: 'dts-sub' }, t('localProvider')),
|
|
430
|
-
cloud && cred.ref ? React.createElement('span', { className: 'dts-sub' }, 'Stored as ' + cred.ref) : null,
|
|
431
|
-
)
|
|
432
|
-
}),
|
|
433
|
-
React.createElement('div', { className: 'dts-row' },
|
|
434
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
435
|
-
React.createElement('span', { className: 'dts-sub' }, t('chainHint')),
|
|
436
|
-
),
|
|
437
|
-
)
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Строки карточки живут в реестре локалей: так их переводит отдельный
|
|
441
|
-
// пакет, не трогая код этого плагина. Английский — язык по умолчанию,
|
|
442
|
-
// на него же приходится откат, если перевода нет.
|
|
443
430
|
const en = {
|
|
431
|
+
'statusOnline': 'Host online',
|
|
432
|
+
'statusOffline': 'Host unreachable',
|
|
433
|
+
'cacheStats': 'Cache',
|
|
434
|
+
|
|
444
435
|
'general': 'General',
|
|
445
436
|
'loading': 'Loading\u2026',
|
|
446
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.',
|
|
@@ -474,6 +465,24 @@ window.__ModuleLoader__.load({
|
|
|
474
465
|
'edgeHint': 'Python CLI from the edge-tts package.',
|
|
475
466
|
'azureRegion': 'Azure region',
|
|
476
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.',
|
|
477
486
|
'chainTitle': 'Provider chain',
|
|
478
487
|
'addProvider': 'Add provider',
|
|
479
488
|
'localProvider': 'Local / CLI provider — no API key',
|
|
@@ -581,6 +590,10 @@ window.__ModuleLoader__.load({
|
|
|
581
590
|
'dockStop': 'Stop speech',
|
|
582
591
|
}
|
|
583
592
|
const ru = {
|
|
593
|
+
'statusOnline': 'Хост на связи',
|
|
594
|
+
'statusOffline': 'Хост недоступен',
|
|
595
|
+
'cacheStats': 'Кэш',
|
|
596
|
+
|
|
584
597
|
'dockLabel': 'Чтение',
|
|
585
598
|
'dockPause': 'Пауза',
|
|
586
599
|
'dockResume': 'Продолжить чтение',
|
|
@@ -619,6 +632,24 @@ window.__ModuleLoader__.load({
|
|
|
619
632
|
'edgeHint': 'Консольная утилита из пакета edge-tts.',
|
|
620
633
|
'azureRegion': 'Azure: регион',
|
|
621
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': 'Отбрасывать самые старые куски сверх этого числа.',
|
|
622
653
|
'chainTitle': 'Цепочка провайдеров',
|
|
623
654
|
'addProvider': 'Добавить провайдера',
|
|
624
655
|
'localProvider': 'Локальный провайдер — ключ не нужен',
|
|
@@ -673,7 +704,7 @@ window.__ModuleLoader__.load({
|
|
|
673
704
|
'removeRegex': 'Регулярка вырезания',
|
|
674
705
|
'removeRegexHint': 'Глобальная регулярка: совпадения удаляются перед синтезом.',
|
|
675
706
|
'customBaseUrl': 'Свой TTS endpoint',
|
|
676
|
-
'customBaseUrlHint': 'OpenAI
|
|
707
|
+
'customBaseUrlHint': 'OpenAI-compatible base URL, e.g. http://localhost:8880/v1; empty disables it.',
|
|
677
708
|
'customKeyEnv': 'Имя ключа для endpoint',
|
|
678
709
|
'customKeyEnvHint': 'Имя credential для Bearer-ключа своего endpointа.',
|
|
679
710
|
|
|
@@ -721,6 +752,77 @@ window.__ModuleLoader__.load({
|
|
|
721
752
|
'messengerTtsEnabledHint': 'Разрешить мосту мессенджеров озвучивать ответы голосовыми заметками.',
|
|
722
753
|
}
|
|
723
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
|
+
|
|
724
826
|
function LocalEnginesEditor(props) {
|
|
725
827
|
const t = props.t || ((key) => key)
|
|
726
828
|
const [models, setModels] = React.useState({})
|
|
@@ -803,96 +905,382 @@ window.__ModuleLoader__.load({
|
|
|
803
905
|
)
|
|
804
906
|
}
|
|
805
907
|
|
|
806
|
-
function TtsSection(props) {
|
|
807
|
-
// Переводчик приходит от слота, потому что в его записи указан locale.
|
|
808
|
-
const t = (props && props.t) || ((key) => key)
|
|
809
|
-
const [draft, setDraft] = React.useState(null)
|
|
810
|
-
const [credentials, setCredentials] = React.useState({})
|
|
811
|
-
const [keyDrafts, setKeyDrafts] = React.useState({})
|
|
812
|
-
const [saved, setSaved] = React.useState(false)
|
|
813
|
-
const [err, setErr] = React.useState('')
|
|
814
|
-
const [integrations, setIntegrations] = React.useState({ voice: { installed: false }, messenger: { installed: false } })
|
|
815
|
-
const ctx = props && props.ctx
|
|
816
|
-
const scope = (ctx && ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope))
|
|
817
|
-
? ((ctx.get && ctx.get('lanSettings')) || ctx.settingsScope).bind({ namespace: NS })
|
|
818
|
-
: null
|
|
819
|
-
const [snap, setSnap] = React.useState(null)
|
|
820
|
-
|
|
821
|
-
React.useEffect(() => {
|
|
822
|
-
if (!scope) return undefined
|
|
823
|
-
let alive = true
|
|
824
|
-
const render = () => { if (alive && scope.getSnapshot) setSnap(scope.getSnapshot()) }
|
|
825
|
-
render()
|
|
826
|
-
if (scope.subscribe) {
|
|
827
|
-
const off = scope.subscribe(render)
|
|
828
|
-
return () => { alive = false; if (typeof off === 'function') off() }
|
|
829
|
-
}
|
|
830
|
-
return () => { alive = false }
|
|
831
|
-
}, [scope])
|
|
832
908
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
+
]
|
|
841
936
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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)))
|
|
847
943
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}).catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
855
|
-
|
|
856
|
-
fetch('/dsh-tts/integrations', { cache: 'no-store' }).then((res) => res.json()).then((data) => {
|
|
857
|
-
if (!alive) return
|
|
858
|
-
if (data && data.ok) setIntegrations(data)
|
|
859
|
-
}).catch(() => {})
|
|
860
|
-
|
|
861
|
-
return () => { alive = false }
|
|
862
|
-
}, [])
|
|
863
|
-
|
|
864
|
-
if (!draft) return React.createElement('div', { className: 'dts-wrap' }, t('loading'))
|
|
865
|
-
|
|
866
|
-
const setTop = (key, v) => setDraft((d) => Object.assign({}, d || {}, { [key]: v }))
|
|
867
|
-
const setDraftKey = (provider, value) => setKeyDrafts((d) => Object.assign({}, d, { [provider]: value }))
|
|
868
|
-
|
|
869
|
-
const commitKey = async (provider, raw) => {
|
|
870
|
-
const value = String(raw != null ? raw : ((keyDrafts && keyDrafts[provider]) || '')).trim()
|
|
871
|
-
if (!value) return
|
|
872
|
-
setErr('')
|
|
873
|
-
const res = await fetch('/dsh-tts/credential', {
|
|
874
|
-
method: 'PUT',
|
|
875
|
-
headers: { 'Content-Type': 'application/json' },
|
|
876
|
-
body: JSON.stringify({ provider: provider, value: value }),
|
|
877
|
-
})
|
|
878
|
-
const data = await res.json().catch(() => ({}))
|
|
879
|
-
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
880
|
-
if (data && data.credentials) setCredentials(data.credentials)
|
|
881
|
-
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
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))
|
|
882
950
|
}
|
|
883
951
|
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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))
|
|
1268
|
+
if (data && data.credentials) setCredentials(data.credentials)
|
|
1269
|
+
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const clearKey = async (provider) => {
|
|
1273
|
+
setErr('')
|
|
1274
|
+
try {
|
|
1275
|
+
const res = await fetch('/dsh-tts/credential', {
|
|
1276
|
+
method: 'DELETE',
|
|
1277
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1278
|
+
body: JSON.stringify({ provider: provider }),
|
|
1279
|
+
})
|
|
1280
|
+
const data = await res.json().catch(() => ({}))
|
|
1281
|
+
if (!res.ok) throw new Error((data && data.error && data.error.message) || ('HTTP ' + res.status))
|
|
1282
|
+
if (data && data.credentials) setCredentials(data.credentials)
|
|
1283
|
+
setKeyDrafts((d) => Object.assign({}, d, { [provider]: '' }))
|
|
896
1284
|
} catch (e) { setErr(String(e && e.message ? e.message : e)) }
|
|
897
1285
|
}
|
|
898
1286
|
|
|
@@ -975,8 +1363,22 @@ window.__ModuleLoader__.load({
|
|
|
975
1363
|
}),
|
|
976
1364
|
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
977
1365
|
|
|
978
|
-
|
|
979
|
-
|
|
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
|
+
)
|
|
980
1382
|
return React.createElement('div', { className: 'dts-wrap' },
|
|
981
1383
|
React.createElement('div', { className: 'dts-block' },
|
|
982
1384
|
titleLine,
|
|
@@ -1051,18 +1453,10 @@ window.__ModuleLoader__.load({
|
|
|
1051
1453
|
boolField('announceApproval', t('announce'),
|
|
1052
1454
|
t('announceHint')),
|
|
1053
1455
|
boolField('skipCode', t('skipCode'),
|
|
1054
|
-
t('skipCodeHint')),
|
|
1055
|
-
boolField('cache', t('cache'),
|
|
1056
|
-
t('cacheHint')),
|
|
1057
|
-
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
1058
|
-
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1059
|
-
React.createElement('select', {
|
|
1060
|
-
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
1061
|
-
onChange: (e) => setTop('longReply', e.target.value),
|
|
1062
|
-
}, ['truncate', 'summarize', 'full'].map((mode) => React.createElement('option', { key: mode, value: mode }, mode))),
|
|
1063
|
-
React.createElement('span', { className: 'dts-sub' }, t('longReplyHint'))),
|
|
1064
|
-
textField('summaryModel', t('summaryModel'), t('summaryModelHint')),
|
|
1065
|
-
numberField('summarySentences', t('summarySentences'), t('summarySentencesHint'), 1),
|
|
1456
|
+
t('skipCodeHint')),
|
|
1457
|
+
boolField('cache', t('cache'),
|
|
1458
|
+
t('cacheHint')),
|
|
1459
|
+
numberField('cacheMaxMb', t('cacheMaxMb'), t('cacheMaxMbHint'), 10),
|
|
1066
1460
|
React.createElement('label', { className: 'dts-field' }, t('longReply'),
|
|
1067
1461
|
React.createElement('select', {
|
|
1068
1462
|
className: 'dts-input', value: (draft && draft.longReply) || 'truncate', disabled: !writable,
|
|
@@ -1087,6 +1481,18 @@ window.__ModuleLoader__.load({
|
|
|
1087
1481
|
textField('edgeBin', t('edgeBin'), t('edgeHint')),
|
|
1088
1482
|
textField('azureRegion', t('azureRegion'), t('azureHint')),
|
|
1089
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
|
+
),
|
|
1090
1496
|
React.createElement('div', { className: 'dts-foot' },
|
|
1091
1497
|
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !writable, onClick: onClearCache }, t('clearCache')),
|
|
1092
1498
|
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !writable, onClick: save }, t('save')),
|
|
@@ -1097,263 +1503,6 @@ window.__ModuleLoader__.load({
|
|
|
1097
1503
|
}
|
|
1098
1504
|
|
|
1099
1505
|
|
|
1100
|
-
const CLIENT_IT_TERMS = [
|
|
1101
|
-
{ from: 'SQL', to: 'сиквел', whole: true, lang: 'ru' },
|
|
1102
|
-
{ from: 'Nginx', to: 'энджинкс', whole: true, lang: 'ru' },
|
|
1103
|
-
{ from: 'Kubernetes', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1104
|
-
{ from: 'K8s', to: 'кубернетис', whole: true, lang: 'ru' },
|
|
1105
|
-
{ from: 'PostgreSQL', to: 'постгрес', whole: true, lang: 'ru' },
|
|
1106
|
-
{ from: 'Redis', to: 'рэдис', whole: true, lang: 'ru' },
|
|
1107
|
-
{ from: 'Docker', to: 'докер', whole: true, lang: 'ru' },
|
|
1108
|
-
{ from: 'API', to: 'апи', whole: true, lang: 'ru' },
|
|
1109
|
-
{ from: 'JSON', to: 'джейсон', whole: true, lang: 'ru' },
|
|
1110
|
-
{ from: 'YAML', to: 'ямл', whole: true, lang: 'ru' },
|
|
1111
|
-
{ from: 'GUI', to: 'гуи', whole: true, lang: 'ru' },
|
|
1112
|
-
{ from: 'CLI', to: 'си элай', whole: true, lang: 'ru' },
|
|
1113
|
-
{ from: 'CI/CD', to: 'си ай си ди', whole: false, lang: 'ru' },
|
|
1114
|
-
{ from: 'PR', to: 'пулл реквест', whole: true, lang: 'ru' },
|
|
1115
|
-
{ from: 'Regex', to: 'регэкс', whole: true, lang: 'ru' },
|
|
1116
|
-
{ from: 'OAuth', to: 'о-аус', whole: true, lang: 'ru' },
|
|
1117
|
-
{ from: 'HTTP', to: 'эйч ти ти пи', whole: true, lang: 'ru' },
|
|
1118
|
-
{ from: 'HTTPS', to: 'эйч ти ти пи эс', whole: true, lang: 'ru' },
|
|
1119
|
-
{ from: 'URL', to: 'юрл', whole: true, lang: 'ru' },
|
|
1120
|
-
{ from: 'SSH', to: 'эс эс эйч', whole: true, lang: 'ru' },
|
|
1121
|
-
{ from: 'IP', to: 'ай пи', whole: true, lang: 'ru' },
|
|
1122
|
-
{ from: 'DNS', to: 'ди эн эс', whole: true, lang: 'ru' },
|
|
1123
|
-
{ from: 'CPU', to: 'си пи ю', whole: true, lang: 'ru' },
|
|
1124
|
-
{ from: 'GPU', to: 'джи пи ю', whole: true, lang: 'ru' },
|
|
1125
|
-
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1126
|
-
]
|
|
1127
|
-
|
|
1128
|
-
// Редактор словаря произношения: таблица правил сверху вниз.
|
|
1129
|
-
function PronEditor(props) {
|
|
1130
|
-
const t = props.t || ((key) => key)
|
|
1131
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
1132
|
-
const change = (i, patch) => {
|
|
1133
|
-
props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
1134
|
-
}
|
|
1135
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
1136
|
-
const add = () => props.onChange(rows.concat([{ from: '', to: '', whole: false, lang: '' }]))
|
|
1137
|
-
const loadIt = () => {
|
|
1138
|
-
const existing = new Set(rows.map((r) => String(r.from).toLowerCase()))
|
|
1139
|
-
const toAdd = CLIENT_IT_TERMS.filter((r) => !existing.has(String(r.from).toLowerCase()))
|
|
1140
|
-
props.onChange(rows.concat(toAdd))
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
return React.createElement('div', { className: 'dts-wrap-in' },
|
|
1144
|
-
rows.map((rule, i) => React.createElement('div', { className: 'dts-row', key: i },
|
|
1145
|
-
React.createElement('input', {
|
|
1146
|
-
className: 'dts-input dts-grow', placeholder: t('pronFrom'),
|
|
1147
|
-
value: rule.from || '', disabled: !props.writable,
|
|
1148
|
-
onChange: (e) => change(i, { from: e.target.value }),
|
|
1149
|
-
}),
|
|
1150
|
-
React.createElement('input', {
|
|
1151
|
-
className: 'dts-input dts-grow', placeholder: t('pronTo'),
|
|
1152
|
-
value: rule.to || '', disabled: !props.writable,
|
|
1153
|
-
onChange: (e) => change(i, { to: e.target.value }),
|
|
1154
|
-
}),
|
|
1155
|
-
React.createElement('label', { className: 'dts-sub', title: t('pronWhole') },
|
|
1156
|
-
React.createElement('input', {
|
|
1157
|
-
type: 'checkbox', checked: !!rule.whole, disabled: !props.writable,
|
|
1158
|
-
onChange: (e) => change(i, { whole: e.target.checked }),
|
|
1159
|
-
}),
|
|
1160
|
-
),
|
|
1161
|
-
React.createElement('input', {
|
|
1162
|
-
className: 'dts-input', style: { maxWidth: '70px' }, placeholder: t('pronLang'),
|
|
1163
|
-
value: rule.lang || '', disabled: !props.writable,
|
|
1164
|
-
onChange: (e) => change(i, { lang: e.target.value }),
|
|
1165
|
-
}),
|
|
1166
|
-
React.createElement('button', {
|
|
1167
|
-
type: 'button', className: 'dts-mini', title: t('previewRule'), disabled: !props.writable,
|
|
1168
|
-
onClick: () => {
|
|
1169
|
-
if (props.onPreviewPhrase) props.onPreviewPhrase(rule.to || rule.from)
|
|
1170
|
-
},
|
|
1171
|
-
}, '\u25b6'),
|
|
1172
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: () => remove(i) }, '\u00d7'),
|
|
1173
|
-
)),
|
|
1174
|
-
React.createElement('div', { className: 'dts-row' },
|
|
1175
|
-
React.createElement('button', { type: 'button', className: 'dts-mini', disabled: !props.writable, onClick: add }, '+'),
|
|
1176
|
-
React.createElement('button', { type: 'button', className: 'dts-link', disabled: !props.writable, onClick: loadIt }, t('loadItDictionary')),
|
|
1177
|
-
React.createElement('span', { className: 'dts-sub' }, t('pronHint')),
|
|
1178
|
-
),
|
|
1179
|
-
)
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
function RolesEditor(props) {
|
|
1183
|
-
const t = props.t || ((key) => key)
|
|
1184
|
-
const value = props.value || {}
|
|
1185
|
-
const [newRole, setNewRole] = React.useState('')
|
|
1186
|
-
const set = (r, patch) => props.onChange(Object.assign({}, value, { [r]: Object.assign({}, value[r] || {}, patch) }))
|
|
1187
|
-
const removeRole = (r) => {
|
|
1188
|
-
const next = Object.assign({}, value)
|
|
1189
|
-
delete next[r]
|
|
1190
|
-
props.onChange(next)
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
const standardKeys = ['reply', 'approval', 'error']
|
|
1194
|
-
const customKeys = Object.keys(value).filter((k) => !standardKeys.includes(k))
|
|
1195
|
-
const allKeys = [...standardKeys, ...customKeys]
|
|
1196
|
-
|
|
1197
|
-
const addCustom = () => {
|
|
1198
|
-
const name = newRole.trim().toLowerCase()
|
|
1199
|
-
if (!name || value[name]) return
|
|
1200
|
-
set(name, { provider: '', model: '', voice: '', chime: '', ssmlStyle: '' })
|
|
1201
|
-
setNewRole('')
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1205
|
-
React.createElement('span', { className: 'dts-sub' }, t('rolesHint')),
|
|
1206
|
-
allKeys.map((r) => React.createElement('div', { className: 'dts-row', key: r },
|
|
1207
|
-
React.createElement('span', { className: 'dts-sub', style: { minWidth: '110px' } }, t('role_' + r) !== ('role_' + r) ? t('role_' + r) : `👤 ${r}`),
|
|
1208
|
-
React.createElement('select', {
|
|
1209
|
-
className: 'dts-input', value: (value[r] && value[r].provider) || '', disabled: !props.writable,
|
|
1210
|
-
onChange: (e) => set(r, { provider: e.target.value }),
|
|
1211
|
-
},
|
|
1212
|
-
React.createElement('option', { value: '' }, '—'),
|
|
1213
|
-
PROVIDERS.map((p) => React.createElement('option', { key: p, value: p }, p)),
|
|
1214
|
-
),
|
|
1215
|
-
React.createElement('input', {
|
|
1216
|
-
className: 'dts-input dts-grow', placeholder: 'model',
|
|
1217
|
-
value: (value[r] && value[r].model) || '', disabled: !props.writable,
|
|
1218
|
-
onChange: (e) => set(r, { model: e.target.value }),
|
|
1219
|
-
}),
|
|
1220
|
-
React.createElement('input', {
|
|
1221
|
-
className: 'dts-input dts-grow', placeholder: 'voice',
|
|
1222
|
-
value: (value[r] && value[r].voice) || '', disabled: !props.writable,
|
|
1223
|
-
onChange: (e) => set(r, { voice: e.target.value }),
|
|
1224
|
-
}),
|
|
1225
|
-
React.createElement('input', {
|
|
1226
|
-
className: 'dts-input', style: { maxWidth: '90px' }, placeholder: t('chime'),
|
|
1227
|
-
value: (value[r] && value[r].chime) || '', disabled: !props.writable,
|
|
1228
|
-
onChange: (e) => set(r, { chime: e.target.value }),
|
|
1229
|
-
}),
|
|
1230
|
-
React.createElement('input', {
|
|
1231
|
-
className: 'dts-input dts-grow', placeholder: 'ssml',
|
|
1232
|
-
value: (value[r] && value[r].ssmlStyle) || '', disabled: !props.writable,
|
|
1233
|
-
onChange: (e) => set(r, { ssmlStyle: e.target.value }),
|
|
1234
|
-
}),
|
|
1235
|
-
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'),
|
|
1236
|
-
!standardKeys.includes(r) ? React.createElement('button', { type: 'button', className: 'dts-mini', title: t('remove'), disabled: !props.writable, onClick: () => removeRole(r) }, '\u00d7') : null,
|
|
1237
|
-
)),
|
|
1238
|
-
React.createElement('div', { className: 'dts-row' },
|
|
1239
|
-
React.createElement('input', {
|
|
1240
|
-
className: 'dts-input', style: { maxWidth: '180px' }, placeholder: t('newSubagentRole'),
|
|
1241
|
-
value: newRole, disabled: !props.writable,
|
|
1242
|
-
onChange: (e) => setNewRole(e.target.value),
|
|
1243
|
-
onKeyDown: (e) => { if (e.key === 'Enter') addCustom() },
|
|
1244
|
-
}),
|
|
1245
|
-
React.createElement('button', { type: 'button', className: 'dts-save', disabled: !props.writable || !newRole.trim(), onClick: addCustom }, t('addSubagentRole')),
|
|
1246
|
-
),
|
|
1247
|
-
)
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
function VoiceDuplexEditor(props) {
|
|
1251
|
-
const t = props.t || ((key) => key)
|
|
1252
|
-
const installed = !!props.installed
|
|
1253
|
-
|
|
1254
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1255
|
-
React.createElement('div', { className: 'dts-h' }, t('voiceDuplexTitle')),
|
|
1256
|
-
React.createElement('div', { className: 'dts-sub' }, t('voiceDuplexHint')),
|
|
1257
|
-
!installed ? React.createElement('div', {
|
|
1258
|
-
style: {
|
|
1259
|
-
padding: '10px 14px',
|
|
1260
|
-
borderRadius: '8px',
|
|
1261
|
-
border: '1px solid var(--dsw-alias-state-warning-primary, #eab308)',
|
|
1262
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1263
|
-
display: 'flex',
|
|
1264
|
-
flexDirection: 'column',
|
|
1265
|
-
gap: '4px',
|
|
1266
|
-
fontSize: '13px',
|
|
1267
|
-
},
|
|
1268
|
-
},
|
|
1269
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary, #eab308)' } },
|
|
1270
|
-
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1271
|
-
),
|
|
1272
|
-
React.createElement('span', { className: 'dts-sub' },
|
|
1273
|
-
t('voiceInstallHint') + ': '
|
|
1274
|
-
),
|
|
1275
|
-
React.createElement('code', {
|
|
1276
|
-
style: {
|
|
1277
|
-
fontFamily: 'monospace',
|
|
1278
|
-
fontSize: '12px',
|
|
1279
|
-
padding: '2px 6px',
|
|
1280
|
-
borderRadius: '4px',
|
|
1281
|
-
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1282
|
-
color: 'var(--dsw-alias-label-primary)',
|
|
1283
|
-
width: 'fit-content',
|
|
1284
|
-
},
|
|
1285
|
-
}, 'dsh plugin --profile web add @goodandready/dsh-voice')
|
|
1286
|
-
) : null,
|
|
1287
|
-
props.boolField('voiceDuplexEnabled', t('voiceDuplex'), t('voiceDuplexHintToggle'), !installed),
|
|
1288
|
-
props.boolField('vadBargeIn', t('vadBargeIn'), t('vadBargeInHint'), !installed),
|
|
1289
|
-
)
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
function MessengerIntegrationEditor(props) {
|
|
1293
|
-
const t = props.t || ((key) => key)
|
|
1294
|
-
const installed = !!props.installed
|
|
1295
|
-
|
|
1296
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1297
|
-
React.createElement('div', { className: 'dts-h' }, t('messengerIntegrationTitle')),
|
|
1298
|
-
React.createElement('div', { className: 'dts-sub' }, t('messengerIntegrationHint')),
|
|
1299
|
-
!installed ? React.createElement('div', {
|
|
1300
|
-
style: {
|
|
1301
|
-
padding: '10px 14px',
|
|
1302
|
-
borderRadius: '8px',
|
|
1303
|
-
border: '1px solid var(--dsw-alias-state-warning-primary, #eab308)',
|
|
1304
|
-
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1305
|
-
display: 'flex',
|
|
1306
|
-
flexDirection: 'column',
|
|
1307
|
-
gap: '4px',
|
|
1308
|
-
fontSize: '13px',
|
|
1309
|
-
},
|
|
1310
|
-
},
|
|
1311
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary, #eab308)' } },
|
|
1312
|
-
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1313
|
-
),
|
|
1314
|
-
React.createElement('span', { className: 'dts-sub' },
|
|
1315
|
-
t('messengerInstallHint') + ': '
|
|
1316
|
-
),
|
|
1317
|
-
React.createElement('code', {
|
|
1318
|
-
style: {
|
|
1319
|
-
fontFamily: 'monospace',
|
|
1320
|
-
fontSize: '12px',
|
|
1321
|
-
padding: '2px 6px',
|
|
1322
|
-
borderRadius: '4px',
|
|
1323
|
-
background: 'var(--dsw-alias-bg-layer-3)',
|
|
1324
|
-
color: 'var(--dsw-alias-label-primary)',
|
|
1325
|
-
width: 'fit-content',
|
|
1326
|
-
},
|
|
1327
|
-
}, 'dsh plugin --profile web add @goodandready/dsh-messenger-gateway')
|
|
1328
|
-
) : null,
|
|
1329
|
-
props.boolField('messengerTtsEnabled', t('messengerTtsEnabled'), t('messengerTtsEnabledHint'), !installed),
|
|
1330
|
-
)
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
// Панель статистики синтеза: живые счётчики хоста.
|
|
1334
|
-
function StatsPanel(props) {
|
|
1335
|
-
const t = props.t || ((key) => key)
|
|
1336
|
-
const [data, setData] = React.useState(null)
|
|
1337
|
-
const loadStats = () => {
|
|
1338
|
-
fetch('/dsh-tts/stats', { cache: 'no-store' }).then((r) => r.json()).then(setData).catch(() => {})
|
|
1339
|
-
}
|
|
1340
|
-
React.useEffect(() => { loadStats() }, [])
|
|
1341
|
-
const reset = () => {
|
|
1342
|
-
fetch('/dsh-tts/stats', { method: 'DELETE' }).then(loadStats).catch(() => {})
|
|
1343
|
-
}
|
|
1344
|
-
return React.createElement('div', { className: 'dts-block' },
|
|
1345
|
-
React.createElement('span', { className: 'dts-sub' }, data
|
|
1346
|
-
? t('statTotal') + ': ' + data.total + ' · ' + t('statHits') + ': ' + data.cacheHits + ' · ' + t('statErrors') + ': ' + data.errors
|
|
1347
|
-
: t('loading')),
|
|
1348
|
-
data && Object.keys(data.providers || {}).map((p) => React.createElement('span', { className: 'dts-sub', key: p },
|
|
1349
|
-
p + ': ' + data.providers[p].n + ' / ' + data.providers[p].ms + 'ms')),
|
|
1350
|
-
React.createElement('button', { type: 'button', className: 'dts-link', onClick: reset }, t('statReset')),
|
|
1351
|
-
)
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// Карточка во вкладке «Настройки плагинов» — разметка и стили по
|
|
1355
|
-
// единому канону (issue #19): li в списке ядра, шапка со сбросом темы,
|
|
1356
|
-
// тело с чертой-разделителем. Шеврон — наш, ядро его не даёт.
|
|
1357
1506
|
function TtsCard(props) {
|
|
1358
1507
|
const t = (props && props.t) || ((key) => key)
|
|
1359
1508
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1381,17 +1530,17 @@ window.__ModuleLoader__.load({
|
|
|
1381
1530
|
),
|
|
1382
1531
|
),
|
|
1383
1532
|
open ? React.createElement('div', { className: 'dts-body' },
|
|
1384
|
-
React.createElement(
|
|
1533
|
+
React.createElement(ErrorBoundary, null,
|
|
1534
|
+
React.createElement(TtsSection, Object.assign({}, props, { compact: true, ctx: props && props.ctx })),
|
|
1535
|
+
),
|
|
1385
1536
|
) : null,
|
|
1386
1537
|
)
|
|
1387
1538
|
}
|
|
1388
1539
|
|
|
1389
|
-
//
|
|
1390
|
-
// открыть недавние озвучки для реплая.
|
|
1540
|
+
// Input-dock button: shows what is playing, stop control, and recents for replay.
|
|
1391
1541
|
function SpeakerControl(props) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
// не донесёт: без него кнопки остались бы без подписей вовсе.
|
|
1542
|
+
// A slot with `locale` injects translations into props; the fallback is the
|
|
1543
|
+
// binder created at registration, in case the slot drops translations.
|
|
1395
1544
|
const t = (props && props.t) || fallbackDockText
|
|
1396
1545
|
const p = usePlayer()
|
|
1397
1546
|
const [showList, setShowList] = React.useState(false)
|
|
@@ -1450,8 +1599,8 @@ window.__ModuleLoader__.load({
|
|
|
1450
1599
|
let fallbackDockText = (key) => key
|
|
1451
1600
|
|
|
1452
1601
|
function registerSpeaker(ctx) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1602
|
+
// `locale` in the slot descriptor is how core injects translations into props.
|
|
1603
|
+
// Without it the dock would show raw keys only.
|
|
1455
1604
|
fallbackDockText = ctx.locale.bind(NS)
|
|
1456
1605
|
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
1457
1606
|
{
|
|
@@ -1466,14 +1615,10 @@ window.__ModuleLoader__.load({
|
|
|
1466
1615
|
}
|
|
1467
1616
|
|
|
1468
1617
|
function registerSettings(ctx) {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
// load plugins» с перечнем ни в чём не повинных соседей.
|
|
1474
|
-
//
|
|
1475
|
-
// Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас —
|
|
1476
|
-
// уступаем, свой английский при этом всё равно встаёт на место.
|
|
1618
|
+
// Dictionary packages may also register languages for other namespaces.
|
|
1619
|
+
// Core throws on duplicate namespace+language; an unguarded call used to
|
|
1620
|
+
// take down the whole plugin ("Failed to load plugins"). Register each
|
|
1621
|
+
// language separately: skip if taken; English still lands.
|
|
1477
1622
|
const addLocale = (locale, dictionary) => {
|
|
1478
1623
|
try {
|
|
1479
1624
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -1485,13 +1630,12 @@ window.__ModuleLoader__.load({
|
|
|
1485
1630
|
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1486
1631
|
return () => { for (const off of undo) off() }
|
|
1487
1632
|
}, 'dsh-tts: словари')
|
|
1488
|
-
|
|
1489
|
-
|
|
1633
|
+
// The sidebar draws the section label, not our component: props.t never
|
|
1634
|
+
// reaches it, so bind the translator to the namespace ourselves.
|
|
1490
1635
|
const t = ctx.locale.bind(NS)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
// не появится молча, без единой ошибки в журнале.
|
|
1636
|
+
// Primary settings home is the plugin-settings tab card. The tab looks up
|
|
1637
|
+
// a slot by entryKey equal to the namespace name: key must equal NS or
|
|
1638
|
+
// the card never appears, with no log error.
|
|
1495
1639
|
const tryPluginItem = () => {
|
|
1496
1640
|
try {
|
|
1497
1641
|
const res = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
@@ -1510,8 +1654,8 @@ window.__ModuleLoader__.load({
|
|
|
1510
1654
|
}
|
|
1511
1655
|
}
|
|
1512
1656
|
if (tryPluginItem()) return
|
|
1513
|
-
|
|
1514
|
-
|
|
1657
|
+
// Fallback for builds without settings.plugin.item: keep the sidebar section
|
|
1658
|
+
// so settings cannot disappear.
|
|
1515
1659
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1516
1660
|
{
|
|
1517
1661
|
name: 'settings.section',
|
|
@@ -1521,7 +1665,7 @@ window.__ModuleLoader__.load({
|
|
|
1521
1665
|
label: () => t('title'),
|
|
1522
1666
|
inject: () => ({ ctx: ctx }),
|
|
1523
1667
|
},
|
|
1524
|
-
TtsSection,
|
|
1668
|
+
(props) => React.createElement(ErrorBoundary, null, React.createElement(TtsSection, props)),
|
|
1525
1669
|
))
|
|
1526
1670
|
}
|
|
1527
1671
|
|
|
@@ -1587,3 +1731,4 @@ window.__ModuleLoader__.load({
|
|
|
1587
1731
|
|
|
1588
1732
|
// ModelManager UI placeholder - manual install buttons for Kokoro/F5
|
|
1589
1733
|
// Actual implementation would show download progress and status
|
|
1734
|
+
|