From 5a4caaf1d00796e202d6fee0e0a5a9dba9cd699c Mon Sep 17 00:00:00 2001 From: ImBenji Date: Tue, 28 Apr 2026 01:37:42 +0100 Subject: [PATCH] enhance lesson generation and focus mode functionality --- app/pages/learn/[id]/index.vue | 39 +++++++++++++++++++++---- server/api/topics/[id]/generate.post.ts | 14 ++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/app/pages/learn/[id]/index.vue b/app/pages/learn/[id]/index.vue index 4d652a2..6bd9903 100644 --- a/app/pages/learn/[id]/index.vue +++ b/app/pages/learn/[id]/index.vue @@ -529,6 +529,7 @@ const focusMode = ref(false); const currentChunk = ref<{ text: string; start: number; end: number } | null>(null); const karaokeAudioEl = ref(null); const questionPlaying = ref(false); +const focusEverActivated = ref(false); if (import.meta.client) { const stored = localStorage.getItem("revisione-focus-mode"); @@ -543,8 +544,10 @@ const hasAudio = computed(() => { const showKaraoke = computed(() => { if (!focusMode.value) return false; const ds = displayStep.value; - return (lessonState.mode === "main" || lessonState.mode === "branch") && - (ds?.type === "concept" || ds?.type === "example"); + if (!((lessonState.mode === "main" || lessonState.mode === "branch") && + (ds?.type === "concept" || ds?.type === "example"))) return false; + // dont show overlay if this step has no audio — avoids perma-"..." with missing TTS + return !!(ds?.audioPath && ds?.audioChunks && (ds.audioChunks as any[]).length > 0); }); const showQuestionOverlay = computed(() => @@ -554,10 +557,24 @@ const showQuestionOverlay = computed(() => questionPlaying.value ); -function toggleFocusMode() { +async function toggleFocusMode() { focusMode.value = !focusMode.value; if (import.meta.client) localStorage.setItem("revisione-focus-mode", String(focusMode.value)); + if (focusMode.value) { + // on first activation, re-fetch lesson so we have the latest audio paths from the DB + if (!focusEverActivated.value) { + focusEverActivated.value = true; + try { + const fresh = await $fetch(`/api/topics/${topicId}/lesson`); + lesson.value = fresh; + const s0 = fresh?.content?.steps?.[0]; + console.log("[focus] step 0 audioPath:", s0?.audioPath, "| chunks:", s0?.audioChunks?.length ?? "none"); + } catch (e) { + console.warn("[focus] re-fetch failed, using cached lesson data", e); + } + } + nextTick(() => startStepAudio()); } else { stopAllAudio(); @@ -686,7 +703,16 @@ function startStepAudio() { if (!s) return; if (s.type === "concept" || s.type === "example" || s.type === "summary") { - if (!s.audioPath || !s.audioChunks) return; + if (!s.audioPath || !s.audioChunks || (s.audioChunks as any[]).length === 0) { + // no usable audio for this step — skip it automatically so focus mode stays alive + if (!isLastMainStep.value) { + lessonState.stepIndex++; + nextTick(() => startStepAudio()); + } else { + completeLesson(); + } + return; + } const audio = karaokeAudioEl.value; if (!audio) return; audio.src = s.audioPath; @@ -705,7 +731,10 @@ function startStepAudio() { } } else if (lessonState.mode === "branch") { const bs = currentBranchStep.value; - if (!bs?.audioPath || !bs.audioChunks) return; + if (!bs?.audioPath || !bs.audioChunks || (bs.audioChunks as any[]).length === 0) { + advanceBranchStep(); + return; + } const audio = karaokeAudioEl.value; if (!audio) return; audio.src = bs.audioPath; diff --git a/server/api/topics/[id]/generate.post.ts b/server/api/topics/[id]/generate.post.ts index e8a8f23..5c6248c 100644 --- a/server/api/topics/[id]/generate.post.ts +++ b/server/api/topics/[id]/generate.post.ts @@ -1,6 +1,6 @@ import { db } from "../../../db/index"; import { topics, lessons } from "../../../db/schema"; -import { eq } from "drizzle-orm"; +import { eq, and } from "drizzle-orm"; import { generateLesson } from "../../../utils/generateLesson"; export default defineEventHandler(async (event) => { @@ -19,6 +19,18 @@ export default defineEventHandler(async (event) => { try { await generateLesson(id); + + // pre-generate the next topic silently + const currentTopic = await db.query.topics.findFirst({ where: eq(topics.id, id) }); + const nextTopic = await db.query.topics.findFirst({ + where: and( + eq(topics.courseId, currentTopic!.courseId), + eq(topics.order, currentTopic!.order + 1), + eq(topics.status, "pending") + ) + }); + if (nextTopic) generateLesson(nextTopic.id); + return { status: "ready" }; } catch (err: any) { console.error(`[generate.post] topic ${id} failed: ${err?.message ?? err}`);