feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes

This commit is contained in:
renato97
2026-07-21 15:31:46 -03:00
commit fa8e0c58ce
120 changed files with 16013 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
export interface Fonema {
letra: string
fonema: string
palabra: string
imageEmoji: string
gesto: string // description of Borel-Maisonny gesture
color: string
orden: number
}
// Vocales first (Jolly Phonics order adapted for Spanish)
export const vocales: Fonema[] = [
{
letra: "a",
fonema: "a",
palabra: "avión",
imageEmoji: "✈️",
gesto: "Abrir la mano como si saludaras, con los dedos juntos.",
color: "#E74C3C",
orden: 1,
},
{
letra: "e",
fonema: "e",
palabra: "elefante",
imageEmoji: "🐘",
gesto: "Señalar hacia la derecha con la mano abierta.",
color: "#3498DB",
orden: 2,
},
{
letra: "i",
fonema: "i",
palabra: "iglú",
imageEmoji: "🏔️",
gesto: "Señalar hacia arriba con el dedo índice.",
color: "#9B59B6",
orden: 3,
},
{
letra: "o",
fonema: "o",
palabra: "oso",
imageEmoji: "🐻",
gesto: "Hacer un círculo con la mano frente a la boca.",
color: "#F39C12",
orden: 4,
},
{
letra: "u",
fonema: "u",
palabra: "uva",
imageEmoji: "🍇",
gesto: "Estirar los dos brazos hacia adelante como un tubo.",
color: "#1ABC9C",
orden: 5,
},
]
export function getFonema(letra: string): Fonema | undefined {
return vocales.find((v) => v.letra === letra)
}
export function getGestoDescription(letra: string): string {
const f = getFonema(letra)
return f?.gesto || ""
}
+56
View File
@@ -0,0 +1,56 @@
export type CPAStage = "concrete" | "pictoric" | "abstract"
export type GalperinStage = "hand" | "external_speech" | "inner_speech" | "mental" | "mastered"
export interface CPAState {
stage: CPAStage
currentValue: number
attempts: number
mastered: boolean
galperinStage: GalperinStage
}
export function getCPAProgression(skillValue: number): CPAStage[] {
return ["concrete", "pictoric", "abstract"]
}
export function createCPAState(value: number): CPAState {
return {
stage: "concrete",
currentValue: value,
attempts: 0,
mastered: false,
galperinStage: "hand",
}
}
export function advanceStage(state: CPAState): CPAState {
const stages: CPAStage[] = ["concrete", "pictoric", "abstract"]
const currentIdx = stages.indexOf(state.stage)
if (currentIdx < stages.length - 1) {
return { ...state, stage: stages[currentIdx + 1], galperinStage: "hand" }
}
return { ...state, mastered: true }
}
export function advanceGalperin(stage: GalperinStage): GalperinStage {
const order: GalperinStage[] = ["hand", "external_speech", "inner_speech", "mental", "mastered"]
const idx = order.indexOf(stage)
if (idx < order.length - 1) return order[idx + 1]
return "mastered"
}
export function getGalperinInstruction(stage: GalperinStage, value: number): string {
switch (stage) {
case "hand":
return `Tocá ${value} cosas con tu dedo`
case "external_speech":
return "Contá en voz alta mientras tocás"
case "inner_speech":
return "Ahora contá susurrando"
case "mental":
return "Pensá el número en tu cabeza y señalalo"
case "mastered":
return "¡Ya sabés este número!"
}
}
+47
View File
@@ -0,0 +1,47 @@
export interface Rod {
value: number
color: string
lengthPx: number
label: string
}
export const rodColors: Record<number, string> = {
1: "#FFFFFF",
2: "#E74C3C",
3: "#2ECC71",
4: "#E91E63",
5: "#F1C40F",
6: "#1ABC9C",
7: "#2C3E50",
8: "#3498DB",
9: "#9B59B6",
10: "#E67E22",
}
const rodLengths: Record<number, number> = {
1: 24,
2: 44,
3: 64,
4: 84,
5: 104,
6: 124,
7: 144,
8: 164,
9: 184,
10: 204,
}
export function getRods(maxValue: number = 10): Rod[] {
return Array.from({ length: maxValue }, (_, i) => ({
value: i + 1,
color: rodColors[i + 1],
lengthPx: rodLengths[i + 1],
label: `${i + 1}`,
}))
}
export function compareRods(a: number, b: number): "mayor" | "menor" | "igual" {
if (a > b) return "mayor"
if (a < b) return "menor"
return "igual"
}
+63
View File
@@ -0,0 +1,63 @@
export type PromptLevel = 0 | 1 | 2 | 3 | 4
// Galperin stages mapped to prompt levels
export const GALPERIN_STAGES = {
HAND: 0, // Concrete: full model, show the answer
EXTERNAL_SPEECH: 1, // Prompts the child to say out loud
INNER_SPEECH: 2, // Partial prompt, whisper
MENTAL: 3, // Verbal cue only
MASTERED: 4, // No prompt
} as const
export interface PromptState {
level: PromptLevel
consecutiveCorrect: number
consecutiveIncorrect: number
}
export function getInitialState(): PromptState {
return {
level: 0,
consecutiveCorrect: 0,
consecutiveIncorrect: 0,
}
}
export function evaluateResponse(
state: PromptState,
correct: boolean,
masterThreshold: number = 3,
regressThreshold: number = 2,
): PromptState {
if (correct) {
const newCorrect = state.consecutiveCorrect + 1
const newLevel =
newCorrect >= masterThreshold
? (Math.min(state.level + 1, 4) as PromptLevel)
: state.level
return {
level: newLevel,
consecutiveCorrect: newCorrect,
consecutiveIncorrect: 0,
}
}
const newIncorrect = state.consecutiveIncorrect + 1
const newLevel =
newIncorrect >= regressThreshold
? (Math.max(state.level - 1, 0) as PromptLevel)
: state.level
return {
level: newLevel,
consecutiveCorrect: 0,
consecutiveIncorrect: newIncorrect,
}
}
export function shouldShowPrompt(level: PromptLevel): boolean {
return level < 3
}
export function isMastered(level: PromptLevel): boolean {
return level >= 4
}