@goodandready/dsh-voice 0.9.30 → 0.9.32
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 +3 -0
- package/README.ru.md +3 -0
- package/README.zh.md +3 -0
- package/lib/client.js +627 -93
- package/lib/dictation-modes.js +28 -0
- package/lib/host-routes.js +53 -19
- package/lib/macros.js +53 -0
- package/lib/polish.js +14 -6
- package/lib/providers.js +3 -1
- package/lib/schema.js +16 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -132,6 +132,14 @@ window.__ModuleLoader__.load({
|
|
|
132
132
|
'bargeIn': 'Barge-in',
|
|
133
133
|
'polishSend': 'Polish whole draft before sending',
|
|
134
134
|
'polishSendHint': 'Run the composed draft through the model right before sending',
|
|
135
|
+
'editablePreview': 'Editable preview before send',
|
|
136
|
+
'editablePreviewHint': 'Show recognized text before sending with editing, comparison and abort options.',
|
|
137
|
+
'editPreview': 'Edit',
|
|
138
|
+
'sendNow': 'Send',
|
|
139
|
+
'cancelPreview': 'Cancel',
|
|
140
|
+
'rawText': 'Raw',
|
|
141
|
+
'polishedText': 'Polished',
|
|
142
|
+
'sessionMismatch': 'Different chat active; canceled to prevent leak.',
|
|
135
143
|
'sessionCommands': 'Voice session commands',
|
|
136
144
|
'sessionCommandsHint': '"send", "cancel", "stop", "continue" act on the session instead of becoming text',
|
|
137
145
|
'polishBaseUrl': 'Offline polish endpoint',
|
|
@@ -240,6 +248,27 @@ window.__ModuleLoader__.load({
|
|
|
240
248
|
'voiceActionsLabel': 'Voice action commands ("send", "cancel", "clear", "new line")',
|
|
241
249
|
'liveInterimPreview': 'Live speech preview (ghost text preview)',
|
|
242
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',
|
|
243
272
|
'jargonTitle': 'Developer Lexicon & IT Jargon',
|
|
244
273
|
'techJargonCorrection': 'Auto-correct technical jargon (GitHub, Docker, pnpm, Kubernetes, etc.)',
|
|
245
274
|
'jargonAdd': 'Add replacement',
|
|
@@ -350,6 +379,14 @@ window.__ModuleLoader__.load({
|
|
|
350
379
|
'bargeIn': '语音插话打断 (Barge-in)',
|
|
351
380
|
'polishSend': '发送前润色整篇草稿',
|
|
352
381
|
'polishSendHint': '发送给智能体前对整个输入框草稿进行一次整体润色',
|
|
382
|
+
'editablePreview': '发送前可编辑预览',
|
|
383
|
+
'editablePreviewHint': '发送前显示识别文本,支持编辑、润色对比和撤销。',
|
|
384
|
+
'editPreview': '编辑',
|
|
385
|
+
'sendNow': '发送',
|
|
386
|
+
'cancelPreview': '取消',
|
|
387
|
+
'rawText': '原文',
|
|
388
|
+
'polishedText': '润色',
|
|
389
|
+
'sessionMismatch': '当前会话已切换,已拦截发送以防止串话。',
|
|
353
390
|
'sessionCommands': '会话语音控制指令',
|
|
354
391
|
'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
|
|
355
392
|
'polishBaseUrl': '离线润色服务端点',
|
|
@@ -458,6 +495,27 @@ window.__ModuleLoader__.load({
|
|
|
458
495
|
'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
|
|
459
496
|
'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
|
|
460
497
|
'structuredPromptVoice': '语音应答交互式选择提示',
|
|
498
|
+
'modeVerbatim': '逐字直录',
|
|
499
|
+
'modeClean': '清爽文本',
|
|
500
|
+
'modeTask': '智能体任务',
|
|
501
|
+
'modeNote': '便签备忘',
|
|
502
|
+
'dictationModeTitle': '听写后处理模式',
|
|
503
|
+
'dictationModeDesc': '选择听写文本后处理方式:逐字直录(零LLM调用)、清爽文本(整理标点去除语气词)、智能体任务(结构化提示词)或便签(要点与行动项)。',
|
|
504
|
+
'dictationPromptsTitle': '听写提示词模板',
|
|
505
|
+
'dictationPromptsDesc': '自定义听写后处理提示词模板。使用 {text} 作为识别语音的占位符。',
|
|
506
|
+
'promptClean': '清爽文本提示词',
|
|
507
|
+
'promptTask': '智能体任务提示词',
|
|
508
|
+
'promptNote': '便签提示词',
|
|
509
|
+
'resetPrompts': '恢复默认模板',
|
|
510
|
+
'voiceMacrosTitle': '语音动作宏',
|
|
511
|
+
'voiceMacrosDesc': '语音触发词自动展开为结构化提示词模板。',
|
|
512
|
+
'addMacro': '添加动作宏',
|
|
513
|
+
'macroTrigger': '语音触发短语',
|
|
514
|
+
'macroTemplate': '展开提示词模板',
|
|
515
|
+
'macroDelete': '删除',
|
|
516
|
+
'rawTab': '原始识别',
|
|
517
|
+
'resultTab': '处理结果',
|
|
518
|
+
'llmUnavailableFallback': 'LLM不可用,已保留原始识别文本',
|
|
461
519
|
'jargonTitle': '开发者词汇与 IT 术语',
|
|
462
520
|
'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
|
|
463
521
|
'jargonAdd': '添加替换项',
|
|
@@ -518,7 +576,20 @@ window.__ModuleLoader__.load({
|
|
|
518
576
|
'.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}' +
|
|
519
577
|
'.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}' +
|
|
520
578
|
'.dvo-jargon-table{width:100%;margin-top:6px;border-collapse:collapse;font-size:12px}' +
|
|
521
|
-
'.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}'
|
|
579
|
+
'.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
|
|
580
|
+
'.dvo-pill-preview{display:flex;flex-direction:column;gap:8px;padding:10px 14px;border-radius:18px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px color-mix(in srgb, var(--dsw-alias-label-primary) 18%, transparent);box-sizing:border-box}' +
|
|
581
|
+
'.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}' +
|
|
582
|
+
'.dvo-preview-tabs{display:inline-flex;align-items:center;gap:4px;background:var(--dsw-alias-bg-layer-2);padding:2px;border-radius:12px;border:1px solid var(--dsw-alias-border-l1)}' +
|
|
583
|
+
'.dvo-tab-btn{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500;padding:3px 10px;border-radius:10px;cursor:pointer;transition:all 150ms ease}' +
|
|
584
|
+
'.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}' +
|
|
585
|
+
'.dvo-tab-btn-active{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb, var(--dsw-alias-label-primary) 15%, transparent)}' +
|
|
586
|
+
'.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}' +
|
|
587
|
+
'.dvo-preview-text{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);line-height:1.4;word-break:break-word;max-height:80px;overflow-y:auto;padding:2px 0}' +
|
|
588
|
+
'.dvo-preview-input{flex:1;font-size:13px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 8px;outline:none;font-family:inherit;width:100%}' +
|
|
589
|
+
'.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
|
|
590
|
+
'.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}' +
|
|
591
|
+
'.dvo-btn-send:hover{opacity:0.9}' +
|
|
592
|
+
'.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)}'
|
|
522
593
|
const cssId = 'dsh-voice/client.module.css'
|
|
523
594
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
|
|
524
595
|
const tag = document.createElement('style')
|
|
@@ -543,7 +614,7 @@ window.__ModuleLoader__.load({
|
|
|
543
614
|
inputActions: null,
|
|
544
615
|
input: null,
|
|
545
616
|
settings: {
|
|
546
|
-
vadSilenceMs: 700, autoSendMs: 4000, stream: false, streamChunkMs: 1200, vadAdapt: 0,
|
|
617
|
+
vadSilenceMs: 700, autoSendMs: 4000, editablePreview: true, stream: false, streamChunkMs: 1200, vadAdapt: 0,
|
|
547
618
|
noiseSuppression: true, contextGlossary: true,
|
|
548
619
|
gatedTurnTaking: true, autoSendVisualRing: true, voiceActions: true,
|
|
549
620
|
techJargonCorrection: true, jargonDictionary: [], liveInterimPreview: true,
|
|
@@ -714,7 +785,26 @@ window.__ModuleLoader__.load({
|
|
|
714
785
|
|
|
715
786
|
function extractContextKeywords() {
|
|
716
787
|
if (!voice.settings || voice.settings.contextGlossary === false) return []
|
|
717
|
-
const
|
|
788
|
+
const chunks = []
|
|
789
|
+
const draft = (voice.input && typeof voice.input.draft === 'string') ? voice.input.draft : ''
|
|
790
|
+
if (draft && draft.length >= 3) chunks.push(draft)
|
|
791
|
+
if (voice.session && Array.isArray(voice.session.messages)) {
|
|
792
|
+
const msgs = voice.session.messages.slice(-5)
|
|
793
|
+
for (const m of msgs) {
|
|
794
|
+
const t = (m && typeof m.content === 'string') ? m.content : ((m && typeof m.text === 'string') ? m.text : '')
|
|
795
|
+
if (t && t.length >= 3) chunks.push(t)
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (typeof document !== 'undefined') {
|
|
799
|
+
try {
|
|
800
|
+
const nodes = document.querySelectorAll('.message-content code, .dsh-message-body code, pre code')
|
|
801
|
+
for (let i = 0; i < Math.min(nodes.length, 10); i++) {
|
|
802
|
+
const ct = nodes[i].textContent
|
|
803
|
+
if (ct && ct.length < 100) chunks.push(ct)
|
|
804
|
+
}
|
|
805
|
+
} catch (_) {}
|
|
806
|
+
}
|
|
807
|
+
const text = chunks.join(' ')
|
|
718
808
|
if (!text || text.length < 3) return []
|
|
719
809
|
const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
|
|
720
810
|
// Linguistic filter: functional stop-word dictionary for vocabulary keyword extraction.
|
|
@@ -746,7 +836,11 @@ window.__ModuleLoader__.load({
|
|
|
746
836
|
|
|
747
837
|
const tryHost = async (chainOverride) => {
|
|
748
838
|
const dataBase64 = await blobToBase64(blob)
|
|
749
|
-
const
|
|
839
|
+
const subMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
840
|
+
|| voice.selectedDictationMode
|
|
841
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
842
|
+
|| 'clean'
|
|
843
|
+
const payload = { dataBase64, mimeType: mime, mode, subMode }
|
|
750
844
|
if (Array.isArray(chainOverride)) {
|
|
751
845
|
payload.chain = chainOverride.map((p) => (typeof p === 'string' ? { provider: p, model: modelMap[p] } : p))
|
|
752
846
|
}
|
|
@@ -1458,7 +1552,14 @@ voice._chainsPromise = null
|
|
|
1458
1552
|
if (targetActions && typeof targetActions.captureInsertion === 'function') {
|
|
1459
1553
|
try { span = targetActions.captureInsertion() } catch (_) {}
|
|
1460
1554
|
}
|
|
1461
|
-
const
|
|
1555
|
+
const initialDraft = (targetInput && typeof targetInput.draft === 'string') ? targetInput.draft : ''
|
|
1556
|
+
const targetSession = voice.session || (targetInput && targetInput.session)
|
|
1557
|
+
const sessionId = (targetSession && (targetSession.sessionId || targetSession.id))
|
|
1558
|
+
|| voice.currentSessionId || ''
|
|
1559
|
+
const snapshotSubMode = voice.selectedDictationMode
|
|
1560
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
1561
|
+
|| 'clean'
|
|
1562
|
+
const target = { actions: targetActions, input: targetInput, session: targetSession, span, initialDraft, sessionId, dictationMode: snapshotSubMode }
|
|
1462
1563
|
voice._activeTarget = target
|
|
1463
1564
|
|
|
1464
1565
|
if (voice.isTtsSpeaking) {
|
|
@@ -1539,7 +1640,12 @@ voice._chainsPromise = null
|
|
|
1539
1640
|
if (mode === 'message') {
|
|
1540
1641
|
const ok = appendDraft(said, target)
|
|
1541
1642
|
if (ok !== false) {
|
|
1542
|
-
|
|
1643
|
+
const usePreview = voice.settings.editablePreview !== false
|
|
1644
|
+
const autoMs = Number(voice.settings.autoSendMs) || 4000
|
|
1645
|
+
const pendingState = usePreview
|
|
1646
|
+
? initPendingPreview(said, target, { autoSendMs: autoMs })
|
|
1647
|
+
: { text: said, target, totalMs: autoMs, leftMs: autoMs }
|
|
1648
|
+
voice.set({ phase: 'pending', pending: pendingState })
|
|
1543
1649
|
} else {
|
|
1544
1650
|
voice.set({ phase: 'idle' })
|
|
1545
1651
|
}
|
|
@@ -1610,7 +1716,24 @@ voice._chainsPromise = null
|
|
|
1610
1716
|
if (voice.lastNote) voice.lastNote.text = text
|
|
1611
1717
|
if (mode === 'message') {
|
|
1612
1718
|
if (ok !== false) {
|
|
1613
|
-
|
|
1719
|
+
const usePreview = voice.settings.editablePreview !== false
|
|
1720
|
+
let polishedText = text
|
|
1721
|
+
if (usePreview && voice.settings.polishSend) {
|
|
1722
|
+
try {
|
|
1723
|
+
const pRes = await fetch('/dsh-voice/polish', {
|
|
1724
|
+
method: 'POST',
|
|
1725
|
+
headers: { 'content-type': 'application/json' },
|
|
1726
|
+
body: JSON.stringify({ text }),
|
|
1727
|
+
})
|
|
1728
|
+
const pData = await pRes.json()
|
|
1729
|
+
if (pData && pData.text) polishedText = pData.text
|
|
1730
|
+
} catch (_) {}
|
|
1731
|
+
}
|
|
1732
|
+
const autoMs = Number(voice.settings.autoSendMs) || 4000
|
|
1733
|
+
const pendingState = usePreview
|
|
1734
|
+
? initPendingPreview(text, target, { autoSendMs: autoMs, polishedText, audioUrl: noteUrl, polishSend: voice.settings.polishSend })
|
|
1735
|
+
: { text, target, totalMs: autoMs, leftMs: autoMs, audioUrl: noteUrl }
|
|
1736
|
+
voice.set({ phase: 'pending', pending: pendingState })
|
|
1614
1737
|
} else {
|
|
1615
1738
|
voice.set({ phase: 'idle' })
|
|
1616
1739
|
}
|
|
@@ -1658,38 +1781,11 @@ voice._chainsPromise = null
|
|
|
1658
1781
|
}
|
|
1659
1782
|
|
|
1660
1783
|
function submitPending() {
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
const text = pending.text
|
|
1664
|
-
const target = pending.target || voice._activeTarget
|
|
1665
|
-
const actions = (target && target.actions) || voice.inputActions
|
|
1666
|
-
voice.pending = null
|
|
1667
|
-
voice.set({ phase: 'idle' })
|
|
1784
|
+
submitPendingPreview(voice)
|
|
1785
|
+
}
|
|
1668
1786
|
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
method: 'POST',
|
|
1672
|
-
headers: { 'content-type': 'application/json' },
|
|
1673
|
-
body: JSON.stringify({ text }),
|
|
1674
|
-
})
|
|
1675
|
-
.then((res) => res.json())
|
|
1676
|
-
.then((data) => {
|
|
1677
|
-
const polished = (data && data.text) || text
|
|
1678
|
-
if (actions && typeof actions.setDraft === 'function') {
|
|
1679
|
-
actions.setDraft(polished)
|
|
1680
|
-
}
|
|
1681
|
-
if (actions && typeof actions.submit === 'function') {
|
|
1682
|
-
actions.submit()
|
|
1683
|
-
}
|
|
1684
|
-
})
|
|
1685
|
-
.catch(() => {
|
|
1686
|
-
if (actions && typeof actions.submit === 'function') {
|
|
1687
|
-
actions.submit()
|
|
1688
|
-
}
|
|
1689
|
-
})
|
|
1690
|
-
} else if (actions && typeof actions.submit === 'function') {
|
|
1691
|
-
actions.submit()
|
|
1692
|
-
}
|
|
1787
|
+
function cancelPending() {
|
|
1788
|
+
cancelPendingPreview(voice)
|
|
1693
1789
|
}
|
|
1694
1790
|
|
|
1695
1791
|
function runSessionCommand(cmd) {
|
|
@@ -1700,8 +1796,7 @@ voice._chainsPromise = null
|
|
|
1700
1796
|
}
|
|
1701
1797
|
|
|
1702
1798
|
function keepPending() {
|
|
1703
|
-
voice
|
|
1704
|
-
voice.set({ phase: 'idle' })
|
|
1799
|
+
keepPendingPreview(voice)
|
|
1705
1800
|
}
|
|
1706
1801
|
// ----------------------------------------------------------- components
|
|
1707
1802
|
function VoiceButtons(props) {
|
|
@@ -1711,7 +1806,15 @@ voice._chainsPromise = null
|
|
|
1711
1806
|
if (v.phase !== 'idle' && v.phase !== 'error' && !hold.armed) return null
|
|
1712
1807
|
const err = v.phase === 'error'
|
|
1713
1808
|
const hk = voice.hotkey ? ` (${voice.hotkey})` : ''
|
|
1809
|
+
const curMode = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
|
|
1714
1810
|
return React.createElement(React.Fragment, null,
|
|
1811
|
+
React.createElement('button', {
|
|
1812
|
+
type: 'button',
|
|
1813
|
+
className: 'dvo-mode-pill-btn',
|
|
1814
|
+
title: t('dictationModeTitle') + ': ' + getDictationModeLabel(curMode, t),
|
|
1815
|
+
'aria-label': t('dictationModeTitle') + ': ' + getDictationModeLabel(curMode, t),
|
|
1816
|
+
onClick: cycleDictationMode,
|
|
1817
|
+
}, getDictationModeLabel(curMode, t)),
|
|
1715
1818
|
React.createElement('button', {
|
|
1716
1819
|
type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
|
|
1717
1820
|
title: err ? v.error : t('dictationBtn'),
|
|
@@ -1930,6 +2033,277 @@ voice._chainsPromise = null
|
|
|
1930
2033
|
}
|
|
1931
2034
|
return cleanup
|
|
1932
2035
|
}
|
|
2036
|
+
// ------------------------------------------------------------------ 43-preview
|
|
2037
|
+
// Editable preview state machine, session pinning & UI component (#170).
|
|
2038
|
+
|
|
2039
|
+
function initPendingPreview(text, target, options = {}) {
|
|
2040
|
+
const autoMs = Number(options.autoSendMs) || 4000
|
|
2041
|
+
const polishedText = options.polishedText || text
|
|
2042
|
+
const isPolished = options.polishSend && polishedText && polishedText !== text
|
|
2043
|
+
const initialDraft = (target && typeof target.initialDraft === 'string')
|
|
2044
|
+
? target.initialDraft
|
|
2045
|
+
: ((target && target.input && typeof target.input.draft === 'string') ? target.input.draft : '')
|
|
2046
|
+
const sessionId = (target && target.sessionId) || ''
|
|
2047
|
+
|
|
2048
|
+
return {
|
|
2049
|
+
text,
|
|
2050
|
+
rawText: text,
|
|
2051
|
+
polishedText,
|
|
2052
|
+
activeText: isPolished ? polishedText : text,
|
|
2053
|
+
selectedTab: isPolished ? 'polished' : 'raw',
|
|
2054
|
+
isEditing: false,
|
|
2055
|
+
target,
|
|
2056
|
+
sessionId,
|
|
2057
|
+
initialDraft,
|
|
2058
|
+
totalMs: autoMs,
|
|
2059
|
+
leftMs: autoMs,
|
|
2060
|
+
paused: false,
|
|
2061
|
+
audioUrl: options.audioUrl || '',
|
|
2062
|
+
sessionMismatch: false,
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function cancelPendingPreview(voice) {
|
|
2067
|
+
const pending = voice.pending
|
|
2068
|
+
if (!pending) {
|
|
2069
|
+
voice.set({ phase: 'idle' })
|
|
2070
|
+
return
|
|
2071
|
+
}
|
|
2072
|
+
const target = pending.target || voice._activeTarget
|
|
2073
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
2074
|
+
|
|
2075
|
+
// Restore untouched composer draft if preview was active
|
|
2076
|
+
if (pending.rawText !== undefined && pending.initialDraft !== undefined) {
|
|
2077
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
2078
|
+
try { actions.setDraft(pending.initialDraft) } catch (_) {}
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
voice.pending = null
|
|
2082
|
+
voice.set({ phase: 'idle' })
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function keepPendingPreview(voice) {
|
|
2086
|
+
const pending = voice.pending
|
|
2087
|
+
if (!pending) {
|
|
2088
|
+
voice.set({ phase: 'idle' })
|
|
2089
|
+
return
|
|
2090
|
+
}
|
|
2091
|
+
const target = pending.target || voice._activeTarget
|
|
2092
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
2093
|
+
|
|
2094
|
+
if (pending.rawText !== undefined) {
|
|
2095
|
+
// Insert final edited text into composer draft without auto-sending
|
|
2096
|
+
const initial = pending.initialDraft || ''
|
|
2097
|
+
const textToInsert = pending.activeText || ''
|
|
2098
|
+
const full = initial
|
|
2099
|
+
? (initial.endsWith(' ') || initial.endsWith('\n') ? (initial + textToInsert) : (initial + ' ' + textToInsert))
|
|
2100
|
+
: textToInsert
|
|
2101
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
2102
|
+
try { actions.setDraft(full) } catch (_) {}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
voice.pending = null
|
|
2106
|
+
voice.set({ phase: 'idle' })
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function submitPendingPreview(voice) {
|
|
2110
|
+
const pending = voice.pending
|
|
2111
|
+
if (!pending) return
|
|
2112
|
+
const target = pending.target || voice._activeTarget
|
|
2113
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
2114
|
+
|
|
2115
|
+
// Session guard (#170): verify that current session matches initiating session
|
|
2116
|
+
const currentSessionId = (target && target.session && (target.session.sessionId || target.session.id))
|
|
2117
|
+
|| (voice.session && (voice.session.sessionId || voice.session.id))
|
|
2118
|
+
|| voice.currentSessionId || ''
|
|
2119
|
+
|
|
2120
|
+
if (pending.sessionId && currentSessionId && pending.sessionId !== currentSessionId) {
|
|
2121
|
+
pending.sessionMismatch = true
|
|
2122
|
+
pending.paused = true
|
|
2123
|
+
voice.notify()
|
|
2124
|
+
return
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
const textToSend = (pending.rawText !== undefined ? pending.activeText : pending.text) || ''
|
|
2128
|
+
voice.pending = null
|
|
2129
|
+
voice.set({ phase: 'idle' })
|
|
2130
|
+
|
|
2131
|
+
if (pending.rawText !== undefined) {
|
|
2132
|
+
const initial = pending.initialDraft || ''
|
|
2133
|
+
const full = initial
|
|
2134
|
+
? (initial.endsWith(' ') || initial.endsWith('\n') ? (initial + textToSend) : (initial + ' ' + textToSend))
|
|
2135
|
+
: textToSend
|
|
2136
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
2137
|
+
try { actions.setDraft(full) } catch (_) {}
|
|
2138
|
+
}
|
|
2139
|
+
if (actions && typeof actions.submit === 'function') {
|
|
2140
|
+
try { actions.submit() } catch (_) {}
|
|
2141
|
+
}
|
|
2142
|
+
} else {
|
|
2143
|
+
// Legacy path without rich preview
|
|
2144
|
+
if (voice.settings.polishSend) {
|
|
2145
|
+
fetch('/dsh-voice/polish', {
|
|
2146
|
+
method: 'POST',
|
|
2147
|
+
headers: { 'content-type': 'application/json' },
|
|
2148
|
+
body: JSON.stringify({ text: textToSend }),
|
|
2149
|
+
})
|
|
2150
|
+
.then((res) => res.json())
|
|
2151
|
+
.then((data) => {
|
|
2152
|
+
const polished = (data && data.text) || textToSend
|
|
2153
|
+
if (actions && typeof actions.setDraft === 'function') actions.setDraft(polished)
|
|
2154
|
+
if (actions && typeof actions.submit === 'function') actions.submit()
|
|
2155
|
+
})
|
|
2156
|
+
.catch(() => {
|
|
2157
|
+
if (actions && typeof actions.submit === 'function') actions.submit()
|
|
2158
|
+
})
|
|
2159
|
+
} else if (actions && typeof actions.submit === 'function') {
|
|
2160
|
+
actions.submit()
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef) {
|
|
2166
|
+
const p = voice.pending
|
|
2167
|
+
const total = p.totalMs || Number(voice.settings.autoSendMs) || 4000
|
|
2168
|
+
const leftMs = Math.max(0, p.leftMs || 0)
|
|
2169
|
+
const left = Math.ceil(leftMs / 1000)
|
|
2170
|
+
const frac = total > 0 ? Math.max(0, Math.min(1, leftMs / total)) : 0
|
|
2171
|
+
const circ = 2 * Math.PI * 14
|
|
2172
|
+
const off = circ * (1 - frac)
|
|
2173
|
+
const hasPolish = p.polishedText && p.polishedText !== p.rawText
|
|
2174
|
+
|
|
2175
|
+
return React.createElement('div', { className: 'dvo-pill-preview', role: 'region', 'aria-label': t('editablePreview') },
|
|
2176
|
+
// Header row: tabs, audio playback, status/countdown, cancel button
|
|
2177
|
+
React.createElement('div', { className: 'dvo-preview-header' },
|
|
2178
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2179
|
+
hasPolish
|
|
2180
|
+
? React.createElement('div', { className: 'dvo-preview-tabs' },
|
|
2181
|
+
React.createElement('button', {
|
|
2182
|
+
type: 'button',
|
|
2183
|
+
className: 'dvo-tab-btn' + (p.selectedTab === 'raw' ? ' dvo-tab-btn-active' : ''),
|
|
2184
|
+
onClick: () => {
|
|
2185
|
+
p.selectedTab = 'raw'
|
|
2186
|
+
p.activeText = p.rawText
|
|
2187
|
+
voice.notify()
|
|
2188
|
+
},
|
|
2189
|
+
}, t('rawText')),
|
|
2190
|
+
React.createElement('button', {
|
|
2191
|
+
type: 'button',
|
|
2192
|
+
className: 'dvo-tab-btn' + (p.selectedTab === 'polished' ? ' dvo-tab-btn-active' : ''),
|
|
2193
|
+
onClick: () => {
|
|
2194
|
+
p.selectedTab = 'polished'
|
|
2195
|
+
p.activeText = p.polishedText
|
|
2196
|
+
voice.notify()
|
|
2197
|
+
},
|
|
2198
|
+
}, t('polishedText')),
|
|
2199
|
+
)
|
|
2200
|
+
: React.createElement('span', { className: 'dvo-count' }, t('rawText')),
|
|
2201
|
+
p.audioUrl
|
|
2202
|
+
? React.createElement('div', { className: 'dvo-audio-wrap' },
|
|
2203
|
+
React.createElement('button', {
|
|
2204
|
+
type: 'button', className: 'dvo-audio-play',
|
|
2205
|
+
title: isPlaying ? t('pause') : t('play'),
|
|
2206
|
+
'aria-label': isPlaying ? t('pause') : t('play'),
|
|
2207
|
+
onClick: () => {
|
|
2208
|
+
const el = audioRef.current
|
|
2209
|
+
if (!el) return
|
|
2210
|
+
if (el.paused) { el.play().catch(() => {}); setIsPlaying(true) }
|
|
2211
|
+
else { el.pause(); setIsPlaying(false) }
|
|
2212
|
+
},
|
|
2213
|
+
}, isPlaying ? pauseIcon() : playIcon()),
|
|
2214
|
+
React.createElement('audio', {
|
|
2215
|
+
ref: audioRef, src: p.audioUrl,
|
|
2216
|
+
onEnded: () => setIsPlaying(false),
|
|
2217
|
+
onPause: () => setIsPlaying(false),
|
|
2218
|
+
onPlay: () => setIsPlaying(true),
|
|
2219
|
+
}),
|
|
2220
|
+
React.createElement('span', { className: 'dvo-audio-time' }, t('listenBack')),
|
|
2221
|
+
)
|
|
2222
|
+
: null,
|
|
2223
|
+
),
|
|
2224
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2225
|
+
(!p.paused && !p.isEditing && v.settings.autoSendVisualRing !== false)
|
|
2226
|
+
? React.createElement('svg', { className: 'dvo-silence-ring', viewBox: '0 0 34 34' },
|
|
2227
|
+
React.createElement('circle', { cx: 17, cy: 17, r: 14, className: 'dvo-ring-bg' }),
|
|
2228
|
+
React.createElement('circle', {
|
|
2229
|
+
cx: 17, cy: 17, r: 14, className: 'dvo-ring-fg',
|
|
2230
|
+
strokeDasharray: `${circ} ${circ}`,
|
|
2231
|
+
strokeDashoffset: off,
|
|
2232
|
+
}),
|
|
2233
|
+
)
|
|
2234
|
+
: null,
|
|
2235
|
+
React.createElement('span', { className: 'dvo-count' },
|
|
2236
|
+
(p.paused || p.isEditing) ? '' : (left + t('secondsShort'))
|
|
2237
|
+
),
|
|
2238
|
+
React.createElement('button', {
|
|
2239
|
+
type: 'button', className: 'dvo-pbtn', title: t('cancelPreview'),
|
|
2240
|
+
'aria-label': t('cancelPreview'),
|
|
2241
|
+
onClick: () => { cancelPending(); focusComposer() },
|
|
2242
|
+
}, xIcon()),
|
|
2243
|
+
),
|
|
2244
|
+
),
|
|
2245
|
+
// Body row: editable input or preview text, Edit button, Send button
|
|
2246
|
+
React.createElement('div', { className: 'dvo-preview-body' },
|
|
2247
|
+
p.isEditing
|
|
2248
|
+
? React.createElement('input', {
|
|
2249
|
+
type: 'text',
|
|
2250
|
+
className: 'dvo-preview-input',
|
|
2251
|
+
value: p.activeText,
|
|
2252
|
+
autoFocus: true,
|
|
2253
|
+
onChange: (e) => {
|
|
2254
|
+
p.activeText = e.target.value
|
|
2255
|
+
voice.notify()
|
|
2256
|
+
},
|
|
2257
|
+
onKeyDown: (e) => {
|
|
2258
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2259
|
+
e.preventDefault()
|
|
2260
|
+
submitPending()
|
|
2261
|
+
}
|
|
2262
|
+
},
|
|
2263
|
+
})
|
|
2264
|
+
: React.createElement('span', { className: 'dvo-preview-text' }, p.activeText),
|
|
2265
|
+
!p.isEditing
|
|
2266
|
+
? React.createElement('button', {
|
|
2267
|
+
type: 'button', className: 'dvo-pbtn', title: t('editPreview'),
|
|
2268
|
+
'aria-label': t('editPreview'),
|
|
2269
|
+
onClick: () => {
|
|
2270
|
+
p.isEditing = true
|
|
2271
|
+
p.paused = true
|
|
2272
|
+
voice.notify()
|
|
2273
|
+
},
|
|
2274
|
+
}, editIcon())
|
|
2275
|
+
: null,
|
|
2276
|
+
React.createElement('button', {
|
|
2277
|
+
type: 'button', className: 'dvo-btn-send', title: t('sendNow'),
|
|
2278
|
+
'aria-label': t('sendNow'),
|
|
2279
|
+
onClick: () => { submitPending(); focusComposer() },
|
|
2280
|
+
}, sendIcon()),
|
|
2281
|
+
),
|
|
2282
|
+
// Mismatch warning banner
|
|
2283
|
+
p.sessionMismatch
|
|
2284
|
+
? React.createElement('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
|
|
2285
|
+
: null,
|
|
2286
|
+
)
|
|
2287
|
+
}
|
|
2288
|
+
// ------------------------------------------------------------------ 45-dictation
|
|
2289
|
+
// Dictation modes UI helper, mode cycler and labels (#264)
|
|
2290
|
+
|
|
2291
|
+
const DICTATION_MODES = ['clean', 'task', 'note', 'verbatim']
|
|
2292
|
+
|
|
2293
|
+
function getDictationModeLabel(m, t) {
|
|
2294
|
+
const trans = t || moduleT
|
|
2295
|
+
if (m === 'verbatim') return trans('modeVerbatim')
|
|
2296
|
+
if (m === 'task') return trans('modeTask')
|
|
2297
|
+
if (m === 'note') return trans('modeNote')
|
|
2298
|
+
return trans('modeClean')
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function cycleDictationMode() {
|
|
2302
|
+
const cur = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
|
|
2303
|
+
const nextIdx = (DICTATION_MODES.indexOf(cur) + 1) % DICTATION_MODES.length
|
|
2304
|
+
voice.selectedDictationMode = DICTATION_MODES[nextIdx]
|
|
2305
|
+
voice.notify()
|
|
2306
|
+
}
|
|
1933
2307
|
// ----------------------------------------------------------- visualizers
|
|
1934
2308
|
let _accentColorCache = { color: '', expires: 0 }
|
|
1935
2309
|
let _softColorCache = { color: '', expires: 0 }
|
|
@@ -2071,6 +2445,8 @@ voice._chainsPromise = null
|
|
|
2071
2445
|
const [isPlaying, setIsPlaying] = React.useState(false)
|
|
2072
2446
|
voice.inputActions = props.inputActions
|
|
2073
2447
|
voice.input = props.input
|
|
2448
|
+
voice.session = props.session
|
|
2449
|
+
voice.currentSessionId = (props.session && (props.session.sessionId || props.session.id)) || voice.currentSessionId || ''
|
|
2074
2450
|
const ctx = props.ctx
|
|
2075
2451
|
|
|
2076
2452
|
// VAD: accumulate silence and cut the phrase when the pause exceeds the threshold.
|
|
@@ -2194,6 +2570,10 @@ voice._chainsPromise = null
|
|
|
2194
2570
|
else voice.notify()
|
|
2195
2571
|
return
|
|
2196
2572
|
}
|
|
2573
|
+
if (p.paused || p.isEditing) {
|
|
2574
|
+
// Confirmation or editing mode freezes auto-send countdown (#170)
|
|
2575
|
+
return
|
|
2576
|
+
}
|
|
2197
2577
|
p.leftMs -= tick
|
|
2198
2578
|
if (p.leftMs <= 0) { submitPending(); return }
|
|
2199
2579
|
voice.notify()
|
|
@@ -2245,7 +2625,12 @@ voice._chainsPromise = null
|
|
|
2245
2625
|
// Live caption of what is heard right now. Until the browser emits a
|
|
2246
2626
|
// final chunk the text is interim and changes on screen.
|
|
2247
2627
|
const caption = voice.caption || (inBrowser ? '' : null)
|
|
2628
|
+
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2629
|
+
|| voice.selectedDictationMode
|
|
2630
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
2631
|
+
|| 'clean'
|
|
2248
2632
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
2633
|
+
React.createElement('span', { className: 'dvo-mode-badge' }, getDictationModeLabel(activeSubMode, t)),
|
|
2249
2634
|
React.createElement('button', {
|
|
2250
2635
|
type: 'button', className: 'dvo-pbtn', title: t('cancel'),
|
|
2251
2636
|
'aria-label': t('cancel'),
|
|
@@ -2274,11 +2659,22 @@ voice._chainsPromise = null
|
|
|
2274
2659
|
}
|
|
2275
2660
|
|
|
2276
2661
|
if (v.phase === 'processing') {
|
|
2662
|
+
const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
|
|
2663
|
+
|| voice.selectedDictationMode
|
|
2664
|
+
|| (voice.settings && voice.settings.dictationMode)
|
|
2665
|
+
|| 'clean'
|
|
2277
2666
|
return React.createElement('div', { className: 'dvo-pill', role: 'region', 'aria-label': t('title') },
|
|
2278
|
-
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
2667
|
+
React.createElement('span', { className: 'dvo-status', 'aria-live': 'polite', 'aria-atomic': 'true' },
|
|
2668
|
+
spinIcon(),
|
|
2669
|
+
React.createElement('span', { className: 'dvo-mode-badge' }, getDictationModeLabel(activeSubMode, t)),
|
|
2670
|
+
t('transcribing'),
|
|
2671
|
+
))
|
|
2279
2672
|
}
|
|
2280
2673
|
|
|
2281
2674
|
if (v.phase === 'pending') {
|
|
2675
|
+
if (voice.pending && voice.pending.rawText !== undefined) {
|
|
2676
|
+
return renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
2677
|
+
}
|
|
2282
2678
|
const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
|
|
2283
2679
|
if (voice.pending && voice.pending.undoOnly) {
|
|
2284
2680
|
// Cancel window for delayed dictation insert (#29-5).
|
|
@@ -2389,6 +2785,9 @@ voice._chainsPromise = null
|
|
|
2389
2785
|
voiceCommands: !!(data && data.voiceCommands),
|
|
2390
2786
|
vadSilenceMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadSilenceMs) || 700,
|
|
2391
2787
|
autoSendMs: Number(data && data.modes && data.modes.message && data.modes.message.autoSendMs) || 4000,
|
|
2788
|
+
dictationMode: String((data && data.dictationMode) || 'clean'),
|
|
2789
|
+
dictationPrompts: (data && data.dictationPrompts) || {},
|
|
2790
|
+
voiceMacros: Array.isArray(data && data.voiceMacros) ? data.voiceMacros : [],
|
|
2392
2791
|
sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
|
|
2393
2792
|
stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
|
|
2394
2793
|
streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
|
|
@@ -2433,7 +2832,7 @@ voice._chainsPromise = null
|
|
|
2433
2832
|
))
|
|
2434
2833
|
const offDock = ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
2435
2834
|
{ name: 'conversation.input.dock', id: 'dsh-voice-rec', order: 0, locale: NS, label: () => t('recordSlot') },
|
|
2436
|
-
(props) => React.createElement(RecordPill, { input: props.input, inputActions: props.inputActions, ctx: ctx }),
|
|
2835
|
+
(props) => React.createElement(RecordPill, { input: props.input, inputActions: props.inputActions, session: props.session, ctx: ctx }),
|
|
2437
2836
|
))
|
|
2438
2837
|
return () => {
|
|
2439
2838
|
if (typeof offRight === 'function') offRight()
|
|
@@ -2728,6 +3127,93 @@ voice._chainsPromise = null
|
|
|
2728
3127
|
),
|
|
2729
3128
|
)
|
|
2730
3129
|
}
|
|
3130
|
+
// ------------------------------------------------------------------ 72-dictation-modes
|
|
3131
|
+
// Dictation modes configuration card and prompt template editors (#264)
|
|
3132
|
+
|
|
3133
|
+
function DictationModesSection(props) {
|
|
3134
|
+
const { t, draft, setTop, writable } = props
|
|
3135
|
+
const prompts = (draft && draft.dictationPrompts) || {}
|
|
3136
|
+
const currentMode = (draft && draft.dictationMode) || 'clean'
|
|
3137
|
+
|
|
3138
|
+
const setPrompt = (key, val) => {
|
|
3139
|
+
const updated = { ...prompts, [key]: val }
|
|
3140
|
+
setTop('dictationPrompts', updated)
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
const resetPrompts = () => {
|
|
3144
|
+
setTop('dictationPrompts', { clean: '', task: '', note: '' })
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
3148
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
3149
|
+
React.createElement('span', null, '🎙️ ' + t('dictationModeTitle')),
|
|
3150
|
+
),
|
|
3151
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('dictationModeDesc')),
|
|
3152
|
+
React.createElement('div', { className: 'cb-grid-2', style: { marginTop: '8px' } },
|
|
3153
|
+
['clean', 'task', 'note', 'verbatim'].map((m) =>
|
|
3154
|
+
React.createElement('label', { key: m, className: 'cb-check-label' },
|
|
3155
|
+
React.createElement('input', {
|
|
3156
|
+
type: 'radio',
|
|
3157
|
+
name: 'dictationMode',
|
|
3158
|
+
value: m,
|
|
3159
|
+
checked: currentMode === m,
|
|
3160
|
+
disabled: !writable,
|
|
3161
|
+
onChange: () => setTop('dictationMode', m),
|
|
3162
|
+
}),
|
|
3163
|
+
getDictationModeLabel(m, t),
|
|
3164
|
+
),
|
|
3165
|
+
),
|
|
3166
|
+
),
|
|
3167
|
+
React.createElement('div', { style: { marginTop: '12px', borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '10px' } },
|
|
3168
|
+
React.createElement('div', { className: 'cb-section-title', style: { fontSize: '13px' } },
|
|
3169
|
+
React.createElement('span', null, t('dictationPromptsTitle')),
|
|
3170
|
+
React.createElement('button', {
|
|
3171
|
+
type: 'button',
|
|
3172
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3173
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
3174
|
+
disabled: !writable,
|
|
3175
|
+
onClick: resetPrompts,
|
|
3176
|
+
}, t('resetPrompts')),
|
|
3177
|
+
),
|
|
3178
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('dictationPromptsDesc')),
|
|
3179
|
+
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' } },
|
|
3180
|
+
React.createElement('div', null,
|
|
3181
|
+
React.createElement('label', { className: 'cb-label' }, t('promptClean')),
|
|
3182
|
+
React.createElement('textarea', {
|
|
3183
|
+
className: 'cb-input',
|
|
3184
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3185
|
+
value: prompts.clean || '',
|
|
3186
|
+
placeholder: 'Default: punctuation & filler-word cleanup',
|
|
3187
|
+
disabled: !writable,
|
|
3188
|
+
onChange: (e) => setPrompt('clean', e.target.value),
|
|
3189
|
+
}),
|
|
3190
|
+
),
|
|
3191
|
+
React.createElement('div', null,
|
|
3192
|
+
React.createElement('label', { className: 'cb-label' }, t('promptTask')),
|
|
3193
|
+
React.createElement('textarea', {
|
|
3194
|
+
className: 'cb-input',
|
|
3195
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3196
|
+
value: prompts.task || '',
|
|
3197
|
+
placeholder: 'Default: structured problem/outcome/constraints/criteria',
|
|
3198
|
+
disabled: !writable,
|
|
3199
|
+
onChange: (e) => setPrompt('task', e.target.value),
|
|
3200
|
+
}),
|
|
3201
|
+
),
|
|
3202
|
+
React.createElement('div', null,
|
|
3203
|
+
React.createElement('label', { className: 'cb-label' }, t('promptNote')),
|
|
3204
|
+
React.createElement('textarea', {
|
|
3205
|
+
className: 'cb-input',
|
|
3206
|
+
style: { minHeight: '48px', resize: 'vertical' },
|
|
3207
|
+
value: prompts.note || '',
|
|
3208
|
+
placeholder: 'Default: key points & action items summary',
|
|
3209
|
+
disabled: !writable,
|
|
3210
|
+
onChange: (e) => setPrompt('note', e.target.value),
|
|
3211
|
+
}),
|
|
3212
|
+
),
|
|
3213
|
+
),
|
|
3214
|
+
),
|
|
3215
|
+
)
|
|
3216
|
+
}
|
|
2731
3217
|
// 5. Hardware & Audio Engines Card
|
|
2732
3218
|
function HardwareOptionsCard(props) {
|
|
2733
3219
|
const t = props.t || moduleT
|
|
@@ -3211,6 +3697,78 @@ voice._chainsPromise = null
|
|
|
3211
3697
|
),
|
|
3212
3698
|
)
|
|
3213
3699
|
}
|
|
3700
|
+
// ------------------------------------------------------------------ 72-macros
|
|
3701
|
+
// Voice action macros configuration card (#167)
|
|
3702
|
+
|
|
3703
|
+
function MacrosSection(props) {
|
|
3704
|
+
const { t, draft, setTop, writable } = props
|
|
3705
|
+
const macros = Array.isArray(draft && draft.voiceMacros) ? draft.voiceMacros : []
|
|
3706
|
+
|
|
3707
|
+
const addMacro = () => {
|
|
3708
|
+
const next = [...macros, { trigger: '', template: '' }]
|
|
3709
|
+
setTop('voiceMacros', next)
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
const updateMacro = (idx, field, val) => {
|
|
3713
|
+
const next = macros.map((item, i) => (i === idx ? { ...item, [field]: val } : item))
|
|
3714
|
+
setTop('voiceMacros', next)
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
const removeMacro = (idx) => {
|
|
3718
|
+
const next = macros.filter((_, i) => i !== idx)
|
|
3719
|
+
setTop('voiceMacros', next)
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
return React.createElement('div', { className: 'cb-section-card' },
|
|
3723
|
+
React.createElement('div', { className: 'cb-section-title' },
|
|
3724
|
+
React.createElement('span', null, '⚡ ' + t('voiceMacrosTitle')),
|
|
3725
|
+
React.createElement('button', {
|
|
3726
|
+
type: 'button',
|
|
3727
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3728
|
+
style: { fontSize: '11px', padding: '2px 8px' },
|
|
3729
|
+
disabled: !writable,
|
|
3730
|
+
onClick: addMacro,
|
|
3731
|
+
}, '+ ' + t('addMacro')),
|
|
3732
|
+
),
|
|
3733
|
+
React.createElement('div', { className: 'cb-field-desc' }, t('voiceMacrosDesc')),
|
|
3734
|
+
macros.length === 0
|
|
3735
|
+
? React.createElement('div', { style: { fontStyle: 'italic', color: 'var(--dsw-alias-label-tertiary)', fontSize: '12px', marginTop: '6px' } },
|
|
3736
|
+
'No macros configured.',
|
|
3737
|
+
)
|
|
3738
|
+
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '6px' } },
|
|
3739
|
+
macros.map((m, idx) =>
|
|
3740
|
+
React.createElement('div', { key: idx, className: 'dvo-macro-item' },
|
|
3741
|
+
React.createElement('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
|
|
3742
|
+
React.createElement('input', {
|
|
3743
|
+
type: 'text',
|
|
3744
|
+
className: 'cb-input',
|
|
3745
|
+
style: { flex: '1', height: '28px', fontSize: '12px' },
|
|
3746
|
+
placeholder: t('macroTrigger'),
|
|
3747
|
+
value: m.trigger || '',
|
|
3748
|
+
disabled: !writable,
|
|
3749
|
+
onChange: (e) => updateMacro(idx, 'trigger', e.target.value),
|
|
3750
|
+
}),
|
|
3751
|
+
React.createElement('button', {
|
|
3752
|
+
type: 'button',
|
|
3753
|
+
className: 'cb-btn cb-btn-secondary',
|
|
3754
|
+
style: { padding: '2px 6px', fontSize: '11px', color: 'var(--dsw-alias-state-error-primary)' },
|
|
3755
|
+
disabled: !writable,
|
|
3756
|
+
onClick: () => removeMacro(idx),
|
|
3757
|
+
}, t('macroDelete')),
|
|
3758
|
+
),
|
|
3759
|
+
React.createElement('textarea', {
|
|
3760
|
+
className: 'cb-input',
|
|
3761
|
+
style: { minHeight: '38px', resize: 'vertical', fontSize: '12px' },
|
|
3762
|
+
placeholder: t('macroTemplate') + ' (use {text} for argument)',
|
|
3763
|
+
value: m.template || '',
|
|
3764
|
+
disabled: !writable,
|
|
3765
|
+
onChange: (e) => updateMacro(idx, 'template', e.target.value),
|
|
3766
|
+
}),
|
|
3767
|
+
),
|
|
3768
|
+
),
|
|
3769
|
+
),
|
|
3770
|
+
)
|
|
3771
|
+
}
|
|
3214
3772
|
// SenseVoice Local ASR (1-Click Setup)
|
|
3215
3773
|
function SensevoiceSection(props) {
|
|
3216
3774
|
const t = props.t || moduleT
|
|
@@ -3364,7 +3922,6 @@ voice._chainsPromise = null
|
|
|
3364
3922
|
const [serverConfig, setServerConfig] = React.useState(null)
|
|
3365
3923
|
const [configLoading, setConfigLoading] = React.useState(true)
|
|
3366
3924
|
const [revision, setRevision] = React.useState(null)
|
|
3367
|
-
|
|
3368
3925
|
React.useEffect(() => {
|
|
3369
3926
|
let alive = true
|
|
3370
3927
|
fetch('/dsh-voice/config', { cache: 'no-store' })
|
|
@@ -3382,7 +3939,6 @@ voice._chainsPromise = null
|
|
|
3382
3939
|
})
|
|
3383
3940
|
return () => { alive = false }
|
|
3384
3941
|
}, [])
|
|
3385
|
-
|
|
3386
3942
|
// Hotkey picker: not a name field — press the key you want.
|
|
3387
3943
|
const [catching, setCatching] = React.useState(false)
|
|
3388
3944
|
const [jargonInput, setJargonInput] = React.useState(null)
|
|
@@ -3405,7 +3961,6 @@ voice._chainsPromise = null
|
|
|
3405
3961
|
document.addEventListener('keydown', onKey, true)
|
|
3406
3962
|
return () => document.removeEventListener('keydown', onKey, true)
|
|
3407
3963
|
}, [catching, ctx])
|
|
3408
|
-
|
|
3409
3964
|
React.useEffect(() => {
|
|
3410
3965
|
let alive = true
|
|
3411
3966
|
const render = () => { if (alive && scope && typeof scope.getSnapshot === 'function') setSnap(scope.getSnapshot()) }
|
|
@@ -3413,7 +3968,6 @@ voice._chainsPromise = null
|
|
|
3413
3968
|
const off = (scope && typeof scope.subscribe === 'function') ? scope.subscribe(render) : () => {}
|
|
3414
3969
|
return () => { alive = false; off() }
|
|
3415
3970
|
}, [scope])
|
|
3416
|
-
|
|
3417
3971
|
const [devices, setDevices] = React.useState([])
|
|
3418
3972
|
React.useEffect(() => {
|
|
3419
3973
|
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
|
|
@@ -3421,11 +3975,9 @@ voice._chainsPromise = null
|
|
|
3421
3975
|
.then((list) => setDevices(list.filter((d) => d.kind === 'audioinput')))
|
|
3422
3976
|
.catch(() => {})
|
|
3423
3977
|
}, [])
|
|
3424
|
-
|
|
3425
3978
|
const ready = !!serverConfig || (!!snap && snap.status === 'ready')
|
|
3426
3979
|
const value = (snap && snap.status === 'ready' && snap.value) ? snap.value : (serverConfig || {})
|
|
3427
3980
|
const writable = ready
|
|
3428
|
-
|
|
3429
3981
|
React.useEffect(() => {
|
|
3430
3982
|
if (ready) return undefined
|
|
3431
3983
|
let tries = 0
|
|
@@ -3442,14 +3994,12 @@ voice._chainsPromise = null
|
|
|
3442
3994
|
}, 1000)
|
|
3443
3995
|
return () => clearInterval(timer)
|
|
3444
3996
|
}, [ready])
|
|
3445
|
-
|
|
3446
3997
|
const deepClone = (obj) => {
|
|
3447
3998
|
if (typeof structuredClone === 'function') {
|
|
3448
3999
|
try { return structuredClone(obj) } catch (e) { /* fallback */ }
|
|
3449
4000
|
}
|
|
3450
4001
|
try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
|
|
3451
4002
|
}
|
|
3452
|
-
|
|
3453
4003
|
const setTop = (key, val) => {
|
|
3454
4004
|
setDraft((d) => Object.assign({}, d || {}, { [key]: val }))
|
|
3455
4005
|
}
|
|
@@ -3470,9 +4020,7 @@ voice._chainsPromise = null
|
|
|
3470
4020
|
}),
|
|
3471
4021
|
hint ? React.createElement('span', { className: 'dvs-sub' }, hint) : null,
|
|
3472
4022
|
)
|
|
3473
|
-
|
|
3474
4023
|
React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
|
|
3475
|
-
|
|
3476
4024
|
React.useEffect(() => {
|
|
3477
4025
|
if (!ready) return
|
|
3478
4026
|
const val = value || {}
|
|
@@ -3491,6 +4039,7 @@ voice._chainsPromise = null
|
|
|
3491
4039
|
wakeWord: String(val.wakeWord || ''),
|
|
3492
4040
|
bargeIn: !!val.bargeIn,
|
|
3493
4041
|
polishSend: !!(val.message && val.message.polishSend),
|
|
4042
|
+
editablePreview: val.message ? val.message.editablePreview !== false : true,
|
|
3494
4043
|
sessionCommands: !!(val.message && val.message.sessionCommands),
|
|
3495
4044
|
polishBaseUrl: String(val.polishBaseUrl || ''),
|
|
3496
4045
|
noiseSuppression: val.noiseSuppression !== false,
|
|
@@ -3503,10 +4052,12 @@ voice._chainsPromise = null
|
|
|
3503
4052
|
techJargonCorrection: val.techJargonCorrection !== false,
|
|
3504
4053
|
liveInterimPreview: val.liveInterimPreview !== false,
|
|
3505
4054
|
structuredPromptVoice: val.structuredPromptVoice !== false,
|
|
4055
|
+
dictationMode: val.dictationMode || 'clean',
|
|
4056
|
+
dictationPrompts: val.dictationPrompts || {},
|
|
4057
|
+
voiceMacros: Array.isArray(val.voiceMacros) ? val.voiceMacros : [],
|
|
3506
4058
|
webgpuModel: String(val.webgpuModel || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'),
|
|
3507
4059
|
})
|
|
3508
4060
|
}, [ready, value])
|
|
3509
|
-
|
|
3510
4061
|
const [statusData, setStatusData] = React.useState(null)
|
|
3511
4062
|
const [statusLatency, setStatusLatency] = React.useState(null)
|
|
3512
4063
|
const fetchStatus = React.useCallback(() => {
|
|
@@ -3530,17 +4081,12 @@ voice._chainsPromise = null
|
|
|
3530
4081
|
setStatusLatency(null)
|
|
3531
4082
|
})
|
|
3532
4083
|
}, [])
|
|
3533
|
-
|
|
3534
4084
|
React.useEffect(() => {
|
|
3535
4085
|
fetchStatus()
|
|
3536
4086
|
}, [fetchStatus])
|
|
3537
|
-
|
|
3538
4087
|
const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
|
|
3539
|
-
|
|
3540
4088
|
const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
|
|
3541
|
-
|
|
3542
4089
|
const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
|
|
3543
|
-
|
|
3544
4090
|
const save = async () => {
|
|
3545
4091
|
setErr(''); setSaved(false)
|
|
3546
4092
|
if (!draft) return
|
|
@@ -3559,7 +4105,6 @@ voice._chainsPromise = null
|
|
|
3559
4105
|
.filter(Boolean)
|
|
3560
4106
|
draft.jargonDictionary = parsed
|
|
3561
4107
|
}
|
|
3562
|
-
|
|
3563
4108
|
let httpOk = false
|
|
3564
4109
|
let httpError = null
|
|
3565
4110
|
try {
|
|
@@ -3581,7 +4126,6 @@ voice._chainsPromise = null
|
|
|
3581
4126
|
} catch (e) {
|
|
3582
4127
|
httpError = e?.message || String(e)
|
|
3583
4128
|
}
|
|
3584
|
-
|
|
3585
4129
|
if (httpOk && scope && typeof scope.set === 'function') {
|
|
3586
4130
|
for (const k of Object.keys(draft)) {
|
|
3587
4131
|
try {
|
|
@@ -3591,12 +4135,10 @@ voice._chainsPromise = null
|
|
|
3591
4135
|
}
|
|
3592
4136
|
}
|
|
3593
4137
|
}
|
|
3594
|
-
|
|
3595
4138
|
if (!httpOk) {
|
|
3596
4139
|
setErr(t('saveFailed') + ': ' + httpError)
|
|
3597
4140
|
return
|
|
3598
4141
|
}
|
|
3599
|
-
|
|
3600
4142
|
const d = draft || {}
|
|
3601
4143
|
voice.hotkey = d.hotkey || ''
|
|
3602
4144
|
if (voice.hotkey) {
|
|
@@ -3628,23 +4170,19 @@ voice._chainsPromise = null
|
|
|
3628
4170
|
structuredPromptVoice: d.structuredPromptVoice !== false,
|
|
3629
4171
|
})
|
|
3630
4172
|
try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
|
|
3631
|
-
|
|
3632
4173
|
setSaved(true); setJargonInput(null)
|
|
3633
4174
|
setTimeout(() => setSaved(false), 2000)
|
|
3634
4175
|
fetchStatus()
|
|
3635
4176
|
}
|
|
3636
|
-
|
|
3637
4177
|
const modeVal = (mode, key, fallback) => {
|
|
3638
4178
|
const m = draft && draft[mode]
|
|
3639
4179
|
return m && m[key] !== undefined ? m[key] : fallback
|
|
3640
4180
|
}
|
|
3641
|
-
|
|
3642
4181
|
const chainOptions = BUILTIN.concat(
|
|
3643
4182
|
(draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
|
|
3644
4183
|
.map((c) => String(c && c.key || '').trim())
|
|
3645
4184
|
.filter((k) => k && BUILTIN.indexOf(k) < 0),
|
|
3646
4185
|
)
|
|
3647
|
-
|
|
3648
4186
|
const langField = (mode) => {
|
|
3649
4187
|
const val = modeVal(mode, 'language', '')
|
|
3650
4188
|
const opts = SUPPORTED_LANGS.slice()
|
|
@@ -3661,7 +4199,6 @@ voice._chainsPromise = null
|
|
|
3661
4199
|
))),
|
|
3662
4200
|
)
|
|
3663
4201
|
}
|
|
3664
|
-
|
|
3665
4202
|
const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
|
|
3666
4203
|
React.createElement('label', null, label),
|
|
3667
4204
|
React.createElement('input', {
|
|
@@ -3670,15 +4207,11 @@ voice._chainsPromise = null
|
|
|
3670
4207
|
}),
|
|
3671
4208
|
React.createElement('span', { className: 'dvs-sub' }, hint),
|
|
3672
4209
|
)
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
4210
|
const statsData = (statusData && statusData.providerStats) || {}
|
|
3676
4211
|
const hostOnline = statusLatency !== null
|
|
3677
4212
|
const whisperRunning = !!(statusData && statusData.whisperRunning)
|
|
3678
4213
|
const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
|
|
3679
|
-
|
|
3680
4214
|
return React.createElement('div', { className: 'cb-page dvo-settings-root' },
|
|
3681
|
-
// 1. Connection & Engine Status Card
|
|
3682
4215
|
React.createElement(ConnectionStatusCard, {
|
|
3683
4216
|
t: t,
|
|
3684
4217
|
hostOnline: hostOnline,
|
|
@@ -3687,8 +4220,6 @@ voice._chainsPromise = null
|
|
|
3687
4220
|
providerCount: providerCount,
|
|
3688
4221
|
onRefresh: fetchStatus,
|
|
3689
4222
|
}),
|
|
3690
|
-
|
|
3691
|
-
// 2. Dictation Mode Card
|
|
3692
4223
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3693
4224
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3694
4225
|
React.createElement('span', null, '🎙️ ' + t('dictationBtn')),
|
|
@@ -3732,8 +4263,6 @@ voice._chainsPromise = null
|
|
|
3732
4263
|
),
|
|
3733
4264
|
textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
|
|
3734
4265
|
),
|
|
3735
|
-
|
|
3736
|
-
// 3. Voice Message Card
|
|
3737
4266
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3738
4267
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3739
4268
|
React.createElement('span', null, '💬 ' + t('messageTitle')),
|
|
@@ -3772,8 +4301,6 @@ voice._chainsPromise = null
|
|
|
3772
4301
|
),
|
|
3773
4302
|
),
|
|
3774
4303
|
),
|
|
3775
|
-
|
|
3776
|
-
// 4. Custom Providers Card
|
|
3777
4304
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3778
4305
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3779
4306
|
React.createElement('span', null, '⚙️ ' + t('customTitle')),
|
|
@@ -3785,8 +4312,6 @@ voice._chainsPromise = null
|
|
|
3785
4312
|
onChange: (v) => setTop('customProviders', v),
|
|
3786
4313
|
}),
|
|
3787
4314
|
),
|
|
3788
|
-
|
|
3789
|
-
// 5. Hardware & Audio Engines Card
|
|
3790
4315
|
React.createElement(HardwareOptionsCard, {
|
|
3791
4316
|
t: t,
|
|
3792
4317
|
draft: draft,
|
|
@@ -3806,8 +4331,6 @@ voice._chainsPromise = null
|
|
|
3806
4331
|
clearGlobalHotkey()
|
|
3807
4332
|
},
|
|
3808
4333
|
}),
|
|
3809
|
-
|
|
3810
|
-
// 6. SenseVoice Local ASR (1-Click Setup)
|
|
3811
4334
|
React.createElement(SensevoiceSection, {
|
|
3812
4335
|
t: t,
|
|
3813
4336
|
sensevoiceState: sensevoiceState,
|
|
@@ -3815,8 +4338,6 @@ voice._chainsPromise = null
|
|
|
3815
4338
|
writable: writable,
|
|
3816
4339
|
onInstall: triggerInstallSensevoice,
|
|
3817
4340
|
}),
|
|
3818
|
-
|
|
3819
|
-
// 7. Turn-Taking & Audio Coordination
|
|
3820
4341
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3821
4342
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3822
4343
|
React.createElement('span', null, '🎙️ ' + t('turnTakingTitle')),
|
|
@@ -3843,8 +4364,6 @@ voice._chainsPromise = null
|
|
|
3843
4364
|
),
|
|
3844
4365
|
),
|
|
3845
4366
|
),
|
|
3846
|
-
|
|
3847
|
-
// 8. Hands-free & Smart Actions
|
|
3848
4367
|
React.createElement('div', { className: 'cb-section-card' },
|
|
3849
4368
|
React.createElement('div', { className: 'cb-section-title' },
|
|
3850
4369
|
React.createElement('span', null, '✨ ' + t('smartActionsTitle')),
|
|
@@ -3888,8 +4407,20 @@ voice._chainsPromise = null
|
|
|
3888
4407
|
),
|
|
3889
4408
|
),
|
|
3890
4409
|
),
|
|
3891
|
-
|
|
3892
|
-
|
|
4410
|
+
// Dictation Modes & Prompts (#264)
|
|
4411
|
+
React.createElement(DictationModesSection, {
|
|
4412
|
+
t: t,
|
|
4413
|
+
draft: draft,
|
|
4414
|
+
setTop: setTop,
|
|
4415
|
+
writable: writable,
|
|
4416
|
+
}),
|
|
4417
|
+
// Voice Action Macros (#167)
|
|
4418
|
+
React.createElement(MacrosSection, {
|
|
4419
|
+
t: t,
|
|
4420
|
+
draft: draft,
|
|
4421
|
+
setTop: setTop,
|
|
4422
|
+
writable: writable,
|
|
4423
|
+
}),
|
|
3893
4424
|
React.createElement(JargonEditor, {
|
|
3894
4425
|
t: t,
|
|
3895
4426
|
draft: draft,
|
|
@@ -3898,15 +4429,11 @@ voice._chainsPromise = null
|
|
|
3898
4429
|
jargonInput: jargonInput,
|
|
3899
4430
|
setJargonInput: setJargonInput,
|
|
3900
4431
|
}),
|
|
3901
|
-
|
|
3902
|
-
// 10. Provider Telemetry & Latency Dashboard
|
|
3903
4432
|
React.createElement(ProviderDashboard, {
|
|
3904
4433
|
t: t,
|
|
3905
4434
|
statsData: statsData,
|
|
3906
4435
|
onRefresh: fetchStatus,
|
|
3907
4436
|
}),
|
|
3908
|
-
|
|
3909
|
-
// 7. Plugin Version & Updates Card
|
|
3910
4437
|
React.createElement(UpdaterCard, {
|
|
3911
4438
|
t: t,
|
|
3912
4439
|
updaterStatus: updaterStatus,
|
|
@@ -3917,8 +4444,6 @@ voice._chainsPromise = null
|
|
|
3917
4444
|
onCheck: checkUpdater,
|
|
3918
4445
|
onUpdateNow: onUpdateNow,
|
|
3919
4446
|
}),
|
|
3920
|
-
|
|
3921
|
-
// Action bar (Save)
|
|
3922
4447
|
React.createElement('div', { className: 'cb-row' },
|
|
3923
4448
|
React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
|
|
3924
4449
|
saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
|
|
@@ -4031,6 +4556,9 @@ voice._chainsPromise = null
|
|
|
4031
4556
|
isAllowedInEditable,
|
|
4032
4557
|
isHotkeyRelease,
|
|
4033
4558
|
extractContextKeywords,
|
|
4559
|
+
DICTATION_MODES,
|
|
4560
|
+
getDictationModeLabel,
|
|
4561
|
+
cycleDictationMode,
|
|
4034
4562
|
resolveBrowserRecognitionLang,
|
|
4035
4563
|
VoiceSection,
|
|
4036
4564
|
cutPhrase,
|
|
@@ -4041,6 +4569,12 @@ voice._chainsPromise = null
|
|
|
4041
4569
|
cancelCurrent,
|
|
4042
4570
|
stopCurrent,
|
|
4043
4571
|
submitPending,
|
|
4572
|
+
cancelPending,
|
|
4573
|
+
keepPending,
|
|
4574
|
+
initPendingPreview,
|
|
4575
|
+
cancelPendingPreview,
|
|
4576
|
+
keepPendingPreview,
|
|
4577
|
+
submitPendingPreview,
|
|
4044
4578
|
loadWebGpuWhisper,
|
|
4045
4579
|
isWebGpuSupported,
|
|
4046
4580
|
isWasmSupported,
|