bep40 commited on
Commit
c889b86
·
verified ·
1 Parent(s): 9541c02

FIX: auto-start voice typing after AI finishes speaking so user can respond without manual mic click. Also stop orphaned VoiceTyper instances before creating new ones to prevent mic disconnect and double-send.

Browse files
Files changed (1) hide show
  1. src/app.js +45 -2
src/app.js CHANGED
@@ -272,6 +272,8 @@ let textMode = false;
272
  let voiceTyper = null;
273
  let config = { lb: false, allowDirect: true };
274
  let sessionInProgress = false;
 
 
275
  let autoGreetingSent = false;
276
  let preFetchedNews = [];
277
  let latestNewsUrl = null;
@@ -1017,6 +1019,8 @@ function showTextChat(show) {
1017
  function onVoiceResult(e) {
1018
  if (!e || !e.transcript) return;
1019
  if (!chatInput) return;
 
 
1020
  const text = e.transcript.trim();
1021
  chatInput.value = text;
1022
  if (e.isFinal && sessionInProgress) {
@@ -1025,6 +1029,10 @@ function onVoiceResult(e) {
1025
  if (triggerRe.test(text)) {
1026
  setCaption("Đang gửi…", "live");
1027
  sendTextMessage();
 
 
 
 
1028
  } else {
1029
  setCaption("Nhấp Send để gửi", "live");
1030
  }
@@ -1034,6 +1042,13 @@ function onVoiceResult(e) {
1034
  /** Start (or restart) Vietnamese voice recognition in text chat mode. */
1035
  function startVoiceTyping() {
1036
  if (!sessionInProgress || !chatInput) return false;
 
 
 
 
 
 
 
1037
  voiceTyper = new VoiceTyper({ lang: "vi-VN", continuous: true, interimResults: true, autoSubmit: false });
1038
  if (!voiceTyper.init()) {
1039
  // Browser may not support Vietnamese locale — try English as fallback
@@ -1046,6 +1061,13 @@ function startVoiceTyping() {
1046
  }
1047
  voiceTyper.onResult = onVoiceResult;
1048
  voiceTyper.onError = (e) => console.warn("[voiceTyper]", e);
 
 
 
 
 
 
 
1049
  voiceTyper.start();
1050
  setCaption("🎤 Listening…", "live");
1051
  return true;
@@ -1055,6 +1077,7 @@ function startVoiceTyping() {
1055
  function stopVoiceTyping() {
1056
  if (voiceTyper && voiceTyper.active) {
1057
  voiceTyper.stop();
 
1058
  setCaption("Voice stopped", "live");
1059
  }
1060
  }
@@ -1075,6 +1098,11 @@ function sendTextMessage() {
1075
  const text = chatInput.value.trim();
1076
  if (!text) return;
1077
 
 
 
 
 
 
1078
  addChatMessage("user", text);
1079
 
1080
  // New topic? re-filter the news list and reset the image sequence.
@@ -1090,8 +1118,8 @@ function sendTextMessage() {
1090
  if (!sendTextViaSession(text)) {
1091
  setCaption("QUEUED…");
1092
  }
1093
- // Auto-restart voice typing for continuous conversation
1094
- setTimeout(() => startVoiceTyping(), 300);
1095
  }
1096
 
1097
  if (newsCloseBtn) {
@@ -1363,7 +1391,18 @@ function onStatus(status) {
1363
  break;
1364
  }
1365
 
 
 
 
 
 
 
 
 
1366
  if (status === "user-speaking") {
 
 
 
1367
  subtitles.classList.remove("visible");
1368
  showTextChat(textMode);
1369
  }
@@ -1660,6 +1699,10 @@ function _attachClientEvents(c) {
1660
  }
1661
  _topicTagsRendered = false;
1662
  try { void renderTopicTags(); } catch (e) { console.warn("[topic] render:", e); }
 
 
 
 
1663
  });
1664
 
1665
  c.addEventListener("toolcall", (e) => {
 
272
  let voiceTyper = null;
273
  let config = { lb: false, allowDirect: true };
274
  let sessionInProgress = false;
275
+ let _lastVoiceText = "";
276
+ let _lastVoiceSendMs = 0;
277
  let autoGreetingSent = false;
278
  let preFetchedNews = [];
279
  let latestNewsUrl = null;
 
1019
  function onVoiceResult(e) {
1020
  if (!e || !e.transcript) return;
1021
  if (!chatInput) return;
1022
+ // Ignore results when AI is speaking or session not in user-turn
1023
+ if (!sessionInProgress) return;
1024
  const text = e.transcript.trim();
1025
  chatInput.value = text;
1026
  if (e.isFinal && sessionInProgress) {
 
1029
  if (triggerRe.test(text)) {
1030
  setCaption("Đang gửi…", "live");
1031
  sendTextMessage();
1032
+ // Stop voice typing after auto-send — user must click mic again to speak
1033
+ if (voiceTyper && voiceTyper.active) {
1034
+ voiceTyper.stop();
1035
+ }
1036
  } else {
1037
  setCaption("Nhấp Send để gửi", "live");
1038
  }
 
1042
  /** Start (or restart) Vietnamese voice recognition in text chat mode. */
1043
  function startVoiceTyping() {
1044
  if (!sessionInProgress || !chatInput) return false;
1045
+ // Don't restart if already listening
1046
+ if (voiceTyper && voiceTyper.active) return true;
1047
+ // FIX: stop any previous VoiceTyper first — orphaned instances steal the mic
1048
+ if (voiceTyper) {
1049
+ voiceTyper.stop();
1050
+ voiceTyper = null;
1051
+ }
1052
  voiceTyper = new VoiceTyper({ lang: "vi-VN", continuous: true, interimResults: true, autoSubmit: false });
1053
  if (!voiceTyper.init()) {
1054
  // Browser may not support Vietnamese locale — try English as fallback
 
1061
  }
1062
  voiceTyper.onResult = onVoiceResult;
1063
  voiceTyper.onError = (e) => console.warn("[voiceTyper]", e);
1064
+ voiceTyper.onSoundStart = () => setCaption("🎤 Listening…", "live");
1065
+ voiceTyper.onSoundEnd = () => {
1066
+ // Keep listening — only stop when AI speaks or user clicks mic again
1067
+ if (voiceTyper && voiceTyper.active) {
1068
+ // Do nothing; continuous listening is intentional
1069
+ }
1070
+ };
1071
  voiceTyper.start();
1072
  setCaption("🎤 Listening…", "live");
1073
  return true;
 
1077
  function stopVoiceTyping() {
1078
  if (voiceTyper && voiceTyper.active) {
1079
  voiceTyper.stop();
1080
+ voiceTyper = null;
1081
  setCaption("Voice stopped", "live");
1082
  }
1083
  }
 
1098
  const text = chatInput.value.trim();
1099
  if (!text) return;
1100
 
1101
+ // Dedup: ignore if this exact message was already sent recently
1102
+ if (text === _lastVoiceText && Date.now() - _lastVoiceSendMs < 3000) return;
1103
+ _lastVoiceText = text;
1104
+ _lastVoiceSendMs = Date.now();
1105
+
1106
  addChatMessage("user", text);
1107
 
1108
  // New topic? re-filter the news list and reset the image sequence.
 
1118
  if (!sendTextViaSession(text)) {
1119
  setCaption("QUEUED…");
1120
  }
1121
+ // Voice typing stays active for continuous conversation;
1122
+ // it will auto-stop when the AI starts speaking.
1123
  }
1124
 
1125
  if (newsCloseBtn) {
 
1391
  break;
1392
  }
1393
 
1394
+ if (status === "ai-speaking") {
1395
+ // AI is speaking — stop voice typing so mic doesn't pick up audio
1396
+ if (voiceTyper && voiceTyper.active) {
1397
+ voiceTyper.stop();
1398
+ }
1399
+ subtitles.classList.add("visible");
1400
+ }
1401
+
1402
  if (status === "user-speaking") {
1403
+ // User's turn to speak — auto-start voice typing so user can speak
1404
+ // immediately without clicking the mic button
1405
+ setTimeout(() => startVoiceTyping(), 400);
1406
  subtitles.classList.remove("visible");
1407
  showTextChat(textMode);
1408
  }
 
1699
  }
1700
  _topicTagsRendered = false;
1701
  try { void renderTopicTags(); } catch (e) { console.warn("[topic] render:", e); }
1702
+ // FIX: auto-start voice typing after AI finishes speaking so the user can respond immediately
1703
+ if (sessionInProgress) {
1704
+ setTimeout(() => startVoiceTyping(), 500);
1705
+ }
1706
  });
1707
 
1708
  c.addEventListener("toolcall", (e) => {