feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { prisma } from "@/lib/db"
|
||||
import { initCurriculum, getLecturaStages, getNumerosStages, getAllLecturaExercises } from "@/curriculum/index"
|
||||
import type { Exercise } from "@/curriculum/types"
|
||||
|
||||
const STAGE_THRESHOLD = 0.8
|
||||
|
||||
async function ensureInitialStage(childId: string, topic: string) {
|
||||
try {
|
||||
const exists = await prisma.curriculumProgress.findFirst({
|
||||
where: { childId, topic },
|
||||
})
|
||||
if (!exists) {
|
||||
await prisma.curriculumProgress.create({
|
||||
data: { childId, topic, stage: 1 },
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Child may not exist yet in FK constraint — proceed gracefully
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCurrentStage(childId: string, topic: string) {
|
||||
await ensureInitialStage(childId, topic)
|
||||
const progress = await prisma.curriculumProgress.findFirst({
|
||||
where: { childId, topic },
|
||||
orderBy: { stage: "asc" },
|
||||
})
|
||||
return progress
|
||||
}
|
||||
|
||||
export async function getNextExercise(
|
||||
childId: string,
|
||||
topic: "lectura" | "numeros",
|
||||
): Promise<Exercise | null> {
|
||||
await initCurriculum()
|
||||
await ensureInitialStage(childId, topic)
|
||||
|
||||
const progress = await prisma.curriculumProgress.findFirst({
|
||||
where: { childId, topic, completedAt: null },
|
||||
orderBy: { stage: "asc" },
|
||||
})
|
||||
|
||||
if (!progress) return null
|
||||
|
||||
const stages = topic === "lectura" ? getLecturaStages() : getNumerosStages()
|
||||
const stage = stages[progress.stage]
|
||||
if (!stage || !stage.exercises.length) return null
|
||||
|
||||
const allAttemps = await prisma.skillAttempt.findMany({
|
||||
where: { exerciseId: { not: null }, sessionLog: { childId } },
|
||||
select: { exerciseId: true, correct: true },
|
||||
})
|
||||
|
||||
const mastered = new Set(
|
||||
allAttemps
|
||||
.filter((a) => a.correct)
|
||||
.map((a) => a.exerciseId),
|
||||
)
|
||||
|
||||
const attempted = new Set(allAttemps.map((a) => a.exerciseId))
|
||||
|
||||
const available = stage.exercises.filter((ex) => {
|
||||
if (mastered.has(ex.id)) return false
|
||||
const prereqsMet = ex.prerequisites.every((p) => mastered.has(p))
|
||||
return prereqsMet
|
||||
})
|
||||
|
||||
if (available.length === 0) {
|
||||
if (attempted.size > 0 && stage.exercises.length > 0) {
|
||||
const masteryRatio = mastered.size / stage.exercises.length
|
||||
if (masteryRatio >= STAGE_THRESHOLD) {
|
||||
await advanceStage(childId, topic, progress.stage)
|
||||
return getNextExercise(childId, topic)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const fsrsCards = await prisma.fsrsCard.findMany({
|
||||
where: { childId, skillCode: { in: available.map((e) => e.skillCode) } },
|
||||
})
|
||||
|
||||
const dueMap = new Map(fsrsCards.map((c) => [c.skillCode, c.dueAt.getTime()]))
|
||||
const now = Date.now()
|
||||
|
||||
const unsorted = [...available]
|
||||
unsorted.sort((a, b) => {
|
||||
const dueA = dueMap.get(a.skillCode) ?? 0
|
||||
const dueB = dueMap.get(b.skillCode) ?? 0
|
||||
const isDueA = dueA <= now ? 0 : 1
|
||||
const isDueB = dueB <= now ? 0 : 1
|
||||
if (isDueA !== isDueB) return isDueA - isDueB
|
||||
if (!attempted.has(a.id) && !attempted.has(b.id)) return 0
|
||||
if (!attempted.has(a.id)) return -1
|
||||
if (!attempted.has(b.id)) return 1
|
||||
return dueA - dueB
|
||||
})
|
||||
|
||||
return unsorted[0] || null
|
||||
}
|
||||
|
||||
async function advanceStage(childId: string, topic: string, currentStage: number) {
|
||||
await prisma.curriculumProgress.updateMany({
|
||||
where: { childId, topic, stage: currentStage },
|
||||
data: { completedAt: new Date() },
|
||||
})
|
||||
const nextStage = currentStage + 1
|
||||
const exists = await prisma.curriculumProgress.findUnique({
|
||||
where: { childId_topic_stage: { childId, topic, stage: nextStage } },
|
||||
})
|
||||
if (!exists) {
|
||||
await prisma.curriculumProgress.create({
|
||||
data: { childId, topic, stage: nextStage },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function getErrorTypeForExercise(
|
||||
exercise: Exercise,
|
||||
correct: boolean,
|
||||
): string | null {
|
||||
if (correct) return null
|
||||
return exercise.errorType || "discriminacion-auditiva"
|
||||
}
|
||||
Reference in New Issue
Block a user