diff --git a/interview-app/public/index.html b/interview-app/public/index.html index f54fdf8..9b49dc9 100644 --- a/interview-app/public/index.html +++ b/interview-app/public/index.html @@ -2245,6 +2245,8 @@

Parent Supervision Required

requiresUserApiKey: false, apiKeyHeader: 'x-anthropic-api-key', anthropicApiKey: '', + lastSpokenNormalized: '', + lastSpokenEndedAt: 0, }; // ===== DOM refs ===== @@ -3387,7 +3389,8 @@

Parent Supervision Required

// Strip markdown + emoji for speech (emoji still shows in chat, just not spoken) const clean = text .replace(/---BUILD_PHASE---[\s\S]*?---END_BUILD_PHASE---/g, '') - .replace(/---PREVIEW---[\s\S]*?---END_PREVIEW---/g, 'Check out this quick preview I made!') + // Avoid speaking preview boilerplate; STT can hear it and create loops. + .replace(/---PREVIEW---[\s\S]*?---END_PREVIEW---/g, '') .replace(/---INTERVIEW_COMPLETE---[\s\S]*---END_INTERVIEW---/g, 'I\'ve put together a summary of your app idea!') .replace(/[#*_`\[\]()>]/g, '') .replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, '') @@ -3397,6 +3400,7 @@

Parent Supervision Required

// Chrome silently stops speaking long text. Split into sentences and chain them. const chunks = clean.match(/[^.!?]+[.!?]+|[^.!?]+$/g) || [clean]; + const normalizedClean = clean.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); state.isSpeaking = true; setBartMood('talking'); @@ -3405,7 +3409,13 @@

Parent Supervision Required

let i = 0; function speakNext() { if (i >= chunks.length) { - setTimeout(() => { state.isSpeaking = false; setBartMood('idle'); resolve(); }, 400); + setTimeout(() => { + state.lastSpokenNormalized = normalizedClean; + state.lastSpokenEndedAt = Date.now(); + state.isSpeaking = false; + setBartMood('idle'); + resolve(); + }, 400); return; } const chunk = chunks[i].trim(); @@ -3426,6 +3436,21 @@

Parent Supervision Required

}); } +function normalizeSpeechText(raw) { + return String(raw || '').toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim(); +} + +function looksLikeAssistantEcho(raw) { + const text = normalizeSpeechText(raw); + if (!text) return false; + if (text === 'check out this quick preview i made') return true; + + const ageMs = Date.now() - (state.lastSpokenEndedAt || 0); + if (!state.lastSpokenNormalized || ageMs > 8000 || text.length < 12) return false; + + return state.lastSpokenNormalized.includes(text) || text.includes(state.lastSpokenNormalized.slice(0, 40)); +} + // ===== Speech Recognition (STT) ===== function initRecognition() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; @@ -3461,6 +3486,11 @@

Parent Supervision Required

silenceTimer = setTimeout(() => { if (state.isListening) { const text = $input.value.trim(); + if (looksLikeAssistantEcho(text)) { + $input.value = ''; + $sendBtn.disabled = true; + return; + } stopListening(); if (text) sendMessage(text); } @@ -3473,6 +3503,27 @@

Parent Supervision Required

const text = $input.value.trim(); clearTimeout(silenceTimer); if (text) { + if (looksLikeAssistantEcho(text)) { + $input.value = ''; + $sendBtn.disabled = true; + if (state.micActive && !state.isWaiting && !state.isSpeaking) { + try { + recog.start(); + return; + } catch (e) { + setTimeout(() => { + if (state.micActive && !state.isWaiting && !state.isSpeaking) { + try { recog.start(); } catch (e2) {} + } + }, 300); + return; + } + } + state.isListening = false; + $micBtn.classList.remove('recording'); + $listeningLabel.classList.remove('visible'); + return; + } // Got text — send it, mic will restart after AI responds state.isListening = false; $micBtn.classList.remove('recording');