enhance lesson generation and focus mode functionality

This commit is contained in:
ImBenji 2026-04-28 01:37:42 +01:00
parent 507bc15dcc
commit 5a4caaf1d0
2 changed files with 47 additions and 6 deletions

View file

@ -529,6 +529,7 @@ const focusMode = ref(false);
const currentChunk = ref<{ text: string; start: number; end: number } | null>(null); const currentChunk = ref<{ text: string; start: number; end: number } | null>(null);
const karaokeAudioEl = ref<HTMLAudioElement | null>(null); const karaokeAudioEl = ref<HTMLAudioElement | null>(null);
const questionPlaying = ref(false); const questionPlaying = ref(false);
const focusEverActivated = ref(false);
if (import.meta.client) { if (import.meta.client) {
const stored = localStorage.getItem("revisione-focus-mode"); const stored = localStorage.getItem("revisione-focus-mode");
@ -543,8 +544,10 @@ const hasAudio = computed(() => {
const showKaraoke = computed(() => { const showKaraoke = computed(() => {
if (!focusMode.value) return false; if (!focusMode.value) return false;
const ds = displayStep.value; const ds = displayStep.value;
return (lessonState.mode === "main" || lessonState.mode === "branch") && if (!((lessonState.mode === "main" || lessonState.mode === "branch") &&
(ds?.type === "concept" || ds?.type === "example"); (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(() => const showQuestionOverlay = computed(() =>
@ -554,10 +557,24 @@ const showQuestionOverlay = computed(() =>
questionPlaying.value questionPlaying.value
); );
function toggleFocusMode() { async function toggleFocusMode() {
focusMode.value = !focusMode.value; focusMode.value = !focusMode.value;
if (import.meta.client) localStorage.setItem("revisione-focus-mode", String(focusMode.value)); if (import.meta.client) localStorage.setItem("revisione-focus-mode", String(focusMode.value));
if (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<any>(`/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()); nextTick(() => startStepAudio());
} else { } else {
stopAllAudio(); stopAllAudio();
@ -686,7 +703,16 @@ function startStepAudio() {
if (!s) return; if (!s) return;
if (s.type === "concept" || s.type === "example" || s.type === "summary") { 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; const audio = karaokeAudioEl.value;
if (!audio) return; if (!audio) return;
audio.src = s.audioPath; audio.src = s.audioPath;
@ -705,7 +731,10 @@ function startStepAudio() {
} }
} else if (lessonState.mode === "branch") { } else if (lessonState.mode === "branch") {
const bs = currentBranchStep.value; 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; const audio = karaokeAudioEl.value;
if (!audio) return; if (!audio) return;
audio.src = bs.audioPath; audio.src = bs.audioPath;

View file

@ -1,6 +1,6 @@
import { db } from "../../../db/index"; import { db } from "../../../db/index";
import { topics, lessons } from "../../../db/schema"; import { topics, lessons } from "../../../db/schema";
import { eq } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { generateLesson } from "../../../utils/generateLesson"; import { generateLesson } from "../../../utils/generateLesson";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
@ -19,6 +19,18 @@ export default defineEventHandler(async (event) => {
try { try {
await generateLesson(id); 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" }; return { status: "ready" };
} catch (err: any) { } catch (err: any) {
console.error(`[generate.post] topic ${id} failed: ${err?.message ?? err}`); console.error(`[generate.post] topic ${id} failed: ${err?.message ?? err}`);