@goodandready/dsh-voice 0.9.30 → 0.9.31
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 +252 -252
- package/README.ru.md +251 -251
- package/README.zh.md +179 -179
- package/lib/client.js +334 -39
- package/lib/host-routes.js +188 -188
- package/lib/schema.js +161 -160
- package/package.json +72 -72
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',
|
|
@@ -350,6 +358,14 @@ window.__ModuleLoader__.load({
|
|
|
350
358
|
'bargeIn': '语音插话打断 (Barge-in)',
|
|
351
359
|
'polishSend': '发送前润色整篇草稿',
|
|
352
360
|
'polishSendHint': '发送给智能体前对整个输入框草稿进行一次整体润色',
|
|
361
|
+
'editablePreview': '发送前可编辑预览',
|
|
362
|
+
'editablePreviewHint': '发送前显示识别文本,支持编辑、润色对比和撤销。',
|
|
363
|
+
'editPreview': '编辑',
|
|
364
|
+
'sendNow': '发送',
|
|
365
|
+
'cancelPreview': '取消',
|
|
366
|
+
'rawText': '原文',
|
|
367
|
+
'polishedText': '润色',
|
|
368
|
+
'sessionMismatch': '当前会话已切换,已拦截发送以防止串话。',
|
|
353
369
|
'sessionCommands': '会话语音控制指令',
|
|
354
370
|
'sessionCommandsHint': '"send"、"cancel"、"stop"、"continue" 执行操作而非转为文本',
|
|
355
371
|
'polishBaseUrl': '离线润色服务端点',
|
|
@@ -518,7 +534,20 @@ window.__ModuleLoader__.load({
|
|
|
518
534
|
'.dvo-progress-bar{height:6px;width:100%;background:var(--dsw-alias-bg-layer-3);border-radius:3px;overflow:hidden}' +
|
|
519
535
|
'.dvo-progress-fill{height:100%;background:var(--dsw-alias-state-info-primary);transition:width 150ms ease}' +
|
|
520
536
|
'.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)}'
|
|
537
|
+
'.dvo-jargon-table td{padding:4px 6px;border-bottom:1px solid var(--dsw-alias-border-l1)}' +
|
|
538
|
+
'.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}' +
|
|
539
|
+
'.dvo-preview-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%}' +
|
|
540
|
+
'.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)}' +
|
|
541
|
+
'.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}' +
|
|
542
|
+
'.dvo-tab-btn:hover{color:var(--dsw-alias-label-primary)}' +
|
|
543
|
+
'.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)}' +
|
|
544
|
+
'.dvo-preview-body{display:flex;align-items:center;gap:8px;width:100%}' +
|
|
545
|
+
'.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}' +
|
|
546
|
+
'.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%}' +
|
|
547
|
+
'.dvo-preview-input:focus{border-color:var(--dsw-alias-state-info-primary)}' +
|
|
548
|
+
'.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
|
+
'.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}'
|
|
522
551
|
const cssId = 'dsh-voice/client.module.css'
|
|
523
552
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-voice"][data-plugin-css="' + cssId + '"]')) {
|
|
524
553
|
const tag = document.createElement('style')
|
|
@@ -543,7 +572,7 @@ window.__ModuleLoader__.load({
|
|
|
543
572
|
inputActions: null,
|
|
544
573
|
input: null,
|
|
545
574
|
settings: {
|
|
546
|
-
vadSilenceMs: 700, autoSendMs: 4000, stream: false, streamChunkMs: 1200, vadAdapt: 0,
|
|
575
|
+
vadSilenceMs: 700, autoSendMs: 4000, editablePreview: true, stream: false, streamChunkMs: 1200, vadAdapt: 0,
|
|
547
576
|
noiseSuppression: true, contextGlossary: true,
|
|
548
577
|
gatedTurnTaking: true, autoSendVisualRing: true, voiceActions: true,
|
|
549
578
|
techJargonCorrection: true, jargonDictionary: [], liveInterimPreview: true,
|
|
@@ -1458,7 +1487,11 @@ voice._chainsPromise = null
|
|
|
1458
1487
|
if (targetActions && typeof targetActions.captureInsertion === 'function') {
|
|
1459
1488
|
try { span = targetActions.captureInsertion() } catch (_) {}
|
|
1460
1489
|
}
|
|
1461
|
-
const
|
|
1490
|
+
const initialDraft = (targetInput && typeof targetInput.draft === 'string') ? targetInput.draft : ''
|
|
1491
|
+
const targetSession = voice.session || (targetInput && targetInput.session)
|
|
1492
|
+
const sessionId = (targetSession && (targetSession.sessionId || targetSession.id))
|
|
1493
|
+
|| voice.currentSessionId || ''
|
|
1494
|
+
const target = { actions: targetActions, input: targetInput, session: targetSession, span, initialDraft, sessionId }
|
|
1462
1495
|
voice._activeTarget = target
|
|
1463
1496
|
|
|
1464
1497
|
if (voice.isTtsSpeaking) {
|
|
@@ -1539,7 +1572,12 @@ voice._chainsPromise = null
|
|
|
1539
1572
|
if (mode === 'message') {
|
|
1540
1573
|
const ok = appendDraft(said, target)
|
|
1541
1574
|
if (ok !== false) {
|
|
1542
|
-
|
|
1575
|
+
const usePreview = voice.settings.editablePreview !== false
|
|
1576
|
+
const autoMs = Number(voice.settings.autoSendMs) || 4000
|
|
1577
|
+
const pendingState = usePreview
|
|
1578
|
+
? initPendingPreview(said, target, { autoSendMs: autoMs })
|
|
1579
|
+
: { text: said, target, totalMs: autoMs, leftMs: autoMs }
|
|
1580
|
+
voice.set({ phase: 'pending', pending: pendingState })
|
|
1543
1581
|
} else {
|
|
1544
1582
|
voice.set({ phase: 'idle' })
|
|
1545
1583
|
}
|
|
@@ -1610,7 +1648,24 @@ voice._chainsPromise = null
|
|
|
1610
1648
|
if (voice.lastNote) voice.lastNote.text = text
|
|
1611
1649
|
if (mode === 'message') {
|
|
1612
1650
|
if (ok !== false) {
|
|
1613
|
-
|
|
1651
|
+
const usePreview = voice.settings.editablePreview !== false
|
|
1652
|
+
let polishedText = text
|
|
1653
|
+
if (usePreview && voice.settings.polishSend) {
|
|
1654
|
+
try {
|
|
1655
|
+
const pRes = await fetch('/dsh-voice/polish', {
|
|
1656
|
+
method: 'POST',
|
|
1657
|
+
headers: { 'content-type': 'application/json' },
|
|
1658
|
+
body: JSON.stringify({ text }),
|
|
1659
|
+
})
|
|
1660
|
+
const pData = await pRes.json()
|
|
1661
|
+
if (pData && pData.text) polishedText = pData.text
|
|
1662
|
+
} catch (_) {}
|
|
1663
|
+
}
|
|
1664
|
+
const autoMs = Number(voice.settings.autoSendMs) || 4000
|
|
1665
|
+
const pendingState = usePreview
|
|
1666
|
+
? initPendingPreview(text, target, { autoSendMs: autoMs, polishedText, audioUrl: noteUrl, polishSend: voice.settings.polishSend })
|
|
1667
|
+
: { text, target, totalMs: autoMs, leftMs: autoMs, audioUrl: noteUrl }
|
|
1668
|
+
voice.set({ phase: 'pending', pending: pendingState })
|
|
1614
1669
|
} else {
|
|
1615
1670
|
voice.set({ phase: 'idle' })
|
|
1616
1671
|
}
|
|
@@ -1658,38 +1713,11 @@ voice._chainsPromise = null
|
|
|
1658
1713
|
}
|
|
1659
1714
|
|
|
1660
1715
|
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' })
|
|
1716
|
+
submitPendingPreview(voice)
|
|
1717
|
+
}
|
|
1668
1718
|
|
|
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
|
-
}
|
|
1719
|
+
function cancelPending() {
|
|
1720
|
+
cancelPendingPreview(voice)
|
|
1693
1721
|
}
|
|
1694
1722
|
|
|
1695
1723
|
function runSessionCommand(cmd) {
|
|
@@ -1700,8 +1728,7 @@ voice._chainsPromise = null
|
|
|
1700
1728
|
}
|
|
1701
1729
|
|
|
1702
1730
|
function keepPending() {
|
|
1703
|
-
voice
|
|
1704
|
-
voice.set({ phase: 'idle' })
|
|
1731
|
+
keepPendingPreview(voice)
|
|
1705
1732
|
}
|
|
1706
1733
|
// ----------------------------------------------------------- components
|
|
1707
1734
|
function VoiceButtons(props) {
|
|
@@ -1930,6 +1957,258 @@ voice._chainsPromise = null
|
|
|
1930
1957
|
}
|
|
1931
1958
|
return cleanup
|
|
1932
1959
|
}
|
|
1960
|
+
// ------------------------------------------------------------------ 43-preview
|
|
1961
|
+
// Editable preview state machine, session pinning & UI component (#170).
|
|
1962
|
+
|
|
1963
|
+
function initPendingPreview(text, target, options = {}) {
|
|
1964
|
+
const autoMs = Number(options.autoSendMs) || 4000
|
|
1965
|
+
const polishedText = options.polishedText || text
|
|
1966
|
+
const isPolished = options.polishSend && polishedText && polishedText !== text
|
|
1967
|
+
const initialDraft = (target && typeof target.initialDraft === 'string')
|
|
1968
|
+
? target.initialDraft
|
|
1969
|
+
: ((target && target.input && typeof target.input.draft === 'string') ? target.input.draft : '')
|
|
1970
|
+
const sessionId = (target && target.sessionId) || ''
|
|
1971
|
+
|
|
1972
|
+
return {
|
|
1973
|
+
text,
|
|
1974
|
+
rawText: text,
|
|
1975
|
+
polishedText,
|
|
1976
|
+
activeText: isPolished ? polishedText : text,
|
|
1977
|
+
selectedTab: isPolished ? 'polished' : 'raw',
|
|
1978
|
+
isEditing: false,
|
|
1979
|
+
target,
|
|
1980
|
+
sessionId,
|
|
1981
|
+
initialDraft,
|
|
1982
|
+
totalMs: autoMs,
|
|
1983
|
+
leftMs: autoMs,
|
|
1984
|
+
paused: false,
|
|
1985
|
+
audioUrl: options.audioUrl || '',
|
|
1986
|
+
sessionMismatch: false,
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function cancelPendingPreview(voice) {
|
|
1991
|
+
const pending = voice.pending
|
|
1992
|
+
if (!pending) {
|
|
1993
|
+
voice.set({ phase: 'idle' })
|
|
1994
|
+
return
|
|
1995
|
+
}
|
|
1996
|
+
const target = pending.target || voice._activeTarget
|
|
1997
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
1998
|
+
|
|
1999
|
+
// Restore untouched composer draft if preview was active
|
|
2000
|
+
if (pending.rawText !== undefined && pending.initialDraft !== undefined) {
|
|
2001
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
2002
|
+
try { actions.setDraft(pending.initialDraft) } catch (_) {}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
voice.pending = null
|
|
2006
|
+
voice.set({ phase: 'idle' })
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function keepPendingPreview(voice) {
|
|
2010
|
+
const pending = voice.pending
|
|
2011
|
+
if (!pending) {
|
|
2012
|
+
voice.set({ phase: 'idle' })
|
|
2013
|
+
return
|
|
2014
|
+
}
|
|
2015
|
+
const target = pending.target || voice._activeTarget
|
|
2016
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
2017
|
+
|
|
2018
|
+
if (pending.rawText !== undefined) {
|
|
2019
|
+
// Insert final edited text into composer draft without auto-sending
|
|
2020
|
+
const initial = pending.initialDraft || ''
|
|
2021
|
+
const textToInsert = pending.activeText || ''
|
|
2022
|
+
const full = initial
|
|
2023
|
+
? (initial.endsWith(' ') || initial.endsWith('\n') ? (initial + textToInsert) : (initial + ' ' + textToInsert))
|
|
2024
|
+
: textToInsert
|
|
2025
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
2026
|
+
try { actions.setDraft(full) } catch (_) {}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
voice.pending = null
|
|
2030
|
+
voice.set({ phase: 'idle' })
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
function submitPendingPreview(voice) {
|
|
2034
|
+
const pending = voice.pending
|
|
2035
|
+
if (!pending) return
|
|
2036
|
+
const target = pending.target || voice._activeTarget
|
|
2037
|
+
const actions = (target && target.actions) || voice.inputActions
|
|
2038
|
+
|
|
2039
|
+
// Session guard (#170): verify that current session matches initiating session
|
|
2040
|
+
const currentSessionId = (target && target.session && (target.session.sessionId || target.session.id))
|
|
2041
|
+
|| (voice.session && (voice.session.sessionId || voice.session.id))
|
|
2042
|
+
|| voice.currentSessionId || ''
|
|
2043
|
+
|
|
2044
|
+
if (pending.sessionId && currentSessionId && pending.sessionId !== currentSessionId) {
|
|
2045
|
+
pending.sessionMismatch = true
|
|
2046
|
+
pending.paused = true
|
|
2047
|
+
voice.notify()
|
|
2048
|
+
return
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
const textToSend = (pending.rawText !== undefined ? pending.activeText : pending.text) || ''
|
|
2052
|
+
voice.pending = null
|
|
2053
|
+
voice.set({ phase: 'idle' })
|
|
2054
|
+
|
|
2055
|
+
if (pending.rawText !== undefined) {
|
|
2056
|
+
const initial = pending.initialDraft || ''
|
|
2057
|
+
const full = initial
|
|
2058
|
+
? (initial.endsWith(' ') || initial.endsWith('\n') ? (initial + textToSend) : (initial + ' ' + textToSend))
|
|
2059
|
+
: textToSend
|
|
2060
|
+
if (actions && typeof actions.setDraft === 'function') {
|
|
2061
|
+
try { actions.setDraft(full) } catch (_) {}
|
|
2062
|
+
}
|
|
2063
|
+
if (actions && typeof actions.submit === 'function') {
|
|
2064
|
+
try { actions.submit() } catch (_) {}
|
|
2065
|
+
}
|
|
2066
|
+
} else {
|
|
2067
|
+
// Legacy path without rich preview
|
|
2068
|
+
if (voice.settings.polishSend) {
|
|
2069
|
+
fetch('/dsh-voice/polish', {
|
|
2070
|
+
method: 'POST',
|
|
2071
|
+
headers: { 'content-type': 'application/json' },
|
|
2072
|
+
body: JSON.stringify({ text: textToSend }),
|
|
2073
|
+
})
|
|
2074
|
+
.then((res) => res.json())
|
|
2075
|
+
.then((data) => {
|
|
2076
|
+
const polished = (data && data.text) || textToSend
|
|
2077
|
+
if (actions && typeof actions.setDraft === 'function') actions.setDraft(polished)
|
|
2078
|
+
if (actions && typeof actions.submit === 'function') actions.submit()
|
|
2079
|
+
})
|
|
2080
|
+
.catch(() => {
|
|
2081
|
+
if (actions && typeof actions.submit === 'function') actions.submit()
|
|
2082
|
+
})
|
|
2083
|
+
} else if (actions && typeof actions.submit === 'function') {
|
|
2084
|
+
actions.submit()
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
function renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef) {
|
|
2090
|
+
const p = voice.pending
|
|
2091
|
+
const total = p.totalMs || Number(voice.settings.autoSendMs) || 4000
|
|
2092
|
+
const leftMs = Math.max(0, p.leftMs || 0)
|
|
2093
|
+
const left = Math.ceil(leftMs / 1000)
|
|
2094
|
+
const frac = total > 0 ? Math.max(0, Math.min(1, leftMs / total)) : 0
|
|
2095
|
+
const circ = 2 * Math.PI * 14
|
|
2096
|
+
const off = circ * (1 - frac)
|
|
2097
|
+
const hasPolish = p.polishedText && p.polishedText !== p.rawText
|
|
2098
|
+
|
|
2099
|
+
return React.createElement('div', { className: 'dvo-pill-preview', role: 'region', 'aria-label': t('editablePreview') },
|
|
2100
|
+
// Header row: tabs, audio playback, status/countdown, cancel button
|
|
2101
|
+
React.createElement('div', { className: 'dvo-preview-header' },
|
|
2102
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2103
|
+
hasPolish
|
|
2104
|
+
? React.createElement('div', { className: 'dvo-preview-tabs' },
|
|
2105
|
+
React.createElement('button', {
|
|
2106
|
+
type: 'button',
|
|
2107
|
+
className: 'dvo-tab-btn' + (p.selectedTab === 'raw' ? ' dvo-tab-btn-active' : ''),
|
|
2108
|
+
onClick: () => {
|
|
2109
|
+
p.selectedTab = 'raw'
|
|
2110
|
+
p.activeText = p.rawText
|
|
2111
|
+
voice.notify()
|
|
2112
|
+
},
|
|
2113
|
+
}, t('rawText')),
|
|
2114
|
+
React.createElement('button', {
|
|
2115
|
+
type: 'button',
|
|
2116
|
+
className: 'dvo-tab-btn' + (p.selectedTab === 'polished' ? ' dvo-tab-btn-active' : ''),
|
|
2117
|
+
onClick: () => {
|
|
2118
|
+
p.selectedTab = 'polished'
|
|
2119
|
+
p.activeText = p.polishedText
|
|
2120
|
+
voice.notify()
|
|
2121
|
+
},
|
|
2122
|
+
}, t('polishedText')),
|
|
2123
|
+
)
|
|
2124
|
+
: React.createElement('span', { className: 'dvo-count' }, t('rawText')),
|
|
2125
|
+
p.audioUrl
|
|
2126
|
+
? React.createElement('div', { className: 'dvo-audio-wrap' },
|
|
2127
|
+
React.createElement('button', {
|
|
2128
|
+
type: 'button', className: 'dvo-audio-play',
|
|
2129
|
+
title: isPlaying ? t('pause') : t('play'),
|
|
2130
|
+
'aria-label': isPlaying ? t('pause') : t('play'),
|
|
2131
|
+
onClick: () => {
|
|
2132
|
+
const el = audioRef.current
|
|
2133
|
+
if (!el) return
|
|
2134
|
+
if (el.paused) { el.play().catch(() => {}); setIsPlaying(true) }
|
|
2135
|
+
else { el.pause(); setIsPlaying(false) }
|
|
2136
|
+
},
|
|
2137
|
+
}, isPlaying ? pauseIcon() : playIcon()),
|
|
2138
|
+
React.createElement('audio', {
|
|
2139
|
+
ref: audioRef, src: p.audioUrl,
|
|
2140
|
+
onEnded: () => setIsPlaying(false),
|
|
2141
|
+
onPause: () => setIsPlaying(false),
|
|
2142
|
+
onPlay: () => setIsPlaying(true),
|
|
2143
|
+
}),
|
|
2144
|
+
React.createElement('span', { className: 'dvo-audio-time' }, t('listenBack')),
|
|
2145
|
+
)
|
|
2146
|
+
: null,
|
|
2147
|
+
),
|
|
2148
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2149
|
+
(!p.paused && !p.isEditing && v.settings.autoSendVisualRing !== false)
|
|
2150
|
+
? React.createElement('svg', { className: 'dvo-silence-ring', viewBox: '0 0 34 34' },
|
|
2151
|
+
React.createElement('circle', { cx: 17, cy: 17, r: 14, className: 'dvo-ring-bg' }),
|
|
2152
|
+
React.createElement('circle', {
|
|
2153
|
+
cx: 17, cy: 17, r: 14, className: 'dvo-ring-fg',
|
|
2154
|
+
strokeDasharray: `${circ} ${circ}`,
|
|
2155
|
+
strokeDashoffset: off,
|
|
2156
|
+
}),
|
|
2157
|
+
)
|
|
2158
|
+
: null,
|
|
2159
|
+
React.createElement('span', { className: 'dvo-count' },
|
|
2160
|
+
(p.paused || p.isEditing) ? '' : (left + t('secondsShort'))
|
|
2161
|
+
),
|
|
2162
|
+
React.createElement('button', {
|
|
2163
|
+
type: 'button', className: 'dvo-pbtn', title: t('cancelPreview'),
|
|
2164
|
+
'aria-label': t('cancelPreview'),
|
|
2165
|
+
onClick: () => { cancelPending(); focusComposer() },
|
|
2166
|
+
}, xIcon()),
|
|
2167
|
+
),
|
|
2168
|
+
),
|
|
2169
|
+
// Body row: editable input or preview text, Edit button, Send button
|
|
2170
|
+
React.createElement('div', { className: 'dvo-preview-body' },
|
|
2171
|
+
p.isEditing
|
|
2172
|
+
? React.createElement('input', {
|
|
2173
|
+
type: 'text',
|
|
2174
|
+
className: 'dvo-preview-input',
|
|
2175
|
+
value: p.activeText,
|
|
2176
|
+
autoFocus: true,
|
|
2177
|
+
onChange: (e) => {
|
|
2178
|
+
p.activeText = e.target.value
|
|
2179
|
+
voice.notify()
|
|
2180
|
+
},
|
|
2181
|
+
onKeyDown: (e) => {
|
|
2182
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2183
|
+
e.preventDefault()
|
|
2184
|
+
submitPending()
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
})
|
|
2188
|
+
: React.createElement('span', { className: 'dvo-preview-text' }, p.activeText),
|
|
2189
|
+
!p.isEditing
|
|
2190
|
+
? React.createElement('button', {
|
|
2191
|
+
type: 'button', className: 'dvo-pbtn', title: t('editPreview'),
|
|
2192
|
+
'aria-label': t('editPreview'),
|
|
2193
|
+
onClick: () => {
|
|
2194
|
+
p.isEditing = true
|
|
2195
|
+
p.paused = true
|
|
2196
|
+
voice.notify()
|
|
2197
|
+
},
|
|
2198
|
+
}, editIcon())
|
|
2199
|
+
: null,
|
|
2200
|
+
React.createElement('button', {
|
|
2201
|
+
type: 'button', className: 'dvo-btn-send', title: t('sendNow'),
|
|
2202
|
+
'aria-label': t('sendNow'),
|
|
2203
|
+
onClick: () => { submitPending(); focusComposer() },
|
|
2204
|
+
}, sendIcon()),
|
|
2205
|
+
),
|
|
2206
|
+
// Mismatch warning banner
|
|
2207
|
+
p.sessionMismatch
|
|
2208
|
+
? React.createElement('div', { className: 'dvo-warn-banner' }, t('sessionMismatch'))
|
|
2209
|
+
: null,
|
|
2210
|
+
)
|
|
2211
|
+
}
|
|
1933
2212
|
// ----------------------------------------------------------- visualizers
|
|
1934
2213
|
let _accentColorCache = { color: '', expires: 0 }
|
|
1935
2214
|
let _softColorCache = { color: '', expires: 0 }
|
|
@@ -2071,6 +2350,8 @@ voice._chainsPromise = null
|
|
|
2071
2350
|
const [isPlaying, setIsPlaying] = React.useState(false)
|
|
2072
2351
|
voice.inputActions = props.inputActions
|
|
2073
2352
|
voice.input = props.input
|
|
2353
|
+
voice.session = props.session
|
|
2354
|
+
voice.currentSessionId = (props.session && (props.session.sessionId || props.session.id)) || voice.currentSessionId || ''
|
|
2074
2355
|
const ctx = props.ctx
|
|
2075
2356
|
|
|
2076
2357
|
// VAD: accumulate silence and cut the phrase when the pause exceeds the threshold.
|
|
@@ -2194,6 +2475,10 @@ voice._chainsPromise = null
|
|
|
2194
2475
|
else voice.notify()
|
|
2195
2476
|
return
|
|
2196
2477
|
}
|
|
2478
|
+
if (p.paused || p.isEditing) {
|
|
2479
|
+
// Confirmation or editing mode freezes auto-send countdown (#170)
|
|
2480
|
+
return
|
|
2481
|
+
}
|
|
2197
2482
|
p.leftMs -= tick
|
|
2198
2483
|
if (p.leftMs <= 0) { submitPending(); return }
|
|
2199
2484
|
voice.notify()
|
|
@@ -2279,6 +2564,9 @@ voice._chainsPromise = null
|
|
|
2279
2564
|
}
|
|
2280
2565
|
|
|
2281
2566
|
if (v.phase === 'pending') {
|
|
2567
|
+
if (voice.pending && voice.pending.rawText !== undefined) {
|
|
2568
|
+
return renderEditablePreviewPill(voice, v, isPlaying, setIsPlaying, audioRef)
|
|
2569
|
+
}
|
|
2282
2570
|
const left = Math.max(0, Math.ceil((voice.pending ? voice.pending.leftMs : 0) / 1000))
|
|
2283
2571
|
if (voice.pending && voice.pending.undoOnly) {
|
|
2284
2572
|
// Cancel window for delayed dictation insert (#29-5).
|
|
@@ -2433,7 +2721,7 @@ voice._chainsPromise = null
|
|
|
2433
2721
|
))
|
|
2434
2722
|
const offDock = ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
2435
2723
|
{ 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 }),
|
|
2724
|
+
(props) => React.createElement(RecordPill, { input: props.input, inputActions: props.inputActions, session: props.session, ctx: ctx }),
|
|
2437
2725
|
))
|
|
2438
2726
|
return () => {
|
|
2439
2727
|
if (typeof offRight === 'function') offRight()
|
|
@@ -3491,6 +3779,7 @@ voice._chainsPromise = null
|
|
|
3491
3779
|
wakeWord: String(val.wakeWord || ''),
|
|
3492
3780
|
bargeIn: !!val.bargeIn,
|
|
3493
3781
|
polishSend: !!(val.message && val.message.polishSend),
|
|
3782
|
+
editablePreview: val.message ? val.message.editablePreview !== false : true,
|
|
3494
3783
|
sessionCommands: !!(val.message && val.message.sessionCommands),
|
|
3495
3784
|
polishBaseUrl: String(val.polishBaseUrl || ''),
|
|
3496
3785
|
noiseSuppression: val.noiseSuppression !== false,
|
|
@@ -4041,6 +4330,12 @@ voice._chainsPromise = null
|
|
|
4041
4330
|
cancelCurrent,
|
|
4042
4331
|
stopCurrent,
|
|
4043
4332
|
submitPending,
|
|
4333
|
+
cancelPending,
|
|
4334
|
+
keepPending,
|
|
4335
|
+
initPendingPreview,
|
|
4336
|
+
cancelPendingPreview,
|
|
4337
|
+
keepPendingPreview,
|
|
4338
|
+
submitPendingPreview,
|
|
4044
4339
|
loadWebGpuWhisper,
|
|
4045
4340
|
isWebGpuSupported,
|
|
4046
4341
|
isWasmSupported,
|