203 lines
6.7 KiB
TypeScript
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>
|
|
)
|
|
}
|