Files
edueasy/app/child/lectura/page.tsx
T

203 lines
6.7 KiB
TypeScript

"use client"
import { useState, useCallback } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { GatoMascota } from "@/components/child/gato-mascota"
import VocalCard from "@/components/child/vocal-card"
import TrazoLetra from "@/components/child/trazo-letra"
import TemporizadorVisual from "@/components/child/temporizador-visual"
import { speak } from "@/lib/speech"
import { vocales } from "@/lib/pedagogia/borel-maisonny"
import { getInitialState, evaluateResponse, isMastered } from "@/lib/pedagogia/errorless"
type Step = "gesto" | "escuchar" | "trazar" | "elegir" | "completo"
export default function LecturaSesion() {
const [currentIndex, setCurrentIndex] = useState(0)
const [step, setStep] = useState<Step>("gesto")
const [promptState, setPromptState] = useState(getInitialState())
const [sessionComplete, setSessionComplete] = useState(false)
const [running, setRunning] = useState(true)
const [score, setScore] = useState(0)
const vocal = vocales[currentIndex]
const handleGestoComplete = useCallback(() => {
speak(`Escuchá. Esta es la letra ${vocal.letra}. ${vocal.palabra}. ${vocal.fonema} ${vocal.fonema} ${vocal.fonema}`)
setStep("escuchar")
}, [vocal])
const handleEscucharComplete = useCallback(() => {
speak(`Ahora trazala con tu dedo`)
setStep("trazar")
}, [])
const handleTrazoComplete = useCallback(() => {
setScore((s) => s + 1)
const newState = evaluateResponse(promptState, true)
setPromptState(newState)
if (isMastered(newState.level)) {
speak(`¡Muy bien! Pasemos a la siguiente`)
if (currentIndex < vocales.length - 1) {
setCurrentIndex((i) => i + 1)
setStep("gesto")
setPromptState(getInitialState())
} else {
setSessionComplete(true)
setRunning(false)
}
} else {
setStep("elegir")
}
}, [promptState, currentIndex])
const handleChoice = useCallback(
(correct: boolean) => {
const newState = evaluateResponse(promptState, correct)
setPromptState(newState)
if (correct) {
setScore((s) => s + 1)
}
if (isMastered(newState.level)) {
if (currentIndex < vocales.length - 1) {
setCurrentIndex((i) => i + 1)
setStep("gesto")
setPromptState(getInitialState())
} else {
setSessionComplete(true)
setRunning(false)
}
}
},
[promptState, currentIndex],
)
if (sessionComplete) {
return (
<div className="flex-1 flex flex-col items-center justify-center p-8 gap-6">
<GatoMascota mood="celebrate" message="¡Terminaste todas las vocales!" size="lg" />
<p className="text-lg font-display">Puntuación: {score}/{vocales.length * 2}</p>
<button
onClick={() => {
setCurrentIndex(0)
setStep("gesto")
setPromptState(getInitialState())
setSessionComplete(false)
setRunning(true)
setScore(0)
}}
className="bg-primary text-primary-foreground touch-target-lg rounded-2xl px-8 py-4 text-xl font-display font-bold"
>
Jugar de nuevo
</button>
</div>
)
}
return (
<div className="flex-1 flex flex-col items-center justify-center p-6 gap-6">
<TemporizadorVisual
durationMinutes={10}
onComplete={() => {
setRunning(false)
setSessionComplete(true)
}}
running={running}
/>
<GatoMascota
mood={step === "gesto" ? "thinking" : step === "completo" ? "celebrate" : "coach"}
message={
step === "gesto"
? `Hacé así: ${vocal.gesto}`
: step === "escuchar"
? "Escuchá bien"
: step === "trazar"
? "Traza con tu dedo"
: ""
}
size="sm"
/>
<AnimatePresence mode="wait">
<motion.div
key={`${vocal.letra}-${step}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{step === "gesto" && (
<div className="flex flex-col items-center gap-4">
<span className="text-8xl">{vocal.imageEmoji}</span>
<p className="text-lg text-foreground/60 italic max-w-xs text-center">
{vocal.gesto}
</p>
<button
onClick={handleGestoComplete}
className="bg-primary text-primary-foreground touch-target-lg rounded-2xl px-8 py-4 text-xl font-display font-bold"
>
¡Lo hice!
</button>
</div>
)}
{step === "escuchar" && (
<div className="flex flex-col items-center gap-6">
<VocalCard
letra={vocal.letra}
fonema={vocal.fonema}
palabra={vocal.palabra}
imageEmoji={vocal.imageEmoji}
color={vocal.color}
onClick={() => {
speak(`${vocal.fonema}... de ${vocal.palabra}`)
}}
/>
<button
onClick={handleEscucharComplete}
className="bg-secondary text-secondary-foreground touch-target-lg rounded-2xl px-8 py-4 text-xl font-display font-bold"
>
Siguiente
</button>
</div>
)}
{step === "trazar" && (
<div className="flex flex-col items-center gap-4">
<TrazoLetra
letra={vocal.letra}
fonema={vocal.fonema}
color={vocal.color}
onComplete={handleTrazoComplete}
promptLevel={promptState.level}
/>
</div>
)}
{step === "elegir" && (
<div className="flex flex-col items-center gap-6">
<p className="text-lg font-display">¿Cuál es la {vocal.letra.toUpperCase()}?</p>
<div className="flex flex-wrap gap-4 justify-center">
{[...vocales].sort(() => Math.random() - 0.5).slice(0, 3).map((v) => (
<button
key={v.letra}
onClick={() => handleChoice(v.letra === vocal.letra)}
className={`touch-target-lg rounded-2xl text-2xl font-display font-bold px-8 py-6 shadow-md active:scale-95 transition-transform`}
style={{ backgroundColor: v.color }}
>
{v.letra.toUpperCase()}
</button>
))}
</div>
</div>
)}
</motion.div>
</AnimatePresence>
</div>
)
}