dsh-live-voice 0.0.2 → 0.2.0
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/CHANGELOG.md +74 -0
- package/README.md +1 -1
- package/lib/client.js +795 -207
- package/lib/server.js +18 -3
- package/package.json +2 -1
- package/scripts/build.ts +1 -1
- package/src/client/chat.ts +7 -0
- package/src/client/components.ts +453 -190
- package/src/client/index.ts +111 -11
- package/src/client/qwen-settings.ts +1 -1
- package/src/client/styles.ts +11 -7
- package/src/core/coordinator.ts +209 -26
- package/src/core/filters.ts +63 -0
- package/src/core/microphone.ts +7 -1
- package/src/core/ownership.ts +1 -1
- package/src/core/settings.ts +102 -3
- package/src/engines/qwen-http-host.ts +3 -9
- package/src/engines/recognition/browser.ts +23 -0
- package/src/engines/speaking/qwen-http.ts +6 -1
package/lib/client.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
var import_react = __toESM(require("react"), 1);
|
|
38
|
+
var import_react_dom = require("react-dom");
|
|
38
39
|
|
|
39
40
|
// src/core/transcript.ts
|
|
40
41
|
var TranscriptDraft = class {
|
|
@@ -122,8 +123,24 @@ var defaultSettings = Object.freeze({
|
|
|
122
123
|
lang: "pt-BR",
|
|
123
124
|
recognitionLang: "pt-BR",
|
|
124
125
|
voice: "",
|
|
126
|
+
inputDeviceId: "",
|
|
127
|
+
outputDeviceId: "",
|
|
128
|
+
recognitionFilterEnabled: true,
|
|
129
|
+
recognitionMinimumWords: 2,
|
|
130
|
+
voiceCommandsEnabled: true,
|
|
131
|
+
voiceCommandSend: "send, send message",
|
|
132
|
+
voiceCommandQueue: "queue, queue message",
|
|
133
|
+
voiceCommandEnd: "end, end conversation",
|
|
134
|
+
voiceCommandMute: "mute, stop listening",
|
|
135
|
+
voiceCommandResume: "resume, start listening",
|
|
136
|
+
voiceCommandStopSpeaking: "stop talking, stop speaking, shut up",
|
|
137
|
+
voiceCommandClear: "clear all, clear message",
|
|
138
|
+
outputCodeFilterEnabled: true,
|
|
139
|
+
outputCodeMaxLines: 5,
|
|
140
|
+
outputCodeNotice: "Look the code on out conversation",
|
|
125
141
|
rate: 1
|
|
126
142
|
});
|
|
143
|
+
var normalizeCommandPhrases = (value, fallback) => typeof value === "string" && value.length <= 1e3 && !value.includes("\0") ? value.split(/[,\r\n]+/).map((phrase) => phrase.trim()).filter(Boolean).slice(0, 20).join(", ") : fallback;
|
|
127
144
|
function normalizeSettings(value) {
|
|
128
145
|
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
129
146
|
return {
|
|
@@ -134,17 +151,92 @@ function normalizeSettings(value) {
|
|
|
134
151
|
voiceDetectionPreset: Object.hasOwn(voiceDetectionPresets, source.voiceDetectionPreset) ? source.voiceDetectionPreset : defaultSettings.voiceDetectionPreset,
|
|
135
152
|
announceAssistantMessages: typeof source.announceAssistantMessages === "boolean" ? source.announceAssistantMessages : defaultSettings.announceAssistantMessages,
|
|
136
153
|
interruptSpeechOnUserMessage: typeof source.interruptSpeechOnUserMessage === "boolean" ? source.interruptSpeechOnUserMessage : defaultSettings.interruptSpeechOnUserMessage,
|
|
137
|
-
sendingMode: ["manual", "
|
|
154
|
+
sendingMode: source.sendingMode === "automatic" ? "queue" : ["manual", "queue", "steer"].includes(source.sendingMode) ? source.sendingMode : defaultSettings.sendingMode,
|
|
138
155
|
autoSendDelaySeconds: Number.isInteger(source.autoSendDelaySeconds) && source.autoSendDelaySeconds >= 2 && source.autoSendDelaySeconds <= 10 ? source.autoSendDelaySeconds : defaultSettings.autoSendDelaySeconds,
|
|
139
156
|
assistantSpeechDelaySeconds: Number.isInteger(source.assistantSpeechDelaySeconds) && source.assistantSpeechDelaySeconds >= 1 && source.assistantSpeechDelaySeconds <= 10 ? source.assistantSpeechDelaySeconds : defaultSettings.assistantSpeechDelaySeconds,
|
|
140
157
|
mode: ["speaker", "headphones"].includes(source.mode) ? source.mode : defaultSettings.mode,
|
|
141
158
|
lang: typeof source.lang === "string" && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(source.lang) ? source.lang : defaultSettings.lang,
|
|
142
159
|
recognitionLang: source.recognitionLang === "auto" || typeof source.recognitionLang === "string" && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(source.recognitionLang) ? source.recognitionLang : typeof source.lang === "string" && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(source.lang) ? source.lang : defaultSettings.recognitionLang,
|
|
143
160
|
voice: typeof source.voice === "string" && source.voice.length <= 200 && !source.voice.includes("\0") ? source.voice : "",
|
|
161
|
+
inputDeviceId: typeof source.inputDeviceId === "string" && source.inputDeviceId.length <= 500 && !source.inputDeviceId.includes("\0") ? source.inputDeviceId : "",
|
|
162
|
+
outputDeviceId: typeof source.outputDeviceId === "string" && source.outputDeviceId.length <= 500 && !source.outputDeviceId.includes("\0") ? source.outputDeviceId : "",
|
|
163
|
+
recognitionFilterEnabled: typeof source.recognitionFilterEnabled === "boolean" ? source.recognitionFilterEnabled : defaultSettings.recognitionFilterEnabled,
|
|
164
|
+
recognitionMinimumWords: Number.isInteger(source.recognitionMinimumWords) && source.recognitionMinimumWords >= 1 && source.recognitionMinimumWords <= 20 ? source.recognitionMinimumWords : defaultSettings.recognitionMinimumWords,
|
|
165
|
+
voiceCommandsEnabled: typeof source.voiceCommandsEnabled === "boolean" ? source.voiceCommandsEnabled : defaultSettings.voiceCommandsEnabled,
|
|
166
|
+
voiceCommandSend: normalizeCommandPhrases(
|
|
167
|
+
source.voiceCommandSend,
|
|
168
|
+
defaultSettings.voiceCommandSend
|
|
169
|
+
),
|
|
170
|
+
voiceCommandQueue: normalizeCommandPhrases(
|
|
171
|
+
source.voiceCommandQueue,
|
|
172
|
+
defaultSettings.voiceCommandQueue
|
|
173
|
+
),
|
|
174
|
+
voiceCommandEnd: normalizeCommandPhrases(
|
|
175
|
+
source.voiceCommandEnd,
|
|
176
|
+
defaultSettings.voiceCommandEnd
|
|
177
|
+
),
|
|
178
|
+
voiceCommandMute: normalizeCommandPhrases(
|
|
179
|
+
source.voiceCommandMute,
|
|
180
|
+
defaultSettings.voiceCommandMute
|
|
181
|
+
),
|
|
182
|
+
voiceCommandResume: normalizeCommandPhrases(
|
|
183
|
+
source.voiceCommandResume,
|
|
184
|
+
defaultSettings.voiceCommandResume
|
|
185
|
+
),
|
|
186
|
+
voiceCommandStopSpeaking: normalizeCommandPhrases(
|
|
187
|
+
source.voiceCommandStopSpeaking,
|
|
188
|
+
defaultSettings.voiceCommandStopSpeaking
|
|
189
|
+
),
|
|
190
|
+
voiceCommandClear: normalizeCommandPhrases(
|
|
191
|
+
source.voiceCommandClear,
|
|
192
|
+
defaultSettings.voiceCommandClear
|
|
193
|
+
),
|
|
194
|
+
outputCodeFilterEnabled: typeof source.outputCodeFilterEnabled === "boolean" ? source.outputCodeFilterEnabled : defaultSettings.outputCodeFilterEnabled,
|
|
195
|
+
outputCodeMaxLines: Number.isInteger(source.outputCodeMaxLines) && source.outputCodeMaxLines >= 0 && source.outputCodeMaxLines <= 100 ? source.outputCodeMaxLines : defaultSettings.outputCodeMaxLines,
|
|
196
|
+
outputCodeNotice: typeof source.outputCodeNotice === "string" && source.outputCodeNotice.trim() && source.outputCodeNotice.length <= 300 && !source.outputCodeNotice.includes("\0") ? source.outputCodeNotice.trim() : defaultSettings.outputCodeNotice,
|
|
144
197
|
rate: Number.isFinite(source.rate) && source.rate >= 0.1 && source.rate <= 3 ? source.rate : 1
|
|
145
198
|
};
|
|
146
199
|
}
|
|
147
200
|
|
|
201
|
+
// src/core/filters.ts
|
|
202
|
+
var words = (text) => String(text || "").trim().match(/[\p{L}\p{N}]+(?:['’_-][\p{L}\p{N}]+)*/gu) || [];
|
|
203
|
+
function normalizeVoiceCommand(text) {
|
|
204
|
+
return String(text || "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}]+/gu, " ").trim().replace(/\s+/g, " ");
|
|
205
|
+
}
|
|
206
|
+
function matchVoiceCommand(text, commands) {
|
|
207
|
+
const candidate = normalizeVoiceCommand(text);
|
|
208
|
+
if (!candidate) return null;
|
|
209
|
+
for (const [action, phrases] of Object.entries(commands || {})) {
|
|
210
|
+
if (String(phrases || "").split(/[,\r\n]+/).some((phrase) => normalizeVoiceCommand(phrase) === candidate))
|
|
211
|
+
return action;
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
function hasMinimumWords(text, minimum = 2) {
|
|
216
|
+
return words(text).length >= minimum;
|
|
217
|
+
}
|
|
218
|
+
function splitSpeechOutput(text, options = {}) {
|
|
219
|
+
const filtered = filterSpeechOutput(text, options).trim();
|
|
220
|
+
if (!filtered) return [];
|
|
221
|
+
return filtered.split(/\r?\n+/u).map((segment) => segment.trim()).filter(Boolean);
|
|
222
|
+
}
|
|
223
|
+
function hasUnclosedCodeFence(text) {
|
|
224
|
+
return (String(text || "").match(/```/g) || []).length % 2 === 1;
|
|
225
|
+
}
|
|
226
|
+
function filterSpeechOutput(text, {
|
|
227
|
+
filterCodeBlocks = true,
|
|
228
|
+
codeBlockMaxLines = 5,
|
|
229
|
+
codeBlockNotice = "Look the code on out conversation"
|
|
230
|
+
} = {}) {
|
|
231
|
+
const source = String(text || "");
|
|
232
|
+
if (!filterCodeBlocks) return source;
|
|
233
|
+
return source.replace(/```[^\n]*\n?([\s\S]*?)```/g, (block, body) => {
|
|
234
|
+
const normalized = body.replace(/\n$/, "");
|
|
235
|
+
const lineCount = normalized ? normalized.split(/\r?\n/).length : 0;
|
|
236
|
+
return lineCount <= codeBlockMaxLines ? normalized : codeBlockNotice;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
148
240
|
// src/core/coordinator.ts
|
|
149
241
|
var message = (error) => error?.message || String(error);
|
|
150
242
|
function cancellable(promise, signal) {
|
|
@@ -179,20 +271,27 @@ var VoiceCoordinator = class {
|
|
|
179
271
|
conversation: false,
|
|
180
272
|
listening: false,
|
|
181
273
|
recognizing: false,
|
|
274
|
+
muted: false,
|
|
182
275
|
speaking: false,
|
|
183
276
|
paused: false,
|
|
184
277
|
starting: false,
|
|
185
278
|
error: null,
|
|
186
279
|
activeMessageId: null,
|
|
280
|
+
answeringQuestion: false,
|
|
187
281
|
autoSendAt: null,
|
|
282
|
+
speechSegmentsRemaining: 0,
|
|
188
283
|
capabilities: {},
|
|
189
284
|
settings: normalizeSettings(settings)
|
|
190
285
|
};
|
|
191
286
|
this.autoSendTimer = null;
|
|
192
287
|
this.autoSendDraft = null;
|
|
288
|
+
this.interruptionTimer = null;
|
|
289
|
+
this.interruptionTranscriptConfirmed = false;
|
|
290
|
+
this.interruptionPausedSpeech = false;
|
|
193
291
|
this.assistantSpeechTimer = null;
|
|
194
292
|
this.assistantSpeechNotBefore = 0;
|
|
195
293
|
this.recognition.lang = this.snapshot.settings.recognitionLang;
|
|
294
|
+
this.meter.deviceId = this.snapshot.settings.inputDeviceId;
|
|
196
295
|
this.getSnapshot = () => this.snapshot;
|
|
197
296
|
this.subscribe = (listener) => {
|
|
198
297
|
if (this.disposed) return () => {
|
|
@@ -232,6 +331,7 @@ var VoiceCoordinator = class {
|
|
|
232
331
|
updateSettings(next) {
|
|
233
332
|
if (this.disposed) return;
|
|
234
333
|
const settings = normalizeSettings({ ...this.snapshot.settings, ...next });
|
|
334
|
+
this.meter.deviceId = settings.inputDeviceId;
|
|
235
335
|
Object.assign(this.recognition, {
|
|
236
336
|
lang: settings.recognitionLang,
|
|
237
337
|
processLocally: settings.recognitionProcessLocally,
|
|
@@ -239,7 +339,7 @@ var VoiceCoordinator = class {
|
|
|
239
339
|
voiceDetectionPreset: settings.voiceDetectionPreset
|
|
240
340
|
});
|
|
241
341
|
this.patch({ settings, error: null });
|
|
242
|
-
if (settings.sendingMode
|
|
342
|
+
if (settings.sendingMode === "manual") this.cancelAutoSend();
|
|
243
343
|
if (Object.hasOwn(next, "announceAssistantMessages") && !settings.announceAssistantMessages) {
|
|
244
344
|
this.queue = [];
|
|
245
345
|
this.assistantSpeechNotBefore = 0;
|
|
@@ -300,6 +400,21 @@ var VoiceCoordinator = class {
|
|
|
300
400
|
startConversation() {
|
|
301
401
|
return this.startListening(true);
|
|
302
402
|
}
|
|
403
|
+
muteListening() {
|
|
404
|
+
if (!this.snapshot.settings.voiceCommandsEnabled) return this.stopListening();
|
|
405
|
+
this.cancelAutoSend();
|
|
406
|
+
this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
|
|
407
|
+
this.transcript.reset();
|
|
408
|
+
this.patch({ muted: true, recognizing: false });
|
|
409
|
+
}
|
|
410
|
+
resumeListeningInput() {
|
|
411
|
+
if (this.snapshot.listening || this.snapshot.starting) {
|
|
412
|
+
this.transcript.reset();
|
|
413
|
+
this.patch({ muted: false, recognizing: false });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
return this.startListening(this.snapshot.conversation);
|
|
417
|
+
}
|
|
303
418
|
startListening(conversation = this.snapshot.conversation) {
|
|
304
419
|
if (this.disposed) return Promise.resolve();
|
|
305
420
|
this.patch({ error: null });
|
|
@@ -369,7 +484,7 @@ var VoiceCoordinator = class {
|
|
|
369
484
|
} else
|
|
370
485
|
this.assistantSpeechNotBefore = Date.now() + this.snapshot.settings.assistantSpeechDelaySeconds * 1e3;
|
|
371
486
|
this.patch({ recognizing: active });
|
|
372
|
-
|
|
487
|
+
this._handleSpeechInterruption(active);
|
|
373
488
|
if (!active) this._drain();
|
|
374
489
|
},
|
|
375
490
|
onError: (error) => {
|
|
@@ -404,16 +519,99 @@ var VoiceCoordinator = class {
|
|
|
404
519
|
}
|
|
405
520
|
});
|
|
406
521
|
}
|
|
522
|
+
_interruptionTranscriptQualifies(text) {
|
|
523
|
+
return hasMinimumWords(text, 1);
|
|
524
|
+
}
|
|
525
|
+
_handleSpeechInterruption(active) {
|
|
526
|
+
if (this.snapshot.settings.mode !== "headphones") return;
|
|
527
|
+
if (active) {
|
|
528
|
+
if (!this.interruptionTranscriptConfirmed || this.interruptionTimer !== null || !this.snapshot.speaking || this.snapshot.paused)
|
|
529
|
+
return;
|
|
530
|
+
this.interruptionTimer = setTimeout(() => {
|
|
531
|
+
this.interruptionTimer = null;
|
|
532
|
+
if (this.interruptionTranscriptConfirmed && this.snapshot.recognizing && this.snapshot.speaking && !this.snapshot.paused) {
|
|
533
|
+
this.interruptionPausedSpeech = true;
|
|
534
|
+
void this.pauseSpeech().then(() => {
|
|
535
|
+
if (this.interruptionPausedSpeech && !this.snapshot.recognizing && this.snapshot.paused) {
|
|
536
|
+
this.interruptionPausedSpeech = false;
|
|
537
|
+
void this.resumeSpeech();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}, this.snapshot.settings.assistantSpeechDelaySeconds * 1e3);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (this.interruptionTimer !== null) clearTimeout(this.interruptionTimer);
|
|
545
|
+
this.interruptionTimer = null;
|
|
546
|
+
this.interruptionTranscriptConfirmed = false;
|
|
547
|
+
if (this.interruptionPausedSpeech && this.snapshot.paused) {
|
|
548
|
+
this.interruptionPausedSpeech = false;
|
|
549
|
+
void this.resumeSpeech();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
407
552
|
onResult({ final = "", interim = "" }) {
|
|
408
553
|
if (this.disposed || !this.snapshot.listening && !this.snapshot.starting) return;
|
|
409
|
-
if (this.snapshot.speaking && !this.snapshot.paused)
|
|
410
|
-
|
|
411
|
-
|
|
554
|
+
if (this.snapshot.speaking && !this.snapshot.paused && this.snapshot.settings.mode === "speaker")
|
|
555
|
+
return;
|
|
556
|
+
if (this.snapshot.settings.mode === "headphones" && this.snapshot.speaking && this._interruptionTranscriptQualifies(final || interim)) {
|
|
557
|
+
this.interruptionTranscriptConfirmed = true;
|
|
558
|
+
this._handleSpeechInterruption(true);
|
|
559
|
+
}
|
|
560
|
+
if (typeof this.composer.handleQuestionResult === "function" && this.composer.handleQuestionResult({ final, interim })) {
|
|
561
|
+
this.cancelAutoSend();
|
|
562
|
+
this.transcript.reset();
|
|
563
|
+
this.patch({ recognizing: !!interim });
|
|
564
|
+
return;
|
|
412
565
|
}
|
|
413
566
|
if (final) {
|
|
567
|
+
const command = this.snapshot.settings.voiceCommandsEnabled && matchVoiceCommand(final, {
|
|
568
|
+
send: this.snapshot.settings.voiceCommandSend,
|
|
569
|
+
queue: this.snapshot.settings.voiceCommandQueue,
|
|
570
|
+
end: this.snapshot.settings.voiceCommandEnd,
|
|
571
|
+
mute: this.snapshot.settings.voiceCommandMute,
|
|
572
|
+
resume: this.snapshot.settings.voiceCommandResume,
|
|
573
|
+
stopSpeaking: this.snapshot.settings.voiceCommandStopSpeaking,
|
|
574
|
+
clear: this.snapshot.settings.voiceCommandClear
|
|
575
|
+
});
|
|
576
|
+
if (command) {
|
|
577
|
+
this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
|
|
578
|
+
this.transcript.reset();
|
|
579
|
+
this.cancelAutoSend();
|
|
580
|
+
this.patch({ recognizing: false });
|
|
581
|
+
if (command === "end") void this.endConversation();
|
|
582
|
+
else if (command === "mute") this.muteListening();
|
|
583
|
+
else if (command === "resume") this.resumeListeningInput();
|
|
584
|
+
else if (command === "stopSpeaking") {
|
|
585
|
+
this.updateSettings({ announceAssistantMessages: false });
|
|
586
|
+
void this.stopSpeech();
|
|
587
|
+
} else if (command === "clear") {
|
|
588
|
+
this.cancelAutoSend();
|
|
589
|
+
this.transcript.reset();
|
|
590
|
+
this.composer.setDraft("");
|
|
591
|
+
} else if (this.snapshot.muted) return;
|
|
592
|
+
else {
|
|
593
|
+
const sendingMode = command === "send" ? "steer" : "queue";
|
|
594
|
+
this.updateSettings({ sendingMode });
|
|
595
|
+
if (this.composer.getDraft().trim() && typeof this.composer.submit === "function")
|
|
596
|
+
this.composer.submit(sendingMode);
|
|
597
|
+
}
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (this.snapshot.muted) {
|
|
601
|
+
this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
|
|
602
|
+
this.transcript.reset();
|
|
603
|
+
this.patch({ recognizing: false });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (this.snapshot.settings.recognitionFilterEnabled && !hasMinimumWords(final, this.snapshot.settings.recognitionMinimumWords)) {
|
|
607
|
+
this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
|
|
608
|
+
this.transcript.reset();
|
|
609
|
+
this.patch({ recognizing: false });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
414
612
|
const next = this.transcript.update(this.composer.getDraft(), final, true);
|
|
415
613
|
this.composer.setDraft(next);
|
|
416
|
-
if (this.snapshot.settings.sendingMode
|
|
614
|
+
if (this.snapshot.settings.sendingMode !== "manual") this.scheduleAutoSend(next);
|
|
417
615
|
}
|
|
418
616
|
if (interim) {
|
|
419
617
|
this.cancelAutoSend();
|
|
@@ -435,8 +633,15 @@ var VoiceCoordinator = class {
|
|
|
435
633
|
const expected = this.autoSendDraft;
|
|
436
634
|
this.autoSendDraft = null;
|
|
437
635
|
this.patch({ autoSendAt: null });
|
|
438
|
-
if (!this.disposed && this.snapshot.settings.sendingMode
|
|
439
|
-
|
|
636
|
+
if (!this.disposed && this.snapshot.settings.sendingMode !== "manual" && expected === this.composer.getDraft()) {
|
|
637
|
+
try {
|
|
638
|
+
this.composer.submit(this.snapshot.settings.sendingMode === "steer" ? "steer" : "queue");
|
|
639
|
+
this.transcript.reset();
|
|
640
|
+
this.recognition.reset?.();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
this.patch({ error: message(error) });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
440
645
|
}, delay);
|
|
441
646
|
}
|
|
442
647
|
cancelAutoSend() {
|
|
@@ -474,6 +679,11 @@ var VoiceCoordinator = class {
|
|
|
474
679
|
this.patch({ conversation: false });
|
|
475
680
|
await Promise.all([this.stopListening(), this.stopSpeech(false)]);
|
|
476
681
|
}
|
|
682
|
+
_syncSpeechSegments() {
|
|
683
|
+
const speechSegmentsRemaining = this.queue.length + (this.snapshot.speaking ? 1 : 0);
|
|
684
|
+
if (speechSegmentsRemaining !== this.snapshot.speechSegmentsRemaining)
|
|
685
|
+
this.patch({ speechSegmentsRemaining });
|
|
686
|
+
}
|
|
477
687
|
_suppressPending() {
|
|
478
688
|
for (const id2 of this.unfinished) this.suppressed.add(id2);
|
|
479
689
|
if (this.snapshot.activeMessageId !== null) this.suppressed.add(this.snapshot.activeMessageId);
|
|
@@ -481,10 +691,15 @@ var VoiceCoordinator = class {
|
|
|
481
691
|
this.queue = [];
|
|
482
692
|
}
|
|
483
693
|
async stopSpeech(resumeListening = true) {
|
|
694
|
+
if (this.interruptionTimer !== null) clearTimeout(this.interruptionTimer);
|
|
695
|
+
this.interruptionTimer = null;
|
|
696
|
+
this.interruptionTranscriptConfirmed = false;
|
|
697
|
+
this.interruptionPausedSpeech = false;
|
|
484
698
|
const epoch = ++this.speechEpoch;
|
|
485
699
|
++this.controlEpoch;
|
|
486
700
|
this._suppressPending();
|
|
487
701
|
this.patch({ speaking: false, paused: false, activeMessageId: null });
|
|
702
|
+
this._syncSpeechSegments();
|
|
488
703
|
const stopped = this.speechBarrier.then(async () => {
|
|
489
704
|
const results = await Promise.allSettled(
|
|
490
705
|
Object.values(this.engines).map((engine) => Promise.resolve().then(() => engine.stop()))
|
|
@@ -538,13 +753,23 @@ var VoiceCoordinator = class {
|
|
|
538
753
|
const stopped = this.stopSpeech(false);
|
|
539
754
|
const epoch = this.speechEpoch;
|
|
540
755
|
await stopped;
|
|
541
|
-
if (
|
|
542
|
-
|
|
756
|
+
if (this.disposed || epoch !== this.speechEpoch || this.speechStopError) return;
|
|
757
|
+
const segments = splitSpeechOutput(text, {
|
|
758
|
+
filterCodeBlocks: this.snapshot.settings.outputCodeFilterEnabled,
|
|
759
|
+
codeBlockMaxLines: this.snapshot.settings.outputCodeMaxLines,
|
|
760
|
+
codeBlockNotice: this.snapshot.settings.outputCodeNotice
|
|
761
|
+
});
|
|
762
|
+
const first = segments.shift();
|
|
763
|
+
if (!first) return;
|
|
764
|
+
this.queue.push(...segments.map((segment) => ({ text: segment, id: messageId, manual: true })));
|
|
765
|
+
this._syncSpeechSegments();
|
|
766
|
+
return this.play(first, messageId, true);
|
|
543
767
|
}
|
|
544
|
-
async play(text, messageId) {
|
|
768
|
+
async play(text, messageId, manual = false) {
|
|
545
769
|
if (this.disposed || !text.trim()) return;
|
|
546
770
|
if (this.snapshot.speaking) {
|
|
547
|
-
this.queue.push({ text, id: messageId });
|
|
771
|
+
this.queue.push({ text, id: messageId, manual });
|
|
772
|
+
this._syncSpeechSegments();
|
|
548
773
|
return;
|
|
549
774
|
}
|
|
550
775
|
const epoch = ++this.speechEpoch;
|
|
@@ -555,6 +780,7 @@ var VoiceCoordinator = class {
|
|
|
555
780
|
return;
|
|
556
781
|
}
|
|
557
782
|
this.patch({ speaking: true, paused: false, activeMessageId: messageId, error: null });
|
|
783
|
+
this._syncSpeechSegments();
|
|
558
784
|
let failed = false;
|
|
559
785
|
try {
|
|
560
786
|
await this.speechBarrier;
|
|
@@ -567,7 +793,8 @@ var VoiceCoordinator = class {
|
|
|
567
793
|
if (epoch !== this.speechEpoch || this.disposed) return;
|
|
568
794
|
await engine.speak(text, {
|
|
569
795
|
voice: this.snapshot.settings.voice || void 0,
|
|
570
|
-
rate: this.snapshot.settings.rate
|
|
796
|
+
rate: this.snapshot.settings.rate,
|
|
797
|
+
outputDeviceId: this.snapshot.settings.outputDeviceId
|
|
571
798
|
});
|
|
572
799
|
} catch (error) {
|
|
573
800
|
if (epoch === this.speechEpoch) {
|
|
@@ -578,6 +805,7 @@ var VoiceCoordinator = class {
|
|
|
578
805
|
} finally {
|
|
579
806
|
if (epoch === this.speechEpoch && !this.disposed) {
|
|
580
807
|
this.patch({ speaking: false, paused: false, activeMessageId: null });
|
|
808
|
+
this._syncSpeechSegments();
|
|
581
809
|
if (!failed && this.queue.length) this._drain();
|
|
582
810
|
else if (this.snapshot.conversation && !this.snapshot.listening && !this.snapshot.starting)
|
|
583
811
|
await this._startInput(true);
|
|
@@ -589,7 +817,7 @@ var VoiceCoordinator = class {
|
|
|
589
817
|
this.assistantSpeechTimer = null;
|
|
590
818
|
}
|
|
591
819
|
_drain() {
|
|
592
|
-
if (this.disposed || !this.snapshot.conversation || !this.snapshot.settings.announceAssistantMessages || this.snapshot.speaking || this.snapshot.recognizing || this.snapshot.starting || !this.queue.length)
|
|
820
|
+
if (this.disposed || !this.snapshot.conversation && !this.queue[0]?.manual || !this.snapshot.settings.announceAssistantMessages && !this.queue[0]?.manual || this.snapshot.speaking || this.snapshot.recognizing || this.snapshot.starting || !this.queue.length)
|
|
593
821
|
return;
|
|
594
822
|
const wait = this.assistantSpeechNotBefore - Date.now();
|
|
595
823
|
if (wait > 0) {
|
|
@@ -603,7 +831,7 @@ var VoiceCoordinator = class {
|
|
|
603
831
|
this._cancelAssistantSpeechTimer();
|
|
604
832
|
this.assistantSpeechNotBefore = 0;
|
|
605
833
|
const next = this.queue.shift();
|
|
606
|
-
void this.play(next.text, next.id);
|
|
834
|
+
void this.play(next.text, next.id, next.manual === true);
|
|
607
835
|
}
|
|
608
836
|
/** Baselines survive conversation toggles; cancelled message IDs remain suppressed. */
|
|
609
837
|
observeMessage(id2, text, { complete = false, baseline = false } = {}) {
|
|
@@ -620,17 +848,19 @@ var VoiceCoordinator = class {
|
|
|
620
848
|
return;
|
|
621
849
|
}
|
|
622
850
|
const remaining = text.slice(offset);
|
|
623
|
-
const boundary = complete ? remaining.length :
|
|
624
|
-
remaining.lastIndexOf(". "),
|
|
625
|
-
remaining.lastIndexOf("? "),
|
|
626
|
-
remaining.lastIndexOf("! "),
|
|
627
|
-
remaining.lastIndexOf("\n")
|
|
628
|
-
) + 1;
|
|
851
|
+
const boundary = complete ? remaining.length : remaining.lastIndexOf("\n") + 1;
|
|
629
852
|
if (boundary <= 0) return;
|
|
630
|
-
|
|
853
|
+
if (this.snapshot.settings.outputCodeFilterEnabled && hasUnclosedCodeFence(remaining.slice(0, boundary)) && !complete)
|
|
854
|
+
return;
|
|
855
|
+
const chunk = filterSpeechOutput(remaining.slice(0, boundary).trim(), {
|
|
856
|
+
filterCodeBlocks: this.snapshot.settings.outputCodeFilterEnabled,
|
|
857
|
+
codeBlockMaxLines: this.snapshot.settings.outputCodeMaxLines,
|
|
858
|
+
codeBlockNotice: this.snapshot.settings.outputCodeNotice
|
|
859
|
+
}).trim();
|
|
631
860
|
this.consumed.set(id2, offset + boundary);
|
|
632
861
|
if (chunk) {
|
|
633
862
|
this.queue.push({ text: chunk, id: id2 });
|
|
863
|
+
this._syncSpeechSegments();
|
|
634
864
|
this._drain();
|
|
635
865
|
}
|
|
636
866
|
}
|
|
@@ -640,7 +870,7 @@ var VoiceCoordinator = class {
|
|
|
640
870
|
this._cancelAssistantSpeechTimer();
|
|
641
871
|
this.disposed = true;
|
|
642
872
|
this.listeners.clear();
|
|
643
|
-
await this.endConversation();
|
|
873
|
+
await this.endConversation(true);
|
|
644
874
|
}
|
|
645
875
|
};
|
|
646
876
|
|
|
@@ -657,7 +887,7 @@ var VoiceOwnership = class {
|
|
|
657
887
|
const acquired = this.tail.then(async () => {
|
|
658
888
|
if (this.closed || epoch !== this.epoch || owner.disposed) return;
|
|
659
889
|
await Promise.all(
|
|
660
|
-
owners.filter((other) => other !== owner).map((other) => other.endConversation())
|
|
890
|
+
owners.filter((other) => other !== owner).map((other) => other.endConversation(true))
|
|
661
891
|
);
|
|
662
892
|
if (this.closed || epoch !== this.epoch || owner.disposed) return;
|
|
663
893
|
outcome = Promise.resolve(action());
|
|
@@ -681,6 +911,7 @@ var VoiceOwnership = class {
|
|
|
681
911
|
var MicrophoneMeter = class {
|
|
682
912
|
constructor(globals = globalThis) {
|
|
683
913
|
this.g = globals;
|
|
914
|
+
this.deviceId = "";
|
|
684
915
|
this.epoch = 0;
|
|
685
916
|
this.current = null;
|
|
686
917
|
this.stream = this.context = this.source = this.analyser = this.samples = null;
|
|
@@ -745,7 +976,12 @@ var MicrophoneMeter = class {
|
|
|
745
976
|
const capture = async () => {
|
|
746
977
|
try {
|
|
747
978
|
const stream = await this.g.navigator.mediaDevices.getUserMedia({
|
|
748
|
-
audio: {
|
|
979
|
+
audio: {
|
|
980
|
+
...this.deviceId ? { deviceId: { exact: this.deviceId } } : {},
|
|
981
|
+
echoCancellation: true,
|
|
982
|
+
noiseSuppression: true,
|
|
983
|
+
autoGainControl: true
|
|
984
|
+
}
|
|
749
985
|
});
|
|
750
986
|
job.stream = stream;
|
|
751
987
|
if (!valid()) {
|
|
@@ -1271,6 +1507,26 @@ var BrowserRecognitionEngine = class {
|
|
|
1271
1507
|
};
|
|
1272
1508
|
recognition.start();
|
|
1273
1509
|
}
|
|
1510
|
+
reset() {
|
|
1511
|
+
const session = this.session;
|
|
1512
|
+
const recognition = session?.recognition;
|
|
1513
|
+
if (!session || !recognition) return false;
|
|
1514
|
+
this._detach(recognition);
|
|
1515
|
+
session.recognition = null;
|
|
1516
|
+
try {
|
|
1517
|
+
recognition.abort();
|
|
1518
|
+
} catch {
|
|
1519
|
+
}
|
|
1520
|
+
this._activity(session, false);
|
|
1521
|
+
if (this.session !== session || session.signal?.aborted) return false;
|
|
1522
|
+
try {
|
|
1523
|
+
this._begin(session);
|
|
1524
|
+
return true;
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
this._fail(session, error);
|
|
1527
|
+
return false;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1274
1530
|
async stop() {
|
|
1275
1531
|
const session = this.session;
|
|
1276
1532
|
if (!session) return;
|
|
@@ -1522,7 +1778,7 @@ var QwenHttpSpeakingEngine = class {
|
|
|
1522
1778
|
};
|
|
1523
1779
|
}
|
|
1524
1780
|
}
|
|
1525
|
-
async speak(text, { rate = 1, signal, lang = this.lang, voice = "aiden" } = {}) {
|
|
1781
|
+
async speak(text, { rate = 1, signal, lang = this.lang, voice = "aiden", outputDeviceId = "" } = {}) {
|
|
1526
1782
|
if (typeof text !== "string") throw new TypeError("Speech text must be a string.");
|
|
1527
1783
|
if (!Number.isFinite(rate) || rate < 0.1 || rate > 3)
|
|
1528
1784
|
throw new RangeError("Speech rate must be between 0.1 and 3.");
|
|
@@ -1555,6 +1811,8 @@ var QwenHttpSpeakingEngine = class {
|
|
|
1555
1811
|
operation.url = this.g.URL.createObjectURL(blob);
|
|
1556
1812
|
const audio = operation.audio = new this.g.Audio(operation.url);
|
|
1557
1813
|
audio.playbackRate = rate;
|
|
1814
|
+
if (outputDeviceId && typeof audio.setSinkId === "function")
|
|
1815
|
+
await audio.setSinkId(outputDeviceId);
|
|
1558
1816
|
await new Promise((resolve, reject) => {
|
|
1559
1817
|
const done = () => {
|
|
1560
1818
|
cleanup();
|
|
@@ -1854,7 +2112,7 @@ function createQwenSettings(React2) {
|
|
|
1854
2112
|
h(
|
|
1855
2113
|
"p",
|
|
1856
2114
|
null,
|
|
1857
|
-
"Host-wide settings for the
|
|
2115
|
+
"Host-wide settings for the Qwen3 ASR + TTS server. Enter any HTTP or HTTPS base URL reachable from the DSH host. The browser accesses it through authenticated DSH routes."
|
|
1858
2116
|
),
|
|
1859
2117
|
field("Qwen API base URL", "baseUrl"),
|
|
1860
2118
|
field("Request timeout (ms)", "timeoutMs", "number"),
|
|
@@ -1906,12 +2164,14 @@ function createComponents(React2) {
|
|
|
1906
2164
|
};
|
|
1907
2165
|
const paths = {
|
|
1908
2166
|
mic: "M9 5a3 3 0 0 1 6 0v7a3 3 0 0 1-6 0V5M6 10v2a6 6 0 0 0 12 0v-2M12 18v4M8 22h8",
|
|
2167
|
+
micOff: "M9 9v3a3 3 0 0 0 5.12 2.12M15 9V5a3 3 0 0 0-5.64-1.42M6 10v2a6 6 0 0 0 9.5 4.88M18 10v2a6 6 0 0 1-.5 2.4M12 18v4M8 22h8M3 3l18 18",
|
|
1909
2168
|
speaker: "M3 9h4l6-5v16l-6-5H3V9M17 8a6 6 0 0 1 0 8M20 5a10 10 0 0 1 0 14",
|
|
1910
2169
|
close: "M6 6l12 12M18 6L6 18",
|
|
1911
2170
|
stop: "M6 6h12v12H6z",
|
|
1912
2171
|
pause: "M8 5v14M16 5v14",
|
|
1913
2172
|
play: "M7 4l13 8-13 8z",
|
|
1914
2173
|
send: "M3 11.5L21 3l-8.5 18-2-7.5L3 11.5zm7.5 2L21 3",
|
|
2174
|
+
queue: "M5 6h14M5 12h10M5 18h6M18 15v6M15 18h6",
|
|
1915
2175
|
speakerOff: "M3 9h4l6-5v16l-6-5H3V9M17 9l5 6M22 9l-5 6"
|
|
1916
2176
|
};
|
|
1917
2177
|
return h("svg", common, h("path", { d: paths[name] || paths.mic }));
|
|
@@ -2052,7 +2312,7 @@ function createComponents(React2) {
|
|
|
2052
2312
|
}, [controller, enabled]);
|
|
2053
2313
|
return h("canvas", { ref, className: "dlv-wave", "aria-hidden": true });
|
|
2054
2314
|
}
|
|
2055
|
-
function RecordingBar({ controller }) {
|
|
2315
|
+
function RecordingBar({ controller, questionOnly = false, overlay = false, overlayStyle }) {
|
|
2056
2316
|
const state = useController(controller);
|
|
2057
2317
|
const [now, setNow] = React2.useState(Date.now());
|
|
2058
2318
|
React2.useEffect(() => {
|
|
@@ -2063,13 +2323,17 @@ function createComponents(React2) {
|
|
|
2063
2323
|
}, [state.autoSendAt]);
|
|
2064
2324
|
const [invoke, error, clearError] = useActions(controller);
|
|
2065
2325
|
const capture = state.starting || state.listening || state.recognizing;
|
|
2326
|
+
if (questionOnly && !state.answeringQuestion) return null;
|
|
2066
2327
|
if (!state.conversation && !capture && !state.speaking && !state.paused && !state.error && !error)
|
|
2067
2328
|
return null;
|
|
2068
2329
|
const remaining = state.autoSendAt ? Math.max(1, Math.ceil((state.autoSendAt - now) / 1e3)) : null;
|
|
2069
|
-
const status = remaining ? `Sending in ${remaining}\u2026` : state.starting ? "Starting microphone\u2026" : state.paused ? "Speech paused" : state.speaking ? "Speaking" : state.recognizing ? "Recognizing speech\u2026" : state.listening ? "Listening \u2014 waiting for speech" : state.conversation ? "Conversation idle" : "Voice ready";
|
|
2330
|
+
const status = state.answeringQuestion && state.recognizing ? "Recognizing answer\u2026" : state.answeringQuestion && state.listening ? "Listening for your answer\u2026" : remaining ? `Sending in ${remaining}\u2026` : state.starting ? "Starting microphone\u2026" : state.paused ? "Speech paused" : state.speaking ? "Speaking" : state.recognizing ? "Recognizing speech\u2026" : state.listening ? "Listening \u2014 waiting for speech" : state.conversation ? "Conversation idle" : "Voice ready";
|
|
2070
2331
|
return h(
|
|
2071
2332
|
"div",
|
|
2072
|
-
{
|
|
2333
|
+
{
|
|
2334
|
+
className: overlay ? "dlv-bar-wrap dlv-question-overlay" : "dlv-bar-wrap",
|
|
2335
|
+
style: overlay ? overlayStyle : void 0
|
|
2336
|
+
},
|
|
2073
2337
|
h(
|
|
2074
2338
|
"div",
|
|
2075
2339
|
{ className: "dlv-pill", role: "group", "aria-label": "Voice controls" },
|
|
@@ -2087,22 +2351,23 @@ function createComponents(React2) {
|
|
|
2087
2351
|
h("span", { className: "dlv-status", role: "status", "aria-live": "polite" }, status),
|
|
2088
2352
|
h(Button, {
|
|
2089
2353
|
className: "dlv-live-toggle",
|
|
2090
|
-
label: "Automatic
|
|
2091
|
-
title: `Automatic
|
|
2092
|
-
icon: "send",
|
|
2093
|
-
visibleLabel: state.settings.sendingMode === "
|
|
2094
|
-
|
|
2095
|
-
"aria-
|
|
2354
|
+
label: "Automatic delivery mode",
|
|
2355
|
+
title: `Automatic delivery: ${state.settings.sendingMode === "steer" ? "send to the running agent" : state.settings.sendingMode === "queue" ? "queue" : "off"}`,
|
|
2356
|
+
icon: state.settings.sendingMode === "queue" ? "queue" : "send",
|
|
2357
|
+
visibleLabel: state.settings.sendingMode === "steer" ? "SEND" : state.settings.sendingMode === "queue" ? "QUEUE" : "OFF",
|
|
2358
|
+
"aria-label": `Automatic delivery: ${state.settings.sendingMode || "manual"}`,
|
|
2359
|
+
"aria-pressed": ["queue", "steer"].includes(state.settings.sendingMode),
|
|
2360
|
+
"data-mode": state.settings.sendingMode || "manual",
|
|
2096
2361
|
onClick: () => invoke("updateSettings", {
|
|
2097
|
-
sendingMode: state.settings.sendingMode === "
|
|
2362
|
+
sendingMode: !["queue", "steer"].includes(state.settings.sendingMode) ? "queue" : state.settings.sendingMode === "queue" ? "steer" : "manual"
|
|
2098
2363
|
})
|
|
2099
2364
|
}),
|
|
2100
2365
|
h(Button, {
|
|
2101
|
-
className: "dlv-live-toggle"
|
|
2366
|
+
className: `dlv-live-toggle${state.speechSegmentsRemaining > 0 ? " dlv-live-toggle-expanded" : ""}`,
|
|
2102
2367
|
label: "Automatic assistant speech",
|
|
2103
|
-
title: `Automatic assistant speech: ${state.settings.announceAssistantMessages !== false ? "on" : "off"}`,
|
|
2368
|
+
title: `Automatic assistant speech: ${state.settings.announceAssistantMessages !== false ? "on" : "off"}${state.speechSegmentsRemaining > 0 ? ` \u2014 ${state.speechSegmentsRemaining} speech segment${state.speechSegmentsRemaining === 1 ? "" : "s"} remaining` : ""}`,
|
|
2104
2369
|
icon: state.settings.announceAssistantMessages !== false ? "speaker" : "speakerOff",
|
|
2105
|
-
visibleLabel: state.settings.announceAssistantMessages
|
|
2370
|
+
visibleLabel: state.settings.announceAssistantMessages === false ? "OFF" : state.speechSegmentsRemaining > 0 ? String(state.speechSegmentsRemaining) : "ON",
|
|
2106
2371
|
role: "switch",
|
|
2107
2372
|
"aria-checked": state.settings.announceAssistantMessages !== false,
|
|
2108
2373
|
onClick: () => invoke("updateSettings", {
|
|
@@ -2120,9 +2385,14 @@ function createComponents(React2) {
|
|
|
2120
2385
|
onClick: () => invoke("startConversation")
|
|
2121
2386
|
}) : null,
|
|
2122
2387
|
capture ? h(Button, {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2388
|
+
className: "dlv-live-toggle dlv-mic-state",
|
|
2389
|
+
label: state.muted ? "Resume listening" : "Ignore composer input",
|
|
2390
|
+
title: `Microphone input: ${state.muted ? "ignoring" : "listening"}`,
|
|
2391
|
+
icon: state.muted ? "micOff" : "mic",
|
|
2392
|
+
visibleLabel: state.muted ? "IGNORING" : "LISTENING",
|
|
2393
|
+
"aria-pressed": Boolean(state.muted),
|
|
2394
|
+
"data-muted": state.muted ? "true" : "false",
|
|
2395
|
+
onClick: () => invoke(state.muted ? "resumeListeningInput" : "muteListening")
|
|
2126
2396
|
}) : null,
|
|
2127
2397
|
state.speaking && !state.paused && state.capabilities[state.settings.engine]?.pause ? h(Button, {
|
|
2128
2398
|
label: "Pause speech",
|
|
@@ -2164,6 +2434,57 @@ function createComponents(React2) {
|
|
|
2164
2434
|
const [invoke, error, clearError] = useActions(controller);
|
|
2165
2435
|
const settings = state.settings || {};
|
|
2166
2436
|
const capabilities = state.capabilities || {};
|
|
2437
|
+
const tabsId = React2.useId();
|
|
2438
|
+
const tabs = [
|
|
2439
|
+
{ id: "conversation", label: "Conversation" },
|
|
2440
|
+
{ id: "speech", label: "Speech" },
|
|
2441
|
+
{ id: "recognition", label: "Speech recognition" }
|
|
2442
|
+
];
|
|
2443
|
+
const tabRefs = React2.useRef([]);
|
|
2444
|
+
const [activeTab, setActiveTab] = React2.useState("conversation");
|
|
2445
|
+
const activateTab = (index) => {
|
|
2446
|
+
const tab = tabs[index];
|
|
2447
|
+
if (!tab) return;
|
|
2448
|
+
setActiveTab(tab.id);
|
|
2449
|
+
tabRefs.current[index]?.focus();
|
|
2450
|
+
};
|
|
2451
|
+
const handleTabKeyDown = (event, index) => {
|
|
2452
|
+
let nextIndex;
|
|
2453
|
+
if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
|
|
2454
|
+
else if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
|
|
2455
|
+
else if (event.key === "Home") nextIndex = 0;
|
|
2456
|
+
else if (event.key === "End") nextIndex = tabs.length - 1;
|
|
2457
|
+
else return;
|
|
2458
|
+
event.preventDefault();
|
|
2459
|
+
activateTab(nextIndex);
|
|
2460
|
+
};
|
|
2461
|
+
const [audioDevices, setAudioDevices] = React2.useState([]);
|
|
2462
|
+
React2.useEffect(() => {
|
|
2463
|
+
let active = true;
|
|
2464
|
+
const refresh = async () => {
|
|
2465
|
+
try {
|
|
2466
|
+
const devices = await globalThis.navigator?.mediaDevices?.enumerateDevices?.();
|
|
2467
|
+
if (active) setAudioDevices(Array.from(devices || []));
|
|
2468
|
+
} catch {
|
|
2469
|
+
if (active) setAudioDevices([]);
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
void refresh();
|
|
2473
|
+
globalThis.navigator?.mediaDevices?.addEventListener?.("devicechange", refresh);
|
|
2474
|
+
return () => {
|
|
2475
|
+
active = false;
|
|
2476
|
+
globalThis.navigator?.mediaDevices?.removeEventListener?.("devicechange", refresh);
|
|
2477
|
+
};
|
|
2478
|
+
}, []);
|
|
2479
|
+
const deviceOptions = (kind, fallback) => [
|
|
2480
|
+
{ value: "", label: "System default" },
|
|
2481
|
+
...audioDevices.filter(
|
|
2482
|
+
(device) => device.kind === kind && device.deviceId && device.deviceId !== "default"
|
|
2483
|
+
).map((device, index) => ({
|
|
2484
|
+
value: device.deviceId,
|
|
2485
|
+
label: device.label || `${fallback} ${index + 1}`
|
|
2486
|
+
}))
|
|
2487
|
+
];
|
|
2167
2488
|
const field = (label, key, options) => h(
|
|
2168
2489
|
"label",
|
|
2169
2490
|
{ key },
|
|
@@ -2186,11 +2507,16 @@ function createComponents(React2) {
|
|
|
2186
2507
|
)
|
|
2187
2508
|
)
|
|
2188
2509
|
);
|
|
2189
|
-
const
|
|
2190
|
-
"
|
|
2191
|
-
{
|
|
2192
|
-
|
|
2193
|
-
|
|
2510
|
+
const panel = (id2, children) => h(
|
|
2511
|
+
"div",
|
|
2512
|
+
{
|
|
2513
|
+
id: `${tabsId}-panel-${id2}`,
|
|
2514
|
+
className: "dlv-settings-panel",
|
|
2515
|
+
role: "tabpanel",
|
|
2516
|
+
"aria-labelledby": `${tabsId}-tab-${id2}`,
|
|
2517
|
+
hidden: activeTab !== id2
|
|
2518
|
+
},
|
|
2519
|
+
...children
|
|
2194
2520
|
);
|
|
2195
2521
|
const subcard = (title, children, open = false) => h(
|
|
2196
2522
|
"details",
|
|
@@ -2204,9 +2530,41 @@ function createComponents(React2) {
|
|
|
2204
2530
|
h("h3", null, "Live Voice"),
|
|
2205
2531
|
onClose ? h(Button, { label: "Close voice settings", icon: "close", onClick: onClose }) : null,
|
|
2206
2532
|
h(
|
|
2207
|
-
"
|
|
2208
|
-
{ className: "dlv-settings-
|
|
2209
|
-
|
|
2533
|
+
"div",
|
|
2534
|
+
{ className: "dlv-settings-tabs", role: "tablist", "aria-label": "Live Voice settings" },
|
|
2535
|
+
tabs.map((tab, index) => {
|
|
2536
|
+
const selected = activeTab === tab.id;
|
|
2537
|
+
return h(
|
|
2538
|
+
"button",
|
|
2539
|
+
{
|
|
2540
|
+
key: tab.id,
|
|
2541
|
+
ref: (element) => {
|
|
2542
|
+
tabRefs.current[index] = element;
|
|
2543
|
+
},
|
|
2544
|
+
id: `${tabsId}-tab-${tab.id}`,
|
|
2545
|
+
type: "button",
|
|
2546
|
+
role: "tab",
|
|
2547
|
+
className: "dlv-settings-tab",
|
|
2548
|
+
"aria-selected": selected,
|
|
2549
|
+
"aria-controls": `${tabsId}-panel-${tab.id}`,
|
|
2550
|
+
"data-active": selected ? "true" : void 0,
|
|
2551
|
+
tabIndex: selected ? 0 : -1,
|
|
2552
|
+
onClick: () => setActiveTab(tab.id),
|
|
2553
|
+
onKeyDown: (event) => handleTabKeyDown(event, index)
|
|
2554
|
+
},
|
|
2555
|
+
tab.label
|
|
2556
|
+
);
|
|
2557
|
+
})
|
|
2558
|
+
),
|
|
2559
|
+
h(
|
|
2560
|
+
"div",
|
|
2561
|
+
{
|
|
2562
|
+
id: `${tabsId}-panel-speech`,
|
|
2563
|
+
className: "dlv-settings-panel",
|
|
2564
|
+
role: "tabpanel",
|
|
2565
|
+
"aria-labelledby": `${tabsId}-tab-speech`,
|
|
2566
|
+
hidden: activeTab !== "speech"
|
|
2567
|
+
},
|
|
2210
2568
|
h(
|
|
2211
2569
|
"div",
|
|
2212
2570
|
{ className: "dlv-settings-card-body" },
|
|
@@ -2227,22 +2585,22 @@ function createComponents(React2) {
|
|
|
2227
2585
|
disabled: capabilities.browser?.supported === false
|
|
2228
2586
|
}
|
|
2229
2587
|
]),
|
|
2230
|
-
settings.engine
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
value: voice.voiceURI || voice.name,
|
|
2234
|
-
label: `${voice.name} \u2014 ${voice.lang || "unknown language"}`
|
|
2235
|
-
}))
|
|
2236
|
-
]) : settings.engine === "say" ? h(
|
|
2237
|
-
"label",
|
|
2588
|
+
settings.engine !== "say" ? field("Output device", "outputDeviceId", deviceOptions("audiooutput", "Audio output")) : h("small", null, "macOS say uses the output selected on the DSH host."),
|
|
2589
|
+
settings.engine === "browser" ? h(
|
|
2590
|
+
React2.Fragment,
|
|
2238
2591
|
null,
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2592
|
+
h(
|
|
2593
|
+
"small",
|
|
2594
|
+
null,
|
|
2595
|
+
"Browser speech synthesis may ignore the selected output device; this browser API normally follows the system default."
|
|
2596
|
+
),
|
|
2597
|
+
field("Local browser voice", "voice", [
|
|
2598
|
+
{ value: "", label: "Automatic local voice" },
|
|
2599
|
+
...(capabilities.browser?.voices || []).map((voice) => ({
|
|
2600
|
+
value: voice.voiceURI || voice.name,
|
|
2601
|
+
label: `${voice.name} \u2014 ${voice.lang || "unknown language"}`
|
|
2602
|
+
}))
|
|
2603
|
+
])
|
|
2246
2604
|
) : null,
|
|
2247
2605
|
settings.engine === "qwen-http" ? h(
|
|
2248
2606
|
React2.Fragment,
|
|
@@ -2257,6 +2615,47 @@ function createComponents(React2) {
|
|
|
2257
2615
|
h(QwenSettings, { key: "qwen-output-settings", controller })
|
|
2258
2616
|
])
|
|
2259
2617
|
) : null,
|
|
2618
|
+
subcard("Filtering", [
|
|
2619
|
+
h(
|
|
2620
|
+
"label",
|
|
2621
|
+
{ key: "output-code-filter", className: "dlv-check" },
|
|
2622
|
+
h("input", {
|
|
2623
|
+
type: "checkbox",
|
|
2624
|
+
checked: settings.outputCodeFilterEnabled !== false,
|
|
2625
|
+
onChange: (event) => invoke("updateSettings", { outputCodeFilterEnabled: event.target.checked })
|
|
2626
|
+
}),
|
|
2627
|
+
" Filter Markdown code blocks before speaking"
|
|
2628
|
+
),
|
|
2629
|
+
h(
|
|
2630
|
+
"label",
|
|
2631
|
+
{ key: "output-code-lines" },
|
|
2632
|
+
"Read code blocks up to this many lines",
|
|
2633
|
+
h("input", {
|
|
2634
|
+
type: "number",
|
|
2635
|
+
min: 0,
|
|
2636
|
+
max: 100,
|
|
2637
|
+
value: settings.outputCodeMaxLines ?? 5,
|
|
2638
|
+
disabled: settings.outputCodeFilterEnabled === false,
|
|
2639
|
+
onChange: (event) => {
|
|
2640
|
+
const value = Number(event.target.value);
|
|
2641
|
+
if (Number.isInteger(value) && value >= 0 && value <= 100)
|
|
2642
|
+
invoke("updateSettings", { outputCodeMaxLines: value });
|
|
2643
|
+
}
|
|
2644
|
+
})
|
|
2645
|
+
),
|
|
2646
|
+
h(
|
|
2647
|
+
"label",
|
|
2648
|
+
{ key: "output-code-notice" },
|
|
2649
|
+
"Replacement phrase for larger code blocks",
|
|
2650
|
+
h("input", {
|
|
2651
|
+
type: "text",
|
|
2652
|
+
maxLength: 300,
|
|
2653
|
+
value: settings.outputCodeNotice || "Look the code on out conversation",
|
|
2654
|
+
disabled: settings.outputCodeFilterEnabled === false,
|
|
2655
|
+
onChange: (event) => invoke("updateSettings", { outputCodeNotice: event.target.value })
|
|
2656
|
+
})
|
|
2657
|
+
)
|
|
2658
|
+
]),
|
|
2260
2659
|
h(
|
|
2261
2660
|
"label",
|
|
2262
2661
|
null,
|
|
@@ -2279,13 +2678,48 @@ function createComponents(React2) {
|
|
|
2279
2678
|
"p",
|
|
2280
2679
|
null,
|
|
2281
2680
|
"Qwen synthesis runs on the DSH host and the generated WAV plays in this browser. macOS say plays on the host; Browser speech plays on this device."
|
|
2681
|
+
),
|
|
2682
|
+
...["qwen-http", "say", "browser"].filter((id2) => capabilities[id2]?.supported === false).map(
|
|
2683
|
+
(id2) => h(
|
|
2684
|
+
"p",
|
|
2685
|
+
{ key: id2, role: "status" },
|
|
2686
|
+
`${id2 === "qwen-http" ? "Qwen3 local" : id2 === "say" ? "macOS say" : "Browser speech"}: ${capabilities[id2].reason}`
|
|
2687
|
+
)
|
|
2688
|
+
),
|
|
2689
|
+
h(
|
|
2690
|
+
"div",
|
|
2691
|
+
{ className: "dlv-settings-actions" },
|
|
2692
|
+
h(
|
|
2693
|
+
"button",
|
|
2694
|
+
{
|
|
2695
|
+
type: "button",
|
|
2696
|
+
disabled: capabilities[settings.engine]?.supported !== true || state.speaking,
|
|
2697
|
+
onClick: () => invoke("speak", "DSH Live Voice. The selected speech output is working.")
|
|
2698
|
+
},
|
|
2699
|
+
state.speaking ? "Testing speech\u2026" : "Test selected speech output"
|
|
2700
|
+
),
|
|
2701
|
+
state.speaking || state.paused ? h(
|
|
2702
|
+
"button",
|
|
2703
|
+
{ type: "button", onClick: () => invoke("stopSpeech") },
|
|
2704
|
+
"Stop speech test"
|
|
2705
|
+
) : null,
|
|
2706
|
+
h(
|
|
2707
|
+
"button",
|
|
2708
|
+
{ type: "button", onClick: () => invoke("refreshCapabilities") },
|
|
2709
|
+
"Refresh available engines"
|
|
2710
|
+
)
|
|
2282
2711
|
)
|
|
2283
2712
|
)
|
|
2284
2713
|
),
|
|
2285
2714
|
h(
|
|
2286
|
-
"
|
|
2287
|
-
{
|
|
2288
|
-
|
|
2715
|
+
"div",
|
|
2716
|
+
{
|
|
2717
|
+
id: `${tabsId}-panel-recognition`,
|
|
2718
|
+
className: "dlv-settings-panel",
|
|
2719
|
+
role: "tabpanel",
|
|
2720
|
+
"aria-labelledby": `${tabsId}-tab-recognition`,
|
|
2721
|
+
hidden: activeTab !== "recognition"
|
|
2722
|
+
},
|
|
2289
2723
|
h(
|
|
2290
2724
|
"div",
|
|
2291
2725
|
{ className: "dlv-settings-card-body" },
|
|
@@ -2307,6 +2741,12 @@ function createComponents(React2) {
|
|
|
2307
2741
|
h("option", { value: "whisper-http" }, "Whisper HTTP \u2014 DSH host")
|
|
2308
2742
|
)
|
|
2309
2743
|
),
|
|
2744
|
+
field("Input device", "inputDeviceId", deviceOptions("audioinput", "Microphone")),
|
|
2745
|
+
settings.recognitionEngine === "browser" ? h(
|
|
2746
|
+
"small",
|
|
2747
|
+
null,
|
|
2748
|
+
"Browser SpeechRecognition may use the browser or system default microphone instead of this selection."
|
|
2749
|
+
) : null,
|
|
2310
2750
|
field("Recognition language", "recognitionLang", [
|
|
2311
2751
|
...settings.recognitionEngine !== "browser" ? [{ value: "auto", label: "Automatic \u2014 detect language" }] : [],
|
|
2312
2752
|
{ value: "pt-BR", label: "Portugu\xEAs (Brasil)" },
|
|
@@ -2351,6 +2791,89 @@ function createComponents(React2) {
|
|
|
2351
2791
|
h(QwenSettings, { key: "qwen-recognition-settings", controller })
|
|
2352
2792
|
]) : null,
|
|
2353
2793
|
h("p", null, "Provider settings change with the selected recognition engine."),
|
|
2794
|
+
subcard("Voice commands", [
|
|
2795
|
+
h(
|
|
2796
|
+
"label",
|
|
2797
|
+
{ key: "voice-commands-enabled", className: "dlv-check" },
|
|
2798
|
+
h("input", {
|
|
2799
|
+
type: "checkbox",
|
|
2800
|
+
checked: settings.voiceCommandsEnabled !== false,
|
|
2801
|
+
onChange: (event) => invoke("updateSettings", { voiceCommandsEnabled: event.target.checked })
|
|
2802
|
+
}),
|
|
2803
|
+
" Enable exact voice commands"
|
|
2804
|
+
),
|
|
2805
|
+
h(
|
|
2806
|
+
"small",
|
|
2807
|
+
{ key: "voice-command-help" },
|
|
2808
|
+
"Separate phrases with commas. Matching ignores capitalization, accents, punctuation, and extra spaces. The entire final chunk must match."
|
|
2809
|
+
),
|
|
2810
|
+
...[
|
|
2811
|
+
["Send to running agent", "voiceCommandSend", "send, send message"],
|
|
2812
|
+
["Put in queue", "voiceCommandQueue", "queue, queue message"],
|
|
2813
|
+
["End conversation", "voiceCommandEnd", "end, end conversation"],
|
|
2814
|
+
["Mute composer input", "voiceCommandMute", "mute, stop listening"],
|
|
2815
|
+
["Resume composer input", "voiceCommandResume", "resume, start listening"],
|
|
2816
|
+
[
|
|
2817
|
+
"Stop assistant speech",
|
|
2818
|
+
"voiceCommandStopSpeaking",
|
|
2819
|
+
"stop talking, stop speaking, shut up"
|
|
2820
|
+
],
|
|
2821
|
+
["Clear composer", "voiceCommandClear", "clear all, clear message"]
|
|
2822
|
+
].map(
|
|
2823
|
+
([label, key, fallback]) => h(
|
|
2824
|
+
"label",
|
|
2825
|
+
{ key },
|
|
2826
|
+
label,
|
|
2827
|
+
h("textarea", {
|
|
2828
|
+
rows: 2,
|
|
2829
|
+
maxLength: 1e3,
|
|
2830
|
+
defaultValue: settings[key] || fallback,
|
|
2831
|
+
disabled: settings.voiceCommandsEnabled === false,
|
|
2832
|
+
onBlur: (event) => invoke("updateSettings", { [key]: event.target.value })
|
|
2833
|
+
})
|
|
2834
|
+
)
|
|
2835
|
+
)
|
|
2836
|
+
]),
|
|
2837
|
+
subcard("Filtering", [
|
|
2838
|
+
h(
|
|
2839
|
+
"label",
|
|
2840
|
+
{ key: "recognition-filter", className: "dlv-check" },
|
|
2841
|
+
h("input", {
|
|
2842
|
+
type: "checkbox",
|
|
2843
|
+
checked: settings.recognitionFilterEnabled !== false,
|
|
2844
|
+
onChange: (event) => invoke("updateSettings", { recognitionFilterEnabled: event.target.checked })
|
|
2845
|
+
}),
|
|
2846
|
+
" Ignore short final transcription chunks"
|
|
2847
|
+
),
|
|
2848
|
+
h(
|
|
2849
|
+
"label",
|
|
2850
|
+
{ key: "recognition-minimum-words" },
|
|
2851
|
+
"Minimum words per final chunk",
|
|
2852
|
+
h("input", {
|
|
2853
|
+
type: "number",
|
|
2854
|
+
min: 1,
|
|
2855
|
+
max: 20,
|
|
2856
|
+
value: settings.recognitionMinimumWords ?? 2,
|
|
2857
|
+
disabled: settings.recognitionFilterEnabled === false,
|
|
2858
|
+
onChange: (event) => {
|
|
2859
|
+
const value = Number(event.target.value);
|
|
2860
|
+
if (Number.isInteger(value) && value >= 1 && value <= 20)
|
|
2861
|
+
invoke("updateSettings", { recognitionMinimumWords: value });
|
|
2862
|
+
}
|
|
2863
|
+
}),
|
|
2864
|
+
h(
|
|
2865
|
+
"small",
|
|
2866
|
+
null,
|
|
2867
|
+
"Final chunks with fewer words are ignored before they reach the composer or automatic delivery."
|
|
2868
|
+
)
|
|
2869
|
+
)
|
|
2870
|
+
]),
|
|
2871
|
+
capabilities.capture?.supported === false ? h("p", { role: "status" }, `Microphone: ${capabilities.capture.reason}`) : capabilities.capture?.permission === "prompt" ? h(
|
|
2872
|
+
"p",
|
|
2873
|
+
{ role: "status" },
|
|
2874
|
+
"Microphone permission will be requested only when you start dictation or a voice conversation."
|
|
2875
|
+
) : null,
|
|
2876
|
+
capabilities.recognition?.supported === false ? h("p", { role: "status" }, capabilities.recognition.reason) : null,
|
|
2354
2877
|
usesPluginVoiceDetection(settings.recognitionEngine) ? h(
|
|
2355
2878
|
"details",
|
|
2356
2879
|
{ className: "dlv-settings-subcard", "aria-label": "Silence detection settings" },
|
|
@@ -2399,135 +2922,100 @@ function createComponents(React2) {
|
|
|
2399
2922
|
) : null
|
|
2400
2923
|
)
|
|
2401
2924
|
),
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2925
|
+
panel("conversation", [
|
|
2926
|
+
h(
|
|
2927
|
+
"label",
|
|
2928
|
+
{ key: "announce", className: "dlv-check" },
|
|
2929
|
+
h("input", {
|
|
2930
|
+
type: "checkbox",
|
|
2931
|
+
checked: settings.announceAssistantMessages !== false,
|
|
2932
|
+
onChange: (event) => invoke("updateSettings", { announceAssistantMessages: event.target.checked })
|
|
2933
|
+
}),
|
|
2934
|
+
" Automatically speak new assistant messages"
|
|
2935
|
+
),
|
|
2936
|
+
h(
|
|
2937
|
+
"p",
|
|
2938
|
+
{ key: "policy" },
|
|
2939
|
+
"During a voice conversation, assistant phrases are announced automatically. Playback waits while you are speaking."
|
|
2940
|
+
),
|
|
2941
|
+
h(
|
|
2942
|
+
"label",
|
|
2943
|
+
{ key: "interrupt-message", className: "dlv-check" },
|
|
2944
|
+
h("input", {
|
|
2945
|
+
type: "checkbox",
|
|
2946
|
+
checked: settings.interruptSpeechOnUserMessage === true,
|
|
2947
|
+
onChange: (event) => invoke("updateSettings", { interruptSpeechOnUserMessage: event.target.checked })
|
|
2948
|
+
}),
|
|
2949
|
+
" Stop assistant speech when I send a message"
|
|
2950
|
+
),
|
|
2951
|
+
h(
|
|
2952
|
+
"p",
|
|
2953
|
+
{ key: "interrupt-message-description", className: "dlv-setting-description" },
|
|
2954
|
+
settings.interruptSpeechOnUserMessage ? "Sending or steering a new user message stops current or paused assistant speech." : "Sending another message does not stop the assistant audio you are already hearing."
|
|
2955
|
+
),
|
|
2956
|
+
field("Listening mode", "mode", [
|
|
2957
|
+
{ value: "speaker", label: "Speakers \u2014 gated listening" },
|
|
2958
|
+
{ value: "headphones", label: "Headphones \u2014 open microphone" }
|
|
2959
|
+
]),
|
|
2960
|
+
h(
|
|
2961
|
+
"p",
|
|
2962
|
+
{ key: "mode-description", className: "dlv-setting-description" },
|
|
2963
|
+
settings.mode === "headphones" ? "Open microphone keeps listening while responses play. When your speech is detected, playback pauses and resumes only when you choose." : "Gated listening releases the microphone while responses play, preventing speaker audio from being recognized. Use Take microphone to interrupt."
|
|
2964
|
+
),
|
|
2965
|
+
h(
|
|
2966
|
+
"label",
|
|
2967
|
+
{ key: "speech-delay" },
|
|
2968
|
+
"Assistant response delay",
|
|
2430
2969
|
h(
|
|
2431
|
-
"
|
|
2432
|
-
{
|
|
2433
|
-
|
|
2970
|
+
"select",
|
|
2971
|
+
{
|
|
2972
|
+
value: String(settings.assistantSpeechDelaySeconds || 3),
|
|
2973
|
+
onChange: (event) => invoke("updateSettings", {
|
|
2974
|
+
assistantSpeechDelaySeconds: Number(event.target.value)
|
|
2975
|
+
})
|
|
2976
|
+
},
|
|
2977
|
+
[1, 2, 3, 4, 5, 6, 8, 10].map(
|
|
2978
|
+
(seconds) => h("option", { key: seconds, value: String(seconds) }, seconds + " seconds")
|
|
2979
|
+
)
|
|
2434
2980
|
),
|
|
2435
|
-
field("Listening mode", "mode", [
|
|
2436
|
-
{ value: "speaker", label: "Speakers \u2014 gated listening" },
|
|
2437
|
-
{ value: "headphones", label: "Headphones \u2014 open microphone" }
|
|
2438
|
-
]),
|
|
2439
2981
|
h(
|
|
2440
|
-
"
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
)
|
|
2982
|
+
"small",
|
|
2983
|
+
null,
|
|
2984
|
+
"After you stop speaking, automatic assistant playback waits for this much continuous silence. Speaking again restarts the wait."
|
|
2985
|
+
)
|
|
2986
|
+
),
|
|
2987
|
+
field("Sending mode", "sendingMode", [
|
|
2988
|
+
{ value: "manual", label: "Off \u2014 review and send manually" },
|
|
2989
|
+
{ value: "queue", label: "Queue \u2014 automatically add after silence" },
|
|
2990
|
+
{ value: "steer", label: "Steer \u2014 automatically send to the running agent" }
|
|
2991
|
+
]),
|
|
2992
|
+
settings.sendingMode !== "manual" ? h(
|
|
2993
|
+
"label",
|
|
2994
|
+
{ key: "delay" },
|
|
2995
|
+
"Send after silence",
|
|
2444
2996
|
h(
|
|
2445
|
-
"
|
|
2446
|
-
{
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
})
|
|
2455
|
-
},
|
|
2456
|
-
[1, 2, 3, 4, 5, 6, 8, 10].map(
|
|
2457
|
-
(seconds) => h("option", { key: seconds, value: String(seconds) }, seconds + " seconds")
|
|
2458
|
-
)
|
|
2459
|
-
),
|
|
2460
|
-
h(
|
|
2461
|
-
"small",
|
|
2462
|
-
null,
|
|
2463
|
-
"After you stop speaking, automatic assistant playback waits for this much continuous silence. Speaking again restarts the wait."
|
|
2997
|
+
"select",
|
|
2998
|
+
{
|
|
2999
|
+
value: String(settings.autoSendDelaySeconds || 4),
|
|
3000
|
+
onChange: (event) => invoke("updateSettings", {
|
|
3001
|
+
autoSendDelaySeconds: Number(event.target.value)
|
|
3002
|
+
})
|
|
3003
|
+
},
|
|
3004
|
+
[2, 3, 4, 5, 6, 8, 10].map(
|
|
3005
|
+
(seconds) => h("option", { key: seconds, value: String(seconds) }, seconds + " seconds")
|
|
2464
3006
|
)
|
|
2465
3007
|
),
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
settings.sendingMode === "automatic" ? h(
|
|
2471
|
-
"label",
|
|
2472
|
-
{ key: "delay" },
|
|
2473
|
-
"Send after silence",
|
|
2474
|
-
h(
|
|
2475
|
-
"select",
|
|
2476
|
-
{
|
|
2477
|
-
value: String(settings.autoSendDelaySeconds || 4),
|
|
2478
|
-
onChange: (event) => invoke("updateSettings", {
|
|
2479
|
-
autoSendDelaySeconds: Number(event.target.value)
|
|
2480
|
-
})
|
|
2481
|
-
},
|
|
2482
|
-
[2, 3, 4, 5, 6, 8, 10].map(
|
|
2483
|
-
(seconds) => h("option", { key: seconds, value: String(seconds) }, seconds + " seconds")
|
|
2484
|
-
)
|
|
2485
|
-
),
|
|
2486
|
-
h(
|
|
2487
|
-
"small",
|
|
2488
|
-
null,
|
|
2489
|
-
"Countdown starts after a final recognized phrase. New speech or edits cancel it."
|
|
2490
|
-
)
|
|
2491
|
-
) : h(
|
|
2492
|
-
"p",
|
|
2493
|
-
{ key: "manual", className: "dlv-setting-description" },
|
|
2494
|
-
"Recognized text stays in the composer until you use the normal DSH Send control."
|
|
3008
|
+
h(
|
|
3009
|
+
"small",
|
|
3010
|
+
null,
|
|
3011
|
+
"Countdown starts after a final recognized phrase. New speech or edits cancel it."
|
|
2495
3012
|
)
|
|
2496
|
-
|
|
2497
|
-
false
|
|
2498
|
-
),
|
|
2499
|
-
capabilities.capture?.supported === false ? h("p", { role: "status" }, `Microphone: ${capabilities.capture.reason}`) : capabilities.capture?.permission === "prompt" ? h(
|
|
2500
|
-
"p",
|
|
2501
|
-
{ role: "status" },
|
|
2502
|
-
"Microphone permission will be requested only when you start dictation or a voice conversation."
|
|
2503
|
-
) : null,
|
|
2504
|
-
capabilities.recognition?.supported === false ? h("p", { role: "status" }, capabilities.recognition.reason) : null,
|
|
2505
|
-
...["qwen-http", "say", "browser"].filter((id2) => capabilities[id2]?.supported === false).map(
|
|
2506
|
-
(id2) => h(
|
|
3013
|
+
) : h(
|
|
2507
3014
|
"p",
|
|
2508
|
-
{ key:
|
|
2509
|
-
|
|
2510
|
-
)
|
|
2511
|
-
),
|
|
2512
|
-
h(
|
|
2513
|
-
"div",
|
|
2514
|
-
{ className: "dlv-settings-actions" },
|
|
2515
|
-
h(
|
|
2516
|
-
"button",
|
|
2517
|
-
{
|
|
2518
|
-
type: "button",
|
|
2519
|
-
disabled: capabilities[settings.engine]?.supported !== true || state.speaking,
|
|
2520
|
-
onClick: () => invoke("speak", "DSH Live Voice. The selected speech output is working.")
|
|
2521
|
-
},
|
|
2522
|
-
state.speaking ? "Testing speech\u2026" : "Test selected speech output"
|
|
2523
|
-
),
|
|
2524
|
-
state.speaking || state.paused ? h("button", { type: "button", onClick: () => invoke("stopSpeech") }, "Stop speech test") : null,
|
|
2525
|
-
h(
|
|
2526
|
-
"button",
|
|
2527
|
-
{ type: "button", onClick: () => invoke("refreshCapabilities") },
|
|
2528
|
-
"Refresh available engines"
|
|
3015
|
+
{ key: "manual", className: "dlv-setting-description" },
|
|
3016
|
+
"Recognized text stays in the composer until you use the normal DSH Send control."
|
|
2529
3017
|
)
|
|
2530
|
-
),
|
|
3018
|
+
]),
|
|
2531
3019
|
h(ErrorText, {
|
|
2532
3020
|
error: error || state.error,
|
|
2533
3021
|
onDismiss: () => {
|
|
@@ -2552,23 +3040,27 @@ var styles = `
|
|
|
2552
3040
|
.dlv-icon-button:disabled{opacity:.4;cursor:default}
|
|
2553
3041
|
.dlv-icon-button:focus-visible,.dlv-settings :is(input,select):focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}
|
|
2554
3042
|
.dlv-bar-wrap{width:100%;min-width:0}
|
|
3043
|
+
.dlv-question-overlay{position:fixed;z-index:10000;bottom:8px;transform:translateX(-50%);max-width:calc(100vw - 32px);pointer-events:none}
|
|
3044
|
+
.dlv-question-overlay .dlv-pill{pointer-events:auto}
|
|
2555
3045
|
.dlv-pill{display:flex;align-items:center;box-sizing:border-box;gap:10px;min-height:52px;border-radius:26px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);padding:0 14px;width:100%;max-width:720px;margin:0 auto;box-shadow:0 8px 24px rgba(0,0,0,.18)}
|
|
2556
3046
|
.dlv-pill-button{width:34px;height:34px;border-radius:50%;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}
|
|
2557
3047
|
.dlv-pill-button:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
2558
|
-
.dlv-live-toggle{width:
|
|
2559
|
-
.dlv-live-toggle svg{width:18px;height:18px}
|
|
2560
|
-
.dlv-toggle-state{
|
|
2561
|
-
.dlv-live-toggle:hover{background:var(--dsw-alias-bg-layer-2)}
|
|
2562
|
-
.dlv-live-toggle
|
|
3048
|
+
.dlv-live-toggle{width:34px;height:34px;padding:0 7px;gap:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:17px;color:var(--dsw-alias-label-primary);transition:width .16s ease,gap .16s ease}
|
|
3049
|
+
.dlv-live-toggle svg{width:18px;height:18px;flex:none}
|
|
3050
|
+
.dlv-toggle-state{max-width:0;opacity:0;overflow:hidden;white-space:nowrap;font-size:10px;font-weight:700;line-height:1;letter-spacing:.04em;text-align:left;transition:max-width .16s ease,opacity .12s ease}
|
|
3051
|
+
.dlv-live-toggle:hover,.dlv-live-toggle:focus-visible,.dlv-live-toggle-expanded{width:78px;gap:5px;background:var(--dsw-alias-bg-layer-2)}
|
|
3052
|
+
.dlv-live-toggle:hover .dlv-toggle-state,.dlv-live-toggle:focus-visible .dlv-toggle-state,.dlv-live-toggle-expanded .dlv-toggle-state{max-width:38px;opacity:1}
|
|
3053
|
+
.dlv-live-toggle[aria-checked=false],.dlv-live-toggle[data-mode=manual],.dlv-mic-state[data-muted=true]{color:var(--dsw-alias-label-tertiary);border-color:var(--dsw-alias-border-l1);opacity:.55}
|
|
3054
|
+
.dlv-mic-state:hover,.dlv-mic-state:focus-visible{width:102px}.dlv-mic-state:hover .dlv-toggle-state,.dlv-mic-state:focus-visible .dlv-toggle-state{max-width:64px}
|
|
2563
3055
|
.dlv-live-toggle[aria-checked=true]{background:var(--dsw-alias-bg-layer-2)}
|
|
2564
3056
|
.dlv-wave{display:block;flex:1 1 180px;min-width:30px;width:100%;height:40px;color:var(--dsw-alias-label-primary)}
|
|
2565
3057
|
.dlv-status{flex:1 1 120px;min-width:0;font-size:13px;line-height:1.4;color:var(--dsw-alias-label-secondary)}
|
|
2566
3058
|
.dlv-error{color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere;font-size:13px;max-width:720px;margin:8px auto}
|
|
2567
3059
|
.dlv-settings{box-sizing:border-box;padding:18px;width:100%;display:grid;gap:16px;max-width:640px;color:var(--dsw-alias-label-primary)}
|
|
2568
3060
|
.dlv-settings h3,.dlv-settings p{margin:0}
|
|
2569
|
-
.dlv-settings-
|
|
3061
|
+
.dlv-settings-tabs{border-bottom:.5px solid var(--dsw-alias-border-l2);display:flex;align-items:flex-end;gap:22px;overflow-x:auto}.dlv-settings-tab{position:relative;flex:none;padding:7px 1px 9px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:13px;line-height:20px;cursor:pointer}.dlv-settings-tab:hover,.dlv-settings-tab[data-active=true]{color:var(--dsw-alias-label-primary)}.dlv-settings-tab[data-active=true]:after,.dlv-settings-tab:focus-visible:after{content:"";position:absolute;right:0;bottom:-1px;left:0;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-label-primary)}.dlv-settings-tab:focus-visible{border-radius:2px;outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;color:var(--dsw-alias-label-primary)}.dlv-settings-panel{min-width:0;padding-top:2px}.dlv-settings-panel[hidden]{display:none}.dlv-settings-group{margin:0;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;min-width:0;display:grid;gap:14px;padding:16px}.dlv-settings-group legend{padding:0 6px;font-weight:600;color:var(--dsw-alias-label-primary)}.dlv-settings-card-body{display:grid;gap:12px;padding:12px 0}.dlv-settings-subcard{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;min-width:0}.dlv-settings-subcard>summary{cursor:pointer;font-weight:650;list-style:none;display:flex;align-items:center;justify-content:space-between;padding:10px}.dlv-settings-subcard>summary::-webkit-details-marker{display:none}.dlv-settings-subcard>summary:after{content:"\u203A";transform:rotate(90deg);transition:transform .15s}.dlv-settings-subcard:not([open])>summary:after{transform:rotate(0)}.dlv-settings-subcard-body{display:grid;gap:10px;padding:0 10px 10px}
|
|
2570
3062
|
.dlv-settings label{display:grid;gap:6px;font-size:14px}.dlv-settings label.dlv-check{display:flex;align-items:center;gap:8px}.dlv-settings label.dlv-check input{width:auto}
|
|
2571
|
-
.dlv-settings :is(input,select){box-sizing:border-box;width:100%;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit}
|
|
3063
|
+
.dlv-settings :is(input,select,textarea){box-sizing:border-box;width:100%;padding:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;font:inherit}
|
|
2572
3064
|
.dlv-settings small,.dlv-settings p{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
|
|
2573
3065
|
.dlv-preset-group{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.dlv-settings label.dlv-preset{display:flex;align-items:flex-start;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;cursor:pointer}.dlv-settings label.dlv-preset:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.dlv-settings label.dlv-preset input{width:auto;margin-top:3px}.dlv-preset span{display:grid;gap:3px}.dlv-vad-summary{font-weight:500}
|
|
2574
3066
|
.dlv-settings-actions{display:flex;flex-wrap:wrap;gap:8px}.dlv-settings-actions button{padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:inherit;cursor:pointer}.dlv-settings-actions button:disabled{opacity:.45;cursor:default}
|
|
@@ -2593,6 +3085,11 @@ function assistantMessages(snapshot) {
|
|
|
2593
3085
|
};
|
|
2594
3086
|
}).sort((a, b) => a.turn - b.turn || a.step - b.step);
|
|
2595
3087
|
}
|
|
3088
|
+
function pendingQuestionSpeech(interaction, index = 0) {
|
|
3089
|
+
if (!interaction || interaction.kind !== "question" || !Array.isArray(interaction.questions)) return "";
|
|
3090
|
+
const question = interaction.questions[index]?.question;
|
|
3091
|
+
return typeof question === "string" ? question.trim() : "";
|
|
3092
|
+
}
|
|
2596
3093
|
function latestUserSequence(snapshot) {
|
|
2597
3094
|
return Math.max(
|
|
2598
3095
|
-1,
|
|
@@ -2606,13 +3103,14 @@ function addressedTurn(messages, messageId) {
|
|
|
2606
3103
|
}
|
|
2607
3104
|
|
|
2608
3105
|
// src/client/index.ts
|
|
2609
|
-
var inject = ["slots", "connection", "uiConversation"];
|
|
3106
|
+
var inject = ["slots", "connection", "uiConversation", "uiSession"];
|
|
2610
3107
|
function apply(ctx) {
|
|
2611
3108
|
const e = import_react.default.createElement;
|
|
2612
3109
|
const { MicrophoneButtons, RecordingBar, SpeakButton, SettingsPanel } = createComponents(import_react.default);
|
|
2613
3110
|
const controllers = /* @__PURE__ */ new Map();
|
|
2614
3111
|
const retiring = /* @__PURE__ */ new Set();
|
|
2615
3112
|
let disposed = false;
|
|
3113
|
+
let voiceModeActive = false;
|
|
2616
3114
|
const ownership = new VoiceOwnership();
|
|
2617
3115
|
const recognitionFor = (settings, meter) => settings.recognitionEngine === "qwen-http" ? new QwenHttpRecognitionEngine({
|
|
2618
3116
|
meter,
|
|
@@ -2635,6 +3133,10 @@ function apply(ctx) {
|
|
|
2635
3133
|
entry.composers.clear();
|
|
2636
3134
|
entry.unsubscribe?.();
|
|
2637
3135
|
entry.unsubscribe = null;
|
|
3136
|
+
entry.unsubscribePendingQuestion?.();
|
|
3137
|
+
entry.unsubscribePendingQuestion = null;
|
|
3138
|
+
entry.questionCapture = null;
|
|
3139
|
+
entry.controller.patch({ answeringQuestion: false });
|
|
2638
3140
|
entry.chatListeners.clear();
|
|
2639
3141
|
if (controllers.get(entry.key) === entry) controllers.delete(entry.key);
|
|
2640
3142
|
const done = run(entry.controller, entry.controller.dispose());
|
|
@@ -2674,9 +3176,32 @@ function apply(ctx) {
|
|
|
2674
3176
|
meter,
|
|
2675
3177
|
composer: {
|
|
2676
3178
|
getDraft: () => entry.draft,
|
|
2677
|
-
submit: () => {
|
|
3179
|
+
submit: (mode = "queue") => {
|
|
2678
3180
|
const owner = [...entry.composers.values()].at(-1);
|
|
2679
|
-
owner?.actions.submit?.();
|
|
3181
|
+
owner?.actions.submit?.(mode);
|
|
3182
|
+
},
|
|
3183
|
+
handleQuestionResult: ({ final, interim }) => {
|
|
3184
|
+
const capture = entry.questionCapture;
|
|
3185
|
+
if (!capture || capture.answering || !final && !interim) return false;
|
|
3186
|
+
if (final?.trim()) {
|
|
3187
|
+
capture.answering = true;
|
|
3188
|
+
capture.answers.push({
|
|
3189
|
+
id: capture.interaction.questions[capture.index].id,
|
|
3190
|
+
selected: [],
|
|
3191
|
+
custom: final.trim()
|
|
3192
|
+
});
|
|
3193
|
+
capture.index++;
|
|
3194
|
+
if (capture.index < capture.interaction.questions.length) {
|
|
3195
|
+
capture.answering = false;
|
|
3196
|
+
const text = pendingQuestionSpeech(capture.interaction, capture.index);
|
|
3197
|
+
if (text) run(entry.controller, entry.controller.speak(text, capture.interaction.key));
|
|
3198
|
+
} else {
|
|
3199
|
+
entry.questionCapture = null;
|
|
3200
|
+
entry.controller.patch({ answeringQuestion: false });
|
|
3201
|
+
run(entry.controller, capture.interaction.answer({ answers: capture.answers }));
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
return true;
|
|
2680
3205
|
},
|
|
2681
3206
|
setDraft: (text) => {
|
|
2682
3207
|
if (disposed || entry.closed) return;
|
|
@@ -2717,6 +3242,30 @@ function apply(ctx) {
|
|
|
2717
3242
|
refresh();
|
|
2718
3243
|
for (const listener of entry.chatListeners) listener();
|
|
2719
3244
|
});
|
|
3245
|
+
const pendingInteractions = ctx.uiSession.pendingInteractions;
|
|
3246
|
+
const refreshPendingQuestion = (baseline = false) => {
|
|
3247
|
+
if (disposed || entry.closed) return;
|
|
3248
|
+
const interaction = pendingInteractions.getSnapshot().get(sessionId);
|
|
3249
|
+
const key2 = interaction?.kind === "question" ? interaction.key : null;
|
|
3250
|
+
if (!key2 || entry.questionCapture && entry.questionCapture.interaction.key !== key2) {
|
|
3251
|
+
entry.questionCapture = null;
|
|
3252
|
+
controller.patch({ answeringQuestion: false });
|
|
3253
|
+
}
|
|
3254
|
+
if (baseline) {
|
|
3255
|
+
entry.pendingQuestionKey = key2;
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
if (!key2 || key2 === entry.pendingQuestionKey) return;
|
|
3259
|
+
entry.pendingQuestionKey = key2;
|
|
3260
|
+
const text = pendingQuestionSpeech(interaction);
|
|
3261
|
+
if (text && controller.getSnapshot().conversation) {
|
|
3262
|
+
entry.questionCapture = { interaction, index: 0, answers: [], answering: false };
|
|
3263
|
+
controller.patch({ answeringQuestion: true });
|
|
3264
|
+
run(controller, controller.speak(text, key2));
|
|
3265
|
+
}
|
|
3266
|
+
};
|
|
3267
|
+
refreshPendingQuestion(true);
|
|
3268
|
+
entry.unsubscribePendingQuestion = pendingInteractions.subscribe(refreshPendingQuestion);
|
|
2720
3269
|
const update = controller.updateSettings.bind(controller);
|
|
2721
3270
|
controller.updateSettings = (next) => {
|
|
2722
3271
|
if (disposed || entry.closed) return;
|
|
@@ -2736,6 +3285,7 @@ function apply(ctx) {
|
|
|
2736
3285
|
const original = controller[method].bind(controller);
|
|
2737
3286
|
controller[method] = (...args) => {
|
|
2738
3287
|
entry.request++;
|
|
3288
|
+
if (method === "endConversation" && args[0] !== true) voiceModeActive = false;
|
|
2739
3289
|
return original(...args);
|
|
2740
3290
|
};
|
|
2741
3291
|
}
|
|
@@ -2744,6 +3294,7 @@ function apply(ctx) {
|
|
|
2744
3294
|
controller[method] = (...args) => {
|
|
2745
3295
|
if (disposed || entry.closed || !entry.refs) return Promise.resolve();
|
|
2746
3296
|
if (method !== "speak" && !entry.composers.size) return Promise.resolve();
|
|
3297
|
+
if (method === "startConversation") voiceModeActive = true;
|
|
2747
3298
|
const request = ++entry.request;
|
|
2748
3299
|
return ownership.run(
|
|
2749
3300
|
controller,
|
|
@@ -2787,19 +3338,14 @@ function apply(ctx) {
|
|
|
2787
3338
|
if (!entry || entry.closed || disposed) return;
|
|
2788
3339
|
return () => {
|
|
2789
3340
|
entry.composers.delete(token.current);
|
|
2790
|
-
if (!entry.composers.size) {
|
|
2791
|
-
entry.request++;
|
|
2792
|
-
queueMicrotask(() => {
|
|
2793
|
-
if (!entry.closed && !entry.composers.size)
|
|
2794
|
-
run(entry.controller, entry.controller.endConversation());
|
|
2795
|
-
});
|
|
2796
|
-
}
|
|
2797
3341
|
};
|
|
2798
3342
|
}, [entry]);
|
|
2799
3343
|
import_react.default.useLayoutEffect(() => {
|
|
2800
3344
|
if (!entry || entry.closed || disposed) return;
|
|
2801
3345
|
if (!input || typeof props.inputActions?.setDraft !== "function") return;
|
|
2802
3346
|
entry.composers.set(token.current, { actions: props.inputActions });
|
|
3347
|
+
if (voiceModeActive && !entry.controller.getSnapshot().conversation)
|
|
3348
|
+
run(entry.controller, entry.controller.startConversation());
|
|
2803
3349
|
}, [entry, input, props.inputActions]);
|
|
2804
3350
|
import_react.default.useLayoutEffect(() => {
|
|
2805
3351
|
if (!entry || entry.closed || disposed || !input) return;
|
|
@@ -2900,6 +3446,47 @@ function apply(ctx) {
|
|
|
2900
3446
|
useComposer(entry, props);
|
|
2901
3447
|
return entry ? e(RecordingBar, { controller: entry.controller }) : null;
|
|
2902
3448
|
}
|
|
3449
|
+
function QuestionStatusView({ entry }) {
|
|
3450
|
+
const snapshot = import_react.default.useSyncExternalStore(
|
|
3451
|
+
entry.controller.subscribe,
|
|
3452
|
+
entry.controller.getSnapshot
|
|
3453
|
+
);
|
|
3454
|
+
const [overlayStyle, setOverlayStyle] = import_react.default.useState();
|
|
3455
|
+
import_react.default.useLayoutEffect(() => {
|
|
3456
|
+
if (!snapshot.answeringQuestion) return;
|
|
3457
|
+
const seat = document.querySelector("[data-composer-seat]");
|
|
3458
|
+
if (!seat) return;
|
|
3459
|
+
const update = () => {
|
|
3460
|
+
const rect = seat.getBoundingClientRect();
|
|
3461
|
+
setOverlayStyle({
|
|
3462
|
+
left: rect.left + rect.width / 2,
|
|
3463
|
+
width: Math.min(720, Math.max(0, rect.width - 32))
|
|
3464
|
+
});
|
|
3465
|
+
};
|
|
3466
|
+
update();
|
|
3467
|
+
const observer = typeof ResizeObserver === "function" ? new ResizeObserver(update) : null;
|
|
3468
|
+
observer?.observe(seat);
|
|
3469
|
+
window.addEventListener("resize", update);
|
|
3470
|
+
return () => {
|
|
3471
|
+
observer?.disconnect();
|
|
3472
|
+
window.removeEventListener("resize", update);
|
|
3473
|
+
};
|
|
3474
|
+
}, [snapshot.answeringQuestion]);
|
|
3475
|
+
const target = typeof document === "undefined" ? null : document.body;
|
|
3476
|
+
return snapshot.answeringQuestion && target && overlayStyle ? (0, import_react_dom.createPortal)(
|
|
3477
|
+
e(RecordingBar, {
|
|
3478
|
+
controller: entry.controller,
|
|
3479
|
+
questionOnly: true,
|
|
3480
|
+
overlay: true,
|
|
3481
|
+
overlayStyle
|
|
3482
|
+
}),
|
|
3483
|
+
target
|
|
3484
|
+
) : null;
|
|
3485
|
+
}
|
|
3486
|
+
function QuestionStatus(props) {
|
|
3487
|
+
const entry = useEntry(props.sessionId, "question-status");
|
|
3488
|
+
return entry ? e(QuestionStatusView, { entry }) : null;
|
|
3489
|
+
}
|
|
2903
3490
|
function ActionView({ entry, messageId }) {
|
|
2904
3491
|
const snapshot = import_react.default.useSyncExternalStore(
|
|
2905
3492
|
entry.controller.subscribe,
|
|
@@ -2934,6 +3521,7 @@ function apply(ctx) {
|
|
|
2934
3521
|
for (const [name, id2, order, component] of [
|
|
2935
3522
|
["conversation.input.right", "live-voice-controls", 6, Buttons],
|
|
2936
3523
|
["conversation.input.dock", "live-voice-status", -100, Dock],
|
|
3524
|
+
["conversation.session.header.utilities", "live-voice-question-status", 100, QuestionStatus],
|
|
2937
3525
|
["conversation.chat.assistant-actions", "live-voice-speak", 5, Action]
|
|
2938
3526
|
])
|
|
2939
3527
|
ctx.slots.inject(
|