dsh-live-voice 0.3.0 → 0.3.2

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +89 -1
  2. package/PLAN.md +25 -1
  3. package/README.md +4 -2
  4. package/docs/ARCHITECTURE.md +9 -16
  5. package/docs/CONFIGURATION.md +17 -3
  6. package/docs/VOICE-LIFECYCLE.md +5 -1
  7. package/lib/client.js +543 -326
  8. package/lib/server.js +158 -5
  9. package/package.json +2 -2
  10. package/src/app/AGENTS.md +7 -0
  11. package/src/app/ARCHITECTURE.md +10 -0
  12. package/src/app/client/apply.tsx +96 -40
  13. package/src/app/client/i18n/catalogs/base.ts +7 -0
  14. package/src/app/client/i18n/catalogs/en.ts +10 -0
  15. package/src/app/client/i18n/catalogs/es.ts +10 -0
  16. package/src/app/client/i18n/catalogs/fr.ts +10 -0
  17. package/src/app/client/i18n/catalogs/hi.ts +10 -0
  18. package/src/app/client/i18n/catalogs/pt-BR.ts +10 -0
  19. package/src/app/client/i18n/catalogs/zh.ts +9 -0
  20. package/src/app/server/apply.ts +4 -0
  21. package/src/app/server/registerRoutes.ts +40 -0
  22. package/src/modules/AGENTS.md +5 -0
  23. package/src/modules/conversation/ARCHITECTURE.md +7 -0
  24. package/src/modules/core/ARCHITECTURE.md +5 -0
  25. package/src/modules/core/coordinator.ts +8 -6
  26. package/src/modules/core/qwen/QwenHttpHost.ts +1 -1
  27. package/src/modules/core/qwen/QwenSettings.tsx +15 -14
  28. package/src/modules/core/settings.ts +26 -8
  29. package/src/modules/core/transcript.ts +5 -43
  30. package/src/modules/recognition/ARCHITECTURE.md +5 -0
  31. package/src/modules/recognition/engines/whisper/WhisperRecognitionEngine.ts +5 -3
  32. package/src/modules/recognition/engines/whisper/WhisperSettings.tsx +19 -20
  33. package/src/modules/settings/ARCHITECTURE.md +5 -0
  34. package/src/modules/settings/components/LiveVoiceSettings.tsx +11 -3
  35. package/src/modules/settings/models/settingsHost.ts +45 -0
  36. package/src/modules/settings/models/settingsStorage.ts +45 -10
  37. package/src/modules/settings/sections/conversation/ConversationSettingsSection.tsx +12 -6
  38. package/src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx +2 -2
  39. package/src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx +3 -3
  40. package/src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx +7 -3
  41. package/src/modules/settings/sections/recognition/SilenceDetectionSettings.tsx +38 -8
  42. package/src/modules/settings/sections/recognition/VoiceCommandSettings.tsx +3 -3
  43. package/src/modules/settings/sections/speak/OutputFilterSettings.tsx +5 -5
  44. package/src/modules/settings/sections/speak/SpeakSettingsSection.tsx +7 -3
  45. package/src/modules/settings/sections/speak/SpeechAdvancedSettings.tsx +4 -4
  46. package/src/modules/settings/sections/speak/SpeechEngineSettings.tsx +2 -2
  47. package/src/modules/settings/services/releases.ts +10 -2
  48. package/src/modules/speak/ARCHITECTURE.md +5 -0
  49. package/src/shared/ARCHITECTURE.md +5 -0
  50. package/src/shared/design-system/forms/DraftField.tsx +79 -0
  51. package/src/shared/design-system/forms/NumberField.tsx +4 -10
  52. package/src/shared/design-system/forms/TextAreaField.tsx +4 -10
  53. package/src/shared/design-system/forms/TextField.tsx +4 -10
  54. package/src/shared/design-system/icons/icons.ts +4 -0
  55. package/src/shared/design-system/layout/SettingsSubcard.tsx +15 -3
  56. package/src/shared/design-system/layout/SettingsTabs.tsx +4 -2
  57. package/src/styles/index.ts +3 -3
package/lib/client.js CHANGED
@@ -34,63 +34,34 @@ __export(apply_exports, {
34
34
  inject: () => inject
35
35
  });
36
36
  module.exports = __toCommonJS(apply_exports);
37
- var import_react53 = __toESM(require("react"), 1);
37
+ var import_react54 = __toESM(require("react"), 1);
38
38
  var import_react_dom = require("react-dom");
39
39
 
40
40
  // src/modules/core/transcript.ts
41
41
  var TranscriptDraft = class {
42
- constructor() {
43
- this.reset();
44
- }
45
42
  reset() {
46
- this.owned = null;
47
- this.edited = false;
48
43
  }
49
44
  update(current, hypothesis, final = false) {
50
- if (this.edited) {
51
- if (final) this.edited = false;
52
- return current;
53
- }
54
- let base = current;
55
- let at = current.length;
56
- if (this.owned) {
57
- const { start, text: text2, before: before2, after: after2 } = this.owned;
58
- if (current === before2 + text2 + after2) {
59
- base = before2 + after2;
60
- at = start;
61
- } else if (text2 && current.indexOf(text2) >= 0 && current.indexOf(text2) === current.lastIndexOf(text2)) {
62
- at = current.indexOf(text2);
63
- base = current.slice(0, at) + current.slice(at + text2.length);
64
- } else {
65
- this.owned = null;
66
- this.edited = !final;
67
- return current;
68
- }
69
- }
70
- const before = base.slice(0, at);
71
- const after = base.slice(at);
72
- const separator = hypothesis && before && !/\s$/.test(before) ? " " : "";
73
- const text = separator + hypothesis;
74
- const result = before + text + after;
75
- this.owned = final ? null : { start: at, text, before, after };
76
- return result;
45
+ if (!final || !hypothesis) return current;
46
+ const separator = current && !/\s$/.test(current) ? " " : "";
47
+ return current + separator + hypothesis;
77
48
  }
78
49
  };
79
50
 
80
51
  // src/modules/core/settings.ts
81
52
  var voiceDetectionPresets = Object.freeze({
82
53
  short: Object.freeze({
83
- silenceMs: 900,
54
+ silenceMs: 500,
84
55
  label: "Short",
85
56
  description: "Send quickly after a short pause."
86
57
  }),
87
58
  natural: Object.freeze({
88
- silenceMs: 1500,
59
+ silenceMs: 1e3,
89
60
  label: "Natural",
90
61
  description: "Allow normal pauses between phrases."
91
62
  }),
92
63
  long: Object.freeze({
93
- silenceMs: 2200,
64
+ silenceMs: 2e3,
94
65
  label: "Long",
95
66
  description: "Wait through longer thinking pauses."
96
67
  })
@@ -116,7 +87,8 @@ var defaultSettings = Object.freeze({
116
87
  recognitionEngine: "browser",
117
88
  recognitionProcessLocally: true,
118
89
  recognitionAutoInstall: true,
119
- voiceDetectionPreset: "natural",
90
+ voiceDetectionPreset: "short",
91
+ voiceDetectionCustomSilenceMs: 1e3,
120
92
  recognitionMaxUtteranceSeconds: 60,
121
93
  microphoneEnabled: true,
122
94
  holdToTalkEnabled: true,
@@ -150,6 +122,10 @@ var defaultSettings = Object.freeze({
150
122
  segmentGapMs: 400
151
123
  });
152
124
  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;
125
+ var customSilenceMinMs = 100;
126
+ var customSilenceMaxMs = 1e4;
127
+ var normalizeCustomSilenceMs = (value) => Number.isInteger(value) && value >= customSilenceMinMs && value <= customSilenceMaxMs ? value : defaultSettings.voiceDetectionCustomSilenceMs;
128
+ var voiceDetectionSilenceMs = (settings) => settings.voiceDetectionPreset === "custom" ? normalizeCustomSilenceMs(settings.voiceDetectionCustomSilenceMs) : (voiceDetectionPresets[settings.voiceDetectionPreset] || voiceDetectionPresets[defaultSettings.voiceDetectionPreset]).silenceMs;
153
129
  function normalizeSettings(value) {
154
130
  const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
155
131
  return {
@@ -157,7 +133,8 @@ function normalizeSettings(value) {
157
133
  recognitionEngine: ["browser", "whisper-http", "qwen-http"].includes(source.recognitionEngine) ? source.recognitionEngine : defaultSettings.recognitionEngine,
158
134
  recognitionProcessLocally: typeof source.recognitionProcessLocally === "boolean" ? source.recognitionProcessLocally : defaultSettings.recognitionProcessLocally,
159
135
  recognitionAutoInstall: typeof source.recognitionAutoInstall === "boolean" ? source.recognitionAutoInstall : defaultSettings.recognitionAutoInstall,
160
- voiceDetectionPreset: Object.hasOwn(voiceDetectionPresets, source.voiceDetectionPreset) ? source.voiceDetectionPreset : defaultSettings.voiceDetectionPreset,
136
+ voiceDetectionPreset: source.voiceDetectionPreset === "custom" || Object.hasOwn(voiceDetectionPresets, source.voiceDetectionPreset) ? source.voiceDetectionPreset : defaultSettings.voiceDetectionPreset,
137
+ voiceDetectionCustomSilenceMs: normalizeCustomSilenceMs(source.voiceDetectionCustomSilenceMs),
161
138
  recognitionMaxUtteranceSeconds: Number.isInteger(source.recognitionMaxUtteranceSeconds) && source.recognitionMaxUtteranceSeconds >= 10 && source.recognitionMaxUtteranceSeconds <= 300 ? source.recognitionMaxUtteranceSeconds : defaultSettings.recognitionMaxUtteranceSeconds,
162
139
  microphoneEnabled: typeof source.microphoneEnabled === "boolean" ? source.microphoneEnabled : defaultSettings.microphoneEnabled,
163
140
  holdToTalkEnabled: typeof source.holdToTalkEnabled === "boolean" ? source.holdToTalkEnabled : defaultSettings.holdToTalkEnabled,
@@ -339,7 +316,8 @@ var VoiceCoordinator = class {
339
316
  lang: this.snapshot.settings.recognitionLang,
340
317
  processLocally: this.snapshot.settings.recognitionProcessLocally,
341
318
  autoInstallLocalPack: this.snapshot.settings.recognitionAutoInstall,
342
- voiceDetectionPreset: this.snapshot.settings.voiceDetectionPreset
319
+ voiceDetectionPreset: this.snapshot.settings.voiceDetectionPreset,
320
+ voiceDetectionCustomSilenceMs: this.snapshot.settings.voiceDetectionCustomSilenceMs
343
321
  });
344
322
  this.patch({ capabilities: { ...this.snapshot.capabilities, recognition: void 0 } });
345
323
  }
@@ -361,7 +339,8 @@ var VoiceCoordinator = class {
361
339
  lang: settings.recognitionLang,
362
340
  processLocally: settings.recognitionProcessLocally,
363
341
  autoInstallLocalPack: settings.recognitionAutoInstall,
364
- voiceDetectionPreset: settings.voiceDetectionPreset
342
+ voiceDetectionPreset: settings.voiceDetectionPreset,
343
+ voiceDetectionCustomSilenceMs: settings.voiceDetectionCustomSilenceMs
365
344
  });
366
345
  this.patch({
367
346
  settings,
@@ -454,7 +433,6 @@ var VoiceCoordinator = class {
454
433
  }
455
434
  muteListening() {
456
435
  this.cancelAutoSend();
457
- this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
458
436
  this.transcript.reset();
459
437
  this.updateSettings({ microphoneEnabled: false });
460
438
  if (!this.snapshot.settings.voiceCommandsEnabled) return this.stopListening();
@@ -636,7 +614,6 @@ var VoiceCoordinator = class {
636
614
  clear: this.snapshot.settings.voiceCommandClear
637
615
  });
638
616
  if (command) {
639
- this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
640
617
  this.transcript.reset();
641
618
  this.cancelAutoSend();
642
619
  this.patch({ recognizing: false });
@@ -660,13 +637,11 @@ var VoiceCoordinator = class {
660
637
  return;
661
638
  }
662
639
  if (this.snapshot.muted) {
663
- this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
664
640
  this.transcript.reset();
665
641
  this.patch({ recognizing: false });
666
642
  return;
667
643
  }
668
644
  if (this.snapshot.settings.recognitionFilterEnabled && !hasMinimumWords(final, this.snapshot.settings.recognitionMinimumWords)) {
669
- this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
670
645
  this.transcript.reset();
671
646
  this.patch({ recognizing: false });
672
647
  return;
@@ -677,7 +652,6 @@ var VoiceCoordinator = class {
677
652
  }
678
653
  if (interim) {
679
654
  this.cancelAutoSend();
680
- this.composer.setDraft(this.transcript.update(this.composer.getDraft(), interim));
681
655
  }
682
656
  if (!interim && this.snapshot.recognizing)
683
657
  this.assistantSpeechNotBefore = Date.now() + this.snapshot.settings.assistantSpeechDelaySeconds * 1e3;
@@ -745,7 +719,6 @@ var VoiceCoordinator = class {
745
719
  });
746
720
  }
747
721
  async cancelDictation() {
748
- this.composer.setDraft(this.transcript.update(this.composer.getDraft(), "", true));
749
722
  await this.stopListening();
750
723
  }
751
724
  async endConversation() {
@@ -1233,7 +1206,7 @@ var VoiceOwnership = class {
1233
1206
  };
1234
1207
 
1235
1208
  // src/modules/recognition/components/RecognitionCapabilityStatus.tsx
1236
- var import_react19 = __toESM(require("react"), 1);
1209
+ var import_react20 = __toESM(require("react"), 1);
1237
1210
 
1238
1211
  // src/app/client/i18n/catalogs/en.ts
1239
1212
  var en = {
@@ -1324,6 +1297,8 @@ var en = {
1324
1297
  "dsh-live-voice.recognition.planned.vote": "Coming Soon \u2014 vote on repo issues",
1325
1298
  "dsh-live-voice.recognition.planned.voxtral": "Voxtral Realtime \u2014 Soon",
1326
1299
  "dsh-live-voice.recognition.planned.webGpu": "Browser WebGPU Inference \u2014 Soon",
1300
+ "dsh-live-voice.recognition.presets.custom.description": "Choose your own silence duration.",
1301
+ "dsh-live-voice.recognition.presets.custom.label": "Custom",
1327
1302
  "dsh-live-voice.recognition.presets.long.description": "Wait through longer thinking pauses.",
1328
1303
  "dsh-live-voice.recognition.presets.long.label": "Long",
1329
1304
  "dsh-live-voice.recognition.presets.natural.description": "Allow normal pauses between phrases.",
@@ -1336,6 +1311,8 @@ var en = {
1336
1311
  "dsh-live-voice.recognition.qwen.endpointHelp": "HTTP API at the configured DSH host URL (default: http://127.0.0.1:8080/inference). Audio uses the authenticated DSH host transcription route.",
1337
1312
  "dsh-live-voice.recognition.qwen.hostHelp": "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.",
1338
1313
  "dsh-live-voice.recognition.qwen.label": "Qwen3 ASR \u2014 HTTP API",
1314
+ "dsh-live-voice.recognition.silenceDetection.customHelp": "Enter a whole number from 100 to 10,000 ms. Saved when you leave the field. Short pauses may split speech; recognition adds its own latency.",
1315
+ "dsh-live-voice.recognition.silenceDetection.customLabel": "Custom pause (milliseconds)",
1339
1316
  "dsh-live-voice.recognition.silenceDetection.duration": "Pause before sending: {milliseconds} ms",
1340
1317
  "dsh-live-voice.recognition.silenceDetection.help": "Controls how long a pause must last before captured speech is sent for recognition.",
1341
1318
  "dsh-live-voice.recognition.silenceDetection.label": "Silence detection",
@@ -1372,6 +1349,9 @@ var en = {
1372
1349
  "dsh-live-voice.settings.delivery.toggle": "Automatic delivery mode",
1373
1350
  "dsh-live-voice.settings.engine.refresh": "Refresh available engines",
1374
1351
  "dsh-live-voice.settings.filters.title": "Filtering",
1352
+ "dsh-live-voice.settings.general.title": "General",
1353
+ "dsh-live-voice.settings.persistence.loadError": "Could not load Live Voice settings from the server. Reload to try again.",
1354
+ "dsh-live-voice.settings.persistence.saveError": "Could not save Live Voice settings on the server. Please try again.",
1375
1355
  "dsh-live-voice.settings.tabs.conversation": "Conversation",
1376
1356
  "dsh-live-voice.settings.tabs.recognition": "Speech recognition",
1377
1357
  "dsh-live-voice.settings.tabs.speak": "Speech",
@@ -1540,6 +1520,8 @@ var es = {
1540
1520
  "dsh-live-voice.recognition.planned.vote": "Pr\xF3ximamente \u2014 vota en las incidencias del repositorio",
1541
1521
  "dsh-live-voice.recognition.planned.voxtral": "Voxtral Realtime \u2014 Pr\xF3ximamente",
1542
1522
  "dsh-live-voice.recognition.planned.webGpu": "Inferencia WebGPU en el navegador \u2014 Pr\xF3ximamente",
1523
+ "dsh-live-voice.recognition.presets.custom.description": "Elige la duraci\xF3n del silencio.",
1524
+ "dsh-live-voice.recognition.presets.custom.label": "Personalizada",
1543
1525
  "dsh-live-voice.recognition.presets.long.description": "Espera durante pausas de reflexi\xF3n m\xE1s largas.",
1544
1526
  "dsh-live-voice.recognition.presets.long.label": "Larga",
1545
1527
  "dsh-live-voice.recognition.presets.natural.description": "Permite pausas normales entre frases.",
@@ -1552,6 +1534,8 @@ var es = {
1552
1534
  "dsh-live-voice.recognition.qwen.endpointHelp": "API HTTP en la URL configurada en el host de DSH (predeterminada: http://127.0.0.1:8080/inference). El audio utiliza la ruta de transcripci\xF3n autenticada del host de DSH.",
1553
1535
  "dsh-live-voice.recognition.qwen.hostHelp": "Configuraci\xF3n para todo el host del servidor Qwen3 ASR + TTS. Introduce una URL base HTTP o HTTPS accesible desde el host de DSH. El navegador accede a ella mediante las rutas autenticadas de DSH.",
1554
1536
  "dsh-live-voice.recognition.qwen.label": "Qwen3 ASR \u2014 API HTTP",
1537
+ "dsh-live-voice.recognition.silenceDetection.customHelp": "Introduce un n\xFAmero entero de 100 a 10.000 ms. Se guarda al salir del campo. Las pausas cortas pueden dividir el habla; el reconocimiento a\xF1ade su propia latencia.",
1538
+ "dsh-live-voice.recognition.silenceDetection.customLabel": "Pausa personalizada (milisegundos)",
1555
1539
  "dsh-live-voice.recognition.silenceDetection.duration": "Pausa antes de enviar: {milliseconds} ms",
1556
1540
  "dsh-live-voice.recognition.silenceDetection.help": "Determina cu\xE1nto debe durar una pausa antes de enviar la voz capturada para su reconocimiento.",
1557
1541
  "dsh-live-voice.recognition.silenceDetection.label": "Detecci\xF3n de silencio",
@@ -1588,6 +1572,9 @@ var es = {
1588
1572
  "dsh-live-voice.settings.delivery.toggle": "Modo de env\xEDo autom\xE1tico",
1589
1573
  "dsh-live-voice.settings.engine.refresh": "Actualizar los motores disponibles",
1590
1574
  "dsh-live-voice.settings.filters.title": "Filtrado",
1575
+ "dsh-live-voice.settings.general.title": "General",
1576
+ "dsh-live-voice.settings.persistence.loadError": "No se pudieron cargar los ajustes de Live Voice del servidor. Recarga para volver a intentarlo.",
1577
+ "dsh-live-voice.settings.persistence.saveError": "No se pudieron guardar los ajustes de Live Voice en el servidor. Int\xE9ntalo de nuevo.",
1591
1578
  "dsh-live-voice.settings.tabs.conversation": "Conversaci\xF3n",
1592
1579
  "dsh-live-voice.settings.tabs.recognition": "Reconocimiento de voz",
1593
1580
  "dsh-live-voice.settings.tabs.speak": "S\xEDntesis de voz",
@@ -1756,6 +1743,8 @@ var fr = {
1756
1743
  "dsh-live-voice.recognition.planned.vote": "Bient\xF4t disponible \u2014 votez dans les tickets du d\xE9p\xF4t",
1757
1744
  "dsh-live-voice.recognition.planned.voxtral": "Voxtral Realtime \u2014 Bient\xF4t disponible",
1758
1745
  "dsh-live-voice.recognition.planned.webGpu": "Inf\xE9rence WebGPU dans le navigateur \u2014 Bient\xF4t disponible",
1746
+ "dsh-live-voice.recognition.presets.custom.description": "Choisissez la dur\xE9e du silence.",
1747
+ "dsh-live-voice.recognition.presets.custom.label": "Personnalis\xE9e",
1759
1748
  "dsh-live-voice.recognition.presets.long.description": "Attend pendant les pauses de r\xE9flexion plus longues.",
1760
1749
  "dsh-live-voice.recognition.presets.long.label": "Longue",
1761
1750
  "dsh-live-voice.recognition.presets.natural.description": "Autorise des pauses normales entre les phrases.",
@@ -1768,6 +1757,8 @@ var fr = {
1768
1757
  "dsh-live-voice.recognition.qwen.endpointHelp": "API HTTP \xE0 l\u2019URL configur\xE9e sur l\u2019h\xF4te DSH (par d\xE9faut : http://127.0.0.1:8080/inference). Le son passe par la route de transcription authentifi\xE9e de l\u2019h\xF4te DSH.",
1769
1758
  "dsh-live-voice.recognition.qwen.hostHelp": "Param\xE8tres communs \xE0 tout l\u2019h\xF4te pour le serveur Qwen3 ASR + TTS. Saisissez une URL de base HTTP ou HTTPS accessible depuis l\u2019h\xF4te DSH. Le navigateur y acc\xE8de via les routes authentifi\xE9es de DSH.",
1770
1759
  "dsh-live-voice.recognition.qwen.label": "Qwen3 ASR \u2014 API HTTP",
1760
+ "dsh-live-voice.recognition.silenceDetection.customHelp": "Saisissez un entier de 100 \xE0 10 000 ms. Enregistr\xE9 en quittant le champ. Les pauses courtes peuvent couper la parole ; la reconnaissance ajoute sa propre latence.",
1761
+ "dsh-live-voice.recognition.silenceDetection.customLabel": "Pause personnalis\xE9e (millisecondes)",
1771
1762
  "dsh-live-voice.recognition.silenceDetection.duration": "Pause avant l\u2019envoi : {milliseconds} ms",
1772
1763
  "dsh-live-voice.recognition.silenceDetection.help": "D\xE9termine la dur\xE9e de pause n\xE9cessaire avant que la parole captur\xE9e soit envoy\xE9e pour reconnaissance.",
1773
1764
  "dsh-live-voice.recognition.silenceDetection.label": "D\xE9tection du silence",
@@ -1804,6 +1795,9 @@ var fr = {
1804
1795
  "dsh-live-voice.settings.delivery.toggle": "Mode d\u2019envoi automatique",
1805
1796
  "dsh-live-voice.settings.engine.refresh": "Actualiser les moteurs disponibles",
1806
1797
  "dsh-live-voice.settings.filters.title": "Filtrage",
1798
+ "dsh-live-voice.settings.general.title": "G\xE9n\xE9ral",
1799
+ "dsh-live-voice.settings.persistence.loadError": "Impossible de charger les param\xE8tres Live Voice depuis le serveur. Rechargez pour r\xE9essayer.",
1800
+ "dsh-live-voice.settings.persistence.saveError": "Impossible de sauvegarder les param\xE8tres Live Voice sur le serveur. R\xE9essayez.",
1807
1801
  "dsh-live-voice.settings.tabs.conversation": "Conversation",
1808
1802
  "dsh-live-voice.settings.tabs.recognition": "Reconnaissance vocale",
1809
1803
  "dsh-live-voice.settings.tabs.speak": "Synth\xE8se vocale",
@@ -1972,6 +1966,8 @@ var hi = {
1972
1966
  "dsh-live-voice.recognition.planned.vote": "\u091C\u0932\u094D\u0926 \u0906 \u0930\u0939\u093E \u0939\u0948 \u2014 \u0930\u093F\u092A\u0949\u091C\u093C\u093F\u091F\u0930\u0940 \u0915\u0947 \u0907\u0936\u094D\u092F\u0942 \u092E\u0947\u0902 \u0935\u094B\u091F \u0926\u0947\u0902",
1973
1967
  "dsh-live-voice.recognition.planned.voxtral": "Voxtral Realtime \u2014 \u091C\u0932\u094D\u0926 \u0906 \u0930\u0939\u093E \u0939\u0948",
1974
1968
  "dsh-live-voice.recognition.planned.webGpu": "\u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u092E\u0947\u0902 WebGPU \u0907\u0928\u094D\u092B\u093C\u0930\u0947\u0902\u0938 \u2014 \u091C\u0932\u094D\u0926 \u0906 \u0930\u0939\u093E \u0939\u0948",
1969
+ "dsh-live-voice.recognition.presets.custom.description": "\u092E\u094C\u0928 \u0915\u0940 \u0905\u0935\u0927\u093F \u0938\u094D\u0935\u092F\u0902 \u091A\u0941\u0928\u0947\u0902\u0964",
1970
+ "dsh-live-voice.recognition.presets.custom.label": "\u0915\u0938\u094D\u091F\u092E",
1975
1971
  "dsh-live-voice.recognition.presets.long.description": "\u0932\u0902\u092C\u0947 \u0938\u094B\u091A-\u0935\u093F\u091A\u093E\u0930 \u0935\u093E\u0932\u0947 \u0935\u093F\u0930\u093E\u092E\u094B\u0902 \u0924\u0915 \u092A\u094D\u0930\u0924\u0940\u0915\u094D\u0937\u093E \u0915\u0930\u0947\u0902\u0964",
1976
1972
  "dsh-live-voice.recognition.presets.long.label": "\u0932\u0902\u092C\u093E",
1977
1973
  "dsh-live-voice.recognition.presets.natural.description": "\u0935\u093E\u0915\u094D\u092F\u093E\u0902\u0936\u094B\u0902 \u0915\u0947 \u092C\u0940\u091A \u0938\u093E\u092E\u093E\u0928\u094D\u092F \u0935\u093F\u0930\u093E\u092E \u0915\u0940 \u0905\u0928\u0941\u092E\u0924\u093F \u0926\u0947\u0902\u0964",
@@ -1984,6 +1980,8 @@ var hi = {
1984
1980
  "dsh-live-voice.recognition.qwen.endpointHelp": "HTTP API, \u0915\u0949\u0928\u094D\u092B\u093C\u093F\u0917\u0930 \u0915\u093F\u090F \u0917\u090F DSH \u0939\u094B\u0938\u094D\u091F URL \u092A\u0930 \u0939\u0948 (\u0921\u093F\u092B\u093C\u0949\u0932\u094D\u091F: http://127.0.0.1:8080/inference)\u0964 \u0911\u0921\u093F\u092F\u094B \u0915\u0947 \u0932\u093F\u090F \u092A\u094D\u0930\u092E\u093E\u0923\u0940\u0915\u0930\u0923 \u0935\u093E\u0932\u0947 DSH \u0939\u094B\u0938\u094D\u091F \u091F\u094D\u0930\u093E\u0902\u0938\u0915\u094D\u0930\u093F\u092A\u094D\u0936\u0928 \u0930\u0942\u091F \u0915\u093E \u0909\u092A\u092F\u094B\u0917 \u0939\u094B\u0924\u093E \u0939\u0948\u0964",
1985
1981
  "dsh-live-voice.recognition.qwen.hostHelp": "Qwen3 ASR + TTS \u0938\u0930\u094D\u0935\u0930 \u0915\u0940 \u092A\u0942\u0930\u0947 \u0939\u094B\u0938\u094D\u091F \u092A\u0930 \u0932\u093E\u0917\u0942 \u0939\u094B\u0928\u0947 \u0935\u093E\u0932\u0940 \u0938\u0947\u091F\u093F\u0902\u0917\u094D\u0938\u0964 \u0915\u094B\u0908 \u092D\u0940 HTTP \u092F\u093E HTTPS \u092C\u0947\u0938 URL \u0926\u0930\u094D\u091C \u0915\u0930\u0947\u0902 \u091C\u093F\u0938 \u0924\u0915 DSH \u0939\u094B\u0938\u094D\u091F \u092A\u0939\u0941\u0901\u091A \u0938\u0915\u0947\u0964 \u092C\u094D\u0930\u093E\u0909\u091C\u093C\u0930 \u092A\u094D\u0930\u092E\u093E\u0923\u0940\u0915\u0930\u0923 \u0935\u093E\u0932\u0947 DSH \u0930\u0942\u091F\u094B\u0902 \u0915\u0947 \u091C\u093C\u0930\u093F\u090F \u0907\u0938\u0947 \u090F\u0915\u094D\u0938\u0947\u0938 \u0915\u0930\u0924\u093E \u0939\u0948\u0964",
1986
1982
  "dsh-live-voice.recognition.qwen.label": "Qwen3 ASR \u2014 HTTP API",
1983
+ "dsh-live-voice.recognition.silenceDetection.customHelp": "100 \u0938\u0947 10,000 ms \u0924\u0915 \u092A\u0942\u0930\u094D\u0923\u093E\u0902\u0915 \u0926\u0930\u094D\u091C \u0915\u0930\u0947\u0902\u0964 \u092B\u093C\u0940\u0932\u094D\u0921 \u091B\u094B\u0921\u093C\u0928\u0947 \u092A\u0930 \u0938\u0939\u0947\u091C\u093E \u091C\u093E\u0924\u093E \u0939\u0948\u0964 \u091B\u094B\u091F\u0947 \u0935\u093F\u0930\u093E\u092E \u092C\u094B\u0932\u0928\u0947 \u0915\u094B \u0916\u0902\u0921\u094B\u0902 \u092E\u0947\u0902 \u092C\u093E\u0901\u091F \u0938\u0915\u0924\u0947 \u0939\u0948\u0902; \u092A\u0939\u091A\u093E\u0928 \u092E\u0947\u0902 \u0905\u0924\u093F\u0930\u093F\u0915\u094D\u0924 \u0938\u092E\u092F \u0932\u0917\u0924\u093E \u0939\u0948\u0964",
1984
+ "dsh-live-voice.recognition.silenceDetection.customLabel": "\u0915\u0938\u094D\u091F\u092E \u0935\u093F\u0930\u093E\u092E (\u092E\u093F\u0932\u0940\u0938\u0947\u0915\u0902\u0921)",
1987
1985
  "dsh-live-voice.recognition.silenceDetection.duration": "\u092D\u0947\u091C\u0928\u0947 \u0938\u0947 \u092A\u0939\u0932\u0947 \u0915\u093E \u0935\u093F\u0930\u093E\u092E: {milliseconds} \u092E\u093F\u0932\u0940\u0938\u0947\u0915\u0902\u0921",
1988
1986
  "dsh-live-voice.recognition.silenceDetection.help": "\u0924\u092F \u0915\u0930\u0924\u093E \u0939\u0948 \u0915\u093F \u0930\u093F\u0915\u0949\u0930\u094D\u0921 \u0915\u0940 \u0917\u0908 \u0906\u0935\u093E\u091C\u093C \u0915\u094B \u092A\u0939\u091A\u093E\u0928 \u0915\u0947 \u0932\u093F\u090F \u092D\u0947\u091C\u0928\u0947 \u0938\u0947 \u092A\u0939\u0932\u0947 \u0935\u093F\u0930\u093E\u092E \u0915\u093F\u0924\u0928\u0940 \u0926\u0947\u0930 \u0930\u0939\u0928\u093E \u091A\u093E\u0939\u093F\u090F\u0964",
1989
1987
  "dsh-live-voice.recognition.silenceDetection.label": "\u092E\u094C\u0928 \u0915\u093E \u092A\u0924\u093E \u0932\u0917\u093E\u0928\u093E",
@@ -2020,6 +2018,9 @@ var hi = {
2020
2018
  "dsh-live-voice.settings.delivery.toggle": "\u0938\u094D\u0935\u091A\u093E\u0932\u093F\u0924 \u092D\u0947\u091C\u0928\u0947 \u0915\u093E \u092E\u094B\u0921",
2021
2019
  "dsh-live-voice.settings.engine.refresh": "\u0909\u092A\u0932\u092C\u094D\u0927 \u0907\u0902\u091C\u0928 \u0930\u0940\u092B\u093C\u094D\u0930\u0947\u0936 \u0915\u0930\u0947\u0902",
2022
2020
  "dsh-live-voice.settings.filters.title": "\u092B\u093C\u093F\u0932\u094D\u091F\u0930\u093F\u0902\u0917",
2021
+ "dsh-live-voice.settings.general.title": "\u0938\u093E\u092E\u093E\u0928\u094D\u092F",
2022
+ "dsh-live-voice.settings.persistence.loadError": "\u0938\u0930\u094D\u0935\u0930 \u0938\u0947 Live Voice \u0938\u0947\u091F\u093F\u0902\u0917 \u0932\u094B\u0921 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940\u0902\u0964 \u092B\u093F\u0930 \u092A\u094D\u0930\u092F\u093E\u0938 \u0915\u0930\u0928\u0947 \u0915\u0947 \u0932\u093F\u090F \u092A\u0947\u091C \u0930\u0940\u0932\u094B\u0921 \u0915\u0930\u0947\u0902\u0964",
2023
+ "dsh-live-voice.settings.persistence.saveError": "\u0938\u0930\u094D\u0935\u0930 \u092A\u0930 Live Voice \u0938\u0947\u091F\u093F\u0902\u0917 \u0938\u0947\u0935 \u0928\u0939\u0940\u0902 \u0939\u094B \u0938\u0915\u0940\u0902\u0964 \u092B\u093F\u0930 \u092A\u094D\u0930\u092F\u093E\u0938 \u0915\u0930\u0947\u0902\u0964",
2023
2024
  "dsh-live-voice.settings.tabs.conversation": "\u092C\u093E\u0924\u091A\u0940\u0924",
2024
2025
  "dsh-live-voice.settings.tabs.recognition": "\u0935\u093E\u0915\u094D \u092A\u0939\u091A\u093E\u0928",
2025
2026
  "dsh-live-voice.settings.tabs.speak": "\u0935\u093E\u091A\u0928",
@@ -2188,6 +2189,8 @@ var ptBR = {
2188
2189
  "dsh-live-voice.recognition.planned.vote": "Em breve \u2014 vote nas issues do reposit\xF3rio",
2189
2190
  "dsh-live-voice.recognition.planned.voxtral": "Voxtral Realtime \u2014 Em breve",
2190
2191
  "dsh-live-voice.recognition.planned.webGpu": "Infer\xEAncia WebGPU no navegador \u2014 Em breve",
2192
+ "dsh-live-voice.recognition.presets.custom.description": "Escolha a dura\xE7\xE3o do sil\xEAncio.",
2193
+ "dsh-live-voice.recognition.presets.custom.label": "Personalizada",
2191
2194
  "dsh-live-voice.recognition.presets.long.description": "Aguarda durante pausas de pensamento mais longas.",
2192
2195
  "dsh-live-voice.recognition.presets.long.label": "Longa",
2193
2196
  "dsh-live-voice.recognition.presets.natural.description": "Permite pausas normais entre frases.",
@@ -2200,6 +2203,8 @@ var ptBR = {
2200
2203
  "dsh-live-voice.recognition.qwen.endpointHelp": "API HTTP na URL configurada do host DSH (padr\xE3o: http://127.0.0.1:8080/inference). O \xE1udio usa a rota de transcri\xE7\xE3o autenticada do host DSH.",
2201
2204
  "dsh-live-voice.recognition.qwen.hostHelp": "Configura\xE7\xF5es de todo o host para o servidor Qwen3 ASR + TTS. Insira qualquer URL base HTTP ou HTTPS acess\xEDvel a partir do host DSH. O navegador acessa atrav\xE9s de rotas autenticadas do DSH.",
2202
2205
  "dsh-live-voice.recognition.qwen.label": "Qwen3 ASR \u2014 API HTTP",
2206
+ "dsh-live-voice.recognition.silenceDetection.customHelp": "Insira um n\xFAmero inteiro de 100 a 10.000 ms. Salvo ao sair do campo. Pausas curtas podem dividir a fala; o reconhecimento acrescenta sua pr\xF3pria lat\xEAncia.",
2207
+ "dsh-live-voice.recognition.silenceDetection.customLabel": "Pausa personalizada (milissegundos)",
2203
2208
  "dsh-live-voice.recognition.silenceDetection.duration": "Pausa antes de enviar: {milliseconds} ms",
2204
2209
  "dsh-live-voice.recognition.silenceDetection.help": "Controla a dura\xE7\xE3o m\xEDnima da pausa antes de enviar a fala capturada para reconhecimento.",
2205
2210
  "dsh-live-voice.recognition.silenceDetection.label": "Detec\xE7\xE3o de sil\xEAncio",
@@ -2236,6 +2241,9 @@ var ptBR = {
2236
2241
  "dsh-live-voice.settings.delivery.toggle": "Modo de entrega autom\xE1tica",
2237
2242
  "dsh-live-voice.settings.engine.refresh": "Atualizar mecanismos dispon\xEDveis",
2238
2243
  "dsh-live-voice.settings.filters.title": "Filtragem",
2244
+ "dsh-live-voice.settings.general.title": "Gerais",
2245
+ "dsh-live-voice.settings.persistence.loadError": "N\xE3o foi poss\xEDvel carregar as configura\xE7\xF5es do Live Voice do servidor. Recarregue para tentar novamente.",
2246
+ "dsh-live-voice.settings.persistence.saveError": "N\xE3o foi poss\xEDvel salvar as configura\xE7\xF5es do Live Voice no servidor. Tente novamente.",
2239
2247
  "dsh-live-voice.settings.tabs.conversation": "Conversa",
2240
2248
  "dsh-live-voice.settings.tabs.recognition": "Reconhecimento de voz",
2241
2249
  "dsh-live-voice.settings.tabs.speak": "Fala",
@@ -2404,6 +2412,8 @@ var zh = {
2404
2412
  "dsh-live-voice.recognition.planned.vote": "\u5373\u5C06\u63A8\u51FA \u2014 \u8BF7\u5728\u4ED3\u5E93\u8BAE\u9898\u4E2D\u6295\u7968",
2405
2413
  "dsh-live-voice.recognition.planned.voxtral": "Voxtral Realtime \u2014 \u5373\u5C06\u63A8\u51FA",
2406
2414
  "dsh-live-voice.recognition.planned.webGpu": "\u6D4F\u89C8\u5668 WebGPU \u63A8\u7406 \u2014 \u5373\u5C06\u63A8\u51FA",
2415
+ "dsh-live-voice.recognition.presets.custom.description": "\u81EA\u884C\u9009\u62E9\u9759\u97F3\u65F6\u957F\u3002",
2416
+ "dsh-live-voice.recognition.presets.custom.label": "\u81EA\u5B9A\u4E49",
2407
2417
  "dsh-live-voice.recognition.presets.long.description": "\u7B49\u5F85\u8F83\u957F\u7684\u601D\u8003\u505C\u987F\u3002",
2408
2418
  "dsh-live-voice.recognition.presets.long.label": "\u957F",
2409
2419
  "dsh-live-voice.recognition.presets.natural.description": "\u5141\u8BB8\u77ED\u8BED\u4E4B\u95F4\u51FA\u73B0\u81EA\u7136\u505C\u987F\u3002",
@@ -2416,6 +2426,8 @@ var zh = {
2416
2426
  "dsh-live-voice.recognition.qwen.endpointHelp": "HTTP API \u4F4D\u4E8E\u914D\u7F6E\u7684 DSH \u4E3B\u673A URL\uFF08\u9ED8\u8BA4\uFF1Ahttp://127.0.0.1:8080/inference\uFF09\u3002\u97F3\u9891\u901A\u8FC7\u9700\u8981\u8EAB\u4EFD\u9A8C\u8BC1\u7684 DSH \u4E3B\u673A\u8F6C\u5199\u8DEF\u7531\u53D1\u9001\u3002",
2417
2427
  "dsh-live-voice.recognition.qwen.hostHelp": "Qwen3 ASR + TTS \u670D\u52A1\u5668\u7684\u4E3B\u673A\u7EA7\u8BBE\u7F6E\u3002\u8BF7\u8F93\u5165 DSH \u4E3B\u673A\u53EF\u8BBF\u95EE\u7684\u4EFB\u610F HTTP \u6216 HTTPS \u57FA\u7840 URL\u3002\u6D4F\u89C8\u5668\u901A\u8FC7\u9700\u8981\u8EAB\u4EFD\u9A8C\u8BC1\u7684 DSH \u8DEF\u7531\u8BBF\u95EE\u8BE5\u670D\u52A1\u5668\u3002",
2418
2428
  "dsh-live-voice.recognition.qwen.label": "Qwen3 ASR \u2014 HTTP API",
2429
+ "dsh-live-voice.recognition.silenceDetection.customHelp": "\u8BF7\u8F93\u5165 100 \u81F3 10,000 ms \u7684\u6574\u6570\u3002\u79BB\u5F00\u8F93\u5165\u6846\u65F6\u4FDD\u5B58\u3002\u77ED\u505C\u987F\u53EF\u80FD\u5207\u5206\u8BED\u97F3\uFF1B\u8BC6\u522B\u8FD8\u9700\u8981\u989D\u5916\u65F6\u95F4\u3002",
2430
+ "dsh-live-voice.recognition.silenceDetection.customLabel": "\u81EA\u5B9A\u4E49\u505C\u987F\uFF08\u6BEB\u79D2\uFF09",
2419
2431
  "dsh-live-voice.recognition.silenceDetection.duration": "\u53D1\u9001\u524D\u505C\u987F\uFF1A{milliseconds} \u6BEB\u79D2",
2420
2432
  "dsh-live-voice.recognition.silenceDetection.help": "\u8BBE\u7F6E\u505C\u987F\u6301\u7EED\u591A\u4E45\u540E\uFF0C\u5C06\u91C7\u96C6\u7684\u8BED\u97F3\u53D1\u9001\u8FDB\u884C\u8BC6\u522B\u3002",
2421
2433
  "dsh-live-voice.recognition.silenceDetection.label": "\u9759\u97F3\u68C0\u6D4B",
@@ -2452,6 +2464,9 @@ var zh = {
2452
2464
  "dsh-live-voice.settings.delivery.toggle": "\u81EA\u52A8\u53D1\u9001\u6A21\u5F0F",
2453
2465
  "dsh-live-voice.settings.engine.refresh": "\u5237\u65B0\u53EF\u7528\u5F15\u64CE",
2454
2466
  "dsh-live-voice.settings.filters.title": "\u8FC7\u6EE4",
2467
+ "dsh-live-voice.settings.general.title": "\u5E38\u89C4",
2468
+ "dsh-live-voice.settings.persistence.loadError": "\u65E0\u6CD5\u4ECE\u670D\u52A1\u5668\u52A0\u8F7D Live Voice \u8BBE\u7F6E\u3002\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
2469
+ "dsh-live-voice.settings.persistence.saveError": "\u65E0\u6CD5\u5728\u670D\u52A1\u5668\u4E0A\u4FDD\u5B58 Live Voice \u8BBE\u7F6E\u3002\u8BF7\u91CD\u8BD5\u3002",
2455
2470
  "dsh-live-voice.settings.tabs.conversation": "\u5BF9\u8BDD",
2456
2471
  "dsh-live-voice.settings.tabs.recognition": "\u8BED\u97F3\u8BC6\u522B",
2457
2472
  "dsh-live-voice.settings.tabs.speak": "\u8BED\u97F3\u5408\u6210",
@@ -2741,6 +2756,10 @@ var iconPaths = {
2741
2756
  skipNext: "M5 5l10 7-10 7V5M19 5v14",
2742
2757
  send: "M3 11.5L21 3l-8.5 18-2-7.5L3 11.5zm7.5 2L21 3",
2743
2758
  queue: "M5 6h14M5 12h10M5 18h6M18 15v6M15 18h6",
2759
+ settings: "M4 7h16M4 17h16M8 4v6M16 14v6",
2760
+ filter: "M3 5h18l-7 8v6l-4 2v-8L3 5z",
2761
+ link: "M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.7 1.7M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.7-1.7",
2762
+ chevron: "M6 9l6 6 6-6",
2744
2763
  close: "M6 6l12 12M18 6 6 18"
2745
2764
  };
2746
2765
 
@@ -2821,56 +2840,113 @@ function CheckboxField({
2821
2840
  }
2822
2841
 
2823
2842
  // src/shared/design-system/forms/NumberField.tsx
2843
+ var import_react12 = __toESM(require("react"), 1);
2844
+
2845
+ // src/shared/design-system/forms/DraftField.tsx
2824
2846
  var import_react11 = __toESM(require("react"), 1);
2825
- function NumberField({ label, ...props }) {
2826
- return /* @__PURE__ */ import_react11.default.createElement("label", null, label, /* @__PURE__ */ import_react11.default.createElement("input", { ...props, type: "number" }));
2847
+ function DraftField({
2848
+ label,
2849
+ value,
2850
+ defaultValue,
2851
+ onCommit,
2852
+ multiline,
2853
+ onInput,
2854
+ onChange,
2855
+ onFocus,
2856
+ onBlur,
2857
+ ...props
2858
+ }) {
2859
+ const saved = String(value ?? defaultValue ?? "");
2860
+ const [draft, setDraft] = import_react11.default.useState(saved);
2861
+ const editing = import_react11.default.useRef(false);
2862
+ const dirty = import_react11.default.useRef(false);
2863
+ import_react11.default.useEffect(() => {
2864
+ if (!editing.current) setDraft(saved);
2865
+ }, [saved]);
2866
+ const inputProps = {
2867
+ ...props,
2868
+ value: draft,
2869
+ onFocus: (event) => {
2870
+ editing.current = true;
2871
+ onFocus?.(event);
2872
+ },
2873
+ onInput: (event) => {
2874
+ editing.current = true;
2875
+ dirty.current = true;
2876
+ setDraft(event.currentTarget.value);
2877
+ onInput?.(event);
2878
+ },
2879
+ onChange: (event) => {
2880
+ editing.current = true;
2881
+ dirty.current = true;
2882
+ setDraft(event.currentTarget.value);
2883
+ onChange?.(event);
2884
+ },
2885
+ onBlur: (event) => {
2886
+ editing.current = false;
2887
+ const raw = event.currentTarget.value;
2888
+ const changed = dirty.current;
2889
+ dirty.current = false;
2890
+ setDraft(saved);
2891
+ const valid = multiline || props.type !== "number" || raw.trim() !== "" && event.currentTarget.validity.valid;
2892
+ if (changed && valid) onCommit?.(raw);
2893
+ onBlur?.(event);
2894
+ }
2895
+ };
2896
+ return /* @__PURE__ */ import_react11.default.createElement("label", null, label, multiline ? /* @__PURE__ */ import_react11.default.createElement("textarea", { ...inputProps }) : /* @__PURE__ */ import_react11.default.createElement("input", { ...inputProps }));
2897
+ }
2898
+
2899
+ // src/shared/design-system/forms/NumberField.tsx
2900
+ function NumberField(props) {
2901
+ return /* @__PURE__ */ import_react12.default.createElement(DraftField, { ...props, type: "number" });
2827
2902
  }
2828
2903
 
2829
2904
  // src/shared/design-system/forms/SelectField.tsx
2830
- var import_react12 = __toESM(require("react"), 1);
2905
+ var import_react13 = __toESM(require("react"), 1);
2831
2906
  function SelectField({ label, options, ...props }) {
2832
- return /* @__PURE__ */ import_react12.default.createElement("label", null, label, /* @__PURE__ */ import_react12.default.createElement("select", { ...props }, options.map((option) => /* @__PURE__ */ import_react12.default.createElement("option", { key: option.value, value: option.value, disabled: option.disabled }, option.label))));
2907
+ return /* @__PURE__ */ import_react13.default.createElement("label", null, label, /* @__PURE__ */ import_react13.default.createElement("select", { ...props }, options.map((option) => /* @__PURE__ */ import_react13.default.createElement("option", { key: option.value, value: option.value, disabled: option.disabled }, option.label))));
2833
2908
  }
2834
2909
 
2835
2910
  // src/shared/design-system/forms/TextAreaField.tsx
2836
- var import_react13 = __toESM(require("react"), 1);
2837
- function TextAreaField({ label, ...props }) {
2838
- return /* @__PURE__ */ import_react13.default.createElement("label", null, label, /* @__PURE__ */ import_react13.default.createElement("textarea", { ...props }));
2911
+ var import_react14 = __toESM(require("react"), 1);
2912
+ function TextAreaField(props) {
2913
+ return /* @__PURE__ */ import_react14.default.createElement(DraftField, { ...props, multiline: true });
2839
2914
  }
2840
2915
 
2841
2916
  // src/shared/design-system/forms/TextField.tsx
2842
- var import_react14 = __toESM(require("react"), 1);
2843
- function TextField({ label, ...props }) {
2844
- return /* @__PURE__ */ import_react14.default.createElement("label", null, label, /* @__PURE__ */ import_react14.default.createElement("input", { ...props, type: "text" }));
2917
+ var import_react15 = __toESM(require("react"), 1);
2918
+ function TextField(props) {
2919
+ return /* @__PURE__ */ import_react15.default.createElement(DraftField, { ...props, type: "text" });
2845
2920
  }
2846
2921
 
2847
2922
  // src/shared/design-system/layout/SettingsCard.tsx
2848
- var import_react15 = __toESM(require("react"), 1);
2923
+ var import_react16 = __toESM(require("react"), 1);
2849
2924
  function SettingsCard({
2850
2925
  children,
2851
2926
  className = ""
2852
2927
  }) {
2853
- return /* @__PURE__ */ import_react15.default.createElement("div", { className: ["dlv-settings-card", className].filter(Boolean).join(" ") }, children);
2928
+ return /* @__PURE__ */ import_react16.default.createElement("div", { className: ["dlv-settings-card", className].filter(Boolean).join(" ") }, children);
2854
2929
  }
2855
2930
 
2856
2931
  // src/shared/design-system/layout/SettingsSection.tsx
2857
- var import_react16 = __toESM(require("react"), 1);
2932
+ var import_react17 = __toESM(require("react"), 1);
2858
2933
 
2859
2934
  // src/shared/design-system/layout/SettingsSubcard.tsx
2860
- var import_react17 = __toESM(require("react"), 1);
2935
+ var import_react18 = __toESM(require("react"), 1);
2861
2936
  function SettingsSubcard({
2862
2937
  title,
2863
2938
  children,
2864
2939
  open = false,
2865
- ariaLabel
2940
+ ariaLabel,
2941
+ icon = "settings"
2866
2942
  }) {
2867
- return /* @__PURE__ */ import_react17.default.createElement("details", { className: "dlv-settings-subcard", open, "aria-label": ariaLabel }, /* @__PURE__ */ import_react17.default.createElement("summary", null, title), /* @__PURE__ */ import_react17.default.createElement("div", { className: "dlv-settings-subcard-body" }, children));
2943
+ return /* @__PURE__ */ import_react18.default.createElement("details", { className: "dlv-settings-subcard", open }, /* @__PURE__ */ import_react18.default.createElement("summary", { "aria-label": ariaLabel }, /* @__PURE__ */ import_react18.default.createElement(Icon, { name: icon, className: "dlv-settings-subcard-icon" }), /* @__PURE__ */ import_react18.default.createElement("span", null, title), /* @__PURE__ */ import_react18.default.createElement(Icon, { name: "chevron", className: "dlv-settings-subcard-chevron" })), /* @__PURE__ */ import_react18.default.createElement("div", { className: "dlv-settings-subcard-body" }, children));
2868
2944
  }
2869
2945
 
2870
2946
  // src/shared/design-system/layout/SettingsTabs.tsx
2871
- var import_react18 = __toESM(require("react"), 1);
2947
+ var import_react19 = __toESM(require("react"), 1);
2872
2948
  function SettingsTabs({ label, tabs, active, idPrefix, onChange }) {
2873
- const refs = import_react18.default.useRef([]);
2949
+ const refs = import_react19.default.useRef([]);
2874
2950
  const activate = (index) => {
2875
2951
  const tab = tabs[index];
2876
2952
  if (!tab) return;
@@ -2887,7 +2963,7 @@ function SettingsTabs({ label, tabs, active, idPrefix, onChange }) {
2887
2963
  event.preventDefault();
2888
2964
  activate(next);
2889
2965
  };
2890
- return /* @__PURE__ */ import_react18.default.createElement("div", { className: "dlv-settings-tabs", role: "tablist", "aria-label": label }, tabs.map((tab, index) => /* @__PURE__ */ import_react18.default.createElement(
2966
+ return /* @__PURE__ */ import_react19.default.createElement("div", { className: "dlv-settings-tabs", role: "tablist", "aria-label": label }, tabs.map((tab, index) => /* @__PURE__ */ import_react19.default.createElement(
2891
2967
  "button",
2892
2968
  {
2893
2969
  key: tab.id,
@@ -2905,7 +2981,8 @@ function SettingsTabs({ label, tabs, active, idPrefix, onChange }) {
2905
2981
  onClick: () => onChange(tab.id),
2906
2982
  onKeyDown: (event) => onKeyDown(event, index)
2907
2983
  },
2908
- tab.label
2984
+ tab.icon ? /* @__PURE__ */ import_react19.default.createElement(Icon, { name: tab.icon, className: "dlv-settings-tab-icon" }) : null,
2985
+ /* @__PURE__ */ import_react19.default.createElement("span", null, tab.label)
2909
2986
  )));
2910
2987
  }
2911
2988
 
@@ -3265,7 +3342,8 @@ var WhisperHttpRecognitionEngine = class {
3265
3342
  constructor({
3266
3343
  globals = globalThis,
3267
3344
  meter,
3268
- voiceDetectionPreset = "natural",
3345
+ voiceDetectionPreset = defaultSettings.voiceDetectionPreset,
3346
+ voiceDetectionCustomSilenceMs = defaultSettings.voiceDetectionCustomSilenceMs,
3269
3347
  maxUtteranceSeconds = 60
3270
3348
  } = {}) {
3271
3349
  this.g = globals;
@@ -3273,11 +3351,12 @@ var WhisperHttpRecognitionEngine = class {
3273
3351
  this.session = null;
3274
3352
  this.lang = "pt-BR";
3275
3353
  this.voiceDetectionPreset = voiceDetectionPreset;
3354
+ this.voiceDetectionCustomSilenceMs = voiceDetectionCustomSilenceMs;
3276
3355
  this.maxUtteranceSeconds = maxUtteranceSeconds;
3277
3356
  this.route = ROUTE;
3278
3357
  }
3279
3358
  get segmentation() {
3280
- return voiceDetectionPresets[this.voiceDetectionPreset] || voiceDetectionPresets.natural;
3359
+ return { silenceMs: voiceDetectionSilenceMs(this) };
3281
3360
  }
3282
3361
  async capability() {
3283
3362
  try {
@@ -3775,16 +3854,16 @@ var QwenHttpSpeakingEngine = class extends HostAudioSpeakingEngine {
3775
3854
  };
3776
3855
 
3777
3856
  // src/modules/settings/components/createLiveVoiceSettings.tsx
3778
- var import_react45 = __toESM(require("react"), 1);
3857
+ var import_react46 = __toESM(require("react"), 1);
3779
3858
 
3780
3859
  // src/modules/settings/components/LiveVoiceSettings.tsx
3781
- var import_react44 = __toESM(require("react"), 1);
3860
+ var import_react45 = __toESM(require("react"), 1);
3782
3861
 
3783
3862
  // src/modules/settings/hooks/useAudioDevices.ts
3784
- var import_react20 = __toESM(require("react"), 1);
3863
+ var import_react21 = __toESM(require("react"), 1);
3785
3864
  function useAudioDevices() {
3786
- const [audioDevices, setAudioDevices] = import_react20.default.useState([]);
3787
- import_react20.default.useEffect(() => {
3865
+ const [audioDevices, setAudioDevices] = import_react21.default.useState([]);
3866
+ import_react21.default.useEffect(() => {
3788
3867
  let active = true;
3789
3868
  const refresh = async () => {
3790
3869
  try {
@@ -3814,17 +3893,17 @@ function createDeviceOptions(devices, kind, systemDefault, fallback) {
3814
3893
  }
3815
3894
 
3816
3895
  // src/modules/conversation/hooks/useConversationActions.ts
3817
- var import_react21 = __toESM(require("react"), 1);
3896
+ var import_react22 = __toESM(require("react"), 1);
3818
3897
  function useConversationActions(controller) {
3819
- const [error, setError] = import_react21.default.useState("");
3820
- const alive = import_react21.default.useRef(true);
3821
- import_react21.default.useEffect(
3898
+ const [error, setError] = import_react22.default.useState("");
3899
+ const alive = import_react22.default.useRef(true);
3900
+ import_react22.default.useEffect(
3822
3901
  () => () => {
3823
3902
  alive.current = false;
3824
3903
  },
3825
3904
  []
3826
3905
  );
3827
- const invoke = import_react21.default.useCallback(
3906
+ const invoke = import_react22.default.useCallback(
3828
3907
  (name, ...args) => {
3829
3908
  setError("");
3830
3909
  try {
@@ -3837,27 +3916,27 @@ function useConversationActions(controller) {
3837
3916
  },
3838
3917
  [controller]
3839
3918
  );
3840
- const clearError = import_react21.default.useCallback(() => setError(""), []);
3919
+ const clearError = import_react22.default.useCallback(() => setError(""), []);
3841
3920
  return { invoke, error, clearError };
3842
3921
  }
3843
3922
 
3844
3923
  // src/modules/conversation/hooks/useConversationController.ts
3845
- var import_react22 = __toESM(require("react"), 1);
3924
+ var import_react23 = __toESM(require("react"), 1);
3846
3925
  function useConversationController(controller) {
3847
- const subscribe = import_react22.default.useCallback(
3926
+ const subscribe = import_react23.default.useCallback(
3848
3927
  (listener) => controller.subscribe(listener),
3849
3928
  [controller]
3850
3929
  );
3851
- const read = import_react22.default.useCallback(() => controller.getSnapshot(), [controller]);
3852
- return import_react22.default.useSyncExternalStore(subscribe, read, read);
3930
+ const read = import_react23.default.useCallback(() => controller.getSnapshot(), [controller]);
3931
+ return import_react23.default.useSyncExternalStore(subscribe, read, read);
3853
3932
  }
3854
3933
 
3855
3934
  // src/modules/settings/hooks/useReleaseStatus.ts
3856
- var import_react23 = __toESM(require("react"), 1);
3935
+ var import_react24 = __toESM(require("react"), 1);
3857
3936
 
3858
3937
  // src/modules/settings/services/releases.ts
3859
- var CURRENT_VERSION = "0.3.0";
3860
- var TESTED_DSH_VERSION = "0.1.6-alpha.2";
3938
+ var CURRENT_VERSION = "0.3.2";
3939
+ var TESTED_DSH_VERSION = "0.2.0-rc.2";
3861
3940
  var REPOSITORY_URL = "https://github.com/victorwads/dsh-live-voice";
3862
3941
  var RELEASES_URL = `${REPOSITORY_URL}/releases`;
3863
3942
  var LATEST_RELEASE_API_URL = "https://api.github.com/repos/victorwads/dsh-live-voice/releases/latest";
@@ -3865,7 +3944,7 @@ var RELEASE_CHECK_STORAGE_KEY = "dsh-live-voice.latest-release-check";
3865
3944
  var RELEASE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3866
3945
  var LIVE_VOICE_BADGE_URL = "https://cdn.simpleicons.org/npm/white";
3867
3946
  var DSH_BADGE_URL = "https://cdn.simpleicons.org/deepseek/white";
3868
- var TESTED_DSH_RELEASE_URL = `https://github.com/deepseek-ai/deepseek-harness/releases/tag/v${TESTED_DSH_VERSION}`;
3947
+ var TESTED_DSH_RELEASE_URL = `https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v${TESTED_DSH_VERSION}`;
3869
3948
  function versionParts(version) {
3870
3949
  const normalized = String(version || "").trim().replace(/^v/i, "").split("+", 1)[0];
3871
3950
  const [core = "", prerelease = ""] = normalized.split("-", 2);
@@ -3917,9 +3996,16 @@ function writeCached(storage, value) {
3917
3996
  } catch {
3918
3997
  }
3919
3998
  }
3999
+ var releaseMemory = /* @__PURE__ */ new Map();
4000
+ var memoryStorage = {
4001
+ getItem: (key) => releaseMemory.get(key) ?? null,
4002
+ setItem: (key, value) => {
4003
+ releaseMemory.set(key, value);
4004
+ }
4005
+ };
3920
4006
  async function checkLatestRelease({
3921
4007
  fetchImpl = globalThis.window?.fetch?.bind(globalThis.window),
3922
- storage = globalThis.window?.localStorage,
4008
+ storage = memoryStorage,
3923
4009
  now = Date.now()
3924
4010
  } = {}) {
3925
4011
  const cached = readCached(storage, now);
@@ -3951,10 +4037,10 @@ function hasNewerRelease(release, currentVersion = CURRENT_VERSION) {
3951
4037
 
3952
4038
  // src/modules/settings/hooks/useReleaseStatus.ts
3953
4039
  function useReleaseStatus() {
3954
- const [latestRelease, setLatestRelease] = import_react23.default.useState(
4040
+ const [latestRelease, setLatestRelease] = import_react24.default.useState(
3955
4041
  null
3956
4042
  );
3957
- import_react23.default.useEffect(() => {
4043
+ import_react24.default.useEffect(() => {
3958
4044
  let active = true;
3959
4045
  void checkLatestRelease().then((result) => {
3960
4046
  if (active) setLatestRelease(result.release);
@@ -3967,10 +4053,10 @@ function useReleaseStatus() {
3967
4053
  }
3968
4054
 
3969
4055
  // src/modules/settings/sections/conversation/ConversationSettingsSection.tsx
3970
- var import_react29 = __toESM(require("react"), 1);
4056
+ var import_react30 = __toESM(require("react"), 1);
3971
4057
 
3972
4058
  // src/modules/settings/sections/conversation/DeliverySettings.tsx
3973
- var import_react24 = __toESM(require("react"), 1);
4059
+ var import_react25 = __toESM(require("react"), 1);
3974
4060
  function DeliverySettings({
3975
4061
  settings,
3976
4062
  updateSettings
@@ -3978,7 +4064,7 @@ function DeliverySettings({
3978
4064
  const { scoped } = useLanguage((ctx) => ctx.settings);
3979
4065
  const mode = settings.sendingMode || "manual";
3980
4066
  const seconds = [2, 3, 4, 5, 6, 8, 10];
3981
- return /* @__PURE__ */ import_react24.default.createElement(import_react24.default.Fragment, null, /* @__PURE__ */ import_react24.default.createElement(
4067
+ return /* @__PURE__ */ import_react25.default.createElement(import_react25.default.Fragment, null, /* @__PURE__ */ import_react25.default.createElement(
3982
4068
  SelectField,
3983
4069
  {
3984
4070
  label: scoped.delivery.label(),
@@ -3990,7 +4076,7 @@ function DeliverySettings({
3990
4076
  ],
3991
4077
  onChange: (event) => updateSettings({ sendingMode: event.target.value })
3992
4078
  }
3993
- ), mode !== "manual" ? /* @__PURE__ */ import_react24.default.createElement(
4079
+ ), mode !== "manual" ? /* @__PURE__ */ import_react25.default.createElement(
3994
4080
  SelectField,
3995
4081
  {
3996
4082
  label: scoped.autoSend.delay(),
@@ -3998,17 +4084,17 @@ function DeliverySettings({
3998
4084
  options: seconds.map((value) => ({ value: String(value), label: String(value) })),
3999
4085
  onChange: (event) => updateSettings({ autoSendDelaySeconds: Number(event.target.value) })
4000
4086
  }
4001
- ) : /* @__PURE__ */ import_react24.default.createElement("p", { className: "dlv-setting-description" }, scoped.delivery.manualLabel()));
4087
+ ) : /* @__PURE__ */ import_react25.default.createElement("p", { className: "dlv-setting-description" }, scoped.delivery.manualLabel()));
4002
4088
  }
4003
4089
 
4004
4090
  // src/modules/settings/sections/conversation/HoldToTalkSettings.tsx
4005
- var import_react25 = __toESM(require("react"), 1);
4091
+ var import_react26 = __toESM(require("react"), 1);
4006
4092
  function HoldToTalkSettings({
4007
4093
  settings,
4008
4094
  updateSettings
4009
4095
  }) {
4010
4096
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4011
- return /* @__PURE__ */ import_react25.default.createElement(
4097
+ return /* @__PURE__ */ import_react26.default.createElement(
4012
4098
  CheckboxField,
4013
4099
  {
4014
4100
  label: recognition.holdToTalk.enabled(),
@@ -4020,14 +4106,14 @@ function HoldToTalkSettings({
4020
4106
  }
4021
4107
 
4022
4108
  // src/modules/settings/sections/conversation/VoiceModeSettings.tsx
4023
- var import_react26 = __toESM(require("react"), 1);
4109
+ var import_react27 = __toESM(require("react"), 1);
4024
4110
  function VoiceModeSettings({
4025
4111
  settings,
4026
4112
  updateSettings
4027
4113
  }) {
4028
4114
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4029
4115
  const mode = settings.mode || "speaker";
4030
- return /* @__PURE__ */ import_react26.default.createElement(import_react26.default.Fragment, null, /* @__PURE__ */ import_react26.default.createElement(
4116
+ return /* @__PURE__ */ import_react27.default.createElement(import_react27.default.Fragment, null, /* @__PURE__ */ import_react27.default.createElement(
4031
4117
  SelectField,
4032
4118
  {
4033
4119
  label: recognition.mode.label(),
@@ -4038,16 +4124,16 @@ function VoiceModeSettings({
4038
4124
  ],
4039
4125
  onChange: (event) => updateSettings({ mode: event.target.value })
4040
4126
  }
4041
- ), /* @__PURE__ */ import_react26.default.createElement("p", { className: "dlv-setting-description" }, mode === "headphones" ? recognition.headphoneMode.help() : recognition.speakerMode.help()));
4127
+ ), /* @__PURE__ */ import_react27.default.createElement("p", { className: "dlv-setting-description" }, mode === "headphones" ? recognition.headphoneMode.help() : recognition.speakerMode.help()));
4042
4128
  }
4043
4129
 
4044
4130
  // src/modules/settings/sections/conversation/ConversationDelaySettings.tsx
4045
- var import_react27 = __toESM(require("react"), 1);
4131
+ var import_react28 = __toESM(require("react"), 1);
4046
4132
  var DELAYS = [1, 2, 3, 4, 5, 6, 8, 10];
4047
4133
  function ConversationDelaySettings({ settings, updateSettings }) {
4048
4134
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
4049
4135
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
4050
- return /* @__PURE__ */ import_react27.default.createElement(import_react27.default.Fragment, null, /* @__PURE__ */ import_react27.default.createElement(
4136
+ return /* @__PURE__ */ import_react28.default.createElement(import_react28.default.Fragment, null, /* @__PURE__ */ import_react28.default.createElement(
4051
4137
  SelectField,
4052
4138
  {
4053
4139
  label: speak.responseDelay.label(),
@@ -4058,40 +4144,41 @@ function ConversationDelaySettings({ settings, updateSettings }) {
4058
4144
  })),
4059
4145
  onChange: (event) => updateSettings({ assistantSpeechDelaySeconds: Number(event.target.value) })
4060
4146
  }
4061
- ), /* @__PURE__ */ import_react27.default.createElement("p", { className: "dlv-setting-description" }, speak.responseDelay.help()));
4147
+ ), /* @__PURE__ */ import_react28.default.createElement("p", { className: "dlv-setting-description" }, speak.responseDelay.help()));
4062
4148
  }
4063
4149
 
4064
4150
  // src/modules/settings/sections/speak/PlaybackPolicySettings.tsx
4065
- var import_react28 = __toESM(require("react"), 1);
4151
+ var import_react29 = __toESM(require("react"), 1);
4066
4152
  function PlaybackPolicySettings({ settings, updateSettings }) {
4067
4153
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
4068
- return /* @__PURE__ */ import_react28.default.createElement(import_react28.default.Fragment, null, /* @__PURE__ */ import_react28.default.createElement(
4154
+ return /* @__PURE__ */ import_react29.default.createElement(import_react29.default.Fragment, null, /* @__PURE__ */ import_react29.default.createElement(
4069
4155
  CheckboxField,
4070
4156
  {
4071
4157
  label: speak.autoPlayback.label(),
4072
4158
  checked: settings.announceAssistantMessages !== false,
4073
4159
  onChange: (event) => updateSettings({ announceAssistantMessages: event.target.checked })
4074
4160
  }
4075
- ), /* @__PURE__ */ import_react28.default.createElement("p", null, speak.autoPlayback.help()), /* @__PURE__ */ import_react28.default.createElement(
4161
+ ), /* @__PURE__ */ import_react29.default.createElement("p", null, speak.autoPlayback.help()), /* @__PURE__ */ import_react29.default.createElement(
4076
4162
  CheckboxField,
4077
4163
  {
4078
4164
  label: speak.interruption.enabled(),
4079
4165
  checked: Boolean(settings.interruptSpeechOnUserMessage),
4080
4166
  onChange: (event) => updateSettings({ interruptSpeechOnUserMessage: event.target.checked })
4081
4167
  }
4082
- ), /* @__PURE__ */ import_react28.default.createElement("p", null, settings.interruptSpeechOnUserMessage ? speak.interruption.enabledHelp() : speak.interruption.disabledHelp()));
4168
+ ), /* @__PURE__ */ import_react29.default.createElement("p", null, settings.interruptSpeechOnUserMessage ? speak.interruption.enabledHelp() : speak.interruption.disabledHelp()));
4083
4169
  }
4084
4170
 
4085
4171
  // src/modules/settings/sections/conversation/ConversationSettingsSection.tsx
4086
4172
  function ConversationSettingsSection(props) {
4087
- return /* @__PURE__ */ import_react29.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react29.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react29.default.createElement(DeliverySettings, { ...props }), /* @__PURE__ */ import_react29.default.createElement(HoldToTalkSettings, { ...props }), /* @__PURE__ */ import_react29.default.createElement(VoiceModeSettings, { ...props }), /* @__PURE__ */ import_react29.default.createElement(PlaybackPolicySettings, { ...props }), /* @__PURE__ */ import_react29.default.createElement(ConversationDelaySettings, { ...props })));
4173
+ const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
4174
+ return /* @__PURE__ */ import_react30.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react30.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react30.default.createElement(SettingsSubcard, { title: settingsLanguage.general.title(), icon: "send" }, /* @__PURE__ */ import_react30.default.createElement("div", { className: "dlv-conversation-fields" }, /* @__PURE__ */ import_react30.default.createElement(DeliverySettings, { ...props }), /* @__PURE__ */ import_react30.default.createElement(HoldToTalkSettings, { ...props }), /* @__PURE__ */ import_react30.default.createElement(VoiceModeSettings, { ...props }), /* @__PURE__ */ import_react30.default.createElement(PlaybackPolicySettings, { ...props }), /* @__PURE__ */ import_react30.default.createElement(ConversationDelaySettings, { ...props })))));
4088
4175
  }
4089
4176
 
4090
4177
  // src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx
4091
- var import_react32 = __toESM(require("react"), 1);
4178
+ var import_react33 = __toESM(require("react"), 1);
4092
4179
 
4093
4180
  // src/modules/core/qwen/QwenSettings.tsx
4094
- var import_react30 = __toESM(require("react"), 1);
4181
+ var import_react31 = __toESM(require("react"), 1);
4095
4182
  var BASE = "/api/dsh-live-voice/qwen";
4096
4183
  var UNLOADED = "dsh-live-voice.speak.qwen.restartRequired";
4097
4184
  async function qwenSettingsRequest(path, { method = "GET", config, signal } = {}, fetchImpl = globalThis.fetch) {
@@ -4120,15 +4207,15 @@ function QwenSettings({ controller }) {
4120
4207
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
4121
4208
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4122
4209
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
4123
- const [draft, setDraft] = import_react30.default.useState({
4210
+ const [draft, setDraft] = import_react31.default.useState({
4124
4211
  baseUrl: "http://127.0.0.1:8080/",
4125
4212
  timeoutMs: 3e5
4126
4213
  });
4127
- const [busy, setBusy] = import_react30.default.useState(true);
4128
- const [loaded, setLoaded] = import_react30.default.useState(false);
4129
- const [error, setError] = import_react30.default.useState("");
4130
- const [message2, setMessage] = import_react30.default.useState("");
4131
- const active = import_react30.default.useRef(null);
4214
+ const [busy, setBusy] = import_react31.default.useState(true);
4215
+ const [loaded, setLoaded] = import_react31.default.useState(false);
4216
+ const [error, setError] = import_react31.default.useState("");
4217
+ const [message2, setMessage] = import_react31.default.useState("");
4218
+ const active = import_react31.default.useRef(null);
4132
4219
  const translateStatus = (value) => ({
4133
4220
  "dsh-live-voice.commons.connection.unsaved": commons.connection.unsaved(),
4134
4221
  "dsh-live-voice.speak.qwen.saved": speak.qwen.saved(),
@@ -4138,7 +4225,7 @@ function QwenSettings({ controller }) {
4138
4225
  "dsh-live-voice.speak.qwen.requestFailed": speak.qwen.requestFailed(),
4139
4226
  "dsh-live-voice.speak.qwen.healthFailed": speak.qwen.healthFailed()
4140
4227
  })[value] ?? value;
4141
- async function run(action) {
4228
+ async function run(action, config = draft) {
4142
4229
  active.current?.abort();
4143
4230
  const abort = new AbortController();
4144
4231
  active.current = abort;
@@ -4156,7 +4243,7 @@ function QwenSettings({ controller }) {
4156
4243
  await controller.endConversation?.();
4157
4244
  const value = await qwenSettingsRequest("/config", {
4158
4245
  method: "PUT",
4159
- config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
4246
+ config: { ...config, timeoutMs: Number(config.timeoutMs) },
4160
4247
  signal: abort.signal
4161
4248
  });
4162
4249
  if (!abort.signal.aborted) {
@@ -4167,7 +4254,7 @@ function QwenSettings({ controller }) {
4167
4254
  } else {
4168
4255
  const value = await qwenSettingsRequest("/test", {
4169
4256
  method: "POST",
4170
- config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
4257
+ config: { ...config, timeoutMs: Number(config.timeoutMs) },
4171
4258
  signal: abort.signal
4172
4259
  });
4173
4260
  if (!abort.signal.aborted) {
@@ -4182,30 +4269,33 @@ function QwenSettings({ controller }) {
4182
4269
  if (!abort.signal.aborted) setBusy(false);
4183
4270
  }
4184
4271
  }
4185
- import_react30.default.useEffect(() => {
4272
+ import_react31.default.useEffect(() => {
4186
4273
  void run("load");
4187
4274
  return () => active.current?.abort();
4188
4275
  }, []);
4189
- const field = (label, key, type = "text") => /* @__PURE__ */ import_react30.default.createElement("label", null, label, /* @__PURE__ */ import_react30.default.createElement(
4190
- "input",
4191
- {
4192
- type,
4193
- value: draft[key],
4194
- disabled: busy || !loaded,
4195
- autoComplete: "off",
4196
- ...type === "number" ? { min: 1e3, max: 6e5, step: 1 } : {},
4197
- onChange: (event) => {
4198
- setDraft({ ...draft, [key]: event.target.value });
4199
- setMessage("dsh-live-voice.commons.connection.unsaved");
4200
- setError("");
4276
+ const field = (label, key, type = "text") => {
4277
+ const Field = type === "number" ? NumberField : TextField;
4278
+ return /* @__PURE__ */ import_react31.default.createElement(
4279
+ Field,
4280
+ {
4281
+ label,
4282
+ value: draft[key],
4283
+ disabled: busy || !loaded,
4284
+ autoComplete: "off",
4285
+ ...type === "number" ? { min: 1e3, max: 6e5, step: 1 } : {},
4286
+ onCommit: (value) => {
4287
+ const config = { ...draft, [key]: value };
4288
+ setDraft(config);
4289
+ void run("save", config);
4290
+ }
4201
4291
  }
4202
- }
4203
- ));
4204
- return /* @__PURE__ */ import_react30.default.createElement(import_react30.default.Fragment, null, /* @__PURE__ */ import_react30.default.createElement("p", null, recognition.qwen.hostHelp()), field(speak.qwen.endpoint(), "baseUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react30.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react30.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, speak.qwen.save()), /* @__PURE__ */ import_react30.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, speak.qwen.test()), /* @__PURE__ */ import_react30.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react30.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react30.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react30.default.createElement("p", { role: "alert" }, translateStatus(error)));
4292
+ );
4293
+ };
4294
+ return /* @__PURE__ */ import_react31.default.createElement(import_react31.default.Fragment, null, /* @__PURE__ */ import_react31.default.createElement("p", null, recognition.qwen.hostHelp()), field(speak.qwen.endpoint(), "baseUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react31.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react31.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, speak.qwen.save()), /* @__PURE__ */ import_react31.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, speak.qwen.test()), /* @__PURE__ */ import_react31.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react31.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react31.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react31.default.createElement("p", { role: "alert" }, translateStatus(error)));
4205
4295
  }
4206
4296
 
4207
4297
  // src/modules/recognition/engines/whisper/WhisperSettings.tsx
4208
- var import_react31 = __toESM(require("react"), 1);
4298
+ var import_react32 = __toESM(require("react"), 1);
4209
4299
  var BASE2 = "/api/dsh-live-voice/whisper";
4210
4300
  var UNLOADED2 = "dsh-live-voice.recognition.whisper.restartRequired";
4211
4301
  async function whisperSettingsRequest(path, { method = "GET", config, signal } = {}, fetchImpl = globalThis.fetch) {
@@ -4234,16 +4324,16 @@ function WhisperSettings({ controller }) {
4234
4324
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
4235
4325
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4236
4326
  const { scoped: settings } = useLanguage((ctx) => ctx.settings);
4237
- const [draft, setDraft] = import_react31.default.useState({
4327
+ const [draft, setDraft] = import_react32.default.useState({
4238
4328
  url: "http://127.0.0.1:8080/inference",
4239
4329
  healthUrl: "/health",
4240
4330
  timeoutMs: 3e4
4241
4331
  });
4242
- const [busy, setBusy] = import_react31.default.useState(true);
4243
- const [loaded, setLoaded] = import_react31.default.useState(false);
4244
- const [error, setError] = import_react31.default.useState("");
4245
- const [message2, setMessage] = import_react31.default.useState("");
4246
- const active = import_react31.default.useRef(null);
4332
+ const [busy, setBusy] = import_react32.default.useState(true);
4333
+ const [loaded, setLoaded] = import_react32.default.useState(false);
4334
+ const [error, setError] = import_react32.default.useState("");
4335
+ const [message2, setMessage] = import_react32.default.useState("");
4336
+ const active = import_react32.default.useRef(null);
4247
4337
  const translateStatus = (value) => ({
4248
4338
  "dsh-live-voice.commons.connection.unsaved": commons.connection.unsaved(),
4249
4339
  "dsh-live-voice.recognition.whisper.saved": recognition.whisper.saved(),
@@ -4253,7 +4343,7 @@ function WhisperSettings({ controller }) {
4253
4343
  "dsh-live-voice.recognition.whisper.requestFailed": recognition.whisper.requestFailed(),
4254
4344
  "dsh-live-voice.recognition.whisper.healthFailed": recognition.whisper.healthFailed()
4255
4345
  })[value] ?? value;
4256
- async function run(action) {
4346
+ async function run(action, config = draft) {
4257
4347
  active.current?.abort();
4258
4348
  const abort = new AbortController();
4259
4349
  active.current = abort;
@@ -4271,7 +4361,7 @@ function WhisperSettings({ controller }) {
4271
4361
  await controller.endConversation?.();
4272
4362
  const value = await whisperSettingsRequest("/config", {
4273
4363
  method: "PUT",
4274
- config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
4364
+ config: { ...config, timeoutMs: Number(config.timeoutMs) },
4275
4365
  signal: abort.signal
4276
4366
  });
4277
4367
  if (!abort.signal.aborted) {
@@ -4282,7 +4372,7 @@ function WhisperSettings({ controller }) {
4282
4372
  } else {
4283
4373
  const value = await whisperSettingsRequest("/test", {
4284
4374
  method: "POST",
4285
- config: { ...draft, timeoutMs: Number(draft.timeoutMs) },
4375
+ config: { ...config, timeoutMs: Number(config.timeoutMs) },
4286
4376
  signal: abort.signal
4287
4377
  });
4288
4378
  if (!abort.signal.aborted) {
@@ -4297,28 +4387,29 @@ function WhisperSettings({ controller }) {
4297
4387
  if (!abort.signal.aborted) setBusy(false);
4298
4388
  }
4299
4389
  }
4300
- import_react31.default.useEffect(() => {
4390
+ import_react32.default.useEffect(() => {
4301
4391
  void run("load");
4302
4392
  return () => active.current?.abort();
4303
4393
  }, []);
4304
- function field(label, key, type = "text") {
4305
- return /* @__PURE__ */ import_react31.default.createElement("label", null, label, /* @__PURE__ */ import_react31.default.createElement(
4306
- "input",
4394
+ const field = (label, key, type = "text") => {
4395
+ const Field = type === "number" ? NumberField : TextField;
4396
+ return /* @__PURE__ */ import_react32.default.createElement(
4397
+ Field,
4307
4398
  {
4308
- type,
4399
+ label,
4309
4400
  value: draft[key],
4310
4401
  disabled: busy || !loaded,
4311
4402
  autoComplete: "off",
4312
4403
  ...type === "number" ? { min: 100, max: 3e5, step: 1 } : {},
4313
- onChange: (event) => {
4314
- setDraft({ ...draft, [key]: event.target.value });
4315
- setMessage("dsh-live-voice.commons.connection.unsaved");
4316
- setError("");
4404
+ onCommit: (value) => {
4405
+ const config = { ...draft, [key]: value };
4406
+ setDraft(config);
4407
+ void run("save", config);
4317
4408
  }
4318
4409
  }
4319
- ));
4320
- }
4321
- return /* @__PURE__ */ import_react31.default.createElement(import_react31.default.Fragment, null, /* @__PURE__ */ import_react31.default.createElement("p", null, settings.whisper.hostHelp()), field(commons.connection.endpoint(), "url"), field(commons.connection.healthEndpoint(), "healthUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react31.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react31.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, recognition.whisper.save()), /* @__PURE__ */ import_react31.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, commons.connection.test()), /* @__PURE__ */ import_react31.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react31.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react31.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react31.default.createElement("p", { role: "alert" }, translateStatus(error)));
4410
+ );
4411
+ };
4412
+ return /* @__PURE__ */ import_react32.default.createElement(import_react32.default.Fragment, null, /* @__PURE__ */ import_react32.default.createElement("p", null, settings.whisper.hostHelp()), field(commons.connection.endpoint(), "url"), field(commons.connection.healthEndpoint(), "healthUrl"), field(commons.connection.timeout(), "timeoutMs", "number"), /* @__PURE__ */ import_react32.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react32.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("save") }, recognition.whisper.save()), /* @__PURE__ */ import_react32.default.createElement("button", { type: "button", disabled: busy || !loaded, onClick: () => run("test") }, commons.connection.test()), /* @__PURE__ */ import_react32.default.createElement("button", { type: "button", disabled: busy, onClick: () => run("load") }, commons.connection.reload())), busy && /* @__PURE__ */ import_react32.default.createElement("p", { role: "status" }, commons.connection.contactingHost()), message2 && /* @__PURE__ */ import_react32.default.createElement("p", { role: "status" }, translateStatus(message2)), error && /* @__PURE__ */ import_react32.default.createElement("p", { role: "alert" }, translateStatus(error)));
4322
4413
  }
4323
4414
 
4324
4415
  // src/modules/settings/sections/recognition/RecognitionEngineSettings.tsx
@@ -4335,7 +4426,7 @@ function RecognitionEngineSettings({
4335
4426
  const setEngine = (next) => updateSettings(
4336
4427
  next === "browser" && settings.recognitionLang === "auto" ? { recognitionEngine: next, recognitionLang: "pt-BR" } : { recognitionEngine: next }
4337
4428
  );
4338
- return /* @__PURE__ */ import_react32.default.createElement(import_react32.default.Fragment, null, /* @__PURE__ */ import_react32.default.createElement(
4429
+ return /* @__PURE__ */ import_react33.default.createElement(import_react33.default.Fragment, null, /* @__PURE__ */ import_react33.default.createElement(
4339
4430
  SelectField,
4340
4431
  {
4341
4432
  label: recognition.engine.label(),
@@ -4359,7 +4450,7 @@ function RecognitionEngineSettings({
4359
4450
  ],
4360
4451
  onChange: (event) => setEngine(event.target.value)
4361
4452
  }
4362
- ), /* @__PURE__ */ import_react32.default.createElement(
4453
+ ), /* @__PURE__ */ import_react33.default.createElement(
4363
4454
  SelectField,
4364
4455
  {
4365
4456
  label: recognition.microphone.device(),
@@ -4372,7 +4463,7 @@ function RecognitionEngineSettings({
4372
4463
  ),
4373
4464
  onChange: (event) => updateSettings({ inputDeviceId: event.target.value })
4374
4465
  }
4375
- ), /* @__PURE__ */ import_react32.default.createElement(
4466
+ ), /* @__PURE__ */ import_react33.default.createElement(
4376
4467
  SelectField,
4377
4468
  {
4378
4469
  label: recognition.language.label(),
@@ -4384,29 +4475,29 @@ function RecognitionEngineSettings({
4384
4475
  ],
4385
4476
  onChange: (event) => updateSettings({ recognitionLang: event.target.value })
4386
4477
  }
4387
- ), engine === "browser" && /* @__PURE__ */ import_react32.default.createElement(import_react32.default.Fragment, null, /* @__PURE__ */ import_react32.default.createElement("p", null, recognition.browser.help()), /* @__PURE__ */ import_react32.default.createElement(
4478
+ ), engine === "browser" && /* @__PURE__ */ import_react33.default.createElement(import_react33.default.Fragment, null, /* @__PURE__ */ import_react33.default.createElement("p", null, recognition.browser.help()), /* @__PURE__ */ import_react33.default.createElement(
4388
4479
  CheckboxField,
4389
4480
  {
4390
4481
  label: recognition.browser.localProcessing(),
4391
4482
  checked: settings.recognitionProcessLocally !== false,
4392
4483
  onChange: (event) => updateSettings({ recognitionProcessLocally: event.target.checked })
4393
4484
  }
4394
- ), settings.recognitionProcessLocally !== false ? /* @__PURE__ */ import_react32.default.createElement(
4485
+ ), settings.recognitionProcessLocally !== false ? /* @__PURE__ */ import_react33.default.createElement(
4395
4486
  CheckboxField,
4396
4487
  {
4397
4488
  label: recognition.browser.autoInstallPack(),
4398
4489
  checked: settings.recognitionAutoInstall !== false,
4399
4490
  onChange: (event) => updateSettings({ recognitionAutoInstall: event.target.checked })
4400
4491
  }
4401
- ) : /* @__PURE__ */ import_react32.default.createElement("p", { role: "status" }, recognition.browser.remoteServiceWarning())), engine === "whisper-http" && /* @__PURE__ */ import_react32.default.createElement(SettingsSubcard, { title: commons.connection.title() }, /* @__PURE__ */ import_react32.default.createElement("p", null, recognition.whisper.captureHelp()), /* @__PURE__ */ import_react32.default.createElement(WhisperSettings, { controller })), engine === "qwen-http" && /* @__PURE__ */ import_react32.default.createElement(SettingsSubcard, { title: commons.connection.title() }, /* @__PURE__ */ import_react32.default.createElement("p", null, recognition.qwen.captureHelp()), settings.engine !== "qwen-http" && /* @__PURE__ */ import_react32.default.createElement(QwenSettings, { controller })));
4492
+ ) : /* @__PURE__ */ import_react33.default.createElement("p", { role: "status" }, recognition.browser.remoteServiceWarning())), engine === "whisper-http" && /* @__PURE__ */ import_react33.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react33.default.createElement("p", null, recognition.whisper.captureHelp()), /* @__PURE__ */ import_react33.default.createElement(WhisperSettings, { controller })), engine === "qwen-http" && /* @__PURE__ */ import_react33.default.createElement(SettingsSubcard, { title: commons.connection.title(), icon: "link" }, /* @__PURE__ */ import_react33.default.createElement("p", null, recognition.qwen.captureHelp()), settings.engine !== "qwen-http" && /* @__PURE__ */ import_react33.default.createElement(QwenSettings, { controller })));
4402
4493
  }
4403
4494
 
4404
4495
  // src/modules/settings/sections/recognition/RecognitionFilterSettings.tsx
4405
- var import_react33 = __toESM(require("react"), 1);
4496
+ var import_react34 = __toESM(require("react"), 1);
4406
4497
  function RecognitionFilterSettings({ settings, updateSettings }) {
4407
4498
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4408
4499
  const enabled = settings.recognitionFilterEnabled !== false;
4409
- return /* @__PURE__ */ import_react33.default.createElement(SettingsSubcard, { title: recognition.minimumWords.label() }, /* @__PURE__ */ import_react33.default.createElement(
4500
+ return /* @__PURE__ */ import_react34.default.createElement(SettingsSubcard, { title: recognition.minimumWords.label(), icon: "filter" }, /* @__PURE__ */ import_react34.default.createElement(
4410
4501
  CheckboxField,
4411
4502
  {
4412
4503
  label: recognition.minimumWords.enabled(),
@@ -4414,7 +4505,7 @@ function RecognitionFilterSettings({ settings, updateSettings }) {
4414
4505
  checked: enabled,
4415
4506
  onChange: (event) => updateSettings({ recognitionFilterEnabled: event.target.checked })
4416
4507
  }
4417
- ), /* @__PURE__ */ import_react33.default.createElement(
4508
+ ), /* @__PURE__ */ import_react34.default.createElement(
4418
4509
  NumberField,
4419
4510
  {
4420
4511
  label: recognition.minimumWords.label(),
@@ -4422,8 +4513,8 @@ function RecognitionFilterSettings({ settings, updateSettings }) {
4422
4513
  max: 20,
4423
4514
  value: settings.recognitionMinimumWords ?? 2,
4424
4515
  disabled: !enabled,
4425
- onChange: (event) => {
4426
- const value = Number(event.target.value);
4516
+ onCommit: (raw) => {
4517
+ const value = Number(raw);
4427
4518
  if (Number.isInteger(value) && value >= 1 && value <= 20)
4428
4519
  updateSettings({ recognitionMinimumWords: value });
4429
4520
  }
@@ -4432,23 +4523,24 @@ function RecognitionFilterSettings({ settings, updateSettings }) {
4432
4523
  }
4433
4524
 
4434
4525
  // src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx
4435
- var import_react37 = __toESM(require("react"), 1);
4526
+ var import_react38 = __toESM(require("react"), 1);
4436
4527
 
4437
4528
  // src/modules/settings/sections/recognition/SilenceDetectionSettings.tsx
4438
- var import_react34 = __toESM(require("react"), 1);
4529
+ var import_react35 = __toESM(require("react"), 1);
4439
4530
  function SilenceDetectionSettings({ settings, updateSettings }) {
4440
4531
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4532
+ const savedCustomMs = normalizeCustomSilenceMs(settings.voiceDetectionCustomSilenceMs);
4441
4533
  if (!usesPluginVoiceDetection(settings.recognitionEngine)) return null;
4442
- const selected = settings.voiceDetectionPreset || "natural";
4443
- return /* @__PURE__ */ import_react34.default.createElement(
4534
+ const selected = settings.voiceDetectionPreset || defaultSettings.voiceDetectionPreset;
4535
+ return /* @__PURE__ */ import_react35.default.createElement(
4444
4536
  SettingsSubcard,
4445
4537
  {
4446
4538
  title: recognition.silenceDetection.label(),
4447
4539
  ariaLabel: recognition.silenceDetection.title(),
4448
- open: true
4540
+ icon: "pause"
4449
4541
  },
4450
- /* @__PURE__ */ import_react34.default.createElement("p", null, recognition.silenceDetection.help()),
4451
- /* @__PURE__ */ import_react34.default.createElement(
4542
+ /* @__PURE__ */ import_react35.default.createElement("p", null, recognition.silenceDetection.help()),
4543
+ /* @__PURE__ */ import_react35.default.createElement(
4452
4544
  NumberField,
4453
4545
  {
4454
4546
  label: recognition.maxUtterance.label(),
@@ -4456,22 +4548,22 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
4456
4548
  max: 300,
4457
4549
  step: 1,
4458
4550
  value: settings.recognitionMaxUtteranceSeconds ?? 60,
4459
- onChange: (event) => {
4460
- const value = Number(event.target.value);
4551
+ onCommit: (raw) => {
4552
+ const value = Number(raw);
4461
4553
  if (Number.isInteger(value) && value >= 10 && value <= 300)
4462
4554
  updateSettings({ recognitionMaxUtteranceSeconds: value });
4463
4555
  }
4464
4556
  }
4465
4557
  ),
4466
- /* @__PURE__ */ import_react34.default.createElement("small", null, recognition.maxUtterance.help()),
4467
- /* @__PURE__ */ import_react34.default.createElement(
4558
+ /* @__PURE__ */ import_react35.default.createElement("small", null, recognition.maxUtterance.help()),
4559
+ /* @__PURE__ */ import_react35.default.createElement(
4468
4560
  "div",
4469
4561
  {
4470
4562
  className: "dlv-preset-group",
4471
4563
  role: "radiogroup",
4472
4564
  "aria-label": recognition.silenceDetection.pauseLabel()
4473
4565
  },
4474
- Object.entries(voiceDetectionPresets).map(([value, preset]) => /* @__PURE__ */ import_react34.default.createElement("label", { key: value, className: "dlv-preset" }, /* @__PURE__ */ import_react34.default.createElement(
4566
+ [...Object.keys(voiceDetectionPresets), "custom"].map((value) => /* @__PURE__ */ import_react35.default.createElement("label", { key: value, className: "dlv-preset" }, /* @__PURE__ */ import_react35.default.createElement(
4475
4567
  "input",
4476
4568
  {
4477
4569
  type: "radio",
@@ -4480,16 +4572,31 @@ function SilenceDetectionSettings({ settings, updateSettings }) {
4480
4572
  checked: selected === value,
4481
4573
  onChange: () => updateSettings({ voiceDetectionPreset: value })
4482
4574
  }
4483
- ), /* @__PURE__ */ import_react34.default.createElement("span", null, /* @__PURE__ */ import_react34.default.createElement("strong", null, recognition.presets[value].label()), /* @__PURE__ */ import_react34.default.createElement("small", null, recognition.presets[value].description()))))
4575
+ ), /* @__PURE__ */ import_react35.default.createElement("span", null, /* @__PURE__ */ import_react35.default.createElement("strong", null, recognition.presets[value].label()), /* @__PURE__ */ import_react35.default.createElement("small", null, recognition.presets[value].description()))))
4484
4576
  ),
4485
- /* @__PURE__ */ import_react34.default.createElement("p", { className: "dlv-vad-summary" }, recognition.silenceDetection.duration({
4486
- milliseconds: voiceDetectionPresets[selected]?.silenceMs || voiceDetectionPresets.natural.silenceMs
4577
+ selected === "custom" && /* @__PURE__ */ import_react35.default.createElement(import_react35.default.Fragment, null, /* @__PURE__ */ import_react35.default.createElement(
4578
+ NumberField,
4579
+ {
4580
+ label: recognition.silenceDetection.customLabel(),
4581
+ min: customSilenceMinMs,
4582
+ max: customSilenceMaxMs,
4583
+ step: 1,
4584
+ value: savedCustomMs,
4585
+ onCommit: (draft) => {
4586
+ const value = Number(draft);
4587
+ if (draft.trim() && Number.isInteger(value) && value >= customSilenceMinMs && value <= customSilenceMaxMs)
4588
+ updateSettings({ voiceDetectionCustomSilenceMs: value });
4589
+ }
4590
+ }
4591
+ ), /* @__PURE__ */ import_react35.default.createElement("small", null, recognition.silenceDetection.customHelp())),
4592
+ /* @__PURE__ */ import_react35.default.createElement("p", { className: "dlv-vad-summary" }, recognition.silenceDetection.duration({
4593
+ milliseconds: voiceDetectionSilenceMs(settings)
4487
4594
  }))
4488
4595
  );
4489
4596
  }
4490
4597
 
4491
4598
  // src/modules/settings/sections/recognition/VoiceCommandSettings.tsx
4492
- var import_react35 = __toESM(require("react"), 1);
4599
+ var import_react36 = __toESM(require("react"), 1);
4493
4600
  var COMMANDS = [
4494
4601
  ["send", "voiceCommandSend", "send message, send it"],
4495
4602
  ["queue", "voiceCommandQueue", "queue message, queue it"],
@@ -4502,52 +4609,53 @@ var COMMANDS = [
4502
4609
  function VoiceCommandSettings({ settings, updateSettings }) {
4503
4610
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4504
4611
  const enabled = settings.voiceCommandsEnabled !== false;
4505
- return /* @__PURE__ */ import_react35.default.createElement(SettingsSubcard, { title: recognition.commands.title() }, /* @__PURE__ */ import_react35.default.createElement(
4612
+ return /* @__PURE__ */ import_react36.default.createElement(SettingsSubcard, { title: recognition.commands.title(), icon: "send" }, /* @__PURE__ */ import_react36.default.createElement(
4506
4613
  CheckboxField,
4507
4614
  {
4508
4615
  label: recognition.commands.enabled(),
4509
4616
  checked: enabled,
4510
4617
  onChange: (event) => updateSettings({ voiceCommandsEnabled: event.target.checked })
4511
4618
  }
4512
- ), /* @__PURE__ */ import_react35.default.createElement("p", { className: "dlv-setting-description" }, recognition.voiceCommands.help()), COMMANDS.map(([label, key, fallback]) => /* @__PURE__ */ import_react35.default.createElement(
4619
+ ), /* @__PURE__ */ import_react36.default.createElement("p", { className: "dlv-setting-description" }, recognition.voiceCommands.help()), COMMANDS.map(([label, key, fallback]) => /* @__PURE__ */ import_react36.default.createElement(
4513
4620
  TextAreaField,
4514
4621
  {
4515
4622
  key,
4516
4623
  rows: 2,
4517
4624
  maxLength: 1e3,
4518
4625
  label: recognition.commands[label](),
4519
- value: settings[key] || fallback,
4626
+ value: settings[key] ?? fallback,
4520
4627
  disabled: !enabled,
4521
- onChange: (event) => updateSettings({ [key]: event.target.value })
4628
+ onCommit: (value) => updateSettings({ [key]: value })
4522
4629
  }
4523
4630
  )));
4524
4631
  }
4525
4632
 
4526
4633
  // src/modules/settings/sections/recognition/RecognitionStatus.tsx
4527
- var import_react36 = __toESM(require("react"), 1);
4634
+ var import_react37 = __toESM(require("react"), 1);
4528
4635
  function RecognitionStatus({ capabilities = {} }) {
4529
4636
  const { scoped: recognition } = useLanguage((ctx) => ctx.recognition);
4530
- return /* @__PURE__ */ import_react36.default.createElement(import_react36.default.Fragment, null, capabilities.capture?.supported === false ? /* @__PURE__ */ import_react36.default.createElement("p", { role: "status" }, recognition.microphone.failure({ reason: capabilities.capture.reason || "" })) : capabilities.capture?.permission === "prompt" ? /* @__PURE__ */ import_react36.default.createElement("p", { role: "status" }, recognition.microphone.permissionHelp()) : null, capabilities.recognition?.supported === false && /* @__PURE__ */ import_react36.default.createElement("p", { role: "status" }, capabilities.recognition.reason));
4637
+ return /* @__PURE__ */ import_react37.default.createElement(import_react37.default.Fragment, null, capabilities.capture?.supported === false ? /* @__PURE__ */ import_react37.default.createElement("p", { role: "status" }, recognition.microphone.failure({ reason: capabilities.capture.reason || "" })) : capabilities.capture?.permission === "prompt" ? /* @__PURE__ */ import_react37.default.createElement("p", { role: "status" }, recognition.microphone.permissionHelp()) : null, capabilities.recognition?.supported === false && /* @__PURE__ */ import_react37.default.createElement("p", { role: "status" }, capabilities.recognition.reason));
4531
4638
  }
4532
4639
 
4533
4640
  // src/modules/settings/sections/recognition/RecognitionSettingsSection.tsx
4534
4641
  function RecognitionSettingsSection(props) {
4535
- return /* @__PURE__ */ import_react37.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react37.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react37.default.createElement(RecognitionEngineSettings, { ...props }), /* @__PURE__ */ import_react37.default.createElement(VoiceCommandSettings, { ...props }), /* @__PURE__ */ import_react37.default.createElement(RecognitionFilterSettings, { ...props }), /* @__PURE__ */ import_react37.default.createElement(SilenceDetectionSettings, { ...props }), /* @__PURE__ */ import_react37.default.createElement(RecognitionStatus, { ...props })));
4642
+ const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
4643
+ return /* @__PURE__ */ import_react38.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react38.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react38.default.createElement(RecognitionStatus, { ...props }), /* @__PURE__ */ import_react38.default.createElement(SettingsSubcard, { title: settingsLanguage.general.title(), icon: "mic" }, /* @__PURE__ */ import_react38.default.createElement(RecognitionEngineSettings, { ...props })), /* @__PURE__ */ import_react38.default.createElement(VoiceCommandSettings, { ...props }), /* @__PURE__ */ import_react38.default.createElement(RecognitionFilterSettings, { ...props }), /* @__PURE__ */ import_react38.default.createElement(SilenceDetectionSettings, { ...props })));
4536
4644
  }
4537
4645
 
4538
4646
  // src/modules/settings/sections/speak/OutputFilterSettings.tsx
4539
- var import_react38 = __toESM(require("react"), 1);
4647
+ var import_react39 = __toESM(require("react"), 1);
4540
4648
  function OutputFilterSettings({ settings, updateSettings }) {
4541
4649
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
4542
4650
  const enabled = settings.outputCodeFilterEnabled !== false;
4543
- return /* @__PURE__ */ import_react38.default.createElement(SettingsSubcard, { title: speak.filters.code.enabled() }, /* @__PURE__ */ import_react38.default.createElement(
4651
+ return /* @__PURE__ */ import_react39.default.createElement(SettingsSubcard, { title: speak.filters.code.enabled(), icon: "filter" }, /* @__PURE__ */ import_react39.default.createElement(
4544
4652
  CheckboxField,
4545
4653
  {
4546
4654
  label: speak.filters.code.enabled(),
4547
4655
  checked: enabled,
4548
4656
  onChange: (event) => updateSettings({ outputCodeFilterEnabled: event.target.checked })
4549
4657
  }
4550
- ), /* @__PURE__ */ import_react38.default.createElement(
4658
+ ), /* @__PURE__ */ import_react39.default.createElement(
4551
4659
  NumberField,
4552
4660
  {
4553
4661
  label: speak.filters.code.maxLines(),
@@ -4555,34 +4663,34 @@ function OutputFilterSettings({ settings, updateSettings }) {
4555
4663
  max: 100,
4556
4664
  value: settings.outputCodeMaxLines ?? 5,
4557
4665
  disabled: !enabled,
4558
- onChange: (event) => {
4559
- const value = Number(event.target.value);
4666
+ onCommit: (raw) => {
4667
+ const value = Number(raw);
4560
4668
  if (Number.isInteger(value) && value >= 0 && value <= 100)
4561
4669
  updateSettings({ outputCodeMaxLines: value });
4562
4670
  }
4563
4671
  }
4564
- ), /* @__PURE__ */ import_react38.default.createElement(
4672
+ ), /* @__PURE__ */ import_react39.default.createElement(
4565
4673
  TextField,
4566
4674
  {
4567
4675
  label: speak.filters.code.replacement(),
4568
- defaultValue: settings.outputCodeNotice || speak.filters.code.notice(),
4676
+ value: settings.outputCodeNotice ?? speak.filters.code.notice(),
4569
4677
  disabled: !enabled,
4570
- onBlur: (event) => updateSettings({ outputCodeNotice: event.target.value })
4678
+ onCommit: (value) => updateSettings({ outputCodeNotice: value })
4571
4679
  }
4572
4680
  ));
4573
4681
  }
4574
4682
 
4575
4683
  // src/modules/settings/sections/speak/SpeakSettingsSection.tsx
4576
- var import_react41 = __toESM(require("react"), 1);
4684
+ var import_react42 = __toESM(require("react"), 1);
4577
4685
 
4578
4686
  // src/modules/settings/sections/speak/SpeechEngineSettings.tsx
4579
- var import_react39 = __toESM(require("react"), 1);
4687
+ var import_react40 = __toESM(require("react"), 1);
4580
4688
  function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
4581
4689
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
4582
4690
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
4583
4691
  const engine = settings.engine || "browser";
4584
4692
  const browserVoices = capabilities?.browser?.voices || [];
4585
- return /* @__PURE__ */ import_react39.default.createElement(import_react39.default.Fragment, null, /* @__PURE__ */ import_react39.default.createElement(
4693
+ return /* @__PURE__ */ import_react40.default.createElement(import_react40.default.Fragment, null, /* @__PURE__ */ import_react40.default.createElement(
4586
4694
  SelectField,
4587
4695
  {
4588
4696
  label: speak.engine.label(),
@@ -4606,7 +4714,7 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
4606
4714
  ],
4607
4715
  onChange: (event) => updateSettings({ engine: event.target.value })
4608
4716
  }
4609
- ), engine === "browser" && /* @__PURE__ */ import_react39.default.createElement(
4717
+ ), engine === "browser" && /* @__PURE__ */ import_react40.default.createElement(
4610
4718
  SelectField,
4611
4719
  {
4612
4720
  label: speak.browser.voice(),
@@ -4620,7 +4728,7 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
4620
4728
  ],
4621
4729
  onChange: (event) => updateSettings({ voice: event.target.value })
4622
4730
  }
4623
- ), engine === "qwen-http" && /* @__PURE__ */ import_react39.default.createElement(
4731
+ ), engine === "qwen-http" && /* @__PURE__ */ import_react40.default.createElement(
4624
4732
  SelectField,
4625
4733
  {
4626
4734
  label: speak.qwen.voice(),
@@ -4628,7 +4736,7 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
4628
4736
  options: qwenVoices,
4629
4737
  onChange: (event) => updateSettings({ voice: event.target.value })
4630
4738
  }
4631
- ), /* @__PURE__ */ import_react39.default.createElement(
4739
+ ), /* @__PURE__ */ import_react40.default.createElement(
4632
4740
  NumberField,
4633
4741
  {
4634
4742
  label: speak.rate.label(),
@@ -4636,9 +4744,9 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
4636
4744
  max: 3,
4637
4745
  step: 0.1,
4638
4746
  value: settings.rate ?? 1,
4639
- onChange: (event) => updateSettings({ rate: Number(event.target.value) })
4747
+ onCommit: (value) => updateSettings({ rate: Number(value) })
4640
4748
  }
4641
- ), /* @__PURE__ */ import_react39.default.createElement(
4749
+ ), /* @__PURE__ */ import_react40.default.createElement(
4642
4750
  NumberField,
4643
4751
  {
4644
4752
  label: speak.segmentGap.label(),
@@ -4646,13 +4754,13 @@ function SpeechEngineSettings({ settings, capabilities, updateSettings }) {
4646
4754
  max: 2e3,
4647
4755
  step: 50,
4648
4756
  value: settings.segmentGapMs ?? 400,
4649
- onChange: (event) => updateSettings({ segmentGapMs: Number(event.target.value) })
4757
+ onCommit: (value) => updateSettings({ segmentGapMs: Number(value) })
4650
4758
  }
4651
- ), capabilities?.[engine === "qwen-http" ? "qwen" : engine]?.supported === false && /* @__PURE__ */ import_react39.default.createElement("p", { role: "status" }, capabilities?.[engine === "qwen-http" ? "qwen" : engine]?.reason || commons.engine.failure()));
4759
+ ), capabilities?.[engine === "qwen-http" ? "qwen" : engine]?.supported === false && /* @__PURE__ */ import_react40.default.createElement("p", { role: "status" }, capabilities?.[engine === "qwen-http" ? "qwen" : engine]?.reason || commons.engine.failure()));
4652
4760
  }
4653
4761
 
4654
4762
  // src/modules/settings/sections/speak/SpeechAdvancedSettings.tsx
4655
- var import_react40 = __toESM(require("react"), 1);
4763
+ var import_react41 = __toESM(require("react"), 1);
4656
4764
  function SpeechAdvancedSettings({
4657
4765
  controller,
4658
4766
  settings,
@@ -4670,7 +4778,7 @@ function SpeechAdvancedSettings({
4670
4778
  commons.systemDefault(),
4671
4779
  speak.output.fallbackName()
4672
4780
  );
4673
- return /* @__PURE__ */ import_react40.default.createElement(import_react40.default.Fragment, null, settings.engine !== "say" ? /* @__PURE__ */ import_react40.default.createElement(
4781
+ return /* @__PURE__ */ import_react41.default.createElement(import_react41.default.Fragment, null, settings.engine !== "say" ? /* @__PURE__ */ import_react41.default.createElement(
4674
4782
  SelectField,
4675
4783
  {
4676
4784
  label: speak.output.device(),
@@ -4678,44 +4786,45 @@ function SpeechAdvancedSettings({
4678
4786
  options: outputOptions,
4679
4787
  onChange: (event) => updateSettings({ outputDeviceId: event.target.value })
4680
4788
  }
4681
- ) : /* @__PURE__ */ import_react40.default.createElement("p", { className: "dlv-setting-description" }, speak.macos.outputHelp()), settings.engine === "qwen-http" && /* @__PURE__ */ import_react40.default.createElement(SettingsSubcard, { title: speak.qwen.connection() }, /* @__PURE__ */ import_react40.default.createElement(QwenSettings, { controller })), /* @__PURE__ */ import_react40.default.createElement(SettingsSubcard, { title: speak.agentContext.label() }, /* @__PURE__ */ import_react40.default.createElement(
4789
+ ) : /* @__PURE__ */ import_react41.default.createElement("p", { className: "dlv-setting-description" }, speak.macos.outputHelp()), settings.engine === "qwen-http" && /* @__PURE__ */ import_react41.default.createElement(SettingsSubcard, { title: speak.qwen.connection(), icon: "link" }, /* @__PURE__ */ import_react41.default.createElement(QwenSettings, { controller })), /* @__PURE__ */ import_react41.default.createElement(SettingsSubcard, { title: speak.agentContext.label(), icon: "settings" }, /* @__PURE__ */ import_react41.default.createElement(
4682
4790
  CheckboxField,
4683
4791
  {
4684
4792
  label: speak.agentContext.enabled(),
4685
4793
  checked: settings.agentVoiceContextEnabled !== false,
4686
4794
  onChange: (event) => updateSettings({ agentVoiceContextEnabled: event.target.checked })
4687
4795
  }
4688
- ), /* @__PURE__ */ import_react40.default.createElement(
4796
+ ), /* @__PURE__ */ import_react41.default.createElement(
4689
4797
  TextAreaField,
4690
4798
  {
4691
4799
  label: speak.agentContext.label(),
4692
- value: settings.agentVoiceContext || defaultAgentVoiceContext,
4800
+ value: settings.agentVoiceContext ?? defaultAgentVoiceContext,
4693
4801
  disabled: settings.agentVoiceContextEnabled === false,
4694
- onChange: (event) => updateSettings({ agentVoiceContext: event.target.value })
4802
+ onCommit: (value) => updateSettings({ agentVoiceContext: value })
4695
4803
  }
4696
- ), /* @__PURE__ */ import_react40.default.createElement(
4804
+ ), /* @__PURE__ */ import_react41.default.createElement(
4697
4805
  "button",
4698
4806
  {
4699
4807
  type: "button",
4700
4808
  onClick: () => updateSettings({ agentVoiceContext: defaultAgentVoiceContext })
4701
4809
  },
4702
4810
  speak.agentContext.restore()
4703
- )), /* @__PURE__ */ import_react40.default.createElement("p", null, speak.engine.playbackHelp()), /* @__PURE__ */ import_react40.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react40.default.createElement("button", { type: "button", onClick: () => invoke("speak", speak.output.testPhrase()) }, speak.output.test()), speaking && /* @__PURE__ */ import_react40.default.createElement("button", { type: "button", onClick: () => invoke("stopSpeech") }, speak.output.stopTest()), /* @__PURE__ */ import_react40.default.createElement("button", { type: "button", onClick: () => invoke("refreshCapabilities") }, settingsLanguage.engine.refresh())));
4811
+ )), /* @__PURE__ */ import_react41.default.createElement("p", null, speak.engine.playbackHelp()), /* @__PURE__ */ import_react41.default.createElement("div", { className: "dlv-settings-actions" }, /* @__PURE__ */ import_react41.default.createElement("button", { type: "button", onClick: () => invoke("speak", speak.output.testPhrase()) }, speak.output.test()), speaking && /* @__PURE__ */ import_react41.default.createElement("button", { type: "button", onClick: () => invoke("stopSpeech") }, speak.output.stopTest()), /* @__PURE__ */ import_react41.default.createElement("button", { type: "button", onClick: () => invoke("refreshCapabilities") }, settingsLanguage.engine.refresh())));
4704
4812
  }
4705
4813
 
4706
4814
  // src/modules/settings/sections/speak/SpeakSettingsSection.tsx
4707
4815
  function SpeakSettingsSection(props) {
4708
- return /* @__PURE__ */ import_react41.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react41.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react41.default.createElement(SpeechEngineSettings, { ...props }), /* @__PURE__ */ import_react41.default.createElement(SpeechAdvancedSettings, { ...props }), /* @__PURE__ */ import_react41.default.createElement(OutputFilterSettings, { ...props })));
4816
+ const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
4817
+ return /* @__PURE__ */ import_react42.default.createElement(SettingsCard, null, /* @__PURE__ */ import_react42.default.createElement("div", { className: "dlv-settings-card-body" }, /* @__PURE__ */ import_react42.default.createElement(SettingsSubcard, { title: settingsLanguage.general.title(), icon: "speaker" }, /* @__PURE__ */ import_react42.default.createElement(SpeechEngineSettings, { ...props }), /* @__PURE__ */ import_react42.default.createElement(SpeechAdvancedSettings, { ...props })), /* @__PURE__ */ import_react42.default.createElement(OutputFilterSettings, { ...props })));
4709
4818
  }
4710
4819
 
4711
4820
  // src/modules/settings/components/SettingsHeader.tsx
4712
- var import_react43 = __toESM(require("react"), 1);
4821
+ var import_react44 = __toESM(require("react"), 1);
4713
4822
 
4714
4823
  // src/modules/settings/components/VersionBadges.tsx
4715
- var import_react42 = __toESM(require("react"), 1);
4824
+ var import_react43 = __toESM(require("react"), 1);
4716
4825
  function VersionBadges() {
4717
4826
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
4718
- return /* @__PURE__ */ import_react42.default.createElement("div", { className: "dlv-version-badges", "aria-label": commons.version.title() }, /* @__PURE__ */ import_react42.default.createElement(
4827
+ return /* @__PURE__ */ import_react43.default.createElement("div", { className: "dlv-version-badges", "aria-label": commons.version.title() }, /* @__PURE__ */ import_react43.default.createElement(
4719
4828
  "a",
4720
4829
  {
4721
4830
  className: "dlv-shields-badge",
@@ -4725,9 +4834,9 @@ function VersionBadges() {
4725
4834
  "aria-label": commons.version.link({ version: CURRENT_VERSION }),
4726
4835
  title: commons.version.label({ version: CURRENT_VERSION })
4727
4836
  },
4728
- /* @__PURE__ */ import_react42.default.createElement("img", { src: LIVE_VOICE_BADGE_URL, alt: "" }),
4729
- /* @__PURE__ */ import_react42.default.createElement("span", null, `v${CURRENT_VERSION}`)
4730
- ), /* @__PURE__ */ import_react42.default.createElement(
4837
+ /* @__PURE__ */ import_react43.default.createElement("img", { src: LIVE_VOICE_BADGE_URL, alt: "" }),
4838
+ /* @__PURE__ */ import_react43.default.createElement("span", null, `v${CURRENT_VERSION}`)
4839
+ ), /* @__PURE__ */ import_react43.default.createElement(
4731
4840
  "a",
4732
4841
  {
4733
4842
  className: "dlv-shields-badge",
@@ -4737,8 +4846,8 @@ function VersionBadges() {
4737
4846
  "aria-label": commons.version.compatibilityLink({ version: TESTED_DSH_VERSION }),
4738
4847
  title: commons.version.compatibility({ version: TESTED_DSH_VERSION })
4739
4848
  },
4740
- /* @__PURE__ */ import_react42.default.createElement("img", { src: DSH_BADGE_URL, alt: "" }),
4741
- /* @__PURE__ */ import_react42.default.createElement("span", null, `v${TESTED_DSH_VERSION}`)
4849
+ /* @__PURE__ */ import_react43.default.createElement("img", { src: DSH_BADGE_URL, alt: "" }),
4850
+ /* @__PURE__ */ import_react43.default.createElement("span", null, `v${TESTED_DSH_VERSION}`)
4742
4851
  ));
4743
4852
  }
4744
4853
 
@@ -4747,7 +4856,7 @@ function SettingsHeader({ onClose }) {
4747
4856
  const { scoped: commons } = useLanguage((ctx) => ctx.commons);
4748
4857
  const { scoped: settings } = useLanguage((ctx) => ctx.settings);
4749
4858
  const { latestRelease, updateAvailable } = useReleaseStatus();
4750
- return /* @__PURE__ */ import_react43.default.createElement("div", { className: "dlv-settings-heading" }, /* @__PURE__ */ import_react43.default.createElement("h3", null, commons.pluginName()), /* @__PURE__ */ import_react43.default.createElement(VersionBadges, null), /* @__PURE__ */ import_react43.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true }), updateAvailable && latestRelease ? /* @__PURE__ */ import_react43.default.createElement(import_react43.default.Fragment, null, /* @__PURE__ */ import_react43.default.createElement(
4859
+ return /* @__PURE__ */ import_react44.default.createElement("div", { className: "dlv-settings-heading" }, /* @__PURE__ */ import_react44.default.createElement("h3", null, commons.pluginName()), /* @__PURE__ */ import_react44.default.createElement(VersionBadges, null), /* @__PURE__ */ import_react44.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true }), updateAvailable && latestRelease ? /* @__PURE__ */ import_react44.default.createElement(import_react44.default.Fragment, null, /* @__PURE__ */ import_react44.default.createElement(
4751
4860
  "a",
4752
4861
  {
4753
4862
  className: "dlv-version-badge dlv-update-badge",
@@ -4757,9 +4866,9 @@ function SettingsHeader({ onClose }) {
4757
4866
  "aria-label": commons.update.link({ version: latestRelease.tag }),
4758
4867
  title: commons.update.version({ version: latestRelease.tag })
4759
4868
  },
4760
- /* @__PURE__ */ import_react43.default.createElement("span", { className: "dlv-update-icon", "aria-hidden": true }, "\u2191"),
4869
+ /* @__PURE__ */ import_react44.default.createElement("span", { className: "dlv-update-icon", "aria-hidden": true }, "\u2191"),
4761
4870
  commons.update.label()
4762
- ), /* @__PURE__ */ import_react43.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true })) : null, /* @__PURE__ */ import_react43.default.createElement(
4871
+ ), /* @__PURE__ */ import_react44.default.createElement("span", { className: "dlv-heading-divider", "aria-hidden": true })) : null, /* @__PURE__ */ import_react44.default.createElement(
4763
4872
  "a",
4764
4873
  {
4765
4874
  className: "dlv-version-badge dlv-star-badge",
@@ -4771,7 +4880,7 @@ function SettingsHeader({ onClose }) {
4771
4880
  },
4772
4881
  "\u2605 ",
4773
4882
  commons.repository.starLabel()
4774
- ), onClose ? /* @__PURE__ */ import_react43.default.createElement(IconButton, { icon: "close", label: settings.close(), onClick: onClose }) : null);
4883
+ ), onClose ? /* @__PURE__ */ import_react44.default.createElement(IconButton, { icon: "close", label: settings.close(), onClick: onClose }) : null);
4775
4884
  }
4776
4885
 
4777
4886
  // src/modules/settings/components/LiveVoiceSettings.tsx
@@ -4780,12 +4889,20 @@ function LiveVoiceSettings({ controller, onClose }) {
4780
4889
  const { scoped: settingsLanguage } = useLanguage((ctx) => ctx.settings);
4781
4890
  const state = useConversationController(controller);
4782
4891
  const { invoke, error, clearError } = useConversationActions(controller);
4783
- const [activeTab, setActiveTab] = import_react44.default.useState("conversation");
4784
- const tabsId = import_react44.default.useId();
4892
+ const [activeTab, setActiveTab] = import_react45.default.useState("conversation");
4893
+ const tabsId = import_react45.default.useId();
4785
4894
  const tabs = [
4786
- { id: "speech", label: settingsLanguage.tabs.speak() },
4787
- { id: "recognition", label: settingsLanguage.tabs.recognition() },
4788
- { id: "conversation", label: settingsLanguage.tabs.conversation() }
4895
+ { id: "speech", label: settingsLanguage.tabs.speak(), icon: "speaker" },
4896
+ {
4897
+ id: "recognition",
4898
+ label: settingsLanguage.tabs.recognition(),
4899
+ icon: "mic"
4900
+ },
4901
+ {
4902
+ id: "conversation",
4903
+ label: settingsLanguage.tabs.conversation(),
4904
+ icon: "send"
4905
+ }
4789
4906
  ];
4790
4907
  const sectionProps = {
4791
4908
  controller,
@@ -4795,7 +4912,7 @@ function LiveVoiceSettings({ controller, onClose }) {
4795
4912
  invoke,
4796
4913
  updateSettings: (next) => invoke("updateSettings", next)
4797
4914
  };
4798
- return /* @__PURE__ */ import_react44.default.createElement("section", { className: "dlv-settings", "aria-label": settingsLanguage.title() }, /* @__PURE__ */ import_react44.default.createElement(SettingsHeader, { onClose }), /* @__PURE__ */ import_react44.default.createElement(
4915
+ return /* @__PURE__ */ import_react45.default.createElement("section", { className: "dlv-settings", "aria-label": settingsLanguage.title() }, /* @__PURE__ */ import_react45.default.createElement(SettingsHeader, { onClose }), /* @__PURE__ */ import_react45.default.createElement(
4799
4916
  SettingsTabs,
4800
4917
  {
4801
4918
  label: settingsLanguage.title(),
@@ -4804,7 +4921,7 @@ function LiveVoiceSettings({ controller, onClose }) {
4804
4921
  idPrefix: tabsId,
4805
4922
  onChange: setActiveTab
4806
4923
  }
4807
- ), tabs.map((tab) => /* @__PURE__ */ import_react44.default.createElement(
4924
+ ), tabs.map((tab) => /* @__PURE__ */ import_react45.default.createElement(
4808
4925
  "div",
4809
4926
  {
4810
4927
  key: tab.id,
@@ -4814,8 +4931,8 @@ function LiveVoiceSettings({ controller, onClose }) {
4814
4931
  "aria-labelledby": `${tabsId}-tab-${tab.id}`,
4815
4932
  hidden: activeTab !== tab.id
4816
4933
  },
4817
- tab.id === "speech" ? /* @__PURE__ */ import_react44.default.createElement(SpeakSettingsSection, { ...sectionProps }) : tab.id === "recognition" ? /* @__PURE__ */ import_react44.default.createElement(RecognitionSettingsSection, { ...sectionProps }) : /* @__PURE__ */ import_react44.default.createElement(ConversationSettingsSection, { ...sectionProps })
4818
- )), /* @__PURE__ */ import_react44.default.createElement(
4934
+ tab.id === "speech" ? /* @__PURE__ */ import_react45.default.createElement(SpeakSettingsSection, { ...sectionProps }) : tab.id === "recognition" ? /* @__PURE__ */ import_react45.default.createElement(RecognitionSettingsSection, { ...sectionProps }) : /* @__PURE__ */ import_react45.default.createElement(ConversationSettingsSection, { ...sectionProps })
4935
+ )), /* @__PURE__ */ import_react45.default.createElement(
4819
4936
  ErrorMessage,
4820
4937
  {
4821
4938
  error: error || state.error,
@@ -4832,17 +4949,64 @@ function LiveVoiceSettings({ controller, onClose }) {
4832
4949
  // src/modules/settings/components/createLiveVoiceSettings.tsx
4833
4950
  function createLiveVoiceSettings(..._legacyArguments) {
4834
4951
  return function SettingsPanel({ controller, onClose }) {
4835
- return /* @__PURE__ */ import_react45.default.createElement(LiveVoiceSettings, { controller, onClose });
4952
+ return /* @__PURE__ */ import_react46.default.createElement(LiveVoiceSettings, { controller, onClose });
4953
+ };
4954
+ }
4955
+
4956
+ // src/modules/settings/models/settingsStorage.ts
4957
+ var SETTINGS_PATH = "/api/dsh-live-voice/settings";
4958
+ function createSettingsClient(fetchImpl = globalThis.fetch) {
4959
+ let settings = normalizeSettings({});
4960
+ let queue = Promise.resolve();
4961
+ const listeners = /* @__PURE__ */ new Set();
4962
+ async function request(method, patch) {
4963
+ const response = await fetchImpl(SETTINGS_PATH, {
4964
+ method,
4965
+ credentials: "same-origin",
4966
+ cache: "no-store",
4967
+ ...patch === void 0 ? {} : { headers: { "content-type": "application/json" }, body: JSON.stringify(patch) }
4968
+ });
4969
+ const body = await response.json();
4970
+ if (!response.ok || body?.ok !== true) throw new Error("settings-request-failed");
4971
+ settings = normalizeSettings(body.value);
4972
+ for (const listener of listeners) listener(settings);
4973
+ return settings;
4974
+ }
4975
+ const ready = request("GET");
4976
+ void ready.catch(() => {
4977
+ });
4978
+ return {
4979
+ ready,
4980
+ getSnapshot: () => settings,
4981
+ subscribe(listener) {
4982
+ listeners.add(listener);
4983
+ return () => {
4984
+ listeners.delete(listener);
4985
+ };
4986
+ },
4987
+ save(patch) {
4988
+ const operation = queue.then(async () => {
4989
+ await ready;
4990
+ return request("PUT", patch);
4991
+ });
4992
+ queue = operation.then(
4993
+ () => {
4994
+ },
4995
+ () => {
4996
+ }
4997
+ );
4998
+ return operation;
4999
+ }
4836
5000
  };
4837
5001
  }
4838
5002
 
4839
5003
  // src/modules/conversation/components/ConversationControls.tsx
4840
- var import_react47 = __toESM(require("react"), 1);
5004
+ var import_react48 = __toESM(require("react"), 1);
4841
5005
 
4842
5006
  // src/modules/conversation/components/MicrophoneButton.tsx
4843
- var import_react46 = __toESM(require("react"), 1);
5007
+ var import_react47 = __toESM(require("react"), 1);
4844
5008
  function MicrophoneButton(props) {
4845
- return /* @__PURE__ */ import_react46.default.createElement(IconButton, { ...props, className: "dlv-mic", icon: "mic" });
5009
+ return /* @__PURE__ */ import_react47.default.createElement(IconButton, { ...props, className: "dlv-mic", icon: "mic" });
4846
5010
  }
4847
5011
 
4848
5012
  // src/modules/conversation/components/ConversationControls.tsx
@@ -4859,14 +5023,14 @@ function ConversationControls({ controller }) {
4859
5023
  const unavailable = capability?.supported === false || capture?.supported === false;
4860
5024
  const reason = capture?.supported === false ? capture.reason : capability?.reason;
4861
5025
  const label = pending ? recognition.microphone.checking() : unavailable ? reason || recognition.status.unavailable() : commons.conversation.start();
4862
- return /* @__PURE__ */ import_react47.default.createElement(import_react47.default.Fragment, null, /* @__PURE__ */ import_react47.default.createElement(
5026
+ return /* @__PURE__ */ import_react48.default.createElement(import_react48.default.Fragment, null, /* @__PURE__ */ import_react48.default.createElement(
4863
5027
  MicrophoneButton,
4864
5028
  {
4865
5029
  label,
4866
5030
  disabled: pending,
4867
5031
  onClick: () => invoke(unavailable ? "explainRecognition" : "startConversation")
4868
5032
  }
4869
- ), /* @__PURE__ */ import_react47.default.createElement(
5033
+ ), /* @__PURE__ */ import_react48.default.createElement(
4870
5034
  ErrorMessage,
4871
5035
  {
4872
5036
  error,
@@ -4878,10 +5042,10 @@ function ConversationControls({ controller }) {
4878
5042
  }
4879
5043
 
4880
5044
  // src/modules/conversation/components/ConversationStatusBar.tsx
4881
- var import_react51 = __toESM(require("react"), 1);
5045
+ var import_react52 = __toESM(require("react"), 1);
4882
5046
 
4883
5047
  // src/modules/conversation/components/DeliveryModeButton.tsx
4884
- var import_react48 = __toESM(require("react"), 1);
5048
+ var import_react49 = __toESM(require("react"), 1);
4885
5049
  function nextDeliveryMode(mode) {
4886
5050
  return mode === "queue" ? "steer" : mode === "steer" ? "manual" : "queue";
4887
5051
  }
@@ -4894,7 +5058,7 @@ function DeliveryModeButton({
4894
5058
  const description = mode === "steer" ? settings.delivery.steerDescription() : mode === "queue" ? commons.queue() : commons.manual();
4895
5059
  const status = settings.delivery.status({ mode: description });
4896
5060
  const visible = mode === "steer" ? commons.send() : mode === "queue" ? commons.delivery.queueBadge() : commons.toggle.offBadge();
4897
- return /* @__PURE__ */ import_react48.default.createElement(
5061
+ return /* @__PURE__ */ import_react49.default.createElement(
4898
5062
  IconButton,
4899
5063
  {
4900
5064
  className: "dlv-live-toggle",
@@ -4910,18 +5074,18 @@ function DeliveryModeButton({
4910
5074
  }
4911
5075
 
4912
5076
  // src/modules/conversation/components/PlaybackControls.tsx
4913
- var import_react49 = __toESM(require("react"), 1);
5077
+ var import_react50 = __toESM(require("react"), 1);
4914
5078
  function PlaybackControls({ state, invoke }) {
4915
5079
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
4916
5080
  const capabilities = state.capabilities?.[state.settings?.engine] ?? {};
4917
- return /* @__PURE__ */ import_react49.default.createElement(import_react49.default.Fragment, null, state.speechSegmentsRemaining > 1 ? /* @__PURE__ */ import_react49.default.createElement(
5081
+ return /* @__PURE__ */ import_react50.default.createElement(import_react50.default.Fragment, null, state.speechSegmentsRemaining > 1 ? /* @__PURE__ */ import_react50.default.createElement(
4918
5082
  IconButton,
4919
5083
  {
4920
5084
  label: speak.playback.next(),
4921
5085
  icon: "skipNext",
4922
5086
  onClick: () => invoke("skipSpeechSegment")
4923
5087
  }
4924
- ) : null, state.speechSegmentsRemaining > 0 && capabilities.pause ? /* @__PURE__ */ import_react49.default.createElement(
5088
+ ) : null, state.speechSegmentsRemaining > 0 && capabilities.pause ? /* @__PURE__ */ import_react50.default.createElement(
4925
5089
  IconButton,
4926
5090
  {
4927
5091
  label: speak.playback.pause(),
@@ -4929,14 +5093,14 @@ function PlaybackControls({ state, invoke }) {
4929
5093
  disabled: !state.speaking || state.paused,
4930
5094
  onClick: () => invoke("pauseSpeech")
4931
5095
  }
4932
- ) : null, state.paused && capabilities.resume ? /* @__PURE__ */ import_react49.default.createElement(
5096
+ ) : null, state.paused && capabilities.resume ? /* @__PURE__ */ import_react50.default.createElement(
4933
5097
  IconButton,
4934
5098
  {
4935
5099
  label: speak.playback.resume(),
4936
5100
  icon: "play",
4937
5101
  onClick: () => invoke("resumeSpeech")
4938
5102
  }
4939
- ) : null, state.speechSegmentsRemaining > 0 ? /* @__PURE__ */ import_react49.default.createElement(
5103
+ ) : null, state.speechSegmentsRemaining > 0 ? /* @__PURE__ */ import_react50.default.createElement(
4940
5104
  IconButton,
4941
5105
  {
4942
5106
  label: speak.playback.stopAll(),
@@ -4947,10 +5111,10 @@ function PlaybackControls({ state, invoke }) {
4947
5111
  }
4948
5112
 
4949
5113
  // src/modules/conversation/components/Waveform.tsx
4950
- var import_react50 = __toESM(require("react"), 1);
5114
+ var import_react51 = __toESM(require("react"), 1);
4951
5115
  function Waveform({ controller, enabled }) {
4952
- const ref = import_react50.default.useRef(null);
4953
- import_react50.default.useEffect(() => {
5116
+ const ref = import_react51.default.useRef(null);
5117
+ import_react51.default.useEffect(() => {
4954
5118
  const canvas = ref.current;
4955
5119
  const context = canvas?.getContext("2d");
4956
5120
  if (!canvas || !context) return;
@@ -5002,7 +5166,7 @@ function Waveform({ controller, enabled }) {
5002
5166
  window.removeEventListener("resize", resize);
5003
5167
  };
5004
5168
  }, [controller, enabled]);
5005
- return /* @__PURE__ */ import_react50.default.createElement("canvas", { ref, className: "dlv-wave", "aria-hidden": true });
5169
+ return /* @__PURE__ */ import_react51.default.createElement("canvas", { ref, className: "dlv-wave", "aria-hidden": true });
5006
5170
  }
5007
5171
 
5008
5172
  // src/modules/conversation/components/conversationStatus.ts
@@ -5033,8 +5197,8 @@ function ConversationStatusBar({
5033
5197
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
5034
5198
  const state = useConversationController(controller);
5035
5199
  const { invoke, error, clearError } = useConversationActions(controller);
5036
- const [now, setNow] = import_react51.default.useState(Date.now());
5037
- import_react51.default.useEffect(() => {
5200
+ const [now, setNow] = import_react52.default.useState(Date.now());
5201
+ import_react52.default.useEffect(() => {
5038
5202
  if (!state.autoSendAt) return;
5039
5203
  setNow(Date.now());
5040
5204
  const timer = setInterval(() => setNow(Date.now()), 200);
@@ -5058,33 +5222,33 @@ function ConversationStatusBar({
5058
5222
  }) : speak.autoPlayback.status({
5059
5223
  state: autoPlayback ? commons.on() : commons.off()
5060
5224
  });
5061
- return /* @__PURE__ */ import_react51.default.createElement(
5225
+ return /* @__PURE__ */ import_react52.default.createElement(
5062
5226
  "div",
5063
5227
  {
5064
5228
  className: overlay ? "dlv-bar-wrap dlv-question-overlay" : "dlv-bar-wrap",
5065
5229
  style: overlay ? overlayStyle : void 0
5066
5230
  },
5067
- /* @__PURE__ */ import_react51.default.createElement("div", { className: "dlv-pill", role: "group", "aria-label": commons.controls.title() }, capture && !state.conversation ? /* @__PURE__ */ import_react51.default.createElement(
5231
+ /* @__PURE__ */ import_react52.default.createElement("div", { className: "dlv-pill", role: "group", "aria-label": commons.controls.title() }, capture && !state.conversation ? /* @__PURE__ */ import_react52.default.createElement(
5068
5232
  IconButton,
5069
5233
  {
5070
5234
  label: recognition.dictation.cancel(),
5071
5235
  icon: "close",
5072
5236
  onClick: () => invoke("cancelDictation")
5073
5237
  }
5074
- ) : null, state.conversation ? /* @__PURE__ */ import_react51.default.createElement(
5238
+ ) : null, state.conversation ? /* @__PURE__ */ import_react52.default.createElement(
5075
5239
  IconButton,
5076
5240
  {
5077
5241
  label: commons.conversation.end(),
5078
5242
  icon: "close",
5079
5243
  onClick: () => invoke("endConversation")
5080
5244
  }
5081
- ) : null, /* @__PURE__ */ import_react51.default.createElement(Waveform, { controller, enabled: Boolean(state.listening) }), /* @__PURE__ */ import_react51.default.createElement("span", { className: "dlv-status", role: "status", "aria-live": "polite" }, status), /* @__PURE__ */ import_react51.default.createElement(
5245
+ ) : null, /* @__PURE__ */ import_react52.default.createElement(Waveform, { controller, enabled: Boolean(state.listening) }), /* @__PURE__ */ import_react52.default.createElement("span", { className: "dlv-status", role: "status", "aria-live": "polite" }, status), /* @__PURE__ */ import_react52.default.createElement(
5082
5246
  DeliveryModeButton,
5083
5247
  {
5084
5248
  mode: state.settings.sendingMode || "manual",
5085
5249
  onChange: (sendingMode) => invoke("updateSettings", { sendingMode })
5086
5250
  }
5087
- ), /* @__PURE__ */ import_react51.default.createElement(
5251
+ ), /* @__PURE__ */ import_react52.default.createElement(
5088
5252
  IconButton,
5089
5253
  {
5090
5254
  className: `dlv-live-toggle${state.speechSegmentsRemaining > 0 ? " dlv-live-toggle-expanded" : ""}`,
@@ -5096,21 +5260,21 @@ function ConversationStatusBar({
5096
5260
  "aria-checked": autoPlayback,
5097
5261
  onClick: () => invoke("updateSettings", { announceAssistantMessages: !autoPlayback })
5098
5262
  }
5099
- ), remaining ? /* @__PURE__ */ import_react51.default.createElement(
5263
+ ), remaining ? /* @__PURE__ */ import_react52.default.createElement(
5100
5264
  IconButton,
5101
5265
  {
5102
5266
  label: settings.autoSend.cancel(),
5103
5267
  icon: "close",
5104
5268
  onClick: () => invoke("cancelAutoSend")
5105
5269
  }
5106
- ) : null, state.conversation && !capture ? /* @__PURE__ */ import_react51.default.createElement(
5270
+ ) : null, state.conversation && !capture ? /* @__PURE__ */ import_react52.default.createElement(
5107
5271
  IconButton,
5108
5272
  {
5109
5273
  label: recognition.microphone.takeControl(),
5110
5274
  icon: "mic",
5111
5275
  onClick: () => invoke("startConversation")
5112
5276
  }
5113
- ) : null, capture ? /* @__PURE__ */ import_react51.default.createElement(
5277
+ ) : null, capture ? /* @__PURE__ */ import_react52.default.createElement(
5114
5278
  IconButton,
5115
5279
  {
5116
5280
  className: "dlv-live-toggle dlv-mic-state",
@@ -5124,8 +5288,8 @@ function ConversationStatusBar({
5124
5288
  "data-muted": state.muted ? "true" : "false",
5125
5289
  onClick: () => invoke(state.muted ? "resumeListeningInput" : "muteListening")
5126
5290
  }
5127
- ) : null, /* @__PURE__ */ import_react51.default.createElement(PlaybackControls, { state, invoke })),
5128
- /* @__PURE__ */ import_react51.default.createElement(
5291
+ ) : null, /* @__PURE__ */ import_react52.default.createElement(PlaybackControls, { state, invoke })),
5292
+ /* @__PURE__ */ import_react52.default.createElement(
5129
5293
  ErrorMessage,
5130
5294
  {
5131
5295
  error: error || state.error,
@@ -5141,7 +5305,7 @@ function ConversationStatusBar({
5141
5305
  }
5142
5306
 
5143
5307
  // src/modules/conversation/components/SpeakButton.tsx
5144
- var import_react52 = __toESM(require("react"), 1);
5308
+ var import_react53 = __toESM(require("react"), 1);
5145
5309
  function SpeakButton({
5146
5310
  active = false,
5147
5311
  disabled = false,
@@ -5150,7 +5314,7 @@ function SpeakButton({
5150
5314
  }) {
5151
5315
  const { scoped: speak } = useLanguage((ctx) => ctx.speak);
5152
5316
  const resolvedLabel = label ?? (active ? speak.playback.stop() : speak.playback.message());
5153
- return /* @__PURE__ */ import_react52.default.createElement(
5317
+ return /* @__PURE__ */ import_react53.default.createElement(
5154
5318
  IconButton,
5155
5319
  {
5156
5320
  className: "dlv-speaker",
@@ -5285,11 +5449,11 @@ var styles = `
5285
5449
  .dlv-settings h3,.dlv-settings p{margin:0}
5286
5450
  .dlv-settings-heading{display:flex;align-items:center;gap:8px;min-width:0}.dlv-settings-heading h3{white-space:nowrap}.dlv-heading-divider{height:1px;min-width:12px;flex:1;background:var(--dsw-alias-border-l1)}.dlv-settings-heading .dlv-star-badge{flex:none}
5287
5451
  .dlv-version-badges{display:flex;flex:none;gap:5px;align-items:center}.dlv-shields-badge{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;height:20px;gap:4px;padding:3px 5px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:10px;font-weight:650;line-height:12px;text-decoration:none}.dlv-shields-badge img{display:block;width:12px;height:12px;max-width:100%}.dlv-shields-badge:focus-visible{border-radius:4px;outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dlv-version-badge{display:inline-flex;align-items:center;min-height:24px;box-sizing:border-box;padding:3px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;line-height:16px;text-decoration:none}.dlv-version-badges a.dlv-version-badge:hover{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}.dlv-star-badge{padding:4px 10px;color:var(--dsw-alias-label-primary)}.dlv-update-icon{margin-right:4px;font-size:14px;line-height:1}.dlv-star-icon{margin-right:4px;color:#f59e0b;font-size:14px;line-height:1;text-shadow:0 0 8px rgba(245,158,11,.28)}.dlv-version-badges a.dlv-star-badge:hover{border-color:#f59e0b;background:rgba(245,158,11,.08)}.dlv-update-badge{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}
5288
- .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}
5452
+ .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;display:inline-flex;align-items:center;gap:6px;flex:none;padding:7px 1px 9px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:16px;font-weight:600;line-height:22px;cursor:pointer}.dlv-settings-tab-icon{width:17px;height:17px;flex:none}.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:18px;padding:8px 0 12px}.dlv-settings-subcard{min-width:0;margin:8px 0 0}.dlv-settings-subcard>summary{cursor:pointer;list-style:none;display:flex;align-items:center;gap:9px;padding:10px 12px;border-bottom:1px solid var(--dsw-alias-border-l1);border-radius:6px 6px 0 0;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 38%,transparent);color:var(--dsw-alias-label-secondary);font-size:14px;font-weight:500;line-height:20px}.dlv-settings-subcard>summary::-webkit-details-marker{display:none}.dlv-settings-subcard>summary:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}.dlv-settings-subcard-icon{width:16px;height:16px;flex:none}.dlv-settings-subcard-chevron{width:16px;height:16px;margin-left:auto;flex:none;transition:transform .15s}.dlv-settings-subcard[open]>summary .dlv-settings-subcard-chevron{transform:rotate(180deg)}.dlv-settings-subcard-body{display:grid;gap:16px;padding:18px 0 6px}
5289
5453
  .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}
5290
5454
  .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}
5291
- .dlv-settings small,.dlv-settings p{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
5292
- .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}
5455
+ .dlv-settings small,.dlv-settings p{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}.dlv-settings .dlv-setting-description{line-height:1.65;margin:3px 0 5px}.dlv-settings .dlv-conversation-fields{display:grid;gap:20px;padding:4px 0}
5456
+ .dlv-preset-group{display:grid;grid-template-columns:repeat(2,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}
5293
5457
  .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}
5294
5458
  @media(max-width:480px){.dlv-settings-heading{gap:6px}.dlv-heading-divider{min-width:4px}.dlv-settings-heading .dlv-star-badge{padding:3px 6px;font-size:10px}.dlv-preset-group{grid-template-columns:1fr}.dlv-pill{flex-wrap:wrap}.dlv-wave{flex-basis:90px}.dlv-status{flex-basis:100px}}
5295
5459
  `;
@@ -5301,6 +5465,8 @@ function apply(ctx) {
5301
5465
  const SettingsPanel = createLiveVoiceSettings(t, ctx.locale);
5302
5466
  const controllers = /* @__PURE__ */ new Map();
5303
5467
  const retiring = /* @__PURE__ */ new Set();
5468
+ const preferences = createSettingsClient();
5469
+ const settingsControllers = /* @__PURE__ */ new Set();
5304
5470
  let disposed = false;
5305
5471
  let voiceModeActive = false;
5306
5472
  const publishVoiceContext = (entry, active = entry?.controller?.getSnapshot().conversation === true) => {
@@ -5327,16 +5493,19 @@ function apply(ctx) {
5327
5493
  "recognitionProcessLocally",
5328
5494
  "recognitionAutoInstall",
5329
5495
  "voiceDetectionPreset",
5496
+ "voiceDetectionCustomSilenceMs",
5330
5497
  "recognitionMaxUtteranceSeconds"
5331
5498
  ]);
5332
5499
  const changesRecognition = (next) => Object.keys(next).some((key) => recognitionSettingKeys.has(key));
5333
5500
  const recognitionFor = (settings, meter) => settings.recognitionEngine === "qwen-http" ? new QwenHttpRecognitionEngine({
5334
5501
  meter,
5335
5502
  voiceDetectionPreset: settings.voiceDetectionPreset,
5503
+ voiceDetectionCustomSilenceMs: settings.voiceDetectionCustomSilenceMs,
5336
5504
  maxUtteranceSeconds: settings.recognitionMaxUtteranceSeconds
5337
5505
  }) : settings.recognitionEngine === "whisper-http" ? new WhisperHttpRecognitionEngine({
5338
5506
  meter,
5339
5507
  voiceDetectionPreset: settings.voiceDetectionPreset,
5508
+ voiceDetectionCustomSilenceMs: settings.voiceDetectionCustomSilenceMs,
5340
5509
  maxUtteranceSeconds: settings.recognitionMaxUtteranceSeconds
5341
5510
  }) : new BrowserRecognitionEngine({
5342
5511
  processLocally: settings.recognitionProcessLocally,
@@ -5346,6 +5515,38 @@ function apply(ctx) {
5346
5515
  if (!disposed && !controller.disposed)
5347
5516
  controller.patch({ error: error?.message || String(error) });
5348
5517
  });
5518
+ async function savePreferences(controller, next) {
5519
+ try {
5520
+ return await preferences.save(next);
5521
+ } catch {
5522
+ const error = new Error(t("dsh-live-voice.settings.persistence.saveError"));
5523
+ if (!disposed && !controller.disposed) controller.patch({ error: error.message });
5524
+ throw error;
5525
+ }
5526
+ }
5527
+ const unsubscribePreferences = preferences.subscribe((settings) => {
5528
+ if (disposed) return;
5529
+ for (const entry of controllers.values()) {
5530
+ if (entry.closed) continue;
5531
+ const previous = entry.controller.getSnapshot().settings;
5532
+ if ([...recognitionSettingKeys].some((key) => previous[key] !== settings[key]))
5533
+ entry.controller.replaceRecognition(recognitionFor(settings, entry.controller.meter));
5534
+ entry.applySettings(settings);
5535
+ }
5536
+ for (const c of settingsControllers) {
5537
+ if (c.disposed) continue;
5538
+ const previous = c.getSnapshot().settings;
5539
+ if ([...recognitionSettingKeys].some((key) => previous[key] !== settings[key]))
5540
+ c.replaceRecognition(recognitionFor(settings, c.meter));
5541
+ c.applySavedSettings(settings);
5542
+ for (const engine of Object.values(c.engines)) engine.lang = settings.lang;
5543
+ run(c, c.refreshCapabilities());
5544
+ }
5545
+ });
5546
+ ctx.effect(
5547
+ () => () => unsubscribePreferences(),
5548
+ "dsh-live-voice: remove preferences subscription"
5549
+ );
5349
5550
  function retire(entry) {
5350
5551
  if (entry.closed) return;
5351
5552
  entry.closed = true;
@@ -5374,6 +5575,10 @@ function apply(ctx) {
5374
5575
  key,
5375
5576
  draft: "",
5376
5577
  pendingDraft: void 0,
5578
+ publishedDraft: void 0,
5579
+ publishedRevision: void 0,
5580
+ pendingRevision: void 0,
5581
+ staleDrafts: /* @__PURE__ */ new Set(),
5377
5582
  composers: /* @__PURE__ */ new Map(),
5378
5583
  refs: 0,
5379
5584
  buttons: 0,
@@ -5381,14 +5586,7 @@ function apply(ctx) {
5381
5586
  closed: false,
5382
5587
  lastVoiceContextBody: null
5383
5588
  };
5384
- let settings = {};
5385
- try {
5386
- settings = normalizeSettings(
5387
- JSON.parse(localStorage.getItem("dsh-live-voice.settings") || "{}")
5388
- );
5389
- } catch {
5390
- settings = normalizeSettings(null);
5391
- }
5589
+ const settings = preferences.getSnapshot();
5392
5590
  const engineBrowser = new BrowserSpeakingEngine({ lang: settings.lang || "pt-BR" });
5393
5591
  const engineSay = new HostAudioSpeakingEngine({
5394
5592
  endpoint: "/api/dsh-live-voice/say/speech",
@@ -5443,8 +5641,12 @@ function apply(ctx) {
5443
5641
  if (disposed || entry.closed) return;
5444
5642
  const owner = [...entry.composers.values()].at(-1);
5445
5643
  if (!owner) return;
5644
+ if (entry.pendingDraft === void 0) entry.staleDrafts.clear();
5645
+ entry.staleDrafts.add(entry.draft);
5646
+ if (entry.publishedDraft !== void 0) entry.staleDrafts.add(entry.publishedDraft);
5446
5647
  entry.draft = text;
5447
5648
  entry.pendingDraft = text;
5649
+ entry.pendingRevision = entry.publishedRevision;
5448
5650
  owner.actions.setDraft(text);
5449
5651
  }
5450
5652
  },
@@ -5517,14 +5719,7 @@ function apply(ctx) {
5517
5719
  controller.updateSettings = (next) => {
5518
5720
  if (disposed || entry.closed) return;
5519
5721
  entry.applySettings(next);
5520
- const settings2 = controller.getSnapshot().settings;
5521
- try {
5522
- localStorage.setItem("dsh-live-voice.settings", JSON.stringify(settings2));
5523
- } catch {
5524
- }
5525
- for (const other of controllers.values()) {
5526
- if (other !== entry && !other.closed) other.applySettings(settings2);
5527
- }
5722
+ return savePreferences(controller, next);
5528
5723
  };
5529
5724
  for (const method of ["stopListening", "cancelDictation", "endConversation", "stopSpeech"]) {
5530
5725
  const original = controller[method].bind(controller);
@@ -5549,7 +5744,12 @@ function apply(ctx) {
5549
5744
  return ownership.run(
5550
5745
  controller,
5551
5746
  [...controllers.values()].map((other) => other.controller).concat([...retiring]),
5552
- () => {
5747
+ async () => {
5748
+ try {
5749
+ await preferences.ready;
5750
+ } catch {
5751
+ throw new Error(t("dsh-live-voice.settings.persistence.loadError"));
5752
+ }
5553
5753
  if (disposed || entry.closed || !entry.refs || request !== entry.request) return;
5554
5754
  if (method !== "speak" && !entry.composers.size) return;
5555
5755
  const result = original(...args);
@@ -5560,12 +5760,16 @@ function apply(ctx) {
5560
5760
  };
5561
5761
  }
5562
5762
  controllers.set(key, entry);
5763
+ void preferences.ready.catch(() => {
5764
+ if (!entry.closed)
5765
+ controller.patch({ error: t("dsh-live-voice.settings.persistence.loadError") });
5766
+ });
5563
5767
  run(controller, controller.refreshCapabilities());
5564
5768
  return entry;
5565
5769
  }
5566
5770
  function useEntry(sessionId, kind) {
5567
- const [entry, setEntry] = import_react53.default.useState(null);
5568
- import_react53.default.useLayoutEffect(() => {
5771
+ const [entry, setEntry] = import_react54.default.useState(null);
5772
+ import_react54.default.useLayoutEffect(() => {
5569
5773
  if (disposed) return;
5570
5774
  const current = get(sessionId);
5571
5775
  current.refs++;
@@ -5585,14 +5789,14 @@ function apply(ctx) {
5585
5789
  function useComposer(entry, props) {
5586
5790
  const subscribedInput = props.useInput?.((value) => value);
5587
5791
  const input = subscribedInput ?? props.input;
5588
- const token = import_react53.default.useRef({});
5589
- import_react53.default.useLayoutEffect(() => {
5792
+ const token = import_react54.default.useRef({});
5793
+ import_react54.default.useLayoutEffect(() => {
5590
5794
  if (!entry || entry.closed || disposed) return;
5591
5795
  return () => {
5592
5796
  entry.composers.delete(token.current);
5593
5797
  };
5594
5798
  }, [entry]);
5595
- import_react53.default.useLayoutEffect(() => {
5799
+ import_react54.default.useLayoutEffect(() => {
5596
5800
  if (!entry || entry.closed || disposed) return;
5597
5801
  if (!input || typeof props.inputActions?.setDraft !== "function") return;
5598
5802
  entry.composers.set(token.current, {
@@ -5618,13 +5822,17 @@ function apply(ctx) {
5618
5822
  if (voiceModeActive && !entry.controller.getSnapshot().conversation)
5619
5823
  run(entry.controller, entry.controller.startConversation());
5620
5824
  }, [entry, input, props.inputActions]);
5621
- import_react53.default.useLayoutEffect(() => {
5825
+ import_react54.default.useLayoutEffect(() => {
5622
5826
  if (!entry || entry.closed || disposed || !input) return;
5623
5827
  const published = typeof input.draft === "string" ? input.draft : "";
5828
+ entry.publishedDraft = published;
5829
+ entry.publishedRevision = input.draftRev;
5830
+ const newerRevision = Number.isInteger(input.draftRev) && Number.isInteger(entry.pendingRevision) && input.draftRev > entry.pendingRevision;
5624
5831
  if (entry.pendingDraft === void 0) entry.draft = published;
5625
- else if (published === entry.pendingDraft) {
5832
+ else if (published === entry.pendingDraft || newerRevision || !entry.staleDrafts.has(published)) {
5626
5833
  entry.draft = published;
5627
5834
  entry.pendingDraft = void 0;
5835
+ entry.staleDrafts.clear();
5628
5836
  }
5629
5837
  entry.controller.composerChanged(entry.draft);
5630
5838
  });
@@ -5632,19 +5840,12 @@ function apply(ctx) {
5632
5840
  function Buttons(props) {
5633
5841
  const entry = useEntry(props.sessionId, "buttons");
5634
5842
  useComposer(entry, props);
5635
- return entry ? /* @__PURE__ */ import_react53.default.createElement(ConversationControls, { controller: entry.controller }) : null;
5843
+ return entry ? /* @__PURE__ */ import_react54.default.createElement(ConversationControls, { controller: entry.controller }) : null;
5636
5844
  }
5637
5845
  function Settings() {
5638
- const [controller, setController] = import_react53.default.useState(null);
5639
- import_react53.default.useEffect(() => {
5640
- let settings;
5641
- try {
5642
- settings = normalizeSettings(
5643
- JSON.parse(localStorage.getItem("dsh-live-voice.settings") || "{}")
5644
- );
5645
- } catch {
5646
- settings = normalizeSettings(null);
5647
- }
5846
+ const [controller, setController] = import_react54.default.useState(null);
5847
+ import_react54.default.useEffect(() => {
5848
+ const settings = preferences.getSnapshot();
5648
5849
  const browser = new BrowserSpeakingEngine({ lang: settings.lang });
5649
5850
  const qwen = new QwenHttpSpeakingEngine({ lang: settings.lang });
5650
5851
  const meter = new MicrophoneMeter();
@@ -5670,9 +5871,21 @@ function apply(ctx) {
5670
5871
  c.speak = (...args) => ownership.run(
5671
5872
  c,
5672
5873
  [...controllers.values()].map((entry) => entry.controller).concat([...retiring]),
5673
- () => speak(...args)
5874
+ async () => {
5875
+ try {
5876
+ await preferences.ready;
5877
+ } catch {
5878
+ throw new Error(t("dsh-live-voice.settings.persistence.loadError"));
5879
+ }
5880
+ if (!disposed && !c.disposed) return speak(...args);
5881
+ }
5674
5882
  );
5675
5883
  const update = c.updateSettings.bind(c);
5884
+ c.applySavedSettings = update;
5885
+ settingsControllers.add(c);
5886
+ void preferences.ready.catch(
5887
+ () => c.patch({ error: t("dsh-live-voice.settings.persistence.loadError") })
5888
+ );
5676
5889
  let settingsRevision = 0;
5677
5890
  c.updateSettings = async (next) => {
5678
5891
  const revision = ++settingsRevision;
@@ -5681,10 +5894,11 @@ function apply(ctx) {
5681
5894
  qwen.lang = c.getSnapshot().settings.lang;
5682
5895
  const settings2 = c.getSnapshot().settings;
5683
5896
  if (changesRecognition(next)) c.replaceRecognition(recognitionFor(settings2, c.meter));
5684
- localStorage.setItem("dsh-live-voice.settings", JSON.stringify(settings2));
5897
+ const saved = savePreferences(c, next);
5685
5898
  run(c, c.refreshCapabilities());
5686
5899
  const active = [...controllers.values()];
5687
- await Promise.allSettled([
5900
+ const [savedSettings] = await Promise.all([
5901
+ saved,
5688
5902
  c.endConversation(),
5689
5903
  ...active.map((entry) => entry.controller.endConversation())
5690
5904
  ]);
@@ -5692,8 +5906,10 @@ function apply(ctx) {
5692
5906
  for (const entry of controllers.values()) {
5693
5907
  if (entry.closed) continue;
5694
5908
  if (changesRecognition(next))
5695
- entry.controller.replaceRecognition(recognitionFor(settings2, entry.controller.meter));
5696
- entry.applySettings(settings2);
5909
+ entry.controller.replaceRecognition(
5910
+ recognitionFor(savedSettings, entry.controller.meter)
5911
+ );
5912
+ entry.applySettings(savedSettings);
5697
5913
  run(entry.controller, entry.controller.refreshCapabilities());
5698
5914
  }
5699
5915
  };
@@ -5703,24 +5919,25 @@ function apply(ctx) {
5703
5919
  refresh();
5704
5920
  return () => {
5705
5921
  document.removeEventListener("dsh-live-voice:capabilitieschanged", refresh);
5922
+ settingsControllers.delete(c);
5706
5923
  void c.dispose();
5707
5924
  };
5708
5925
  }, []);
5709
- return controller ? /* @__PURE__ */ import_react53.default.createElement(SettingsPanel, { controller }) : null;
5926
+ return controller ? /* @__PURE__ */ import_react54.default.createElement(SettingsPanel, { controller }) : null;
5710
5927
  }
5711
5928
  registerSettingsSlot(ctx, Settings, () => t("dsh-live-voice.commons.pluginName"));
5712
5929
  function Dock(props) {
5713
5930
  const entry = useEntry(props.sessionId, "dock");
5714
5931
  useComposer(entry, props);
5715
- return entry ? /* @__PURE__ */ import_react53.default.createElement(ConversationStatusBar, { controller: entry.controller }) : null;
5932
+ return entry ? /* @__PURE__ */ import_react54.default.createElement(ConversationStatusBar, { controller: entry.controller }) : null;
5716
5933
  }
5717
5934
  function QuestionStatusView({ entry }) {
5718
- const snapshot = import_react53.default.useSyncExternalStore(
5935
+ const snapshot = import_react54.default.useSyncExternalStore(
5719
5936
  entry.controller.subscribe,
5720
5937
  entry.controller.getSnapshot
5721
5938
  );
5722
- const [overlayStyle, setOverlayStyle] = import_react53.default.useState();
5723
- import_react53.default.useLayoutEffect(() => {
5939
+ const [overlayStyle, setOverlayStyle] = import_react54.default.useState();
5940
+ import_react54.default.useLayoutEffect(() => {
5724
5941
  if (!snapshot.answeringQuestion) return;
5725
5942
  const seat = document.querySelector("[data-composer-seat]");
5726
5943
  if (!seat) return;
@@ -5742,7 +5959,7 @@ function apply(ctx) {
5742
5959
  }, [snapshot.answeringQuestion]);
5743
5960
  const target = typeof document === "undefined" ? null : document.body;
5744
5961
  return snapshot.answeringQuestion && target && overlayStyle ? (0, import_react_dom.createPortal)(
5745
- /* @__PURE__ */ import_react53.default.createElement(
5962
+ /* @__PURE__ */ import_react54.default.createElement(
5746
5963
  ConversationStatusBar,
5747
5964
  {
5748
5965
  controller: entry.controller,
@@ -5756,19 +5973,19 @@ function apply(ctx) {
5756
5973
  }
5757
5974
  function QuestionStatus(props) {
5758
5975
  const entry = useEntry(props.sessionId, "question-status");
5759
- return entry ? /* @__PURE__ */ import_react53.default.createElement(QuestionStatusView, { entry }) : null;
5976
+ return entry ? /* @__PURE__ */ import_react54.default.createElement(QuestionStatusView, { entry }) : null;
5760
5977
  }
5761
5978
  function ActionView({ entry, messageId }) {
5762
- const snapshot = import_react53.default.useSyncExternalStore(
5979
+ const snapshot = import_react54.default.useSyncExternalStore(
5763
5980
  entry.controller.subscribe,
5764
5981
  entry.controller.getSnapshot
5765
5982
  );
5766
- const chat = import_react53.default.useSyncExternalStore(entry.subscribeChat, entry.readChat);
5983
+ const chat = import_react54.default.useSyncExternalStore(entry.subscribeChat, entry.readChat);
5767
5984
  const message2 = addressedTurn(assistantMessages(chat), messageId);
5768
5985
  const capability = snapshot.capabilities[snapshot.settings.engine];
5769
5986
  const active = snapshot.speaking && message2.id === snapshot.activeMessageId;
5770
5987
  const unavailable = capability?.supported !== true;
5771
- return /* @__PURE__ */ import_react53.default.createElement(
5988
+ return /* @__PURE__ */ import_react54.default.createElement(
5772
5989
  SpeakButton,
5773
5990
  {
5774
5991
  active,
@@ -5783,7 +6000,7 @@ function apply(ctx) {
5783
6000
  }
5784
6001
  function Action(props) {
5785
6002
  const entry = useEntry(props.sessionId, "action");
5786
- return entry ? /* @__PURE__ */ import_react53.default.createElement(ActionView, { entry, messageId: props.messageId }) : null;
6003
+ return entry ? /* @__PURE__ */ import_react54.default.createElement(ActionView, { entry, messageId: props.messageId }) : null;
5787
6004
  }
5788
6005
  ctx.effect(() => {
5789
6006
  const style = document.createElement("style");