feat: EduEasy checkpoint - plan, scaffolding, pedagogia modules, components child+parent, API routes, Prisma schema, Docker infra, Better Auth

This commit is contained in:
Renato
2026-07-20 17:58:49 +02:00
commit e69b18abaa
49 changed files with 9842 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 || ""
}
+37
View File
@@ -0,0 +1,37 @@
export type CPAStage = "concrete" | "pictoric" | "abstract"
export interface CPAState {
stage: CPAStage
currentValue: number
attempts: number
mastered: boolean
}
/**
* CPA (Concrete-Pictórico-Abstracto) progression engine.
*
* - Concrete: show objects (animated) for the child to count with finger.
* - Pictoric: show images of objects, then hide them progressively.
* - Abstract: show only the number symbol → child says/selects the quantity.
*/
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,
}
}
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] }
}
return { ...state, mastered: true }
}
+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: "#D35400",
8: "#3498DB",
9: "#9B59B6",
10: "#FF5733",
}
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
}