@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.
@@ -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
- }