From e6b5aa4b9b3ee2273ede8fc90bddd6bfde2322f7 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 11:59:18 +0000 Subject: [PATCH] fix: mobile menu overlay, speech bubble timing, TTS static file fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mobile nav menu now overlays content (absolute positioning) instead of pushing it down - Speech bubbles stay visible for minimum 400ms even when TTS resolves instantly or fails - kokoroPlayCached checks audio cache and loads static files even when Kokoro worker hasn't loaded — fixes TTS not playing on production - CORS_ORIGIN env now supports comma-separated origins - Rename "VIDEO REPLAY" to play icon + "REPLAY" Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/FightViewer.vue | 15 ++++++++++--- frontend/src/components/NavBar.vue | 2 +- frontend/src/game/audio/voice.ts | 7 +++--- frontend/src/game/tts.ts | 29 +++++++++++++++++++------ server/src/app.ts | 10 +++++++-- 5 files changed, 47 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue index 4c2c680..5853014 100644 --- a/frontend/src/components/FightViewer.vue +++ b/frontend/src/components/FightViewer.vue @@ -441,8 +441,11 @@ async function _doReplay() { scene.showSpeechBubble('a', round.botAResponse.slice(0, 60), 8) scene.startTalking('a') } + // Ensure bubble stays visible for at least 400ms even if TTS resolves instantly + const minBubbleA = sleep(400).catch(() => {}) if (doTTS) await playAnswerNow(props.fight.botA!.name, round.botAResponse) - else await sleep(insanityMode.value ? 30 : 400) + await minBubbleA + if (!doTTS && !insanityMode.value) await sleep(400) scene?.stopTalking('a') scene?.hideSpeechBubble('a') } @@ -460,8 +463,10 @@ async function _doReplay() { scene.showSpeechBubble('b', round.botBResponse.slice(0, 60), 8) scene.startTalking('b') } + const minBubbleB = sleep(400).catch(() => {}) if (doTTS) await playAnswerNow(props.fight.botB!.name, round.botBResponse) - else await sleep(insanityMode.value ? 30 : 400) + await minBubbleB + if (!doTTS && !insanityMode.value) await sleep(400) scene?.stopTalking('b') scene?.hideSpeechBubble('b') } @@ -809,7 +814,11 @@ async function _doReplay() { :disabled="isReplaying" @click="replay" > - {{ isReplaying ? 'FIGHTING...' : 'VIDEO REPLAY' }} + FIGHTING... + + + REPLAY +