feat: multi-profile system with Francesca and Sebastian curricula
- Add Profile enum (ISABELLA, FRANCESCA, SEBASTIAN) to Child model - Add ?profile= query param override to /api/curriculum/next - Pass profile prop from profile-specific pages to ExerciseSession - Fix curriculum import paths (engine uses @/curriculum/ alias) - Clear stale curriculumCache on dev reload - Fix self-referential prerequisites in Francesca etapa-1 - Fix prerequisite chains in Francesca etapa-2,3,4 and Sebastian etapa-2,3 - Add Francesca curriculum: 2-3 digit sums, subtractions, multiplications, short readings - Add Sebastian curriculum: 2-3 digit sums, interactive exercises, Argentine history, flags - All 44 E2E tests pass, TypeScript compiles cleanly
This commit is contained in:
@@ -1,9 +1,53 @@
|
||||
import { prisma } from "@/lib/db"
|
||||
import { initCurriculum, getLecturaStages, getNumerosStages, getAllLecturaExercises } from "@/curriculum/index"
|
||||
import type { Exercise } from "@/curriculum/types"
|
||||
import type { Exercise, Stage } from "@/curriculum/types"
|
||||
|
||||
const STAGE_THRESHOLD = 0.8
|
||||
|
||||
const PROFILE_CURRICULA: Record<string, Record<string, { folder: string; stages: number[] }>> = {
|
||||
ISABELLA: {
|
||||
lectura: { folder: "lectura", stages: [1, 2, 3, 4, 5, 6, 7, 8, 9] },
|
||||
numeros: { folder: "numeros", stages: [1, 2, 3, 4, 5, 6, 7, 8] },
|
||||
},
|
||||
FRANCESCA: {
|
||||
matematicas: { folder: "francesca", stages: [1, 2, 3, 4] },
|
||||
lecturas: { folder: "francesca-lectura", stages: [1, 2] },
|
||||
},
|
||||
SEBASTIAN: {
|
||||
sumas: { folder: "sebastian", stages: [1, 2, 3] },
|
||||
historia: { folder: "sebastian-historia", stages: [1, 2] },
|
||||
banderas: { folder: "sebastian-banderas", stages: [1] },
|
||||
},
|
||||
}
|
||||
|
||||
const curriculumCache: Record<string, Record<string, Stage>> = {}
|
||||
|
||||
async function getStagesForProfile(
|
||||
profile: string,
|
||||
topic: string,
|
||||
): Promise<Record<string, Stage>> {
|
||||
const key = `${profile}-${topic}`
|
||||
// Clear stale cache on every dev call (Next.js hot-reload shares module-level vars)
|
||||
if (curriculumCache[key]) delete curriculumCache[key]
|
||||
|
||||
if (curriculumCache[key]) return curriculumCache[key]
|
||||
|
||||
const config = PROFILE_CURRICULA[profile]?.[topic]
|
||||
if (!config) return {}
|
||||
|
||||
const stages: Record<string, Stage> = {}
|
||||
for (const s of config.stages) {
|
||||
try {
|
||||
const mod = await import(`@/curriculum/${config.folder}/etapa-${s}`)
|
||||
stages[String(s)] = mod.default
|
||||
} catch {
|
||||
// stage not yet implemented
|
||||
}
|
||||
}
|
||||
|
||||
curriculumCache[key] = stages
|
||||
return stages
|
||||
}
|
||||
|
||||
async function ensureInitialStage(childId: string, topic: string) {
|
||||
try {
|
||||
const exists = await prisma.curriculumProgress.findFirst({
|
||||
@@ -30,9 +74,9 @@ export async function getCurrentStage(childId: string, topic: string) {
|
||||
|
||||
export async function getNextExercise(
|
||||
childId: string,
|
||||
topic: "lectura" | "numeros",
|
||||
topic: string,
|
||||
profile: string = "ISABELLA",
|
||||
): Promise<Exercise | null> {
|
||||
await initCurriculum()
|
||||
await ensureInitialStage(childId, topic)
|
||||
|
||||
const progress = await prisma.curriculumProgress.findFirst({
|
||||
@@ -42,8 +86,8 @@ export async function getNextExercise(
|
||||
|
||||
if (!progress) return null
|
||||
|
||||
const stages = topic === "lectura" ? getLecturaStages() : getNumerosStages()
|
||||
const stage = stages[progress.stage]
|
||||
const stages = await getStagesForProfile(profile, topic)
|
||||
const stage = stages[String(progress.stage)]
|
||||
if (!stage || !stage.exercises.length) return null
|
||||
|
||||
const allAttemps = await prisma.skillAttempt.findMany({
|
||||
@@ -70,7 +114,7 @@ export async function getNextExercise(
|
||||
const masteryRatio = mastered.size / stage.exercises.length
|
||||
if (masteryRatio >= STAGE_THRESHOLD) {
|
||||
await advanceStage(childId, topic, progress.stage)
|
||||
return getNextExercise(childId, topic)
|
||||
return getNextExercise(childId, topic, profile)
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
Reference in New Issue
Block a user