feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import type { Stage, Exercise } from '../types'
|
||||
|
||||
type Info = {
|
||||
letra: string
|
||||
palabra: string
|
||||
emoji: string
|
||||
gesto: string
|
||||
silaba: string
|
||||
palabraLower: string
|
||||
}
|
||||
|
||||
const moduleA: Info[] = [
|
||||
{ letra: 'A', palabra: 'ÁRBOL', emoji: '🌳', gesto: 'Abrí la mano como una boca grande', silaba: 'a', palabraLower: 'árbol' },
|
||||
{ letra: 'E', palabra: 'ELEFANTE', emoji: '🐘', gesto: 'Estirá la mano como una trompa', silaba: 'e', palabraLower: 'elefante' },
|
||||
{ letra: 'I', palabra: 'INDIO', emoji: '🪶', gesto: 'Señalá el cielo con el dedo', silaba: 'i', palabraLower: 'indio' },
|
||||
{ letra: 'O', palabra: 'OJO', emoji: '👁️', gesto: 'Redondeá la mano como una O', silaba: 'o', palabraLower: 'ojo' },
|
||||
{ letra: 'U', palabra: 'UNO', emoji: '1️⃣', gesto: 'Señalá el suelo con la mano', silaba: 'u', palabraLower: 'uno' },
|
||||
]
|
||||
|
||||
const moduleB: Info[] = [
|
||||
{ letra: 'M', palabra: 'MAMÁ', emoji: '👩', gesto: 'Manos en la boca como mmm', silaba: 'ma', palabraLower: 'mamá' },
|
||||
{ letra: 'P', palabra: 'PAPÁ', emoji: '👨', gesto: 'Manos que abren y cierran como pompa', silaba: 'pa', palabraLower: 'papá' },
|
||||
{ letra: 'L', palabra: 'LUNA', emoji: '🌙', gesto: 'Lengua arriba como lámpara', silaba: 'la', palabraLower: 'luna' },
|
||||
{ letra: 'S', palabra: 'SOL', emoji: '☀️', gesto: 'Dedos como serpiente sss', silaba: 'sa', palabraLower: 'sol' },
|
||||
]
|
||||
|
||||
const moduleC: Info[] = [
|
||||
{ letra: 'T', palabra: 'TASA', emoji: '☕', gesto: 'Golpecito con la mano', silaba: 'ta', palabraLower: 'tasa' },
|
||||
{ letra: 'N', palabra: 'NIDO', emoji: '🪺', gesto: 'Nariz como nido', silaba: 'na', palabraLower: 'nido' },
|
||||
{ letra: 'D', palabra: 'DEDO', emoji: '👉', gesto: 'Dedo señalando', silaba: 'da', palabraLower: 'dedo' },
|
||||
{ letra: 'R', palabra: 'RATÓN', emoji: '🐭', gesto: 'Lengua que vibra como rrr', silaba: 'ra', palabraLower: 'ratón' },
|
||||
]
|
||||
|
||||
const allModules = [moduleA, moduleB, moduleC]
|
||||
|
||||
function getDistractors(pool: Info[], current: Info, count: number): Info[] {
|
||||
return pool.filter((x) => x.letra !== current.letra).slice(0, count)
|
||||
}
|
||||
|
||||
function getPoolIndex(info: Info): number {
|
||||
for (let i = 0; i < allModules.length; i++) {
|
||||
if (allModules[i].find((x) => x.letra === info.letra)) return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function buildExercises(): Exercise[] {
|
||||
const all: Exercise[] = []
|
||||
let prevId: string | null = 'lec4-r-completa'
|
||||
|
||||
for (const pool of allModules) {
|
||||
for (const info of pool) {
|
||||
const MIN = info.letra.toLowerCase()
|
||||
const dist = getDistractors(pool, info, 3)
|
||||
|
||||
// 1. Descubrimiento del par
|
||||
all.push({
|
||||
id: `lec5-${MIN}-descubre`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'sensibilizacion',
|
||||
prerequisites: prevId ? [prevId] : [],
|
||||
content: {
|
||||
instruction: `${info.letra} y ${MIN} suenan igual`,
|
||||
audioText: `${info.letra} grande y ${MIN} chica suenan igual: ${gestoKey(info.gesto)}`,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Descubrir que ${info.letra} y ${MIN} son la misma letra.`,
|
||||
})
|
||||
prevId = `lec5-${MIN}-descubre`
|
||||
|
||||
// 2. Práctica emparejada: Tocá Mm
|
||||
const pairOptions = [
|
||||
{ label: `${info.letra}${MIN}`, correct: true, id: MIN },
|
||||
...dist.map((d) => ({
|
||||
label: `${d.letra}${d.letra.toLowerCase()}`,
|
||||
correct: false,
|
||||
id: d.letra.toLowerCase(),
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-pareja`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `Tocá la ${info.letra}${MIN}`,
|
||||
audioText: `¿Dónde está ${info.letra}${MIN}?`,
|
||||
options: pairOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Asociar el par ${info.letra}${MIN}.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-pareja`
|
||||
|
||||
// 3. Buscar mayúscula entre minúsculas: Tocá la M
|
||||
const mayusOptions = [
|
||||
{ label: info.letra, correct: true, id: MIN },
|
||||
...dist.map((d) => ({
|
||||
label: d.letra.toLowerCase(),
|
||||
correct: false,
|
||||
id: `${d.letra.toLowerCase()}-may`,
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-busca-mayus`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `Tocá la ${info.letra} grande`,
|
||||
audioText: `¿Dónde está la ${info.letra} grande?`,
|
||||
options: mayusOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Reconocer ${info.letra} mayúscula entre minúsculas.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-busca-mayus`
|
||||
|
||||
// 4. Buscar minúscula entre mayúsculas: Tocá la m (CLAVE)
|
||||
const minusOptions = [
|
||||
{ label: MIN, correct: true, id: MIN },
|
||||
...dist.map((d) => ({
|
||||
label: d.letra,
|
||||
correct: false,
|
||||
id: `${d.letra.toLowerCase()}-min`,
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-busca-minus`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `Tocá la ${MIN} chica`,
|
||||
audioText: `¿Dónde está la ${MIN} chica?`,
|
||||
options: minusOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Reconocer ${MIN} minúscula entre mayúsculas — discriminación clave para dislexia.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-busca-minus`
|
||||
|
||||
// 5. Pareja inversa: ¿Cuál es la pareja de M? → m
|
||||
const parejaMayusOptions = [
|
||||
{ label: MIN, correct: true, id: `${MIN}-pm` },
|
||||
...dist.map((d) => ({
|
||||
label: d.letra.toLowerCase(),
|
||||
correct: false,
|
||||
id: `${d.letra.toLowerCase()}-pm`,
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-pareja-mayus`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `¿Cuál es la pareja de ${info.letra}?`,
|
||||
audioText: `Buscá la ${MIN} chica, pareja de la ${info.letra} grande`,
|
||||
options: parejaMayusOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Asociar la mayúscula ${info.letra} con su minúscula ${MIN}.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-pareja-mayus`
|
||||
|
||||
// 6. Pareja inversa 2: ¿Cuál es la pareja de m? → M
|
||||
const parejaMinusOptions = [
|
||||
{ label: info.letra, correct: true, id: `${MIN}-pm2` },
|
||||
...dist.map((d) => ({
|
||||
label: d.letra,
|
||||
correct: false,
|
||||
id: `${d.letra.toLowerCase()}-pm2`,
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-pareja-minus`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `¿Cuál es la pareja de ${MIN}?`,
|
||||
audioText: `Buscá la ${info.letra} grande, pareja de la ${MIN} chica`,
|
||||
options: parejaMinusOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Asociar la minúscula ${MIN} con su mayúscula ${info.letra}.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-pareja-minus`
|
||||
|
||||
// 7. Sílabas en minúscula: Tocá ma
|
||||
const silabaOptions = [
|
||||
{ label: info.silaba, correct: true, id: `${MIN}-sil` },
|
||||
...dist.map((d) => ({
|
||||
label: d.silaba,
|
||||
correct: false,
|
||||
id: `${d.letra.toLowerCase()}-sil`,
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-silaba`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `Tocá ${info.silaba}`,
|
||||
audioText: `¿Dónde está ${info.silaba}?`,
|
||||
options: silabaOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Leer la sílaba ${info.silaba} en minúscula.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-silaba`
|
||||
|
||||
// 8. Palabra en minúscula: Tocá mamá
|
||||
const palabraOptions = [
|
||||
{ label: info.palabraLower, correct: true, id: `${MIN}-pal` },
|
||||
...dist.map((d) => ({
|
||||
label: d.palabraLower,
|
||||
correct: false,
|
||||
id: `${d.letra.toLowerCase()}-pal`,
|
||||
})),
|
||||
].sort(() => Math.random() - 0.5)
|
||||
|
||||
all.push({
|
||||
id: `lec5-${MIN}-palabra`,
|
||||
stage: 5,
|
||||
skillCode: `puente-${MIN}`,
|
||||
type: 'eleccion',
|
||||
prerequisites: [prevId],
|
||||
content: {
|
||||
instruction: `Tocá ${info.palabraLower}`,
|
||||
audioText: `¿Dónde dice ${info.palabraLower}?`,
|
||||
options: palabraOptions,
|
||||
emoji: info.emoji,
|
||||
},
|
||||
functionalContext: `Leer la palabra ${info.palabraLower} en minúscula.`,
|
||||
errorType: 'confusion-letra',
|
||||
})
|
||||
prevId = `lec5-${MIN}-palabra`
|
||||
}
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
function gestoKey(gesto: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'Abrí': 'a-a-a',
|
||||
'Estirá': 'e-e-e',
|
||||
'Señalá el cielo': 'i-i-i',
|
||||
'Redondeá': 'o-o-o',
|
||||
'Señalá el suelo': 'u-u-u',
|
||||
'Manos en la boca': 'mmm',
|
||||
'Manos que abren': 'p-p-p',
|
||||
'Dedos como serpiente': 'sss',
|
||||
'Lengua arriba': 'l-l-l',
|
||||
'Golpecito': 't-t-t',
|
||||
'Nariz como nido': 'n-n-n',
|
||||
'Dedo señalando': 'd-d-d',
|
||||
'Lengua que vibra': 'rrr',
|
||||
}
|
||||
const key = Object.keys(map).find((k) => gesto.startsWith(k))
|
||||
return key ? map[key] : gesto
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 5,
|
||||
name: 'Puente Mayúscula-Minúscula',
|
||||
description: 'Asociar letras mayúsculas con sus correspondientes minúsculas. 8 ejercicios por letra — 104 en total.',
|
||||
exercises: buildExercises(),
|
||||
}
|
||||
|
||||
export default stage
|
||||
Reference in New Issue
Block a user