@goodandready/dsh-voice 0.9.31 → 0.9.33
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 +255 -252
- package/README.ru.md +254 -251
- package/README.zh.md +182 -179
- package/lib/client.js +791 -60
- package/lib/dictation-modes.js +28 -0
- package/lib/host-routes.js +222 -188
- package/lib/macros.js +53 -0
- package/lib/polish.js +14 -6
- package/lib/providers.js +3 -1
- package/lib/schema.js +173 -161
- package/lib/updater.js +75 -8
- package/package.json +72 -72
package/lib/client.js
CHANGED
|
@@ -248,6 +248,27 @@ window.__ModuleLoader__.load({
|
|
|
248
248
|
'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
|
|
249
249
|
'liveInterimPreview': 'Live speech preview (ghost text preview)',
|
|
250
250
|
'structuredPromptVoice': 'Spoken answers for interactive choice prompts',
|
|
251
|
+
'modeVerbatim': 'Verbatim',
|
|
252
|
+
'modeClean': 'Clean Text',
|
|
253
|
+
'modeTask': 'Agent Task',
|
|
254
|
+
'modeNote': 'Note',
|
|
255
|
+
'dictationModeTitle': 'Dictation Post-Processing Mode',
|
|
256
|
+
'dictationModeDesc': 'Select how dictated speech is processed before insertion: Verbatim (0 LLM calls), Clean text (formatting & filler removal), Agent task (structured prompt), or Note (key points & actions).',
|
|
257
|
+
'dictationPromptsTitle': 'Dictation Prompt Templates',
|
|
258
|
+
'dictationPromptsDesc': 'Custom prompt templates for dictation post-processing. Use {text} as placeholder for recognized speech.',
|
|
259
|
+
'promptClean': 'Clean text prompt',
|
|
260
|
+
'promptTask': 'Agent task prompt',
|
|
261
|
+
'promptNote': 'Note prompt',
|
|
262
|
+
'resetPrompts': 'Reset to defaults',
|
|
263
|
+
'voiceMacrosTitle': 'Voice Action Macros',
|
|
264
|
+
'voiceMacrosDesc': 'Spoken trigger phrases automatically expanded into structured prompt templates.',
|
|
265
|
+
'addMacro': 'Add macro',
|
|
266
|
+
'macroTrigger': 'Spoken trigger phrase',
|
|
267
|
+
'macroTemplate': 'Expanded prompt template',
|
|
268
|
+
'macroDelete': 'Delete',
|
|
269
|
+
'rawTab': 'Raw STT',
|
|
270
|
+
'resultTab': 'Result',
|
|
271
|
+
'llmUnavailableFallback': 'LLM unavailable, showing raw transcript',
|
|
251
272
|
'jargonTitle': 'Developer Lexicon & IT Jargon',
|
|
252
273
|
'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
|
|
253
274
|
'jargonAdd': 'Add replacement',
|
|
@@ -258,6 +279,49 @@ window.__ModuleLoader__.load({
|
|
|
258
279
|
'assistantSpeakingGated': 'Assistant speaking (gated mode)',
|
|
259
280
|
'jargonFormatHint': ' (spoken -> replacement)',
|
|
260
281
|
'errorPrefix': 'Error',
|
|
282
|
+
'wizardTitle': 'First-Run Setup & Diagnostics Wizard',
|
|
283
|
+
'wizardDesc': 'Step-by-step diagnostic test: check secure context, microphone permissions, audio level, and STT round-trip.',
|
|
284
|
+
'wizardBtnLaunch': 'Launch Mic & STT Wizard',
|
|
285
|
+
'wizardClose': 'Close Wizard',
|
|
286
|
+
'wizardStep1': '1. Audio & Environment',
|
|
287
|
+
'wizardStep2': '2. Live Signal Test',
|
|
288
|
+
'wizardStep3': '3. STT Transcription Test',
|
|
289
|
+
'wizardSecureContextOk': 'Secure context / HTTPS verified',
|
|
290
|
+
'wizardSecureContextBad': 'Insecure context (HTTPS or localhost required)',
|
|
291
|
+
'wizardMediaDevicesOk': 'Browser mediaDevices API supported',
|
|
292
|
+
'wizardMediaDevicesBad': 'Browser mediaDevices API missing',
|
|
293
|
+
'wizardMediaRecorderOk': 'MediaRecorder API supported',
|
|
294
|
+
'wizardMediaRecorderBad': 'MediaRecorder API unsupported in this browser',
|
|
295
|
+
'wizardMicPermissionOk': 'Microphone permission granted',
|
|
296
|
+
'wizardMicPermissionPrompt': 'Microphone permission not yet granted',
|
|
297
|
+
'wizardMicPermissionBad': 'Microphone permission denied',
|
|
298
|
+
'wizardRequestMic': 'Request Microphone Access',
|
|
299
|
+
'wizardMicLabel': 'Microphone input device:',
|
|
300
|
+
'wizardStartSignalCheck': 'Start Audio Level Test',
|
|
301
|
+
'wizardStopSignalCheck': 'Stop Audio Test',
|
|
302
|
+
'wizardSignalOk': 'Audio signal detected (RMS: {level}%)',
|
|
303
|
+
'wizardSignalZero': 'Zero audio level detected - speak into microphone or select correct device',
|
|
304
|
+
'wizardSignalTesting': 'Listening for audio signal… speak into your microphone',
|
|
305
|
+
'wizardProviderLabel': 'Active STT engine & model:',
|
|
306
|
+
'wizardProcessingArea': 'Processing area:',
|
|
307
|
+
'wizardAreaBrowser': 'In-browser (WebGPU / WebSpeech)',
|
|
308
|
+
'wizardAreaServer': 'Local MiniPC server (SenseVoice / Whisper)',
|
|
309
|
+
'wizardAreaCloud': 'Remote cloud provider',
|
|
310
|
+
'wizardStartTestRecord': 'Record 3-Second Test Phrase',
|
|
311
|
+
'wizardRecordingTest': 'Recording test phrase… speak now ({left}s)',
|
|
312
|
+
'wizardTranscribingTest': 'Transcribing test phrase…',
|
|
313
|
+
'wizardTestResult': 'STT Test Result:',
|
|
314
|
+
'wizardTestSuccess': 'STT round-trip verified successfully! Latency: {latency}ms',
|
|
315
|
+
'wizardTestEmpty': 'No speech detected in test recording. Please speak clearly and try again.',
|
|
316
|
+
'wizardTestFailed': 'STT test failed: {error}',
|
|
317
|
+
'wizardNextStepHint': 'Next step: {hint}',
|
|
318
|
+
'wizardHintPermission': 'Click "Request Microphone Access" and allow browser permission prompt.',
|
|
319
|
+
'wizardHintDevice': 'Check microphone connection or select a different input device.',
|
|
320
|
+
'wizardHintHttps': 'Open DeepSeek Harness via HTTPS or localhost address.',
|
|
321
|
+
'wizardHintProvider': 'Configure STT engine credentials or install SenseVoice model below.',
|
|
322
|
+
'wizardFinishSuccess': 'Setup & Verification Complete',
|
|
323
|
+
'wizardSkip': 'Skip Wizard',
|
|
324
|
+
'wizardStatusCompleted': 'Microphone and STT verified',
|
|
261
325
|
}
|
|
262
326
|
|
|
263
327
|
const zh = {
|
|
@@ -474,6 +538,27 @@ window.__ModuleLoader__.load({
|
|
|
474
538
|
'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
|
|
475
539
|
'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
|
|
476
540
|
'structuredPromptVoice': '语音应答交互式选择提示',
|
|
541
|
+
'modeVerbatim': '逐字直录',
|
|
542
|
+
'modeClean': '清爽文本',
|
|
543
|
+
'modeTask': '智能体任务',
|
|
544
|
+
'modeNote': '便签备忘',
|
|
545
|
+
'dictationModeTitle': '听写后处理模式',
|
|
546
|
+
'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
|
|
547
|
+
'dictationPromptsTitle': '听写提示词模板',
|
|
548
|
+
'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
|
|
549
|
+
'promptClean': '清爽文本提示词',
|
|
550
|
+
'promptTask': '智能体任务提示词',
|
|
551
|
+
'promptNote': '便签提示词',
|
|
552
|
+
'resetPrompts': '恢复默认模板',
|
|
553
|
+
'voiceMacrosTitle': '语音动作宏',
|
|
554
|
+
'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
|
|
555
|
+
'addMacro': '添加动作宏',
|
|
556
|
+
'macroTrigger': '语音触发短语',
|
|
557
|
+
'macroTemplate': '展开提示词模板',
|
|
558
|
+
'macroDelete': '删除',
|
|
559
|
+
'rawTab': '原始识别',
|
|
560
|
+
'resultTab': '处理结果',
|
|
561
|
+
'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
|
|
477
562
|
'jargonTitle': '开发者词汇与 IT 术语',
|
|
478
563
|
'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
|
|
479
564
|
'jargonAdd': '添加替换项',
|
|
@@ -484,6 +569,49 @@ window.__ModuleLoader__.load({
|
|
|
484
569
|
'assistantSpeakingGated': '助手正在发言(门控模式)',
|
|
485
570
|
'jargonFormatHint': ' (口语 -> 替换文本)',
|
|
486
571
|
'errorPrefix': '错误',
|
|
572
|
+
'wizardTitle': '首次启动与麦克风诊断向导',
|
|
573
|
+
'wizardDesc': '分步诊断检查:安全上下文、麦克风权限、音频信号电平及实时 STT 回环测试。',
|
|
574
|
+
'wizardBtnLaunch': '启动麦克风与 STT 诊断向导',
|
|
575
|
+
'wizardClose': '关闭向导',
|
|
576
|
+
'wizardStep1': '1. 音频与运行环境',
|
|
577
|
+
'wizardStep2': '2. 实时信号测试',
|
|
578
|
+
'wizardStep3': '3. STT 识别回环测试',
|
|
579
|
+
'wizardSecureContextOk': '安全上下文 / HTTPS 已验证',
|
|
580
|
+
'wizardSecureContextBad': '非安全上下文(需 HTTPS 或 localhost)',
|
|
581
|
+
'wizardMediaDevicesOk': '浏览器 mediaDevices API 支持正常',
|
|
582
|
+
'wizardMediaDevicesBad': '浏览器缺少 mediaDevices API 支持',
|
|
583
|
+
'wizardMediaRecorderOk': 'MediaRecorder API 支持正常',
|
|
584
|
+
'wizardMediaRecorderBad': '当前浏览器不支持 MediaRecorder API',
|
|
585
|
+
'wizardMicPermissionOk': '麦克风权限已授予',
|
|
586
|
+
'wizardMicPermissionPrompt': '尚未获得麦克风授权',
|
|
587
|
+
'wizardMicPermissionBad': '麦克风权限被拒绝',
|
|
588
|
+
'wizardRequestMic': '请求麦克风权限',
|
|
589
|
+
'wizardMicLabel': '麦克风输入设备:',
|
|
590
|
+
'wizardStartSignalCheck': '开始音频电平测试',
|
|
591
|
+
'wizardStopSignalCheck': '停止音频测试',
|
|
592
|
+
'wizardSignalOk': '检测到音频信号 (RMS: {level}%)',
|
|
593
|
+
'wizardSignalZero': '未检测到声音信号 — 请对着麦克风说话或选择正确设备',
|
|
594
|
+
'wizardSignalTesting': '正在监听声音信号… 请对着麦克风发声',
|
|
595
|
+
'wizardProviderLabel': '当前语音引擎与模型:',
|
|
596
|
+
'wizardProcessingArea': '处理区域:',
|
|
597
|
+
'wizardAreaBrowser': '浏览器端处理 (WebGPU / WebSpeech)',
|
|
598
|
+
'wizardAreaServer': '本地服务器处理 (SenseVoice / Whisper)',
|
|
599
|
+
'wizardAreaCloud': '远程云端服务',
|
|
600
|
+
'wizardStartTestRecord': '录制 3 秒测试语句',
|
|
601
|
+
'wizardRecordingTest': '正在录制测试音频… 请说话 ({left}秒)',
|
|
602
|
+
'wizardTranscribingTest': '正在请求语音识别…',
|
|
603
|
+
'wizardTestResult': 'STT 测试结果:',
|
|
604
|
+
'wizardTestSuccess': 'STT 链路验证成功!耗时:{latency}毫秒',
|
|
605
|
+
'wizardTestEmpty': '录音中未检测到有效语音,请清晰发音后重试。',
|
|
606
|
+
'wizardTestFailed': 'STT 测试失败:{error}',
|
|
607
|
+
'wizardNextStepHint': '建议下一步:{hint}',
|
|
608
|
+
'wizardHintPermission': '点击“请求麦克风权限”并在浏览器弹窗中允许访问。',
|
|
609
|
+
'wizardHintDevice': '检查麦克风硬件连接或切换选择其他输入设备。',
|
|
610
|
+
'wizardHintHttps': '请通过 HTTPS 或 localhost 访问 DeepSeek Harness。',
|
|
611
|
+
'wizardHintProvider': '请在下方配置 STT 密钥凭据或一键安装 SenseVoice 本地模型。',
|
|
612
|
+
'wizardFinishSuccess': '设置与验证已完成',
|
|
613
|
+
'wizardSkip': '跳过向导',
|
|
614
|
+
'wizardStatusCompleted': '麦克风与 STT 诊断已就绪',
|
|
487
615
|
}
|
|
488
616
|
|
|
489
617
|
// Strings are also needed outside components — in recording handlers and
|
|
@@ -547,7 +675,7 @@ window.__ModuleLoader__.load({
|
|
|
547
675
|
'.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
|
|
548
676
|
'.dvo-btn-send{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:50%;cursor:pointer;padding:0;flex:none;box-sizing:border-box;border:none;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-1);transition:opacity 150ms ease}' +
|
|
549
677
|
'.dvo-btn-send:hover{opacity:0.9}' +
|
|
550
|
-
'.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}'
|
|
678
|
+
'.dvo-warn-banner{font-size:12px;color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-error-primary);padding:4px 8px;border-radius:6px;margin-top:4px}' + '.dvo-mode-pill-btn{display:inline-flex;align-items:center;justify-content:center;height:28px;padding:0 8px;font-size:11px;font-weight:600;border-radius:14px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);user-select:none;touch-action:manipulation;transition:all 150ms ease}' + '.dvo-mode-pill-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}' + '.dvo-mode-badge{font-size:11px;font-weight:600;padding:2px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);margin-right:4px}' + '.dvo-macro-item{display:flex;flex-direction:column;gap:6px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;margin-top:6px}' + '.dvo-macro-actions{display:flex;justify-content:flex-end;gap:6px}' + '.dvo-fallback-badge{font-size:11px;padding:2px 6px;border-radius:4px;color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 12%, transparent);border:1px solid var(--dsw-alias-state-warning-primary)}'
|
|
551
679
|
const cssId = 'dsh-voice/client.module.css'
|
|
552
680
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
|
|
553
681
|
const tag = document.createElement('style')
|
|
@@ -743,7 +871,26 @@ window.__ModuleLoader__.load({
|
|
|
743
871
|
|
|
744
872
|
function extractContextKeywords() {
|
|
745
873
|
if (!voice.settings || voice.settings.contextGlossary === false) return []
|
|
746
|
-
const
|
|
874
|
+
const chunks = []
|
|
875
|
+
const draft = (voice.input && typeof voice.input.draft === 'string') ? voice.input.draft : ''
|
|
876
|
+
if (draft && draft.length >= 3) chunks.push(draft)
|
|
877
|
+
if (voice.session && Array.isArray(voice.session.messages)) {
|
|
878
|
+
const msgs = voice.session.messages.slice(-5)
|
|
879
|
+
for (const m of msgs) {
|
|
880
|
+
const t = (m && typeof m.content === 'string') ? m.content : ((m && typeof m.text === 'string') ? m.text : '')
|
|
881
|
+
if (t && t.length >= 3) chunks.push(t)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (typeof document !== 'undefined') {
|
|
885
|
+
try {
|
|
886
|
+
const nodes = document.querySelectorAll('.message-content code, .dsh-message-body code, pre code')
|
|
887
|
+
for (let i = 0; i < Math.min(nodes.length, 10); i++) {
|
|
888
|
+
const ct = nodes[i].textContent
|
|
889
|
+
if (ct && ct.length < 100) chunks.push(ct)
|
|
890
|
+
}
|
|
891
|
+
} catch (_) {}
|
|
892
|
+
}
|
|
893
|
+
const text = chunks.join(' ')
|
|
747
894
|
if (!text || text.length < 3) return []
|
|
748
895
|
const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
|
|
749
896
|
// Linguistic filter: functional stop-word dictionary for vocabulary keyword extraction.
|
|
@@ -775,7 +922,11 @@ window.__ModuleLoader__.load({
|
|
|
775
922
|
|
|
776
923
|
const tryHost = async (chainOverride) => {
|
|
777
924
|
const dataBase64 = await blobToBase64(blob)
|
|
778
|
-
const
|
|
925
|
+
const subMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
926
|
+
|| voice.selectedDictationMode
|
|
927
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
928
|
+
|| 'clean'
|
|
929
|
+
const payload = { dataBase64, mimeType: mime, mode, subMode }
|
|
779
930
|
if (Array.isArray(chainOverride)) {
|
|
780
931
|
payload.chain = chainOverride.map((p) => (typeof p === 'string' ? { provider: p, model: modelMap[p] } : p))
|
|
781
932
|
}
|
|
@@ -1491,7 +1642,10 @@ voice._chainsPromise = null
|
|
|
1491
1642
|
const targetSession = voice.session || (targetInput && targetInput.session)
|
|
1492
1643
|
const sessionId = (targetSession && (targetSession.sessionId || targetSession.id))
|
|
1493
1644
|
|| voice.currentSessionId || ''
|
|
1494
|
-
const
|
|
1645
|
+
const snapshotSubMode = voice.selectedDictationMode
|
|
1646
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
1647
|
+
|| 'clean'
|
|
1648
|
+
const target = { actions: targetActions, input: targetInput, session: targetSession, span, initialDraft, sessionId, dictationMode: snapshotSubMode }
|
|
1495
1649
|
voice._activeTarget = target
|
|
1496
1650
|
|
|
1497
1651
|
if (voice.isTtsSpeaking) {
|
|
@@ -1738,7 +1892,15 @@ voice._chainsPromise = null
|
|
|
1738
1892
|
if (v.phase !== 'idle' && v.phase !== 'error' && !hold.armed) return null
|
|
1739
1893
|
const err = v.phase === 'error'
|
|
1740
1894
|
const hk = voice.hotkey ? ` (${voice.hotkey})` : ''
|
|
1895
|
+
const curMode = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
|
|
1741
1896
|
return React.createElement(React.Fragment, null,
|
|
1897
|
+
React.createElement('button', {
|
|
1898
|
+
type: 'button',
|
|
1899
|
+
className: 'dvo-mode-pill-btn',
|
|
1900
|
+
title: t('dictationModeTitle') + ': ' + getDictationModeLabel(curMode, t),
|
|
1901
|
+
'aria-label': t('dictationModeTitle') + ': ' + getDictationModeLabel(curMode, t),
|
|
1902
|
+
onClick: cycleDictationMode,
|
|
1903
|
+
}, getDictationModeLabel(curMode, t)),
|
|
1742
1904
|
React.createElement('button', {
|
|
1743
1905
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
1744
1906
|
title: err ? v.error : t('dictationBtn'),
|
|
@@ -2208,6 +2370,25 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2208
2370
|
? React.createElement('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
|
|
2209
2371
|
: null,
|
|
2210
2372
|
)
|
|
2373
|
+
}
|
|
2374
|
+
// ------------------------------------------------------------------ 45-dictation
|
|
2375
|
+
// Dictation modes UI helper, mode cycler and labels (#264)
|
|
2376
|
+
|
|
2377
|
+
const DICTATION_MODES = ['clean', 'task', 'note', 'verbatim']
|
|
2378
|
+
|
|
2379
|
+
function getDictationModeLabel(m, t) {
|
|
2380
|
+
const trans = t || moduleT
|
|
2381
|
+
if (m === 'verbatim') return trans('modeVerbatim')
|
|
2382
|
+
if (m === 'task') return trans('modeTask')
|
|
2383
|
+
if (m === 'note') return trans('modeNote')
|
|
2384
|
+
return trans('modeClean')
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function cycleDictationMode() {
|
|
2388
|
+
const cur = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
|
|
2389
|
+
const nextIdx = (DICTATION_MODES.indexOf(cur) + 1) % DICTATION_MODES.length
|
|
2390
|
+
voice.selectedDictationMode = DICTATION_MODES[nextIdx]
|
|
2391
|
+
voice.notify()
|
|
2211
2392
|
}
|
|
2212
2393
|
// ----------------------------------------------------------- visualizers
|
|
2213
2394
|
let _accentColorCache = { color: '', expires: 0 }
|
|
@@ -2530,7 +2711,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2530
2711
|
// Live caption of what is heard right now. Until the browser emits a
|
|
2531
2712
|
// final chunk the text is interim and changes on screen.
|
|
2532
2713
|
const caption = voice.caption || (inBrowser ? '' : null)
|
|
2714
|
+
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2715
|
+
|| voice.selectedDictationMode
|
|
2716
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
2717
|
+
|| 'clean'
|
|
2533
2718
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
2719
|
+
React.createElement('span', { className: 'dvo-mode-badge' }, getDictationModeLabel(activeSubMode, t)),
|
|
2534
2720
|
React.createElement('button', {
|
|
2535
2721
|
type: 'button', className: 'dvo-pbtn', title: t('cancel'),
|
|
2536
2722
|
'aria-label': t('cancel'),
|
|
@@ -2559,8 +2745,16 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2559
2745
|
}
|
|
2560
2746
|
|
|
2561
2747
|
if (v.phase === 'processing') {
|
|
2748
|
+
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2749
|
+
|| voice.selectedDictationMode
|
|
2750
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
2751
|
+
|| 'clean'
|
|
2562
2752
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
2563
|
-
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
2753
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
2754
|
+
spinIcon(),
|
|
2755
|
+
React.createElement('span', { className: 'dvo-mode-badge' }, getDictationModeLabel(activeSubMode, t)),
|
|
2756
|
+
t('transcribing'),
|
|
2757
|
+
))
|
|
2564
2758
|
}
|
|
2565
2759
|
|
|
2566
2760
|
if (v.phase === 'pending') {
|
|
@@ -2677,6 +2871,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
2677
2871
|
voiceCommands: !!(data && data.voiceCommands),
|
|
2678
2872
|
vadSilenceMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadSilenceMs) || 700,
|
|
2679
2873
|
autoSendMs: Number(data && data.modes && data.modes.message && data.modes.message.autoSendMs) || 4000,
|
|
2874
|
+
dictationMode: String((data && data.dictationMode) || 'clean'),
|
|
2875
|
+
dictationPrompts: (data && data.dictationPrompts) || {},
|
|
2876
|
+
voiceMacros: Array.isArray(data && data.voiceMacros) ? data.voiceMacros : [],
|
|
2680
2877
|
sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
|
|
2681
2878
|
stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
|
|
2682
2879
|
streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
|
|
@@ -3016,6 +3213,93 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3016
3213
|
),
|
|
3017
3214
|
)
|
|
3018
3215
|
}
|
|
3216
|
+
// ------------------------------------------------------------------ 72-dictation-modes
|
|
3217
|
+
// Dictation modes configuration card and prompt template editors (#264)
|
|
3218
|
+
|
|
3219
|
+
function DictationModesSection(props) {
|
|
3220
|
+
const { t, draft, setTop, writable } = props
|
|
3221
|
+
const prompts = (draft && draft.dictationPrompts) || {}
|
|
3222
|
+
const currentMode = (draft && draft.dictationMode) || 'clean'
|
|
3223
|
+
|
|
3224
|
+
const setPrompt = (key, val) => {
|
|
3225
|
+
const updated = { ...prompts, [key]: val }
|
|
3226
|
+
setTop('dictationPrompts', updated)
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
const resetPrompts = () => {
|
|
3230
|
+
setTop('dictationPrompts', { clean: '', task: '', note: '' })
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
3234
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
3235
|
+
React.createElement('span', null, '🎙️ ' + t('dictationModeTitle')),
|
|
3236
|
+
),
|
|
3237
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('dictationModeDesc')),
|
|
3238
|
+
React.createElement('div', { className: 'cb-grid-2', style: { marginTop: '8px' } },
|
|
3239
|
+
['clean', 'task', 'note', 'verbatim'].map((m) =>
|
|
3240
|
+
React.createElement('label', { key: m, className: 'cb-check-label' },
|
|
3241
|
+
React.createElement('input', {
|
|
3242
|
+
type: 'radio',
|
|
3243
|
+
name: 'dictationMode',
|
|
3244
|
+
value: m,
|
|
3245
|
+
checked: currentMode === m,
|
|
3246
|
+
disabled: !writable,
|
|
3247
|
+
onChange: () => setTop('dictationMode', m),
|
|
3248
|
+
}),
|
|
3249
|
+
getDictationModeLabel(m, t),
|
|
3250
|
+
),
|
|
3251
|
+
),
|
|
3252
|
+
),
|
|
3253
|
+
React.createElement('div', { style: { marginTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '10px' } },
|
|
3254
|
+
React.createElement('div', { className: 'cb-section-title', style: { fontSize: '13px' } },
|
|
3255
|
+
React.createElement('span', null, t('dictationPromptsTitle')),
|
|
3256
|
+
React.createElement('button', {
|
|
3257
|
+
type: 'button',
|
|
3258
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3259
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
3260
|
+
disabled: !writable,
|
|
3261
|
+
onClick: resetPrompts,
|
|
3262
|
+
}, t('resetPrompts')),
|
|
3263
|
+
),
|
|
3264
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('dictationPromptsDesc')),
|
|
3265
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' } },
|
|
3266
|
+
React.createElement('div', null,
|
|
3267
|
+
React.createElement('label', { className: 'cb-label' }, t('promptClean')),
|
|
3268
|
+
React.createElement('textarea', {
|
|
3269
|
+
className: 'cb-input',
|
|
3270
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3271
|
+
value: prompts.clean || '',
|
|
3272
|
+
placeholder: 'Default: punctuation & filler-word cleanup',
|
|
3273
|
+
disabled: !writable,
|
|
3274
|
+
onChange: (e) => setPrompt('clean', e.target.value),
|
|
3275
|
+
}),
|
|
3276
|
+
),
|
|
3277
|
+
React.createElement('div', null,
|
|
3278
|
+
React.createElement('label', { className: 'cb-label' }, t('promptTask')),
|
|
3279
|
+
React.createElement('textarea', {
|
|
3280
|
+
className: 'cb-input',
|
|
3281
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3282
|
+
value: prompts.task || '',
|
|
3283
|
+
placeholder: 'Default: structured problem/outcome/constraints/criteria',
|
|
3284
|
+
disabled: !writable,
|
|
3285
|
+
onChange: (e) => setPrompt('task', e.target.value),
|
|
3286
|
+
}),
|
|
3287
|
+
),
|
|
3288
|
+
React.createElement('div', null,
|
|
3289
|
+
React.createElement('label', { className: 'cb-label' }, t('promptNote')),
|
|
3290
|
+
React.createElement('textarea', {
|
|
3291
|
+
className: 'cb-input',
|
|
3292
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3293
|
+
value: prompts.note || '',
|
|
3294
|
+
placeholder: 'Default: key points & action items summary',
|
|
3295
|
+
disabled: !writable,
|
|
3296
|
+
onChange: (e) => setPrompt('note', e.target.value),
|
|
3297
|
+
}),
|
|
3298
|
+
),
|
|
3299
|
+
),
|
|
3300
|
+
),
|
|
3301
|
+
)
|
|
3302
|
+
}
|
|
3019
3303
|
// 5. Hardware & Audio Engines Card
|
|
3020
3304
|
function HardwareOptionsCard(props) {
|
|
3021
3305
|
const t = props.t || moduleT
|
|
@@ -3499,6 +3783,78 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3499
3783
|
),
|
|
3500
3784
|
)
|
|
3501
3785
|
}
|
|
3786
|
+
// ------------------------------------------------------------------ 72-macros
|
|
3787
|
+
// Voice action macros configuration card (#167)
|
|
3788
|
+
|
|
3789
|
+
function MacrosSection(props) {
|
|
3790
|
+
const { t, draft, setTop, writable } = props
|
|
3791
|
+
const macros = Array.isArray(draft && draft.voiceMacros) ? draft.voiceMacros : []
|
|
3792
|
+
|
|
3793
|
+
const addMacro = () => {
|
|
3794
|
+
const next = [...macros, { trigger: '', template: '' }]
|
|
3795
|
+
setTop('voiceMacros', next)
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
const updateMacro = (idx, field, val) => {
|
|
3799
|
+
const next = macros.map((item, i) => (i === idx ? { ...item, [field]: val } : item))
|
|
3800
|
+
setTop('voiceMacros', next)
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
const removeMacro = (idx) => {
|
|
3804
|
+
const next = macros.filter((_, i) => i !== idx)
|
|
3805
|
+
setTop('voiceMacros', next)
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
3809
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
3810
|
+
React.createElement('span', null, '⚡ ' + t('voiceMacrosTitle')),
|
|
3811
|
+
React.createElement('button', {
|
|
3812
|
+
type: 'button',
|
|
3813
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3814
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
3815
|
+
disabled: !writable,
|
|
3816
|
+
onClick: addMacro,
|
|
3817
|
+
}, '+ ' + t('addMacro')),
|
|
3818
|
+
),
|
|
3819
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('voiceMacrosDesc')),
|
|
3820
|
+
macros.length === 0
|
|
3821
|
+
? React.createElement('div', { style: { fontStyle: 'italic', color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', marginTop: '6px' } },
|
|
3822
|
+
'No macros configured.',
|
|
3823
|
+
)
|
|
3824
|
+
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '6px' } },
|
|
3825
|
+
macros.map((m, idx) =>
|
|
3826
|
+
React.createElement('div', { key: idx, className: 'dvo-macro-item' },
|
|
3827
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3828
|
+
React.createElement('input', {
|
|
3829
|
+
type: 'text',
|
|
3830
|
+
className: 'cb-input',
|
|
3831
|
+
style: { flex: '1', height: '28px', fontSize: '12px' },
|
|
3832
|
+
placeholder: t('macroTrigger'),
|
|
3833
|
+
value: m.trigger || '',
|
|
3834
|
+
disabled: !writable,
|
|
3835
|
+
onChange: (e) => updateMacro(idx, 'trigger', e.target.value),
|
|
3836
|
+
}),
|
|
3837
|
+
React.createElement('button', {
|
|
3838
|
+
type: 'button',
|
|
3839
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3840
|
+
style: { padding: '2px 6px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },
|
|
3841
|
+
disabled: !writable,
|
|
3842
|
+
onClick: () => removeMacro(idx),
|
|
3843
|
+
}, t('macroDelete')),
|
|
3844
|
+
),
|
|
3845
|
+
React.createElement('textarea', {
|
|
3846
|
+
className: 'cb-input',
|
|
3847
|
+
style: { minHeight: '38px', resize: 'vertical', fontSize: '12px' },
|
|
3848
|
+
placeholder: t('macroTemplate') + ' (use {text} for argument)',
|
|
3849
|
+
value: m.template || '',
|
|
3850
|
+
disabled: !writable,
|
|
3851
|
+
onChange: (e) => updateMacro(idx, 'template', e.target.value),
|
|
3852
|
+
}),
|
|
3853
|
+
),
|
|
3854
|
+
),
|
|
3855
|
+
),
|
|
3856
|
+
)
|
|
3857
|
+
}
|
|
3502
3858
|
// SenseVoice Local ASR (1-Click Setup)
|
|
3503
3859
|
function SensevoiceSection(props) {
|
|
3504
3860
|
const t = props.t || moduleT
|
|
@@ -3553,14 +3909,21 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3553
3909
|
const whisperRunning = props.whisperRunning
|
|
3554
3910
|
const providerCount = props.providerCount
|
|
3555
3911
|
const onRefresh = props.onRefresh
|
|
3912
|
+
const onOpenWizard = props.onOpenWizard
|
|
3556
3913
|
|
|
3557
3914
|
return React.createElement('div', { className: 'cb-section-card' },
|
|
3558
3915
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3559
3916
|
React.createElement('span', null, t('statusTitle')),
|
|
3560
|
-
React.createElement('
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3917
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3918
|
+
onOpenWizard ? React.createElement('button', {
|
|
3919
|
+
type: 'button', className: 'cb-btn cb-btn-primary', style: { fontSize: '12px', padding: '4px 10px' },
|
|
3920
|
+
onClick: onOpenWizard,
|
|
3921
|
+
}, '✨ ' + t('wizardBtnLaunch')) : null,
|
|
3922
|
+
React.createElement('button', {
|
|
3923
|
+
type: 'button', className: 'cb-btn', style: { fontSize: '12px', padding: '4px 8px' },
|
|
3924
|
+
onClick: onRefresh,
|
|
3925
|
+
}, '🔄 ' + t('refreshStats')),
|
|
3926
|
+
),
|
|
3564
3927
|
),
|
|
3565
3928
|
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center', gap: '12px', flexWrap: 'wrap' } },
|
|
3566
3929
|
React.createElement('span', { className: 'cb-badge ' + (hostOnline ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
@@ -3652,7 +4015,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3652
4015
|
const [serverConfig, setServerConfig] = React.useState(null)
|
|
3653
4016
|
const [configLoading, setConfigLoading] = React.useState(true)
|
|
3654
4017
|
const [revision, setRevision] = React.useState(null)
|
|
3655
|
-
|
|
3656
4018
|
React.useEffect(() => {
|
|
3657
4019
|
let alive = true
|
|
3658
4020
|
fetch('/dsh-voice/config', { cache: 'no-store' })
|
|
@@ -3662,6 +4024,7 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3662
4024
|
setServerConfig(data.config)
|
|
3663
4025
|
if (typeof data.revision === 'number') setRevision(data.revision)
|
|
3664
4026
|
setDraft((curr) => curr || deepClone(data.config))
|
|
4027
|
+
if (data.config.wizardCompleted !== true) setShowWizard(true)
|
|
3665
4028
|
}
|
|
3666
4029
|
})
|
|
3667
4030
|
.catch(() => {})
|
|
@@ -3670,10 +4033,10 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3670
4033
|
})
|
|
3671
4034
|
return () => { alive = false }
|
|
3672
4035
|
}, [])
|
|
3673
|
-
|
|
3674
4036
|
// Hotkey picker: not a name field — press the key you want.
|
|
3675
4037
|
const [catching, setCatching] = React.useState(false)
|
|
3676
4038
|
const [jargonInput, setJargonInput] = React.useState(null)
|
|
4039
|
+
const [showWizard, setShowWizard] = React.useState(false)
|
|
3677
4040
|
React.useEffect(() => {
|
|
3678
4041
|
if (!catching) return undefined
|
|
3679
4042
|
const onKey = (event) => {
|
|
@@ -3693,7 +4056,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3693
4056
|
document.addEventListener('keydown', onKey, true)
|
|
3694
4057
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
3695
4058
|
}, [catching, ctx])
|
|
3696
|
-
|
|
3697
4059
|
React.useEffect(() => {
|
|
3698
4060
|
let alive = true
|
|
3699
4061
|
const render = () => { if (alive && scope && typeof scope.getSnapshot === 'function') setSnap(scope.getSnapshot()) }
|
|
@@ -3701,7 +4063,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3701
4063
|
const off = (scope && typeof scope.subscribe === 'function') ? scope.subscribe(render) : () => {}
|
|
3702
4064
|
return () => { alive = false; off() }
|
|
3703
4065
|
}, [scope])
|
|
3704
|
-
|
|
3705
4066
|
const [devices, setDevices] = React.useState([])
|
|
3706
4067
|
React.useEffect(() => {
|
|
3707
4068
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
|
|
@@ -3709,11 +4070,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3709
4070
|
.then((list) => setDevices(list.filter((d) => d.kind === 'audioinput')))
|
|
3710
4071
|
.catch(() => {})
|
|
3711
4072
|
}, [])
|
|
3712
|
-
|
|
3713
4073
|
const ready = !!serverConfig || (!!snap && snap.status === 'ready')
|
|
3714
4074
|
const value = (snap && snap.status === 'ready' && snap.value) ? snap.value : (serverConfig || {})
|
|
3715
4075
|
const writable = ready
|
|
3716
|
-
|
|
3717
4076
|
React.useEffect(() => {
|
|
3718
4077
|
if (ready) return undefined
|
|
3719
4078
|
let tries = 0
|
|
@@ -3730,14 +4089,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3730
4089
|
}, 1000)
|
|
3731
4090
|
return () => clearInterval(timer)
|
|
3732
4091
|
}, [ready])
|
|
3733
|
-
|
|
3734
4092
|
const deepClone = (obj) => {
|
|
3735
4093
|
if (typeof structuredClone === 'function') {
|
|
3736
4094
|
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
3737
4095
|
}
|
|
3738
4096
|
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
3739
4097
|
}
|
|
3740
|
-
|
|
3741
4098
|
const setTop = (key, val) => {
|
|
3742
4099
|
setDraft((d) => Object.assign({}, d || {}, { [key]: val }))
|
|
3743
4100
|
}
|
|
@@ -3758,9 +4115,7 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3758
4115
|
}),
|
|
3759
4116
|
hint ? React.createElement('span', { className: 'dvs-sub' }, hint) : null,
|
|
3760
4117
|
)
|
|
3761
|
-
|
|
3762
4118
|
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
3763
|
-
|
|
3764
4119
|
React.useEffect(() => {
|
|
3765
4120
|
if (!ready) return
|
|
3766
4121
|
const val = value || {}
|
|
@@ -3792,10 +4147,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3792
4147
|
techJargonCorrection: val.techJargonCorrection !== false,
|
|
3793
4148
|
liveInterimPreview: val.liveInterimPreview !== false,
|
|
3794
4149
|
structuredPromptVoice: val.structuredPromptVoice !== false,
|
|
4150
|
+
dictationMode: val.dictationMode || 'clean',
|
|
4151
|
+
dictationPrompts: val.dictationPrompts || {},
|
|
4152
|
+
voiceMacros: Array.isArray(val.voiceMacros) ? val.voiceMacros : [],
|
|
3795
4153
|
webgpuModel: String(val.webgpuModel || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'),
|
|
3796
4154
|
})
|
|
3797
4155
|
}, [ready, value])
|
|
3798
|
-
|
|
3799
4156
|
const [statusData, setStatusData] = React.useState(null)
|
|
3800
4157
|
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
3801
4158
|
const fetchStatus = React.useCallback(() => {
|
|
@@ -3819,17 +4176,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3819
4176
|
setStatusLatency(null)
|
|
3820
4177
|
})
|
|
3821
4178
|
}, [])
|
|
3822
|
-
|
|
3823
4179
|
React.useEffect(() => {
|
|
3824
4180
|
fetchStatus()
|
|
3825
4181
|
}, [fetchStatus])
|
|
3826
|
-
|
|
3827
4182
|
const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
|
|
3828
|
-
|
|
3829
4183
|
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3830
|
-
|
|
3831
4184
|
const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3832
|
-
|
|
3833
4185
|
const save = async () => {
|
|
3834
4186
|
setErr(''); setSaved(false)
|
|
3835
4187
|
if (!draft) return
|
|
@@ -3848,7 +4200,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3848
4200
|
.filter(Boolean)
|
|
3849
4201
|
draft.jargonDictionary = parsed
|
|
3850
4202
|
}
|
|
3851
|
-
|
|
3852
4203
|
let httpOk = false
|
|
3853
4204
|
let httpError = null
|
|
3854
4205
|
try {
|
|
@@ -3870,7 +4221,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3870
4221
|
} catch (e) {
|
|
3871
4222
|
httpError = e?.message || String(e)
|
|
3872
4223
|
}
|
|
3873
|
-
|
|
3874
4224
|
if (httpOk && scope && typeof scope.set === 'function') {
|
|
3875
4225
|
for (const k of Object.keys(draft)) {
|
|
3876
4226
|
try {
|
|
@@ -3880,12 +4230,10 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3880
4230
|
}
|
|
3881
4231
|
}
|
|
3882
4232
|
}
|
|
3883
|
-
|
|
3884
4233
|
if (!httpOk) {
|
|
3885
4234
|
setErr(t('saveFailed') + ': ' + httpError)
|
|
3886
4235
|
return
|
|
3887
4236
|
}
|
|
3888
|
-
|
|
3889
4237
|
const d = draft || {}
|
|
3890
4238
|
voice.hotkey = d.hotkey || ''
|
|
3891
4239
|
if (voice.hotkey) {
|
|
@@ -3917,23 +4265,19 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3917
4265
|
structuredPromptVoice: d.structuredPromptVoice !== false,
|
|
3918
4266
|
})
|
|
3919
4267
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
3920
|
-
|
|
3921
4268
|
setSaved(true); setJargonInput(null)
|
|
3922
4269
|
setTimeout(() => setSaved(false), 2000)
|
|
3923
4270
|
fetchStatus()
|
|
3924
4271
|
}
|
|
3925
|
-
|
|
3926
4272
|
const modeVal = (mode, key, fallback) => {
|
|
3927
4273
|
const m = draft && draft[mode]
|
|
3928
4274
|
return m && m[key] !== undefined ? m[key] : fallback
|
|
3929
4275
|
}
|
|
3930
|
-
|
|
3931
4276
|
const chainOptions = BUILTIN.concat(
|
|
3932
4277
|
(draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
|
|
3933
4278
|
.map((c) => String(c && c.key || '').trim())
|
|
3934
4279
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
3935
4280
|
)
|
|
3936
|
-
|
|
3937
4281
|
const langField = (mode) => {
|
|
3938
4282
|
const val = modeVal(mode, 'language', '')
|
|
3939
4283
|
const opts = SUPPORTED_LANGS.slice()
|
|
@@ -3950,7 +4294,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3950
4294
|
))),
|
|
3951
4295
|
)
|
|
3952
4296
|
}
|
|
3953
|
-
|
|
3954
4297
|
const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
3955
4298
|
React.createElement('label', null, label),
|
|
3956
4299
|
React.createElement('input', {
|
|
@@ -3959,15 +4302,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3959
4302
|
}),
|
|
3960
4303
|
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
3961
4304
|
)
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
4305
|
const statsData = (statusData && statusData.providerStats) || {}
|
|
3965
4306
|
const hostOnline = statusLatency !== null
|
|
3966
4307
|
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
3967
4308
|
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
3968
|
-
|
|
3969
4309
|
return React.createElement('div', { className: 'cb-page dvo-settings-root' },
|
|
3970
|
-
// 1. Connection & Engine Status Card
|
|
3971
4310
|
React.createElement(ConnectionStatusCard, {
|
|
3972
4311
|
t: t,
|
|
3973
4312
|
hostOnline: hostOnline,
|
|
@@ -3975,9 +4314,15 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
3975
4314
|
whisperRunning: whisperRunning,
|
|
3976
4315
|
providerCount: providerCount,
|
|
3977
4316
|
onRefresh: fetchStatus,
|
|
4317
|
+
onOpenWizard: () => setShowWizard((v) => !v),
|
|
3978
4318
|
}),
|
|
3979
|
-
|
|
3980
|
-
|
|
4319
|
+
showWizard ? React.createElement(FirstRunWizard, {
|
|
4320
|
+
t: t,
|
|
4321
|
+
draft: draft,
|
|
4322
|
+
setTop: setTop,
|
|
4323
|
+
writable: writable,
|
|
4324
|
+
onClose: () => setShowWizard(false),
|
|
4325
|
+
}) : null,
|
|
3981
4326
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3982
4327
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3983
4328
|
React.createElement('span', null, '🎙️ ' + t('dictationBtn')),
|
|
@@ -4021,8 +4366,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4021
4366
|
),
|
|
4022
4367
|
textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
|
|
4023
4368
|
),
|
|
4024
|
-
|
|
4025
|
-
// 3. Voice Message Card
|
|
4026
4369
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4027
4370
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4028
4371
|
React.createElement('span', null, '💬 ' + t('messageTitle')),
|
|
@@ -4061,8 +4404,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4061
4404
|
),
|
|
4062
4405
|
),
|
|
4063
4406
|
),
|
|
4064
|
-
|
|
4065
|
-
// 4. Custom Providers Card
|
|
4066
4407
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4067
4408
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4068
4409
|
React.createElement('span', null, '⚙️ ' + t('customTitle')),
|
|
@@ -4074,8 +4415,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4074
4415
|
onChange: (v) => setTop('customProviders', v),
|
|
4075
4416
|
}),
|
|
4076
4417
|
),
|
|
4077
|
-
|
|
4078
|
-
// 5. Hardware & Audio Engines Card
|
|
4079
4418
|
React.createElement(HardwareOptionsCard, {
|
|
4080
4419
|
t: t,
|
|
4081
4420
|
draft: draft,
|
|
@@ -4095,8 +4434,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4095
4434
|
clearGlobalHotkey()
|
|
4096
4435
|
},
|
|
4097
4436
|
}),
|
|
4098
|
-
|
|
4099
|
-
// 6. SenseVoice Local ASR (1-Click Setup)
|
|
4100
4437
|
React.createElement(SensevoiceSection, {
|
|
4101
4438
|
t: t,
|
|
4102
4439
|
sensevoiceState: sensevoiceState,
|
|
@@ -4104,8 +4441,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4104
4441
|
writable: writable,
|
|
4105
4442
|
onInstall: triggerInstallSensevoice,
|
|
4106
4443
|
}),
|
|
4107
|
-
|
|
4108
|
-
// 7. Turn-Taking & Audio Coordination
|
|
4109
4444
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4110
4445
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4111
4446
|
React.createElement('span', null, '🎙️ ' + t('turnTakingTitle')),
|
|
@@ -4132,8 +4467,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4132
4467
|
),
|
|
4133
4468
|
),
|
|
4134
4469
|
),
|
|
4135
|
-
|
|
4136
|
-
// 8. Hands-free & Smart Actions
|
|
4137
4470
|
React.createElement('div', { className: 'cb-section-card' },
|
|
4138
4471
|
React.createElement('div', { className: 'cb-section-title' },
|
|
4139
4472
|
React.createElement('span', null, '✨ ' + t('smartActionsTitle')),
|
|
@@ -4177,8 +4510,20 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4177
4510
|
),
|
|
4178
4511
|
),
|
|
4179
4512
|
),
|
|
4180
|
-
|
|
4181
|
-
|
|
4513
|
+
// Dictation Modes & Prompts (#264)
|
|
4514
|
+
React.createElement(DictationModesSection, {
|
|
4515
|
+
t: t,
|
|
4516
|
+
draft: draft,
|
|
4517
|
+
setTop: setTop,
|
|
4518
|
+
writable: writable,
|
|
4519
|
+
}),
|
|
4520
|
+
// Voice Action Macros (#167)
|
|
4521
|
+
React.createElement(MacrosSection, {
|
|
4522
|
+
t: t,
|
|
4523
|
+
draft: draft,
|
|
4524
|
+
setTop: setTop,
|
|
4525
|
+
writable: writable,
|
|
4526
|
+
}),
|
|
4182
4527
|
React.createElement(JargonEditor, {
|
|
4183
4528
|
t: t,
|
|
4184
4529
|
draft: draft,
|
|
@@ -4187,15 +4532,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4187
4532
|
jargonInput: jargonInput,
|
|
4188
4533
|
setJargonInput: setJargonInput,
|
|
4189
4534
|
}),
|
|
4190
|
-
|
|
4191
|
-
// 10. Provider Telemetry & Latency Dashboard
|
|
4192
4535
|
React.createElement(ProviderDashboard, {
|
|
4193
4536
|
t: t,
|
|
4194
4537
|
statsData: statsData,
|
|
4195
4538
|
onRefresh: fetchStatus,
|
|
4196
4539
|
}),
|
|
4197
|
-
|
|
4198
|
-
// 7. Plugin Version & Updates Card
|
|
4199
4540
|
React.createElement(UpdaterCard, {
|
|
4200
4541
|
t: t,
|
|
4201
4542
|
updaterStatus: updaterStatus,
|
|
@@ -4206,8 +4547,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4206
4547
|
onCheck: checkUpdater,
|
|
4207
4548
|
onUpdateNow: onUpdateNow,
|
|
4208
4549
|
}),
|
|
4209
|
-
|
|
4210
|
-
// Action bar (Save)
|
|
4211
4550
|
React.createElement('div', { className: 'cb-row' },
|
|
4212
4551
|
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
|
|
4213
4552
|
saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
|
|
@@ -4215,6 +4554,395 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4215
4554
|
),
|
|
4216
4555
|
)
|
|
4217
4556
|
}
|
|
4557
|
+
// ------------------------------------------------------- first-run wizard (#265)
|
|
4558
|
+
function FirstRunWizard(props) {
|
|
4559
|
+
const t = props.t || moduleT
|
|
4560
|
+
const draft = props.draft
|
|
4561
|
+
const setTop = props.setTop
|
|
4562
|
+
const writable = props.writable
|
|
4563
|
+
const onClose = props.onClose
|
|
4564
|
+
|
|
4565
|
+
const isSecure = typeof window !== 'undefined' ? (window.isSecureContext || window.location.protocol === 'https:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') : true
|
|
4566
|
+
const hasMediaDevices = typeof navigator !== 'undefined' && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function'
|
|
4567
|
+
const hasMediaRecorder = typeof MediaRecorder !== 'undefined'
|
|
4568
|
+
|
|
4569
|
+
const [micPermission, setMicPermission] = React.useState('prompt')
|
|
4570
|
+
const [devices, setDevices] = React.useState([])
|
|
4571
|
+
const [selectedDev, setSelectedDev] = React.useState((draft && draft.micDeviceId) || '')
|
|
4572
|
+
const [signalTesting, setSignalTesting] = React.useState(false)
|
|
4573
|
+
const [currentRms, setCurrentRms] = React.useState(0)
|
|
4574
|
+
const [peakRms, setPeakRms] = React.useState(0)
|
|
4575
|
+
const [testingStt, setTestingStt] = React.useState(false)
|
|
4576
|
+
const [testCountdown, setTestCountdown] = React.useState(0)
|
|
4577
|
+
const [testResult, setTestResult] = React.useState(null)
|
|
4578
|
+
const [testError, setTestError] = React.useState('')
|
|
4579
|
+
const [testLatency, setTestLatency] = React.useState(null)
|
|
4580
|
+
|
|
4581
|
+
const signalStreamRef = React.useRef(null)
|
|
4582
|
+
const audioCtxRef = React.useRef(null)
|
|
4583
|
+
const animFrameRef = React.useRef(null)
|
|
4584
|
+
|
|
4585
|
+
React.useEffect(() => {
|
|
4586
|
+
if (typeof navigator !== 'undefined' && navigator.permissions && navigator.permissions.query) {
|
|
4587
|
+
navigator.permissions.query({ name: 'microphone' })
|
|
4588
|
+
.then((p) => {
|
|
4589
|
+
if (p) {
|
|
4590
|
+
setMicPermission(p.state)
|
|
4591
|
+
p.onchange = () => setMicPermission(p.state)
|
|
4592
|
+
}
|
|
4593
|
+
})
|
|
4594
|
+
.catch(() => {})
|
|
4595
|
+
}
|
|
4596
|
+
}, [])
|
|
4597
|
+
|
|
4598
|
+
const refreshDevices = () => {
|
|
4599
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
|
|
4600
|
+
navigator.mediaDevices.enumerateDevices()
|
|
4601
|
+
.then((list) => {
|
|
4602
|
+
const audioInputs = list.filter((d) => d.kind === 'audioinput')
|
|
4603
|
+
setDevices(audioInputs)
|
|
4604
|
+
})
|
|
4605
|
+
.catch(() => {})
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
React.useEffect(() => {
|
|
4609
|
+
refreshDevices()
|
|
4610
|
+
}, [])
|
|
4611
|
+
|
|
4612
|
+
const stopSignalTest = () => {
|
|
4613
|
+
if (animFrameRef.current) {
|
|
4614
|
+
cancelAnimationFrame(animFrameRef.current)
|
|
4615
|
+
animFrameRef.current = null
|
|
4616
|
+
}
|
|
4617
|
+
if (signalStreamRef.current) {
|
|
4618
|
+
try { signalStreamRef.current.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
|
|
4619
|
+
signalStreamRef.current = null
|
|
4620
|
+
}
|
|
4621
|
+
if (audioCtxRef.current) {
|
|
4622
|
+
try { audioCtxRef.current.close() } catch (_) {}
|
|
4623
|
+
audioCtxRef.current = null
|
|
4624
|
+
}
|
|
4625
|
+
setSignalTesting(false)
|
|
4626
|
+
setCurrentRms(0)
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
React.useEffect(() => {
|
|
4630
|
+
return () => {
|
|
4631
|
+
stopSignalTest()
|
|
4632
|
+
}
|
|
4633
|
+
}, [])
|
|
4634
|
+
|
|
4635
|
+
const requestPermissionAndTestSignal = async () => {
|
|
4636
|
+
stopSignalTest()
|
|
4637
|
+
setTestError('')
|
|
4638
|
+
try {
|
|
4639
|
+
const constraints = {
|
|
4640
|
+
audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
|
|
4641
|
+
}
|
|
4642
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
4643
|
+
signalStreamRef.current = stream
|
|
4644
|
+
setMicPermission('granted')
|
|
4645
|
+
refreshDevices()
|
|
4646
|
+
|
|
4647
|
+
const AC = typeof AudioContext !== 'undefined' ? AudioContext
|
|
4648
|
+
: (typeof webkitAudioContext !== 'undefined' ? webkitAudioContext : null)
|
|
4649
|
+
if (!AC) return
|
|
4650
|
+
const actx = new AC()
|
|
4651
|
+
audioCtxRef.current = actx
|
|
4652
|
+
if (actx.state === 'suspended') { actx.resume().catch(() => {}) }
|
|
4653
|
+
const src = actx.createMediaStreamSource(stream)
|
|
4654
|
+
const analyser = actx.createAnalyser()
|
|
4655
|
+
analyser.fftSize = 128
|
|
4656
|
+
src.connect(analyser)
|
|
4657
|
+
|
|
4658
|
+
setSignalTesting(true)
|
|
4659
|
+
const buf = new Uint8Array(analyser.frequencyBinCount)
|
|
4660
|
+
|
|
4661
|
+
const checkLevel = () => {
|
|
4662
|
+
if (!signalStreamRef.current) return
|
|
4663
|
+
analyser.getByteFrequencyData(buf)
|
|
4664
|
+
let sum = 0
|
|
4665
|
+
for (let i = 0; i < buf.length; i++) sum += buf[i]
|
|
4666
|
+
const rms = Math.min(100, Math.round((sum / buf.length / 255) * 100 * 2.2))
|
|
4667
|
+
setCurrentRms(rms)
|
|
4668
|
+
setPeakRms((prev) => Math.max(prev, rms))
|
|
4669
|
+
animFrameRef.current = requestAnimationFrame(checkLevel)
|
|
4670
|
+
}
|
|
4671
|
+
animFrameRef.current = requestAnimationFrame(checkLevel)
|
|
4672
|
+
} catch (err) {
|
|
4673
|
+
setMicPermission('denied')
|
|
4674
|
+
setTestError(String(err && err.message ? err.message : err))
|
|
4675
|
+
stopSignalTest()
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
const runSttTest = async () => {
|
|
4680
|
+
stopSignalTest()
|
|
4681
|
+
setTestError('')
|
|
4682
|
+
setTestResult(null)
|
|
4683
|
+
setTestLatency(null)
|
|
4684
|
+
setTestingStt(true)
|
|
4685
|
+
setTestCountdown(3)
|
|
4686
|
+
|
|
4687
|
+
let testStream = null
|
|
4688
|
+
let recorder = null
|
|
4689
|
+
const chunks = []
|
|
4690
|
+
|
|
4691
|
+
try {
|
|
4692
|
+
const constraints = {
|
|
4693
|
+
audio: selectedDev ? { deviceId: { exact: selectedDev } } : true,
|
|
4694
|
+
}
|
|
4695
|
+
testStream = await navigator.mediaDevices.getUserMedia(constraints)
|
|
4696
|
+
let mime = 'audio/webm;codecs=opus'
|
|
4697
|
+
if (!MediaRecorder.isTypeSupported(mime)) mime = ''
|
|
4698
|
+
recorder = mime ? new MediaRecorder(testStream, { mimeType: mime }) : new MediaRecorder(testStream)
|
|
4699
|
+
recorder.ondataavailable = (e) => {
|
|
4700
|
+
if (e.data && e.data.size > 0) chunks.push(e.data)
|
|
4701
|
+
}
|
|
4702
|
+
recorder.start()
|
|
4703
|
+
|
|
4704
|
+
let left = 3
|
|
4705
|
+
const countdownTimer = setInterval(() => {
|
|
4706
|
+
left -= 1
|
|
4707
|
+
setTestCountdown(left)
|
|
4708
|
+
if (left <= 0) {
|
|
4709
|
+
clearInterval(countdownTimer)
|
|
4710
|
+
if (recorder.state !== 'inactive') recorder.stop()
|
|
4711
|
+
}
|
|
4712
|
+
}, 1000)
|
|
4713
|
+
|
|
4714
|
+
await new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
|
|
4715
|
+
|
|
4716
|
+
try { testStream.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
|
|
4717
|
+
|
|
4718
|
+
const blob = new Blob(chunks, { type: mime || 'audio/webm' })
|
|
4719
|
+
if (blob.size < 400) {
|
|
4720
|
+
setTestError(t('wizardTestEmpty'))
|
|
4721
|
+
setTestingStt(false)
|
|
4722
|
+
return
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
setTestCountdown(0)
|
|
4726
|
+
const startMs = Date.now()
|
|
4727
|
+
const dataBase64 = await blobToBase64(blob)
|
|
4728
|
+
const payload = {
|
|
4729
|
+
dataBase64,
|
|
4730
|
+
mimeType: mime || recorder.mimeType || 'audio/webm',
|
|
4731
|
+
mode: 'dictation',
|
|
4732
|
+
subMode: 'clean',
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4735
|
+
const res = await fetch('/dsh-voice/transcribe', {
|
|
4736
|
+
method: 'POST',
|
|
4737
|
+
headers: { 'content-type': 'application/json' },
|
|
4738
|
+
body: JSON.stringify(payload),
|
|
4739
|
+
})
|
|
4740
|
+
const data = await res.json()
|
|
4741
|
+
const latency = Date.now() - startMs
|
|
4742
|
+
setTestLatency(latency)
|
|
4743
|
+
|
|
4744
|
+
if (!res.ok || !data || !data.ok) {
|
|
4745
|
+
const msg = (data && data.error && data.error.message) || `HTTP ${res.status}`
|
|
4746
|
+
setTestError(msg)
|
|
4747
|
+
} else {
|
|
4748
|
+
const recognized = String(data.text || '').trim()
|
|
4749
|
+
if (recognized) {
|
|
4750
|
+
setTestResult(recognized)
|
|
4751
|
+
if (typeof setTop === 'function') {
|
|
4752
|
+
setTop('wizardCompleted', true)
|
|
4753
|
+
}
|
|
4754
|
+
} else {
|
|
4755
|
+
setTestError(t('wizardTestEmpty'))
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
} catch (err) {
|
|
4759
|
+
setTestError(String(err && err.message ? err.message : err))
|
|
4760
|
+
} finally {
|
|
4761
|
+
if (testStream) {
|
|
4762
|
+
try { testStream.getTracks().forEach((trk) => trk.stop()) } catch (_) {}
|
|
4763
|
+
}
|
|
4764
|
+
setTestingStt(false)
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// Determine active chain info
|
|
4769
|
+
const dictationChain = (draft && draft.dictation && draft.dictation.chain) || []
|
|
4770
|
+
const activeProvider = (dictationChain[0] && (dictationChain[0].provider || dictationChain[0])) || 'sensevoice'
|
|
4771
|
+
let areaLabel = t('wizardAreaServer')
|
|
4772
|
+
if (activeProvider === 'browser' || activeProvider === 'browser-webgpu') {
|
|
4773
|
+
areaLabel = t('wizardAreaBrowser')
|
|
4774
|
+
} else if (activeProvider !== 'sensevoice' && activeProvider !== 'local-whisper') {
|
|
4775
|
+
areaLabel = t('wizardAreaCloud')
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4778
|
+
let nextStepHint = ''
|
|
4779
|
+
if (!isSecure) {
|
|
4780
|
+
nextStepHint = t('wizardHintHttps')
|
|
4781
|
+
} else if (micPermission === 'denied') {
|
|
4782
|
+
nextStepHint = t('wizardHintPermission')
|
|
4783
|
+
} else if (peakRms === 0 && signalTesting) {
|
|
4784
|
+
nextStepHint = t('wizardHintDevice')
|
|
4785
|
+
} else if (testError) {
|
|
4786
|
+
nextStepHint = t('wizardHintProvider')
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
return React.createElement('div', { className: 'cb-section-card', style: { borderColor: 'var(--dsw-alias-state-brand-primary)' } },
|
|
4790
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
4791
|
+
React.createElement('span', null, '✨ ' + t('wizardTitle')),
|
|
4792
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px' } },
|
|
4793
|
+
testResult ? React.createElement('span', { className: 'cb-badge cb-badge-ok' }, '✓ ' + t('wizardStatusCompleted')) : null,
|
|
4794
|
+
onClose ? React.createElement('button', {
|
|
4795
|
+
type: 'button', className: 'cb-btn cb-btn-mini', onClick: onClose, title: t('wizardClose'),
|
|
4796
|
+
}, '✕') : null,
|
|
4797
|
+
),
|
|
4798
|
+
),
|
|
4799
|
+
React.createElement('div', { className: 'cb-section-desc' }, t('wizardDesc')),
|
|
4800
|
+
|
|
4801
|
+
// Step 1: Environment & Permissions
|
|
4802
|
+
React.createElement('div', { className: 'dvs-card' },
|
|
4803
|
+
React.createElement('div', { className: 'dvs-h' }, t('wizardStep1')),
|
|
4804
|
+
React.createElement('div', { className: 'cb-row' },
|
|
4805
|
+
React.createElement('span', { className: 'cb-badge ' + (isSecure ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
4806
|
+
isSecure ? ('✓ ' + t('wizardSecureContextOk')) : ('✗ ' + t('wizardSecureContextBad'))
|
|
4807
|
+
),
|
|
4808
|
+
React.createElement('span', { className: 'cb-badge ' + (hasMediaDevices ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
4809
|
+
hasMediaDevices ? ('✓ ' + t('wizardMediaDevicesOk')) : ('✗ ' + t('wizardMediaDevicesBad'))
|
|
4810
|
+
),
|
|
4811
|
+
React.createElement('span', { className: 'cb-badge ' + (hasMediaRecorder ? 'cb-badge-ok' : 'cb-badge-err') },
|
|
4812
|
+
hasMediaRecorder ? ('✓ ' + t('wizardMediaRecorderOk')) : ('✗ ' + t('wizardMediaRecorderBad'))
|
|
4813
|
+
),
|
|
4814
|
+
React.createElement('span', { className: 'cb-badge ' + (micPermission === 'granted' ? 'cb-badge-ok' : (micPermission === 'denied' ? 'cb-badge-err' : 'cb-badge-warn')) },
|
|
4815
|
+
micPermission === 'granted' ? ('✓ ' + t('wizardMicPermissionOk')) : (micPermission === 'denied' ? ('✗ ' + t('wizardMicPermissionBad')) : ('· ' + t('wizardMicPermissionPrompt')))
|
|
4816
|
+
),
|
|
4817
|
+
),
|
|
4818
|
+
React.createElement('div', { className: 'cb-field', style: { marginTop: '8px' } },
|
|
4819
|
+
React.createElement('label', null, t('wizardMicLabel')),
|
|
4820
|
+
React.createElement('div', { className: 'cb-row' },
|
|
4821
|
+
React.createElement('select', {
|
|
4822
|
+
value: selectedDev,
|
|
4823
|
+
style: { flex: 1, minWidth: '240px' },
|
|
4824
|
+
onChange: (e) => {
|
|
4825
|
+
setSelectedDev(e.target.value)
|
|
4826
|
+
if (typeof setTop === 'function') setTop('micDeviceId', e.target.value)
|
|
4827
|
+
stopSignalTest()
|
|
4828
|
+
},
|
|
4829
|
+
},
|
|
4830
|
+
React.createElement('option', { value: '' }, 'Default System Microphone'),
|
|
4831
|
+
devices.map((d) => React.createElement('option', { key: d.deviceId, value: d.deviceId }, d.label || `Microphone ${d.deviceId.slice(0, 8)}…`))
|
|
4832
|
+
),
|
|
4833
|
+
React.createElement('button', {
|
|
4834
|
+
type: 'button',
|
|
4835
|
+
className: 'cb-btn',
|
|
4836
|
+
onClick: requestPermissionAndTestSignal,
|
|
4837
|
+
}, t('wizardRequestMic')),
|
|
4838
|
+
),
|
|
4839
|
+
),
|
|
4840
|
+
),
|
|
4841
|
+
|
|
4842
|
+
// Step 2: Live Audio Signal Test
|
|
4843
|
+
React.createElement('div', { className: 'dvs-card' },
|
|
4844
|
+
React.createElement('div', { className: 'dvs-h' }, t('wizardStep2')),
|
|
4845
|
+
React.createElement('div', { className: 'cb-row', style: { alignItems: 'center' } },
|
|
4846
|
+
React.createElement('button', {
|
|
4847
|
+
type: 'button',
|
|
4848
|
+
className: 'cb-btn ' + (signalTesting ? 'cb-btn-danger' : 'cb-btn-primary'),
|
|
4849
|
+
onClick: signalTesting ? stopSignalTest : requestPermissionAndTestSignal,
|
|
4850
|
+
}, signalTesting ? t('wizardStopSignalCheck') : t('wizardStartSignalCheck')),
|
|
4851
|
+
signalTesting ? React.createElement('span', { className: 'dvs-sub' }, t('wizardSignalTesting')) : null,
|
|
4852
|
+
),
|
|
4853
|
+
signalTesting ? React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
4854
|
+
React.createElement('div', {
|
|
4855
|
+
style: {
|
|
4856
|
+
height: '14px',
|
|
4857
|
+
borderRadius: '7px',
|
|
4858
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
4859
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
4860
|
+
overflow: 'hidden',
|
|
4861
|
+
position: 'relative',
|
|
4862
|
+
},
|
|
4863
|
+
},
|
|
4864
|
+
React.createElement('div', {
|
|
4865
|
+
style: {
|
|
4866
|
+
width: `${currentRms}%`,
|
|
4867
|
+
height: '100%',
|
|
4868
|
+
background: currentRms > 5 ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)',
|
|
4869
|
+
transition: 'width 0.05s linear',
|
|
4870
|
+
},
|
|
4871
|
+
}),
|
|
4872
|
+
),
|
|
4873
|
+
React.createElement('span', { className: currentRms > 5 ? 'dvs-ok' : 'dvs-sub' },
|
|
4874
|
+
currentRms > 5
|
|
4875
|
+
? t('wizardSignalOk').replace('{level}', currentRms)
|
|
4876
|
+
: t('wizardSignalZero')
|
|
4877
|
+
),
|
|
4878
|
+
) : null,
|
|
4879
|
+
),
|
|
4880
|
+
|
|
4881
|
+
// Step 3: Transcription Round-Trip Test
|
|
4882
|
+
React.createElement('div', { className: 'dvs-card' },
|
|
4883
|
+
React.createElement('div', { className: 'dvs-h' }, t('wizardStep3')),
|
|
4884
|
+
React.createElement('div', { className: 'cb-row' },
|
|
4885
|
+
React.createElement('span', { className: 'dvs-sub' }, `${t('wizardProviderLabel')} ${activeProvider}`),
|
|
4886
|
+
React.createElement('span', { className: 'cb-badge' }, `${t('wizardProcessingArea')} ${areaLabel}`),
|
|
4887
|
+
),
|
|
4888
|
+
React.createElement('div', { className: 'cb-row', style: { marginTop: '8px' } },
|
|
4889
|
+
React.createElement('button', {
|
|
4890
|
+
type: 'button',
|
|
4891
|
+
className: 'cb-btn cb-btn-primary',
|
|
4892
|
+
disabled: testingStt || micPermission === 'denied',
|
|
4893
|
+
onClick: runSttTest,
|
|
4894
|
+
}, testingStt
|
|
4895
|
+
? (testCountdown > 0 ? t('wizardRecordingTest').replace('{left}', testCountdown) : t('wizardTranscribingTest'))
|
|
4896
|
+
: t('wizardStartTestRecord')
|
|
4897
|
+
),
|
|
4898
|
+
),
|
|
4899
|
+
testResult ? React.createElement('div', { style: { marginTop: '8px', display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
4900
|
+
React.createElement('div', { className: 'dvs-ok' },
|
|
4901
|
+
'✓ ' + t('wizardTestSuccess').replace('{latency}', testLatency || 0)
|
|
4902
|
+
),
|
|
4903
|
+
React.createElement('blockquote', {
|
|
4904
|
+
style: {
|
|
4905
|
+
margin: '4px 0 0 0',
|
|
4906
|
+
padding: '8px 12px',
|
|
4907
|
+
borderRadius: '6px',
|
|
4908
|
+
background: 'var(--dsw-alias-bg-layer-3)',
|
|
4909
|
+
borderLeft: '4px solid var(--dsw-alias-state-success-primary)',
|
|
4910
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
4911
|
+
fontStyle: 'italic',
|
|
4912
|
+
},
|
|
4913
|
+
}, `“${testResult}”`),
|
|
4914
|
+
) : null,
|
|
4915
|
+
testError ? React.createElement('div', { className: 'cb-alert-err', style: { marginTop: '8px' } },
|
|
4916
|
+
t('wizardTestFailed').replace('{error}', testError)
|
|
4917
|
+
) : null,
|
|
4918
|
+
),
|
|
4919
|
+
|
|
4920
|
+
// Next Step Hint / Feedback
|
|
4921
|
+
nextStepHint ? React.createElement('div', { className: 'dvs-wait', style: { fontStyle: 'italic' } },
|
|
4922
|
+
t('wizardNextStepHint').replace('{hint}', nextStepHint)
|
|
4923
|
+
) : null,
|
|
4924
|
+
|
|
4925
|
+
// Wizard Bottom Actions
|
|
4926
|
+
React.createElement('div', { className: 'cb-row', style: { justifyContent: 'space-between', marginTop: '6px' } },
|
|
4927
|
+
React.createElement('button', {
|
|
4928
|
+
type: 'button',
|
|
4929
|
+
className: 'cb-btn',
|
|
4930
|
+
onClick: () => {
|
|
4931
|
+
if (typeof setTop === 'function') setTop('wizardCompleted', true)
|
|
4932
|
+
if (onClose) onClose()
|
|
4933
|
+
},
|
|
4934
|
+
}, t('wizardSkip')),
|
|
4935
|
+
testResult ? React.createElement('button', {
|
|
4936
|
+
type: 'button',
|
|
4937
|
+
className: 'cb-btn cb-btn-primary',
|
|
4938
|
+
onClick: () => {
|
|
4939
|
+
if (typeof setTop === 'function') setTop('wizardCompleted', true)
|
|
4940
|
+
if (onClose) onClose()
|
|
4941
|
+
},
|
|
4942
|
+
}, '✓ ' + t('wizardFinishSuccess')) : null,
|
|
4943
|
+
),
|
|
4944
|
+
)
|
|
4945
|
+
}
|
|
4218
4946
|
// Card in Settings → Plugins → Plugin settings (#18)
|
|
4219
4947
|
function PluginCard(props) {
|
|
4220
4948
|
const page = !!(props && props.view === 'page')
|
|
@@ -4320,6 +5048,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
|
4320
5048
|
isAllowedInEditable,
|
|
4321
5049
|
isHotkeyRelease,
|
|
4322
5050
|
extractContextKeywords,
|
|
5051
|
+
DICTATION_MODES,
|
|
5052
|
+
getDictationModeLabel,
|
|
5053
|
+
cycleDictationMode,
|
|
4323
5054
|
resolveBrowserRecognitionLang,
|
|
4324
5055
|
VoiceSection,
|
|
4325
5056
|
cutPhrase,
|