Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions interview-app/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2245,6 +2245,8 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>
requiresUserApiKey: false,
apiKeyHeader: 'x-anthropic-api-key',
anthropicApiKey: '',
lastSpokenNormalized: '',
lastSpokenEndedAt: 0,
};

// ===== DOM refs =====
Expand Down Expand Up @@ -3387,7 +3389,8 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>
// 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, '')
Expand All @@ -3397,6 +3400,7 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>

// 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');
Expand All @@ -3405,7 +3409,13 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>
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();
Expand All @@ -3426,6 +3436,21 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>
});
}

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;
Expand Down Expand Up @@ -3461,6 +3486,11 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>
silenceTimer = setTimeout(() => {
if (state.isListening) {
const text = $input.value.trim();
if (looksLikeAssistantEcho(text)) {
$input.value = '';
$sendBtn.disabled = true;
return;
}
stopListening();
if (text) sendMessage(text);
}
Expand All @@ -3473,6 +3503,27 @@ <h3 id="parent-gate-title">Parent Supervision Required</h3>
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');
Expand Down