@goodandready/dsh-voice 0.9.31 → 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/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',
@@ -474,6 +495,27 @@ window.__ModuleLoader__.load({
474
495
  'voiceActionsLabel': '语音动作指令 ("发送", "取消", "清空", "换行")',
475
496
  'liveInterimPreview': '实时语音文本预览 (幽灵文本)',
476
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不可用,已保留原始识别文本',
477
519
  'jargonTitle': '开发者词汇与 IT 术语',
478
520
  'techJargonCorrection': '自动校正开发术语 (GitHub, Docker, pnpm, Kubernetes 等)',
479
521
  'jargonAdd': '添加替换项',
@@ -547,7 +589,7 @@ window.__ModuleLoader__.load({
547
589
  '.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
548
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}' +
549
591
  '.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}'
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)}'
551
593
  const cssId = 'dsh-voice/client.module.css'
552
594
  if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
553
595
  const tag = document.createElement('style')
@@ -743,7 +785,26 @@ window.__ModuleLoader__.load({
743
785
 
744
786
  function extractContextKeywords() {
745
787
  if (!voice.settings || voice.settings.contextGlossary === false) return []
746
- const text = (voice.input && typeof voice.input.draft === 'string') ? voice.input.draft : ''
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(' ')
747
808
  if (!text || text.length < 3) return []
748
809
  const matches = text.match(/[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{2,29}/g) || []
749
810
  // Linguistic filter: functional stop-word dictionary for vocabulary keyword extraction.
@@ -775,7 +836,11 @@ window.__ModuleLoader__.load({
775
836
 
776
837
  const tryHost = async (chainOverride) => {
777
838
  const dataBase64 = await blobToBase64(blob)
778
- const payload = { dataBase64, mimeType: mime, mode }
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 }
779
844
  if (Array.isArray(chainOverride)) {
780
845
  payload.chain = chainOverride.map((p) => (typeof p === 'string' ? { provider: p, model: modelMap[p] } : p))
781
846
  }
@@ -1491,7 +1556,10 @@ voice._chainsPromise = null
1491
1556
  const targetSession = voice.session || (targetInput && targetInput.session)
1492
1557
  const sessionId = (targetSession && (targetSession.sessionId || targetSession.id))
1493
1558
  || voice.currentSessionId || ''
1494
- const target = { actions: targetActions, input: targetInput, session: targetSession, span, initialDraft, sessionId }
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 }
1495
1563
  voice._activeTarget = target
1496
1564
 
1497
1565
  if (voice.isTtsSpeaking) {
@@ -1738,7 +1806,15 @@ voice._chainsPromise = null
1738
1806
  if (v.phase !== 'idle' && v.phase !== 'error' && !hold.armed) return null
1739
1807
  const err = v.phase === 'error'
1740
1808
  const hk = voice.hotkey ? ` (${voice.hotkey})` : ''
1809
+ const curMode = voice.selectedDictationMode || (voice.settings && voice.settings.dictationMode) || 'clean'
1741
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)),
1742
1818
  React.createElement('button', {
1743
1819
  type: 'button', className: 'dvo-btn', 'data-err': err ? '1' : '0',
1744
1820
  title: err ? v.error : t('dictationBtn'),
@@ -2208,6 +2284,25 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2208
2284
  ? React.createElement('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
2209
2285
  : null,
2210
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()
2211
2306
  }
2212
2307
  // ----------------------------------------------------------- visualizers
2213
2308
  let _accentColorCache = { color: '', expires: 0 }
@@ -2530,7 +2625,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2530
2625
  // Live caption of what is heard right now. Until the browser emits a
2531
2626
  // final chunk the text is interim and changes on screen.
2532
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'
2533
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)),
2534
2634
  React.createElement('button', {
2535
2635
  type: 'button', className: 'dvo-pbtn', title: t('cancel'),
2536
2636
  'aria-label': t('cancel'),
@@ -2559,8 +2659,16 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2559
2659
  }
2560
2660
 
2561
2661
  if (v.phase === 'processing') {
2662
+ const activeSubMode = (voice._activeTarget && voice._activeTarget.dictationMode)
2663
+ || voice.selectedDictationMode
2664
+ || (voice.settings && voice.settings.dictationMode)
2665
+ || 'clean'
2562
2666
  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' }, spinIcon(), t('transcribing')))
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
+ ))
2564
2672
  }
2565
2673
 
2566
2674
  if (v.phase === 'pending') {
@@ -2677,6 +2785,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
2677
2785
  voiceCommands: !!(data && data.voiceCommands),
2678
2786
  vadSilenceMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.vadSilenceMs) || 700,
2679
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 : [],
2680
2791
  sendDelayMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.sendDelayMs) || 0,
2681
2792
  stream: !!(data && data.modes && data.modes.dictation && data.modes.dictation.stream),
2682
2793
  streamChunkMs: Number(data && data.modes && data.modes.dictation && data.modes.dictation.streamChunkMs) || 1200,
@@ -3016,6 +3127,93 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3016
3127
  ),
3017
3128
  )
3018
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
+ }
3019
3217
  // 5. Hardware & Audio Engines Card
3020
3218
  function HardwareOptionsCard(props) {
3021
3219
  const t = props.t || moduleT
@@ -3499,6 +3697,78 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3499
3697
  ),
3500
3698
  )
3501
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
+ }
3502
3772
  // SenseVoice Local ASR (1-Click Setup)
3503
3773
  function SensevoiceSection(props) {
3504
3774
  const t = props.t || moduleT
@@ -3652,7 +3922,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3652
3922
  const [serverConfig, setServerConfig] = React.useState(null)
3653
3923
  const [configLoading, setConfigLoading] = React.useState(true)
3654
3924
  const [revision, setRevision] = React.useState(null)
3655
-
3656
3925
  React.useEffect(() => {
3657
3926
  let alive = true
3658
3927
  fetch('/dsh-voice/config', { cache: 'no-store' })
@@ -3670,7 +3939,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3670
3939
  })
3671
3940
  return () => { alive = false }
3672
3941
  }, [])
3673
-
3674
3942
  // Hotkey picker: not a name field — press the key you want.
3675
3943
  const [catching, setCatching] = React.useState(false)
3676
3944
  const [jargonInput, setJargonInput] = React.useState(null)
@@ -3693,7 +3961,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3693
3961
  document.addEventListener('keydown', onKey, true)
3694
3962
  return () => document.removeEventListener('keydown', onKey, true)
3695
3963
  }, [catching, ctx])
3696
-
3697
3964
  React.useEffect(() => {
3698
3965
  let alive = true
3699
3966
  const render = () => { if (alive && scope && typeof scope.getSnapshot === 'function') setSnap(scope.getSnapshot()) }
@@ -3701,7 +3968,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3701
3968
  const off = (scope && typeof scope.subscribe === 'function') ? scope.subscribe(render) : () => {}
3702
3969
  return () => { alive = false; off() }
3703
3970
  }, [scope])
3704
-
3705
3971
  const [devices, setDevices] = React.useState([])
3706
3972
  React.useEffect(() => {
3707
3973
  if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return
@@ -3709,11 +3975,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3709
3975
  .then((list) => setDevices(list.filter((d) => d.kind === 'audioinput')))
3710
3976
  .catch(() => {})
3711
3977
  }, [])
3712
-
3713
3978
  const ready = !!serverConfig || (!!snap && snap.status === 'ready')
3714
3979
  const value = (snap && snap.status === 'ready' && snap.value) ? snap.value : (serverConfig || {})
3715
3980
  const writable = ready
3716
-
3717
3981
  React.useEffect(() => {
3718
3982
  if (ready) return undefined
3719
3983
  let tries = 0
@@ -3730,14 +3994,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3730
3994
  }, 1000)
3731
3995
  return () => clearInterval(timer)
3732
3996
  }, [ready])
3733
-
3734
3997
  const deepClone = (obj) => {
3735
3998
  if (typeof structuredClone === 'function') {
3736
3999
  try { return structuredClone(obj) } catch (e) { /* fallback */ }
3737
4000
  }
3738
4001
  try { return JSON.parse(JSON.stringify(obj)) } catch (e) { return Object.assign({}, obj) }
3739
4002
  }
3740
-
3741
4003
  const setTop = (key, val) => {
3742
4004
  setDraft((d) => Object.assign({}, d || {}, { [key]: val }))
3743
4005
  }
@@ -3758,9 +4020,7 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3758
4020
  }),
3759
4021
  hint ? React.createElement('span', { className: 'dvs-sub' }, hint) : null,
3760
4022
  )
3761
-
3762
4023
  React.useEffect(() => { if (ready && draft === null) setDraft(deepClone(value)) }, [ready, draft, value])
3763
-
3764
4024
  React.useEffect(() => {
3765
4025
  if (!ready) return
3766
4026
  const val = value || {}
@@ -3792,10 +4052,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3792
4052
  techJargonCorrection: val.techJargonCorrection !== false,
3793
4053
  liveInterimPreview: val.liveInterimPreview !== false,
3794
4054
  structuredPromptVoice: val.structuredPromptVoice !== false,
4055
+ dictationMode: val.dictationMode || 'clean',
4056
+ dictationPrompts: val.dictationPrompts || {},
4057
+ voiceMacros: Array.isArray(val.voiceMacros) ? val.voiceMacros : [],
3795
4058
  webgpuModel: String(val.webgpuModel || (voice.settings && voice.settings.webgpuModel) || 'onnx-community/whisper-tiny'),
3796
4059
  })
3797
4060
  }, [ready, value])
3798
-
3799
4061
  const [statusData, setStatusData] = React.useState(null)
3800
4062
  const [statusLatency, setStatusLatency] = React.useState(null)
3801
4063
  const fetchStatus = React.useCallback(() => {
@@ -3819,17 +4081,12 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3819
4081
  setStatusLatency(null)
3820
4082
  })
3821
4083
  }, [])
3822
-
3823
4084
  React.useEffect(() => {
3824
4085
  fetchStatus()
3825
4086
  }, [fetchStatus])
3826
-
3827
4087
  const { sensevoiceState, installingSensevoice, triggerInstallSensevoice } = useSensevoiceInstaller(fetchStatus)
3828
-
3829
4088
  const { testingMic, testLevel, toggleTestMic, gateDbVal, gateThresholdPercent } = useMicTester(draft, value, setErr)
3830
-
3831
4089
  const { updaterStatus, updating, updaterMsg, updaterMsgKind, updaterLoading, checkUpdater, onUpdateNow } = usePluginUpdater()
3832
-
3833
4090
  const save = async () => {
3834
4091
  setErr(''); setSaved(false)
3835
4092
  if (!draft) return
@@ -3848,7 +4105,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3848
4105
  .filter(Boolean)
3849
4106
  draft.jargonDictionary = parsed
3850
4107
  }
3851
-
3852
4108
  let httpOk = false
3853
4109
  let httpError = null
3854
4110
  try {
@@ -3870,7 +4126,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3870
4126
  } catch (e) {
3871
4127
  httpError = e?.message || String(e)
3872
4128
  }
3873
-
3874
4129
  if (httpOk && scope && typeof scope.set === 'function') {
3875
4130
  for (const k of Object.keys(draft)) {
3876
4131
  try {
@@ -3880,12 +4135,10 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3880
4135
  }
3881
4136
  }
3882
4137
  }
3883
-
3884
4138
  if (!httpOk) {
3885
4139
  setErr(t('saveFailed') + ': ' + httpError)
3886
4140
  return
3887
4141
  }
3888
-
3889
4142
  const d = draft || {}
3890
4143
  voice.hotkey = d.hotkey || ''
3891
4144
  if (voice.hotkey) {
@@ -3917,23 +4170,19 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3917
4170
  structuredPromptVoice: d.structuredPromptVoice !== false,
3918
4171
  })
3919
4172
  try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
3920
-
3921
4173
  setSaved(true); setJargonInput(null)
3922
4174
  setTimeout(() => setSaved(false), 2000)
3923
4175
  fetchStatus()
3924
4176
  }
3925
-
3926
4177
  const modeVal = (mode, key, fallback) => {
3927
4178
  const m = draft && draft[mode]
3928
4179
  return m && m[key] !== undefined ? m[key] : fallback
3929
4180
  }
3930
-
3931
4181
  const chainOptions = BUILTIN.concat(
3932
4182
  (draft && Array.isArray(draft.customProviders) ? draft.customProviders : [])
3933
4183
  .map((c) => String(c && c.key || '').trim())
3934
4184
  .filter((k) => k && BUILTIN.indexOf(k) < 0),
3935
4185
  )
3936
-
3937
4186
  const langField = (mode) => {
3938
4187
  const val = modeVal(mode, 'language', '')
3939
4188
  const opts = SUPPORTED_LANGS.slice()
@@ -3950,7 +4199,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3950
4199
  ))),
3951
4200
  )
3952
4201
  }
3953
-
3954
4202
  const numField = (mode, key, label, hint) => React.createElement('div', { className: 'cb-field' },
3955
4203
  React.createElement('label', null, label),
3956
4204
  React.createElement('input', {
@@ -3959,15 +4207,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3959
4207
  }),
3960
4208
  React.createElement('span', { className: 'dvs-sub' }, hint),
3961
4209
  )
3962
-
3963
-
3964
4210
  const statsData = (statusData && statusData.providerStats) || {}
3965
4211
  const hostOnline = statusLatency !== null
3966
4212
  const whisperRunning = !!(statusData && statusData.whisperRunning)
3967
4213
  const providerCount = (statusData && Array.isArray(statusData.providers)) ? statusData.providers.length : chainOptions.length
3968
-
3969
4214
  return React.createElement('div', { className: 'cb-page dvo-settings-root' },
3970
- // 1. Connection & Engine Status Card
3971
4215
  React.createElement(ConnectionStatusCard, {
3972
4216
  t: t,
3973
4217
  hostOnline: hostOnline,
@@ -3976,8 +4220,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
3976
4220
  providerCount: providerCount,
3977
4221
  onRefresh: fetchStatus,
3978
4222
  }),
3979
-
3980
- // 2. Dictation Mode Card
3981
4223
  React.createElement('div', { className: 'cb-section-card' },
3982
4224
  React.createElement('div', { className: 'cb-section-title' },
3983
4225
  React.createElement('span', null, '🎙️ ' + t('dictationBtn')),
@@ -4021,8 +4263,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4021
4263
  ),
4022
4264
  textField('wakeWord', t('wakeWord'), t('wakeWordHint')),
4023
4265
  ),
4024
-
4025
- // 3. Voice Message Card
4026
4266
  React.createElement('div', { className: 'cb-section-card' },
4027
4267
  React.createElement('div', { className: 'cb-section-title' },
4028
4268
  React.createElement('span', null, '💬 ' + t('messageTitle')),
@@ -4061,8 +4301,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4061
4301
  ),
4062
4302
  ),
4063
4303
  ),
4064
-
4065
- // 4. Custom Providers Card
4066
4304
  React.createElement('div', { className: 'cb-section-card' },
4067
4305
  React.createElement('div', { className: 'cb-section-title' },
4068
4306
  React.createElement('span', null, '⚙️ ' + t('customTitle')),
@@ -4074,8 +4312,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4074
4312
  onChange: (v) => setTop('customProviders', v),
4075
4313
  }),
4076
4314
  ),
4077
-
4078
- // 5. Hardware & Audio Engines Card
4079
4315
  React.createElement(HardwareOptionsCard, {
4080
4316
  t: t,
4081
4317
  draft: draft,
@@ -4095,8 +4331,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4095
4331
  clearGlobalHotkey()
4096
4332
  },
4097
4333
  }),
4098
-
4099
- // 6. SenseVoice Local ASR (1-Click Setup)
4100
4334
  React.createElement(SensevoiceSection, {
4101
4335
  t: t,
4102
4336
  sensevoiceState: sensevoiceState,
@@ -4104,8 +4338,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4104
4338
  writable: writable,
4105
4339
  onInstall: triggerInstallSensevoice,
4106
4340
  }),
4107
-
4108
- // 7. Turn-Taking & Audio Coordination
4109
4341
  React.createElement('div', { className: 'cb-section-card' },
4110
4342
  React.createElement('div', { className: 'cb-section-title' },
4111
4343
  React.createElement('span', null, '🎙️ ' + t('turnTakingTitle')),
@@ -4132,8 +4364,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4132
4364
  ),
4133
4365
  ),
4134
4366
  ),
4135
-
4136
- // 8. Hands-free & Smart Actions
4137
4367
  React.createElement('div', { className: 'cb-section-card' },
4138
4368
  React.createElement('div', { className: 'cb-section-title' },
4139
4369
  React.createElement('span', null, '✨ ' + t('smartActionsTitle')),
@@ -4177,8 +4407,20 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4177
4407
  ),
4178
4408
  ),
4179
4409
  ),
4180
-
4181
- // 9. Developer Lexicon & IT Jargon
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
+ }),
4182
4424
  React.createElement(JargonEditor, {
4183
4425
  t: t,
4184
4426
  draft: draft,
@@ -4187,15 +4429,11 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4187
4429
  jargonInput: jargonInput,
4188
4430
  setJargonInput: setJargonInput,
4189
4431
  }),
4190
-
4191
- // 10. Provider Telemetry & Latency Dashboard
4192
4432
  React.createElement(ProviderDashboard, {
4193
4433
  t: t,
4194
4434
  statsData: statsData,
4195
4435
  onRefresh: fetchStatus,
4196
4436
  }),
4197
-
4198
- // 7. Plugin Version & Updates Card
4199
4437
  React.createElement(UpdaterCard, {
4200
4438
  t: t,
4201
4439
  updaterStatus: updaterStatus,
@@ -4206,8 +4444,6 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4206
4444
  onCheck: checkUpdater,
4207
4445
  onUpdateNow: onUpdateNow,
4208
4446
  }),
4209
-
4210
- // Action bar (Save)
4211
4447
  React.createElement('div', { className: 'cb-row' },
4212
4448
  React.createElement('button', { type: 'button', className: 'cb-btn cb-btn-primary', disabled: !writable, onClick: save }, t('save')),
4213
4449
  saved ? React.createElement('span', { className: 'dvs-ok' }, t('saved')) : null,
@@ -4320,6 +4556,9 @@ function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
4320
4556
  isAllowedInEditable,
4321
4557
  isHotkeyRelease,
4322
4558
  extractContextKeywords,
4559
+ DICTATION_MODES,
4560
+ getDictationModeLabel,
4561
+ cycleDictationMode,
4323
4562
  resolveBrowserRecognitionLang,
4324
4563
  VoiceSection,
4325
4564
  cutPhrase,