@goodandready/dsh-voice 0.8.25 → 0.8.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/README.ru.md +10 -0
- package/README.zh.md +10 -0
- package/lib/client.js +216 -12
- package/lib/index.js +2 -0
- package/package.json +4 -2
- package/lib/client-src/00-open.js +0 -24
- package/lib/client-src/10-locale.js +0 -165
- package/lib/client-src/20-css.js +0 -42
- package/lib/client-src/30-core.js +0 -286
- package/lib/client-src/40-recording.js +0 -395
- package/lib/client-src/41-buttons.js +0 -31
- package/lib/client-src/50-visualizers.js +0 -121
- package/lib/client-src/60-composer.js +0 -308
- package/lib/client-src/70-settings-base.js +0 -126
- package/lib/client-src/71-chains.js +0 -88
- package/lib/client-src/72-voice-section.js +0 -597
- package/lib/client-src/73-plugin-card.js +0 -38
- package/lib/client-src/90-close.js +0 -23
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
function RecordPill(props) {
|
|
2
|
-
const v = useVoice()
|
|
3
|
-
const canvasRef = React.useRef(null)
|
|
4
|
-
const audioRef = React.useRef(null)
|
|
5
|
-
const [isPlaying, setIsPlaying] = React.useState(false)
|
|
6
|
-
voice.inputActions = props.inputActions
|
|
7
|
-
voice.input = props.input
|
|
8
|
-
const ctx = props.ctx
|
|
9
|
-
|
|
10
|
-
// VAD: accumulate silence and cut the phrase when the pause exceeds the threshold.
|
|
11
|
-
React.useEffect(() => {
|
|
12
|
-
if (v.phase !== 'recording') return
|
|
13
|
-
const tick = 50
|
|
14
|
-
const dispose = ctx.interval(() => {
|
|
15
|
-
const rec = voice.rec
|
|
16
|
-
if (!rec) return
|
|
17
|
-
const level = currentLevel(rec)
|
|
18
|
-
voice.levels.push(level)
|
|
19
|
-
if (voice.levels.length > 150) voice.levels.shift()
|
|
20
|
-
if (level > 0.06) { rec.hadSpeech = true; rec.silenceMs = 0 }
|
|
21
|
-
else if (rec.hadSpeech) rec.silenceMs += tick
|
|
22
|
-
const speaking = level > 0.06
|
|
23
|
-
if (voice.speaking !== speaking) { voice.speaking = speaking; voice.notify() }
|
|
24
|
-
const adapt = Number(voice.settings.vadAdapt) || 0
|
|
25
|
-
let effectiveVad = Number(voice.settings.vadSilenceMs) || 700
|
|
26
|
-
if (adapt > 0 && rec.hadSpeech) {
|
|
27
|
-
// Adaptive threshold (#41): speech density over ~1s (20 samples).
|
|
28
|
-
// Dense speech -> lower threshold (cut more precisely);
|
|
29
|
-
// pause-heavy -> threshold rises toward base (do not cut on breaths).
|
|
30
|
-
const win = voice.levels.slice(-20)
|
|
31
|
-
const density = win.length ? win.filter((v) => v > 0.06).length / win.length : 0
|
|
32
|
-
const k = adapt * (density - 0.5) * 2
|
|
33
|
-
effectiveVad = Math.max(150, Math.round(Number(voice.settings.vadSilenceMs) * (1 - k)))
|
|
34
|
-
}
|
|
35
|
-
// Continuous mode (#40): cut on a timer while speech continues,
|
|
36
|
-
// without waiting for a long pause.
|
|
37
|
-
const stream = !!voice.settings.stream && rec.mode === 'dictation'
|
|
38
|
-
if (stream && rec.hadSpeech && !rec.cutting) {
|
|
39
|
-
rec.streamMs += tick
|
|
40
|
-
const chunk = Number(voice.settings.streamChunkMs) || 1200
|
|
41
|
-
if (rec.streamMs >= chunk) { cutPhrase(); return }
|
|
42
|
-
} else {
|
|
43
|
-
rec.streamMs = 0
|
|
44
|
-
}
|
|
45
|
-
if (rec.mode === 'dictation' && rec.hadSpeech && rec.silenceMs >= effectiveVad) {
|
|
46
|
-
rec.streamMs = 0
|
|
47
|
-
cutPhrase()
|
|
48
|
-
}
|
|
49
|
-
}, tick)
|
|
50
|
-
return () => dispose()
|
|
51
|
-
}, [v.phase])
|
|
52
|
-
|
|
53
|
-
// Waveform / visualizer animation: requestAnimationFrame with HiDPI scaling
|
|
54
|
-
React.useEffect(() => {
|
|
55
|
-
if (v.phase !== 'recording') return
|
|
56
|
-
let frame = 0
|
|
57
|
-
let animId = null
|
|
58
|
-
let running = true
|
|
59
|
-
|
|
60
|
-
const render = () => {
|
|
61
|
-
if (!running) return
|
|
62
|
-
const canvas = canvasRef.current
|
|
63
|
-
if (canvas) {
|
|
64
|
-
const g = canvas.getContext('2d')
|
|
65
|
-
const dpr = typeof window !== 'undefined' ? (window.devicePixelRatio || 1) : 1
|
|
66
|
-
const rect = canvas.getBoundingClientRect()
|
|
67
|
-
const cssW = rect.width || 720
|
|
68
|
-
const cssH = rect.height || 40
|
|
69
|
-
const targetW = Math.max(1, Math.round(cssW * dpr))
|
|
70
|
-
const targetH = Math.max(1, Math.round(cssH * dpr))
|
|
71
|
-
|
|
72
|
-
if (canvas.width !== targetW || canvas.height !== targetH) {
|
|
73
|
-
canvas.width = targetW
|
|
74
|
-
canvas.height = targetH
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
g.save()
|
|
78
|
-
g.scale(dpr, dpr)
|
|
79
|
-
g.clearRect(0, 0, cssW, cssH)
|
|
80
|
-
|
|
81
|
-
if (!voice.waveColor) {
|
|
82
|
-
try { voice.waveColor = getComputedStyle(canvas).color || '#fff' } catch (e) { voice.waveColor = '#fff' }
|
|
83
|
-
}
|
|
84
|
-
const levels = voice.levels
|
|
85
|
-
const style = (voice.settings && voice.settings.visualizerStyle) || 'liquid-wave'
|
|
86
|
-
frame++
|
|
87
|
-
|
|
88
|
-
if (style === 'off') {
|
|
89
|
-
g.beginPath()
|
|
90
|
-
g.globalAlpha = 0.25
|
|
91
|
-
g.strokeStyle = voice.waveColor
|
|
92
|
-
g.lineWidth = 1
|
|
93
|
-
g.moveTo(10, cssH / 2)
|
|
94
|
-
g.lineTo(cssW - 10, cssH / 2)
|
|
95
|
-
g.stroke()
|
|
96
|
-
g.globalAlpha = 1
|
|
97
|
-
} else if (style === 'dynamic-orb') {
|
|
98
|
-
drawDynamicOrb(g, cssW, cssH, levels, voice.waveColor, frame)
|
|
99
|
-
} else if (style === 'bars') {
|
|
100
|
-
drawClassicBars(g, cssW, cssH, levels, voice.waveColor)
|
|
101
|
-
} else {
|
|
102
|
-
drawLiquidWave(g, cssW, cssH, levels, voice.waveColor, frame)
|
|
103
|
-
}
|
|
104
|
-
g.restore()
|
|
105
|
-
}
|
|
106
|
-
animId = requestAnimationFrame(render)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
animId = requestAnimationFrame(render)
|
|
110
|
-
return () => {
|
|
111
|
-
running = false
|
|
112
|
-
if (animId) cancelAnimationFrame(animId)
|
|
113
|
-
}
|
|
114
|
-
}, [v.phase])
|
|
115
|
-
|
|
116
|
-
// Message-mode cancel window.
|
|
117
|
-
React.useEffect(() => {
|
|
118
|
-
if (v.phase !== 'pending') return
|
|
119
|
-
const tick = 100
|
|
120
|
-
const dispose = ctx.interval(() => {
|
|
121
|
-
const p = voice.pending
|
|
122
|
-
if (!p) return
|
|
123
|
-
if (p.undoOnly) {
|
|
124
|
-
// Undo-only mode (#29-5): when the window expires just hide the
|
|
125
|
-
// panel; the text either stayed or was already undone.
|
|
126
|
-
p.leftMs -= tick
|
|
127
|
-
if (p.leftMs <= 0) voice.set({ phase: 'idle' })
|
|
128
|
-
else voice.notify()
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
p.leftMs -= tick
|
|
132
|
-
if (p.leftMs <= 0) { submitPending(); return }
|
|
133
|
-
voice.notify()
|
|
134
|
-
}, tick)
|
|
135
|
-
return () => dispose()
|
|
136
|
-
}, [v.phase])
|
|
137
|
-
|
|
138
|
-
if (v.phase === 'idle') {
|
|
139
|
-
if (!voice.showPlayer || !voice.lastNote || !voice.lastNote.url) return null
|
|
140
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
141
|
-
React.createElement('div', { className: 'dvo-audio-wrap' },
|
|
142
|
-
React.createElement('button', {
|
|
143
|
-
type: 'button', className: 'dvo-audio-play',
|
|
144
|
-
title: isPlaying ? t('pause') : t('play'),
|
|
145
|
-
onClick: () => {
|
|
146
|
-
const el = audioRef.current
|
|
147
|
-
if (!el) return
|
|
148
|
-
if (el.paused) { el.play().catch(() => {}); setIsPlaying(true) }
|
|
149
|
-
else { el.pause(); setIsPlaying(false) }
|
|
150
|
-
},
|
|
151
|
-
}, isPlaying ? pauseIcon() : playIcon()),
|
|
152
|
-
React.createElement('audio', {
|
|
153
|
-
ref: audioRef, src: voice.lastNote.url,
|
|
154
|
-
onEnded: () => setIsPlaying(false),
|
|
155
|
-
onPause: () => setIsPlaying(false),
|
|
156
|
-
onPlay: () => setIsPlaying(true),
|
|
157
|
-
}),
|
|
158
|
-
React.createElement('span', { className: 'dvo-audio-time' }, t('lastRecording')),
|
|
159
|
-
),
|
|
160
|
-
React.createElement('span', { className: 'dvo-status' }, voice.lastNote.text || ''),
|
|
161
|
-
React.createElement('button', {
|
|
162
|
-
type: 'button', className: 'dvo-pbtn', title: t('hide'),
|
|
163
|
-
onClick: () => {
|
|
164
|
-
if (audioRef.current) audioRef.current.pause()
|
|
165
|
-
setIsPlaying(false)
|
|
166
|
-
voice.set({ showPlayer: false })
|
|
167
|
-
},
|
|
168
|
-
}, xIcon()),
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (v.phase === 'recording') {
|
|
173
|
-
const inBrowser = !!voice.browser
|
|
174
|
-
const rec = voice.rec
|
|
175
|
-
const hint = v.mode === 'dictation' ? t('dictationPill') : t('messagePill')
|
|
176
|
-
// Live caption of what is heard right now. Until the browser emits a
|
|
177
|
-
// final chunk the text is interim and changes on screen.
|
|
178
|
-
const caption = voice.caption || (inBrowser ? '' : null)
|
|
179
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
180
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('cancel'), onClick: cancelCurrent }, xIcon()),
|
|
181
|
-
inBrowser
|
|
182
|
-
? null
|
|
183
|
-
: React.createElement('canvas', { className: 'dvo-wave', ref: canvasRef, width: 720, height: 40 }),
|
|
184
|
-
React.createElement('span', { className: 'dvo-status' },
|
|
185
|
-
caption
|
|
186
|
-
? caption
|
|
187
|
-
: (voice.holding
|
|
188
|
-
? t('holdHint')
|
|
189
|
-
: (inBrowser
|
|
190
|
-
? t('listening')
|
|
191
|
-
: (rec && rec.hadSpeech ? (v.speaking ? t('speaking') : t('silence')) : hint)))),
|
|
192
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('stop'), onClick: stopCurrent }, stopIcon()),
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (v.phase === 'processing') {
|
|
197
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
198
|
-
React.createElement('span', { className: 'dvo-status' }, spinIcon(), t('transcribing')))
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (v.phase === 'pending') {
|
|
202
|
-
const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
|
|
203
|
-
if (voice.pending && voice.pending.undoOnly) {
|
|
204
|
-
// Cancel window for delayed dictation insert (#29-5).
|
|
205
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
206
|
-
React.createElement('span', { className: 'dvo-status' }, t('undo'), ': ', left, t('secondsShort')),
|
|
207
|
-
React.createElement('button', {
|
|
208
|
-
type: 'button', className: 'dvo-pbtn', title: t('undo'),
|
|
209
|
-
onClick: async () => {
|
|
210
|
-
const msg = await undoLastInsert()
|
|
211
|
-
voice.set({ phase: 'idle', error: msg === t('undone') ? '' : msg })
|
|
212
|
-
},
|
|
213
|
-
}, xIcon()),
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
217
|
-
voice.lastNote && voice.lastNote.url
|
|
218
|
-
? React.createElement('div', { className: 'dvo-audio-wrap' },
|
|
219
|
-
React.createElement('button', {
|
|
220
|
-
type: 'button', className: 'dvo-audio-play',
|
|
221
|
-
title: isPlaying ? t('pause') : t('play'),
|
|
222
|
-
onClick: () => {
|
|
223
|
-
const el = audioRef.current
|
|
224
|
-
if (!el) return
|
|
225
|
-
if (el.paused) { el.play().catch(() => {}); setIsPlaying(true) }
|
|
226
|
-
else { el.pause(); setIsPlaying(false) }
|
|
227
|
-
},
|
|
228
|
-
}, isPlaying ? pauseIcon() : playIcon()),
|
|
229
|
-
React.createElement('audio', {
|
|
230
|
-
ref: audioRef, src: voice.lastNote.url,
|
|
231
|
-
onEnded: () => setIsPlaying(false),
|
|
232
|
-
onPause: () => setIsPlaying(false),
|
|
233
|
-
onPlay: () => setIsPlaying(true),
|
|
234
|
-
}),
|
|
235
|
-
React.createElement('span', { className: 'dvo-audio-time' }, t('listenBack')),
|
|
236
|
-
)
|
|
237
|
-
: null,
|
|
238
|
-
React.createElement('span', { className: 'dvo-status' }, t('sendingIn')),
|
|
239
|
-
React.createElement('span', { className: 'dvo-count' }, left + t('secondsShort')),
|
|
240
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('keepPending'), onClick: keepPending }, xIcon()),
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return React.createElement('div', { className: 'dvo-pill' },
|
|
245
|
-
React.createElement('span', { className: 'dvo-status dvo-err' }, warnIcon(), v.error),
|
|
246
|
-
React.createElement('button', { type: 'button', className: 'dvo-pbtn', title: t('hide'), onClick: () => voice.set({ phase: 'idle', error: '' }) }, xIcon()),
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// --------------------------------------------------------------- slots
|
|
251
|
-
function registerComposer(ctx) {
|
|
252
|
-
// The hotkey lives for as long as the plugin is applied and can change
|
|
253
|
-
// without restart: the settings card broadcasts and the composer reloads.
|
|
254
|
-
ctx.effect(() => {
|
|
255
|
-
let dispose = () => {}
|
|
256
|
-
let alive = true
|
|
257
|
-
|
|
258
|
-
const reload = () => {
|
|
259
|
-
fetch('/dsh-voice/status', { cache: 'no-store' })
|
|
260
|
-
.then((res) => res.json())
|
|
261
|
-
.then((data) => {
|
|
262
|
-
if (!alive) return
|
|
263
|
-
dispose()
|
|
264
|
-
dispose = () => {}
|
|
265
|
-
const key = data && data.hotkey
|
|
266
|
-
voice.hotkey = key || ''
|
|
267
|
-
if (key) dispose = installHotkey(ctx, key, 'message')
|
|
268
|
-
// The composer needs fresh settings without opening the card.
|
|
269
|
-
Object.assign(voice.settings, {
|
|
270
|
-
beep: !!(data && data.beep),
|
|
271
|
-
micDeviceId: String((data && data.micDeviceId) || ''),
|
|
272
|
-
historyLimit: Number(data && data.historyLimit),
|
|
273
|
-
voiceCommands: !!(data && data.voiceCommands),
|
|
274
|
-
sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
|
|
275
|
-
stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
|
|
276
|
-
streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
|
|
277
|
-
vadAdapt: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadAdapt) || 0,
|
|
278
|
-
wakeWord: String((data && data.wakeWord) || ''),
|
|
279
|
-
bargeIn: !!(data && data.bargeIn),
|
|
280
|
-
polishSend: !!(data && data.modes && data.modes.message && data.modes.message.polishSend),
|
|
281
|
-
sessionCommands: !!(data && data.modes && data.modes.message && data.modes.message.sessionCommands),
|
|
282
|
-
noiseSuppression: data && data.noiseSuppression !== false,
|
|
283
|
-
contextGlossary: data && data.contextGlossary !== false,
|
|
284
|
-
visualizerStyle: (data && data.visualizerStyle) || 'liquid-wave',
|
|
285
|
-
})
|
|
286
|
-
})
|
|
287
|
-
.catch(() => { /* no host hint — no hotkey */ })
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
reload()
|
|
291
|
-
window.addEventListener('dsh-voice:settings-saved', reload)
|
|
292
|
-
return () => {
|
|
293
|
-
alive = false
|
|
294
|
-
window.removeEventListener('dsh-voice:settings-saved', reload)
|
|
295
|
-
dispose()
|
|
296
|
-
}
|
|
297
|
-
}, 'dsh-voice: hold hotkey')
|
|
298
|
-
|
|
299
|
-
ctx.slots.inject('conversation.input.right', () => ctx.slots.register(
|
|
300
|
-
{ name: 'conversation.input.right', id: '@goodandready/dsh-voice', order: 6, label: () => t('title') },
|
|
301
|
-
(props) => React.createElement(VoiceButtons, { input: props.input, inputActions: props.inputActions }),
|
|
302
|
-
))
|
|
303
|
-
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
304
|
-
{ name: 'conversation.input.dock', id: 'dsh-voice-rec', order: 0, label: () => t('recordSlot') },
|
|
305
|
-
(props) => React.createElement(RecordPill, { input: props.input, inputActions: props.inputActions, ctx: ctx }),
|
|
306
|
-
))
|
|
307
|
-
}
|
|
308
|
-
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
// ------------------------------------------------------- settings page
|
|
2
|
-
const BUILTIN = [
|
|
3
|
-
'browser', 'deepgram', 'groq', 'hf', 'local-whisper', 'sensevoice',
|
|
4
|
-
// Presets: address and model are filled on the host; only a key is needed.
|
|
5
|
-
'openai', 'siliconflow', 'deepinfra', 'fireworks', 'mistral', 'openrouter',
|
|
6
|
-
]
|
|
7
|
-
const TEMPLATES = ['openai-transcriptions', 'openai-chat-audio']
|
|
8
|
-
// Resolve at render time: t() must not be captured before locale bind.
|
|
9
|
-
const MODEL_HINT_KEYS = {
|
|
10
|
-
browser: 'browserHint',
|
|
11
|
-
openai: 'openaiHint',
|
|
12
|
-
siliconflow: 'siliconflowHint',
|
|
13
|
-
deepinfra: 'deepinfraHint',
|
|
14
|
-
fireworks: 'fireworksHint',
|
|
15
|
-
mistral: 'mistralHint',
|
|
16
|
-
openrouter: 'openrouterHint',
|
|
17
|
-
'local-whisper': 'localHint',
|
|
18
|
-
sensevoice: 'sensevoiceHint',
|
|
19
|
-
}
|
|
20
|
-
const MODEL_HINT_STATIC = {
|
|
21
|
-
deepgram: 'nova-2',
|
|
22
|
-
groq: 'whisper-large-v3-turbo',
|
|
23
|
-
hf: 'openai/whisper-large-v3',
|
|
24
|
-
}
|
|
25
|
-
function modelHint(provider) {
|
|
26
|
-
const key = MODEL_HINT_KEYS[provider]
|
|
27
|
-
if (key) return t(key)
|
|
28
|
-
return MODEL_HINT_STATIC[provider] || ''
|
|
29
|
-
}
|
|
30
|
-
const LANGS = ['auto', 'ru', 'en', 'uk', 'de']
|
|
31
|
-
|
|
32
|
-
const SET_CSS =
|
|
33
|
-
'.cb-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}' +
|
|
34
|
-
'.cb-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:16px 18px;display:flex;flex-direction:column;gap:12px}' +
|
|
35
|
-
'.cb-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
36
|
-
'.cb-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-4px;line-height:1.4}' +
|
|
37
|
-
'.cb-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}' +
|
|
38
|
-
'.cb-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:12px}' +
|
|
39
|
-
'.cb-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:4px;font-weight:500}' +
|
|
40
|
-
'.cb-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
|
|
41
|
-
'.cb-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
|
|
42
|
-
'.cb-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
|
|
43
|
-
'.cb-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
44
|
-
'.cb-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}' +
|
|
45
|
-
'.cb-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 12px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}' +
|
|
46
|
-
'.cb-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed, var(--dsw-alias-border-l2))}' +
|
|
47
|
-
'.cb-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}' +
|
|
48
|
-
'.cb-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.88}' +
|
|
49
|
-
'.cb-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}' +
|
|
50
|
-
'.cb-btn-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;width:28px;height:28px;cursor:pointer;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;font-size:13px}' +
|
|
51
|
-
'.cb-btn-mini:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}' +
|
|
52
|
-
'.cb-field{display:flex;flex-direction:column;gap:6px;padding:6px 0;font-size:13px;color:var(--dsw-alias-label-primary)}' +
|
|
53
|
-
'.cb-field select,.cb-field input[type="text"],.cb-field input[type="number"],.cb-field textarea{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:13px;box-sizing:border-box}' +
|
|
54
|
-
'.cb-field textarea{height:auto;padding:8px 10px;font-family:inherit}' +
|
|
55
|
-
'.cb-check-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:var(--dsw-alias-label-primary);user-select:none}' +
|
|
56
|
-
'.cb-check-label input[type="checkbox"]{width:16px;height:16px;cursor:pointer}' +
|
|
57
|
-
'.cb-alert-err{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px}' +
|
|
58
|
-
'.dvs-wrap{display:flex;flex-direction:column;gap:18px;padding:4px 0;max-width:960px}' +
|
|
59
|
-
'.dvs-block{display:flex;flex-direction:column;gap:10px}' +
|
|
60
|
-
'.dvs-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
61
|
-
'.dvs-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
62
|
-
'.dvs-row{display:flex;gap:8px;align-items:center}' +
|
|
63
|
-
'.dvs-row select,.dvs-row input{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:6px 10px;font-size:13px}' +
|
|
64
|
-
'.dvs-row .dvs-model{flex:1}' +
|
|
65
|
-
'.dvs-card{display:flex;flex-direction:column;gap:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px}' +
|
|
66
|
-
'.dvs-card input,.dvs-card select{background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
|
|
67
|
-
'.dvs-wait{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5;max-width:520px}' +
|
|
68
|
-
'.dvs-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
|
|
69
|
-
'.dvs-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
|
|
70
|
-
'.dvo-pcard{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px}' +
|
|
71
|
-
'.dvo-phead{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}' +
|
|
72
|
-
'.dvo-pheadtext{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}' +
|
|
73
|
-
'.dvo-ptitle{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}' +
|
|
74
|
-
'.dvo-pdesc{color:var(--dsw-alias-label-secondary);font-size:13px}' +
|
|
75
|
-
'.dvo-pchev{flex:none;display:flex;color:var(--dsw-alias-label-secondary);transition:transform .15s ease}' +
|
|
76
|
-
'.dvo-pcardOpen .dvo-pchev{transform:rotate(180deg)}' +
|
|
77
|
-
'.dvo-pbody{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}'
|
|
78
|
-
const setCssId = 'dsh-voice/settings.module.css'
|
|
79
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + setCssId + '"]')) {
|
|
80
|
-
const tag = document.createElement('style')
|
|
81
|
-
tag.textContent = SET_CSS
|
|
82
|
-
tag.setAttribute('data-dsh-plugin', 'dsh-voice')
|
|
83
|
-
tag.dataset.pluginCss = setCssId
|
|
84
|
-
document.head.appendChild(tag)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function createErrorBoundary() {
|
|
88
|
-
if (!React || typeof React.Component !== 'function') {
|
|
89
|
-
return function NoopBoundary(props) { return props?.children || null }
|
|
90
|
-
}
|
|
91
|
-
return class ErrorBoundary extends React.Component {
|
|
92
|
-
constructor(props) {
|
|
93
|
-
super(props)
|
|
94
|
-
this.state = { hasError: false, error: null }
|
|
95
|
-
}
|
|
96
|
-
static getDerivedStateFromError(error) {
|
|
97
|
-
return { hasError: true, error }
|
|
98
|
-
}
|
|
99
|
-
componentDidCatch(error, errorInfo) {
|
|
100
|
-
console.error('[dsh-voice] React Error:', error, errorInfo)
|
|
101
|
-
}
|
|
102
|
-
render() {
|
|
103
|
-
if (this.state.hasError) {
|
|
104
|
-
return React.createElement(
|
|
105
|
-
'div',
|
|
106
|
-
{ className: 'cb-alert-err', style: { margin: '12px 0', padding: '14px', borderRadius: '8px' } },
|
|
107
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ ' + t('uiError')),
|
|
108
|
-
React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
|
|
109
|
-
React.createElement(
|
|
110
|
-
'button',
|
|
111
|
-
{
|
|
112
|
-
type: 'button',
|
|
113
|
-
className: 'cb-btn',
|
|
114
|
-
style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
|
|
115
|
-
onClick: () => this.setState({ hasError: false, error: null }),
|
|
116
|
-
},
|
|
117
|
-
t('retry'),
|
|
118
|
-
),
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
return this.props?.children || null
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const ErrorBoundary = createErrorBoundary()
|
|
126
|
-
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
// One chain editor: provider+model rows with reordering.
|
|
2
|
-
function ChainEditor(props) {
|
|
3
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
4
|
-
const change = (i, patch) => {
|
|
5
|
-
const next = rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r))
|
|
6
|
-
props.onChange(next)
|
|
7
|
-
}
|
|
8
|
-
const move = (i, delta) => {
|
|
9
|
-
const j = i + delta
|
|
10
|
-
if (j < 0 || j >= rows.length) return
|
|
11
|
-
const next = rows.slice()
|
|
12
|
-
const tmp = next[i]; next[i] = next[j]; next[j] = tmp
|
|
13
|
-
props.onChange(next)
|
|
14
|
-
}
|
|
15
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
16
|
-
const add = () => props.onChange(rows.concat([{ provider: 'local-whisper', model: '' }]))
|
|
17
|
-
const options = Array.isArray(props.options) && props.options.length ? props.options : BUILTIN
|
|
18
|
-
|
|
19
|
-
return React.createElement('div', { className: 'dvs-block' },
|
|
20
|
-
rows.map((row, i) => React.createElement('div', { className: 'dvs-row', key: i },
|
|
21
|
-
React.createElement('select', {
|
|
22
|
-
value: row.provider, disabled: !props.writable,
|
|
23
|
-
onChange: (e) => change(i, { provider: e.target.value }),
|
|
24
|
-
}, (options.indexOf(row.provider) < 0 ? options.concat([row.provider]) : options)
|
|
25
|
-
.map((p) => React.createElement('option', { key: p, value: p }, p))),
|
|
26
|
-
React.createElement('input', {
|
|
27
|
-
className: 'dvs-model', value: row.model || '', disabled: !props.writable,
|
|
28
|
-
placeholder: modelHint(row.provider), onChange: (e) => change(i, { model: e.target.value }),
|
|
29
|
-
}),
|
|
30
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('up'), disabled: !props.writable, onClick: () => move(i, -1) }, '↑'),
|
|
31
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('down'), disabled: !props.writable, onClick: () => move(i, 1) }, '↓'),
|
|
32
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('remove'), disabled: !props.writable, onClick: () => remove(i) }, '×'),
|
|
33
|
-
)),
|
|
34
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
35
|
-
React.createElement('button', { type: 'button', className: 'dvs-mini', title: t('addProvider'), disabled: !props.writable, onClick: add }, '+'),
|
|
36
|
-
React.createElement('span', { className: 'dvs-sub' }, t('chainHint')),
|
|
37
|
-
),
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Custom providers: name, API template, where to call, how to authorize.
|
|
42
|
-
function CustomEditor(props) {
|
|
43
|
-
const rows = Array.isArray(props.value) ? props.value : []
|
|
44
|
-
const change = (i, patch) => props.onChange(rows.map((r, k) => (k === i ? Object.assign({}, r, patch) : r)))
|
|
45
|
-
const remove = (i) => props.onChange(rows.filter((_, k) => k !== i))
|
|
46
|
-
const add = () => props.onChange(rows.concat([
|
|
47
|
-
{ key: '', template: 'openai-transcriptions', baseURL: '', model: '', keyEnv: '', prompt: '' },
|
|
48
|
-
]))
|
|
49
|
-
const field = (i, row, name, placeholder, wide) => React.createElement('input', {
|
|
50
|
-
className: wide ? 'dvs-model' : '', value: row[name] || '', placeholder: placeholder,
|
|
51
|
-
disabled: !props.writable, onChange: (e) => change(i, { [name]: e.target.value }),
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return React.createElement('div', { className: 'dvs-block' },
|
|
55
|
-
rows.map((row, i) => React.createElement('div', { className: 'dvs-card', key: i },
|
|
56
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
57
|
-
field(i, row, 'key', t('customName')),
|
|
58
|
-
React.createElement('select', {
|
|
59
|
-
value: row.template || 'openai-transcriptions', disabled: !props.writable,
|
|
60
|
-
onChange: (e) => change(i, { template: e.target.value }),
|
|
61
|
-
}, TEMPLATES.map((t) => React.createElement('option', { key: t, value: t }, t))),
|
|
62
|
-
React.createElement('button', {
|
|
63
|
-
type: 'button', className: 'dvs-mini', title: t('remove'),
|
|
64
|
-
disabled: !props.writable, onClick: () => remove(i),
|
|
65
|
-
}, '\u00d7'),
|
|
66
|
-
),
|
|
67
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
68
|
-
field(i, row, 'baseURL', 'https://openrouter.ai/api/v1', true),
|
|
69
|
-
),
|
|
70
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
71
|
-
field(i, row, 'model', t('customModel'), true),
|
|
72
|
-
field(i, row, 'keyEnv', t('customKeyName')),
|
|
73
|
-
),
|
|
74
|
-
row.template === 'openai-chat-audio'
|
|
75
|
-
? React.createElement('div', { className: 'dvs-row' },
|
|
76
|
-
field(i, row, 'prompt', t('customModelHint'), true))
|
|
77
|
-
: null,
|
|
78
|
-
)),
|
|
79
|
-
React.createElement('div', { className: 'dvs-row' },
|
|
80
|
-
React.createElement('button', {
|
|
81
|
-
type: 'button', className: 'dvs-mini', title: t('addCustom'),
|
|
82
|
-
disabled: !props.writable, onClick: add,
|
|
83
|
-
}, '+'),
|
|
84
|
-
React.createElement('span', { className: 'dvs-sub' },
|
|
85
|
-
t('openrouterWarning')),
|
|
86
|
-
),
|
|
87
|
-
)
|
|
88
|
-
}
|