Manual explicativo para clases · IBDP Computer Science SL
Cómo está construida la web EnrolUAE
Este repositorio interactivo documenta la aplicación web EnrolUAE de principio a fin: la identidad de marca, los cinco lenguajes utilizados (HTML, CSS, JavaScript, JSON y Python), la arquitectura, cada componente interactivo, los algoritmos y el código fuente completo explicado bloque a bloque, con demostraciones en vivo que ejecutan el código real.
Las familias hispanohablantes que se trasladan a los Emiratos Árabes Unidos encuentran la información sobre matriculación escolar dispersa, en inglés y con un sistema educativo distinto al de origen. EnrolUAE centraliza esa información en una aplicación web interactiva, bilingüe (con traducción automática a otros diez idiomas) y con un asistente conversacional.
Proceso de matriculación paso a paso (6 pasos, con progreso guardado).
Lista personalizada e interactiva de documentos (15 documentos, marcables).
Sección de currículos con IB, británico y americano: estructura por cursos, características y evaluación, más comparador y calculadora de curso.
Preguntas frecuentes en un archivo JSON con 8 preguntas relevantes para familias hispanas.
Búsqueda con ocho grupos de palabras clave predefinidas («documentos», «plan de estudios», «costos», «matrícula»…).
Chatbot «Linda» basado en reglas con 86 temas + 28 colegios (el cliente exigía 15), tolerante a erratas, con contexto, avatar animado y respuesta alternativa que redirige a las FAQ.
Diseño responsive sin desplazamiento horizontal, verificado automáticamente en tres tamaños de pantalla.
Ampliaciones v2: identidad de marca premium, fotografías reales de Dubái, iconografía propia, traductor multiidioma y capa Python (API + validador + pruebas).
Ampliaciones v3: mapa interactivo de 28 colegios (Leaflet/OpenStreetMap) con costos, religión y analíticas por colegio, sección monográfica de GEMS World Academy, logotipo vectorial, cursor lápiz, avatar animado de Linda y chat desplegable en móvil.
Ampliaciones v4: capa de comprensión (NLU) de Linda —tipo de pregunta, entidades, búsqueda combinada de colegios con relajación de filtros y distancia geográfica, atributos con memoria de contexto, comparación de colegios, multipregunta, edad → curso, aclaración y búsqueda profunda—, idéntica en JavaScript y Python (paridad 39/39 preguntas); cuotas reales 2025-26 y fotos reales de los 28 colegios.
2Lenguajes y tecnologías
La aplicación está construida con las tres tecnologías estándar de la web, un formato de datos y un lenguaje de servidor. No usa frameworks (React, Vue, Flask…) y la única librería externa es Leaflet (mapa, código abierto) cargada desde CDN: todo el resto del código es propio, lo que facilita explicarlo, evaluarlo y ejecutarlo sin instalar dependencias.
server.py: servidor HTTP + API REST (http.server).
chatbot_engine.py: el mismo algoritmo que en JS.
data_validator.py y unittest.
¿Por qué el mismo algoritmo en JavaScript y en Python?
Decisión de diseñoEl motor del chatbot existe en dos lenguajes con el mismo comportamiento. El navegador usa la versión JavaScript (funciona incluso sin servidor) y, si server.py está en marcha, puede cambiar al motor Python a través de la API. Esto permite: (1) comparar en clase la misma lógica en dos sintaxis; (2) probar el motor con unittest sin navegador; (3) demostrar la arquitectura cliente–servidor. En la sección 10 hay una demo que envía la misma pregunta a ambos motores.
Paradigmas y técnicas
Programación modular: cada función de la web es un módulo (Search, Process, Documents, Curricula, Estimator, FAQ, ChatUI, Translator).
Orientación a objetos: la clase RuleBasedChatbot (JS y Python) encapsula estado (contexto) y comportamiento (reply()).
Programación dirigida por eventos: clics, formularios, IntersectionObserver, evento personalizado languagechange.
Programación asíncrona:Promise.all para cargar seis JSON en paralelo; fetch a la API con tiempo límite.
Cliente–servidor: API REST en Python con JSON como formato de intercambio.
Estructuras de datos: arrays, objetos/diccionarios, Set, listas ordenadas por clave compuesta, conjuntos de posiciones cubiertas.
Algoritmos: normalización de texto, distancia de Levenshtein (programación dinámica), puntuación con desempate, filtrado por reglas.
3Estructura de archivos
Haz clic en cualquier archivo del árbol para ver qué hace, de qué se responsabiliza y cuántas líneas tiene (contadas en vivo). El botón «Ver código explicado» abre el archivo en el explorador de la sección 11.
Selecciona un archivo →
4Arquitectura y flujo de datos
La aplicación separa datos (JSON), lógica (JavaScript en el navegador y Python en el servidor) y presentación (HTML + CSS). Puede funcionar en dos modos. Haz clic en cada bloque.
→→→
↔→→
Selecciona un bloque del diagrama para ver su explicación.
Los dos modos de ejecución
Modo estático
Modo servidor Python
Cómo se arranca
Cualquier servidor de archivos (Live Server, python -m http.server, Netlify…)
python server.py o doble clic en start-server.bat
Motor del chatbot
JavaScript en el navegador (chatbot.js)
Python en el servidor (/api/chat), con JavaScript como respaldo
Búsqueda
JavaScript
JavaScript (y /api/search disponible)
Validación de datos
Manual (python python/data_validator.py)
También en vivo: /api/validate
Detección
Al cargar, ChatUI.detectPython() pide /api/health con un límite de 1,5 s. Si responde, activa el motor Python; si no, se queda en JavaScript. El usuario puede alternar con la insignia «Motor».
Secuencia de arranque del navegador
index.html enlaza styles.css y los scripts en orden: icons.js → i18n.js → translate.js → chatbot.js → app.js.
Al dispararse DOMContentLoaded: applyTranslations() traduce los textos estáticos y renderIcons() sustituye los <i data-icon> por SVG.
Nav.init() activa el menú, el resaltado de sección y el menú de idiomas (Translator.init()).
loadData() descarga los seis JSON en paralelo y los guarda en DATA.
Cada módulo ejecuta init() → registra eventos y llama a render().
ChatUI.detectPython() comprueba si existe la API y elige el motor.
Si cambia el idioma, setLanguage() emite languagechange y todos los módulos vuelven a renderizar.
5Identidad de marca y diseño visual
El logotipo
Un badge cuadrado redondeado en azul marino (#0B2A3A) contiene tres ideas en un solo trazo dorado: la aguja del Burj Khalifa (Dubái, aspiración), un libro abierto (educación) y tres figuras redondeadas (la familia: dos adultos y un niño). El degradado dorado (#A8861B → #E8C766) aporta el carácter premium; el marino transmite confianza institucional. Funciona a 44 px en la cabecera, como favicon y como icono de app.
Nombre:Enrol (matricular, en inglés británico, el más usado en Dubái) + UAE en dorado. En el código, la clase .brand-accent colorea la segunda parte.
Paleta de colores (haz clic para copiar el código)
Inspirada en Dubái: el marino de la noche sobre el Golfo, el teal de las aguas, el dorado del desierto y los neutros arena/marfil. Todos los colores son variables CSS en :root.
Tipografía
Playfair Display
Titulares (--font-head). Serif editorial de alto contraste: transmite prestigio y calma, como una revista de viajes.
Inter
Texto e interfaz (--font-body). Sans-serif neutra, muy legible en pantallas pequeñas y en 12 idiomas.
JetBrains Mono
Código en este manual. Monoespaciada con ligaduras desactivadas para enseñar sintaxis sin ambigüedad.
El logotipo se dibujó a mano como SVG (assets/img/logo.svg, 3 KB): un círculo con degradado dorado, disco marino, anillo interior, la aguja del Burj Khalifa en tramos escalonados (con líneas de retranqueo), dos crecientes construidos con dos arcos de radios distintos y un libro abierto con páginas curvas. Al ser vectorial es nítido en la cabecera (44 px), el favicon y a tamaño póster. El cursor también es SVG (cursor-pencil.svg): un lápiz con la punta como hotspot en (6,26); en enlaces y botones cambia a la variante dorada. Se declara con cursor: url(…) 6 26, auto y los navegadores sin soporte usan la flecha normal.
Iconografía propia (SVG)
En lugar de emojis (que cambian de aspecto según el sistema operativo), la web usa un set de 60+ iconos SVG dibujados en una cuadrícula de 24×24 con trazo de 1,8 px y extremos redondeados (js/icons.js). Heredan el color del texto (currentColor) y escalan con la tipografía. Pasa el ratón para ver el nombre; haz clic para copiar el código HTML.
const ICONS = {
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
// …60 iconos más
};
function icon(name, cls = "") {
return `<svg class="icon ${cls}" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${ICONS[name]}</svg>`;
}
// En HTML: <i data-icon="search"></i> → renderIcons() lo sustituye por el SVG
Fotografía: real + fotorrealista
La v2 sustituye las ilustraciones planas por fotografía. Hay dos tipos, siempre etiquetados en la web:
Fotografías reales de Dubái (etiqueta «Foto real»): obtenidas de Wikimedia Commons a través de su API pública, filtrando por licencias libres (CC0, CC BY, CC BY-SA). Incluyen el Burj Khalifa, Dubai Marina y un aula real de The Arbor School Dubai. La atribución a cada autor aparece en el pie de la web, como exige la licencia.
Renders fotorrealistas (etiqueta «Render fotorrealista»): las escenas de familias hispanas frente al Burj Khalifa no existen en bancos libres de derechos, así que se generaron con un modelo de imágenes a partir de descripciones detalladas (óptica, hora del día, vestimenta, composición). Se marcan como tales por honestidad con el usuario.
Estrategia desktop-first con cuatro puntos de ruptura. Prueba a cambiar el dispositivo en la vista previa en vivo:
Móvil · 375 px
Técnicas utilizadas
Técnica
Dónde
Efecto
meta viewport
index.html
El móvil usa su ancho real.
CSS Grid con repeat()
.feature-grid, .stepper, .gallery
5 → 3 → 2 → 1 columnas según el ancho.
Hero fotográfico con degradado
.hero-overlay
En escritorio oscurece la izquierda (texto); en móvil oscurece la mitad inferior y deja la foto arriba.
width: min(100%, Npx)
Imágenes
Nunca más anchas que el contenedor.
Tablas apiladas
.compare-table en < 720 px
Cada celda muestra su cabecera con attr(data-label); sin scroll horizontal.
Menú hamburguesa
.nav-toggle en < 900 px
Navegación e idiomas en un panel vertical.
Objetivos táctiles ≥ 44 px
.btn, .chip, casillas
Todo se puede pulsar con el dedo.
7Idiomas y traductor multiidioma
La web tiene dos capas de idioma:
1. Idiomas nativos (ES / EN)
Textos escritos por nosotros. La interfaz vive en el diccionario I18N de i18n.js (cada elemento lleva data-i18n="clave"); los datos viven en los JSON como objetos { "en": …, "es": … } y se leen con pick(). Cambio instantáneo y sin conexión.
2. Traducción automática (10 idiomas)
Árabe, francés, portugués, hindi, urdu, filipino, ruso, chino, alemán e italiano mediante el widget de Google Translate, cargado solo cuando el usuario lo pide (translate.js). El menú propio fija la cookie googtrans=/es/ar, carga el script y oculta la barra de Google. Para árabe y urdu se activa dir="rtl". Si no hay conexión, aparece un aviso y la web sigue en ES/EN.
// i18n.js — texto estático y de datos
function t(key) { return I18N[currentLang][key] || I18N.en[key] || key; }
function pick(obj){ return obj[currentLang] || obj.en || ""; }
function setLanguage(lang) {
currentLang = lang; localStorage.setItem("enroluae-lang", lang);
applyTranslations(); // recorre data-i18n, -placeholder, -aria, -alt
document.dispatchEvent(new CustomEvent("languagechange"));
}
// translate.js — traducción automática
applyMachine(code) {
document.cookie = `googtrans=/${currentLang}/${code}; path=/`; // Google la lee al cargar
this.loadWidget().then(() => { combo.value = code; combo.dispatchEvent(new Event("change")); });
}
Demo en vivo: cambia el idioma nativo de este bloque
8Componentes interactivos
Cada función de la web es un componente independiente. Para cada uno: qué hace, archivos implicados, cómo funciona y una demo en vivo con el código real.
8.1 Cabecera, navegación y menú de idiomas
index.htmlstyles.cssapp.js › Navtranslate.js
Cabecera sticky con efecto cristal (backdrop-filter), logo, menú, botón hamburguesa en móvil y el menú de 12 idiomas. Un IntersectionObserver resalta el enlace de la sección visible.
Ocho grupos de palabras clave predefinidas con variantes ES/EN y resultados asociados, más búsqueda dinámica en las FAQ. Sin resultados → chips con las palabras disponibles.
Demo en vivo (JSON real)
Escribe una palabra para ver la normalización y los grupos detectados.
8.3 Proceso paso a paso
data/steps.jsonapp.js › Process
Stepper de seis pasos con estado current y done (un Set persistido). Cada clic cambia el estado y render() redibuja lista, detalle y progreso. Cada paso tiene también aliases para que el chatbot pueda explicarlo («¿qué hago en el paso 4?»).
Patrón estado → renderCada módulo guarda su estado en variables y regenera todo su HTML con template literals; es el principio de los frameworks modernos, implementado a mano.
8.4 Lista de documentos personalizada
data/documents.jsonapp.js › Documents
Cada documento tiene rules; applies(doc, profile) decide si se muestra según origen, visado, edad y necesidades especiales. Los aliases alimentan al chatbot.
Tarjetas fotográficas con pestañas (estructura / características / evaluación), comparador (filas × currículos seleccionados) y calculadora de curso según la edad el 31 de agosto.
Demo en vivo: calculadora de edad
8.6 Estimador de costos
app.js › Estimator
Recalcula el total en tiempo real (evento input): matrícula por currículo y nivel + extras + transporte opcional × hijos con 10 % de descuento desde el segundo. Importes con toLocaleString() según idioma.
8.7 Preguntas frecuentes (JSON)
data/faq.jsonapp.js › FAQ
Ocho preguntas renderizadas como <details> con filtro que resalta coincidencias. Las FAQ también forman parte de la base de conocimiento del chatbot (segundo nivel).
Preguntas cargadas desde el JSON
8.8 Chatbot «Linda» v4 — motor de reglas + capa de comprensión (NLU)
Un chatbot basado en reglas no usa inteligencia artificial: compara el texto del usuario con palabras clave predefinidas. La base de conocimiento tiene 86 reglas en 6 categorías y se enriquece automáticamente con las 8 FAQ, los 15 documentos, los 6 pasos y los 28 colegios. La v4 añade encima una capa de comprensión del lenguaje (NLU) sin IA: antes de puntuar reglas, analyse() extrae el tipo de pregunta (cuánto / cuántos / cuándo / dónde / cuál / sí-no…) y las entidades (edad, presupuesto en AED, currículos, calificación KHDA, zona de Dubái convertida a coordenadas, español, religión, etapa, orden, colegios nombrados, atributo preguntado y negaciones). Con eso decide, por niveles, qué estrategia responde mejor.
Multipregunta: «¿qué documentos necesito y cuánto cuestan las cuotas?» se divide por «y / además / ?», se responde cada parte y se unen numeradas.
Comparación: dos colegios nombrados («compara Dubai College con JESS», «X o Y, ¿cuál es mejor?») → tabla lado a lado (zona, currículo, cuotas 2025-26, KHDA, español, ratio, distancia entre ambos) y veredicto.
Atributo de un colegio con memoria de slots: «¿cuánto cuesta Dubai College?» responde solo las cuotas; y tras hablar de un colegio, «¿y tiene español?», «¿dónde está?», «¿es bueno?» se refieren a ese colegio (13 atributos).
Búsqueda combinada: «colegios británicos outstanding con español por menos de 60k cerca de Dubai Marina» filtra los 28 colegios por todos los criterios a la vez (zona = distancia haversine ≤ 6 km), ordena (precio, calificación, tamaño o distancia) y, si no hay resultados, relaja los filtros en orden y explica qué quitó. Los refinamientos («¿y solo los IB?») heredan los filtros anteriores.
Edad → curso: «mi hijo tiene siete años» → FS/Year, KG/Grade y ciclo francés con la regla del 31 de agosto.
Reglas por palabras clave (el motor v2, intacto), con contexto y nivel FAQ.
Aclaración: dos candidatos igual de débiles de categorías distintas → «¿te refieres a A o a B?» con chips.
Búsqueda profunda: antes del fallback, las palabras significativas se buscan dentro de las respuestas de todas las reglas.
Cada respuesta incluye ahora una píldora «Entendí: …» con las entidades detectadas, para que el usuario (y el evaluador) vea qué interpretó el motor. El pipeline clásico sigue siendo:
Normalizar: minúsculas, sin acentos ni signos.
Puntuar (nivel principal): por cada palabra clave encontrada, +N puntos (N = palabras de la clave). Cada palabra del usuario solo puede puntuar una vez por regla. Claves de ≥ 7 letras toleran una errata (Levenshtein ≤ 1). Las reglas específicas suman su priority como bonificación.
Contexto: si nada coincide y el mensaje es corto, se reintenta con la pregunta anterior («¿y el británico?»).
Nivel FAQ: si sigue sin coincidencia, se puntúan las reglas generadas desde las FAQ.
Small talk: saludos, gracias, adiós, ayuda, «¿quién eres?».
Fallback: respuesta alternativa con enlace a las FAQ y sugerencias.
Respuesta enriquecida: confianza (alta ≥ 3, media ≥ 2, baja), regla detectada, motor usado y hasta tres preguntas relacionadas.
Demo en vivo: mira cómo puntúa el motor real (JavaScript)
Escribe una pregunta para ver la normalización, la puntuación de cada regla y la respuesta elegida.
Base de conocimiento (86 reglas por categoría + 28 colegios)
8.9 Mapa interactivo de colegios
data/schools.jsonjs/map.jsLeaflet 1.9 (CDN)
La única librería externa del proyecto: Leaflet (código abierto, licencia BSD) con teselas de OpenStreetMap, sin clave de API ni coste. El módulo SchoolsMap lee schools.json (28 colegios con coordenadas aproximadas, cuotas, calificación KHDA, religión/ética, género, alumnos, nacionalidades, español, web y email) y ofrece:
Filtros combinables: currículo (chips), calificación KHDA, cuota mínima máxima (slider), «con español» y búsqueda por nombre/zona. filtered() aplica los cinco criterios con un único Array.filter.
Marcadores por currículo:L.divIcon con HTML propio (color por currículo, iniciales del colegio); el seleccionado pulsa en dorado.
Lista sincronizada con el mapa y ficha completa al pulsar: foto, hechos, analíticas (cuota media vs media del mapa, ratio alumnos/profesor, nivel KHDA), botones de web, email (mailto: con asunto y cuerpo preparados), «Preguntar a Linda» e informe KHDA.
Modo sin conexión: si Leaflet no carga, la lista y las fichas siguen funcionando.
Datos aproximados y honestidadLas cuotas, calificaciones y cifras de alumnos son orientativas y así se etiquetan en cada ficha; el email solo se incluye cuando el colegio lo publica (el resto enlaza a su web). El validador comprueba coordenadas dentro de Dubái, cuota mínima < máxima, calificación válida y que la imagen exista.
8.10 Sección GWA en profundidad
data/gwa.jsonapp.js › GWA
Un perfil monográfico de GEMS World Academy renderizado desde gwa.json: cabecera con render del campus y seis datos clave, y pestañas (Resumen, Itinerario IB, Admisión y costes con barras por etapa, Campus, Analíticas con valoraciones KHDA por área, Familias hispanohablantes). Los botones enlazan a mailto: con plantilla, la página de admisiones, el marcador de GWA en el mapa y una pregunta directa a Linda. El mismo patrón estado → render: cambiar de pestaña actualiza GWA.tab y vuelve a pintar.
8.11 Linda: avatar animado, colegios y chat en móvil
js/avatar.jsapp.js › ChatUIstyles.css › 12b
Avatar vectorial animado.lindaAvatar() devuelve un SVG de estilo editorial dibujado con formas simples (cara ovalada, pelo recogido, gafas doradas finas, blazer azul marino, auricular discreto). Las animaciones son solo CSS: parpadeo cada 4 s (scaleY de los párpados), flotación suave, y cuatro estados que cambia setLindaState(): thinking (mira arriba mientras espera la API), talking (los labios se abren y cierran y asiente), happy (sonrisa más amplia y destello dorado) e idle.
Colegios en el chat. Cuando la respuesta trae school, la burbuja incluye una tarjeta con foto, zona, currículo, calificación, cuotas, religión y botones «Ver en el mapa», «Email» y «Sitio web»; si trae una lista (schools), muestra chips que abren cada ficha en el mapa.
Chat desplegable en móvil. En pantallas ≤ 900 px el botón flotante añade body.chat-open, y el CSS convierte la ventana de chat en un panel fijo a pantalla completa con cabecera compacta y botón de cierre (también con Escape). En escritorio el botón desplaza la página hasta la sección.
Demo en vivo: estados de Linda
9Algoritmos clave (pseudocódigo)
9.1 Normalización de texto
FUNCTION normalise(text)
text ← toLowerCase(text)
text ← removeAccents(text) // "matrícula" → "matricula"
text ← replaceNonAlphanumeric(text, " ")
RETURN collapseSpaces(trim(text))
END FUNCTION
9.2 Distancia de Levenshtein (tolerancia a erratas)
FUNCTION distance(a, b) // nº mínimo de inserciones, borrados o cambios
IF a = b THEN RETURN 0
IF |len(a) − len(b)| > 1 THEN RETURN 2 // corte rápido: nunca aceptamos más de 1
prev ← [0, 1, 2, …, len(b)]
FOR i ← 1 TO len(a)
cur ← [i]
FOR j ← 1 TO len(b)
cost ← 0 IF a[i] = b[j] ELSE 1
cur[j] ← MIN(prev[j] + 1, cur[j−1] + 1, prev[j−1] + cost)
END FOR
prev ← cur
END FOR
RETURN prev[len(b)]
END FUNCTION
9.3 Puntuación de una regla con consumo único de palabras
FUNCTION score(rule, tokens)
covered ← ∅ ; score ← 0
FOR EACH key IN rule.keys ORDERED BY wordCount DESC // frases largas primero
pos ← findKeyword(tokens, key) // −1 si no está
IF pos ≥ 0 AND ninguna posición pos..pos+n−1 ∈ covered THEN
covered ← covered ∪ {pos..pos+n−1}
score ← score + n // n = palabras de la clave
END IF
END FOR
IF score > 0 THEN score ← score + MAX(0, rule.priority) // bonificación de especificidad
RETURN score
END FUNCTION
9.4 Respuesta por niveles con contexto
FUNCTION reply(userText) // v4
pieces ← splitQuestions(userText) // «… y …», «?»
IF count(pieces) > 1 THEN RETURN merge(replyOne(p) FOR p IN pieces)
RETURN replyOne(normalise(userText))
END FUNCTION
FUNCTION replyOne(text)
a ← analyse(text) // tipo de pregunta + entidades + colegios nombrados
IF a.compare THEN RETURN compareAnswer(a.schools[0], a.schools[1])
IF (a.schools = 1 OR ctx.school recent) AND a.attribute THEN RETURN attributeAnswer(school, a.attribute)
IF a.filters ≥ 1 AND (aboutSchools OR a.filters ≥ 2 OR a.area OR refining) THEN
results ← searchSchools(merge(ctx.search, a)) // relaja filtros si está vacío
RETURN searchAnswer(results)
IF a.age THEN RETURN gradeAnswer(a.age)
best ← bestOf(scoreAll(text, MAIN))
IF best = NULL AND lastText ≠ "" AND wordCount(text) ≤ 6 THEN
best ← bestOf(scoreAll(lastText + " " + text, MAIN) EXCEPT lastRule) // contexto
IF best = NULL THEN best ← bestOf(scoreAll(text, FAQ))
IF best ≠ NULL THEN
lastText ← text ; lastRule ← best
RETURN { answer: best.answer, confidence: level(best.score), related: relatedFor(best) }
IF isSmallTalk(text) THEN RETURN smallTalkAnswer
deep ← deepSearch(text) // palabras significativas dentro de las respuestas
IF deep ≠ NULL THEN RETURN deep.answer (confianza baja)
RETURN fallbackAnswer // enlaza a las FAQ + sugerencias
END FUNCTION
9.5 Complejidad
Con R ≈ 86 intenciones y K ≈ 12 palabras clave de media, una pregunta de T ≈ 8 palabras cuesta O(R·K·T) ≈ 8 000 comparaciones de cadenas, más las distancias de Levenshtein solo cuando las longitudes son compatibles. En la práctica: menos de 5 ms en el navegador y en Python. Las palabras clave se normalizan y ordenan una única vez en el constructor.
10Capa Python: servidor, API, validador y pruebas
La carpeta python/ y el archivo server.py añaden un backend escrito solo con la biblioteca estándar de Python (sin pip install). Cuatro piezas:
server.py
Clase EnrolHandler que hereda de SimpleHTTPRequestHandler: sirve los archivos estáticos y atiende las rutas /api/*. ThreadingHTTPServer atiende varias peticiones a la vez. Cada navegador tiene su sesión (contexto del chatbot) identificada por un id.
chatbot_engine.py
Clase RuleBasedChatbot con el mismo algoritmo que chatbot.js: normalise, distance, find_keyword, score_all, reply. Usa unicodedata para los acentos y comprensiones de listas.
data_validator.py
Recorre los seis JSON y detecta textos bilingües incompletos, ids duplicados, enlaces a secciones inexistentes, related rotos y palabras clave repetidas entre reglas. Devuelve errores y avisos.
test_chatbot.py
27 pruebas unittest: normalización, Levenshtein, prefijos, erratas, 27 preguntas predefinidas, documentos y pasos, fallback, desempate, contexto, small talk, confianza, relacionadas y, en v4, entidades NLU, búsqueda combinada con refinamiento y relajación, distancia por zona, atributos con memoria, comparación, edad → curso, multipregunta, etiqueta «Entendí» y búsqueda profunda.
Respuesta completa del motor (tipo, regla, puntuación, confianza, respuesta ES/EN, enlace, relacionadas)
GET /api/search?q=
texto
grupos de palabras clave y FAQ coincidentes
GET /api/stats
—
nº de elementos por archivo JSON
GET /api/validate
—
{ errors: [], warnings: [] }
GET /api/source?file=
ruta (lista blanca)
código fuente del archivo (usado por la sección 11)
class EnrolHandler(SimpleHTTPRequestHandler):
def do_POST(self):
if urlparse(self.path).path != "/api/chat":
return self.send_json({"error": "not found"}, HTTPStatus.NOT_FOUND)
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length) or b"{}")
result = session_bot(payload.get("session", "anon")).reply(payload.get("text", ""))
result["engine"] = "python"
self.send_json(result)
Demo en vivo: estado del servidor Python
Pulsa un botón. Si el manual se abrió con un servidor estático (sin Python), verás el aviso correspondiente.
Demo en vivo: la misma pregunta en los dos motores
Compara que ambas implementaciones devuelven la misma regla, puntuación y confianza.
Ejecutar las herramientas Python
python server.py 8080 # web + API (o doble clic en start-server.bat)
python python/data_validator.py # informe de validación de los JSON
python -m unittest python/test_chatbot.py # 15 pruebas unitarias
python python/chatbot_engine.py # chat por consola con el motor Python
11Código completo explicado
Aquí está todo el código fuente del proyecto, cargado en vivo desde los archivos reales (siempre actualizado), con resaltado de sintaxis, números de línea y una explicación bloque a bloque a la derecha. Haz clic en una explicación para resaltar las líneas a las que se refiere; haz clic en una línea del código para saber a qué bloque pertenece.
Cargando…
12Persistencia con localStorage
Sin base de datos: el estado del usuario se guarda en el navegador como pares clave → texto (JSON.stringify / JSON.parse). El servidor Python solo guarda en memoria el contexto de la conversación por sesión.
Clave
Contenido
Módulo
enroluae-lang
"es" | "en"
i18n.js
enroluae-mt
idioma de traducción automática activo
translate.js
enroluae-steps-done
ids de pasos completados
Process
enroluae-docs-checked
ids de documentos marcados
Documents
enroluae-docs-profile
las 4 respuestas del personalizador
Documents
enroluae-chat-session
id de sesión para la API Python
ChatUI
enroluae-chat-engine
"js" | "python" (preferencia)
ChatUI
Estado actual guardado en este navegador
13Accesibilidad
Enlace «Saltar al contenido» y navegación completa con teclado (:focus-visible dorado).
Todas las imágenes decorativas llevan alt=""; las informativas tienen texto alternativo traducido (data-i18n-alt).
Iconos SVG con aria-hidden="true": el texto adyacente es el que se lee.
Contraste AA: texto marino sobre marfil; blanco sobre marino; dorado solo en tamaños grandes o con peso 700.
Soporte RTL (dir="rtl") para árabe y urdu.
Todo el HTML generado pasa por escapeHtml(); la API Python limita la longitud de entrada y usa lista blanca de archivos.
14Plan de pruebas
Tres niveles de prueba, todos automatizados:
Nivel
Herramienta
Qué comprueba
Resultado
Unitario
unittest (Python)
27 pruebas del motor: normalización, Levenshtein, prefijos, erratas, 27 preguntas → regla esperada, documentos/pasos, fallback → FAQ, desempate, contexto, confianza, relacionadas + 10 pruebas de la capa NLU v4
27/27 ✔
Datos
data_validator.py
JSON válidos, bilingües completos, ids únicos, enlaces existentes, ≥ 8 FAQ, ≥ 8 documentos, ≥ 3 currículos, ≥ 15 reglas, ≥ 4 grupos de búsqueda
0 errores ✔
Extremo a extremo
Playwright (Chrome)
En 375 / 820 / 1366 px: sin errores de consola, sin scroll horizontal, idioma, búsqueda, checklist, proceso, currículos, comparador, calculadora, estimador, FAQ, chatbot (motor JS y Python), menú móvil, manual
✔
Depuración real durante el desarrollo (v2)Los tests unitarios detectaron tres defectos del nuevo motor antes de llegar al navegador: (1) la tolerancia a erratas hacía que «currículo» puntuara también como «curricula» → se implementó el consumo único de palabras; (2) las reglas generadas desde las FAQ, con etiquetas amplias, secuestraban preguntas → se convirtieron en un segundo nivel; (3) «cuanto» coincidía con «cuando» por una errata → la tolerancia se limitó a palabras de 7+ letras. Cada corrección quedó fijada con una prueba.
15Cómo ejecutar y extender el proyecto
Ejecutar
Con Python (recomendado): doble clic en start-server.bat o python server.py → http://localhost:8080. Activa la API y el motor Python.
Sin Python: cualquier servidor estático (Live Server, npx serve, Netlify, GitHub Pages). Todo funciona con el motor JavaScript.
Añadir una regla al chatbot
{
"id": "school-lunch", "category": "practical", "priority": 1,
"question": { "en": "Is there a canteen?", "es": "¿Hay comedor?" },
"keywords": ["comedor", "canteen", "lunch box"],
"answer": { "en": "Most schools…", "es": "La mayoría de colegios…" },
"link": "#costs", "related": ["hidden-costs"]
}
Después ejecuta python python/data_validator.py para comprobar enlaces y relacionados, y añade un caso a test_chatbot.py. Ambos motores la usarán automáticamente.
Añadir una FAQ, un documento o un paso
Basta con añadir el objeto al JSON correspondiente (con aliases en documentos y pasos): la web lo renderiza, el buscador lo indexa y el chatbot lo incorpora a su base de conocimiento.
Añadir un idioma nativo
Añade la clave (p. ej. "fr") al objeto I18N, marca el idioma como native: true en LANGUAGES (translate.js) y añade el campo "fr" a los objetos bilingües de los JSON.
16Autoevaluación para clase
Pulsa una opción para comprobar si es correcta.
17Glosario
API REST
Interfaz por HTTP en la que cada ruta (/api/chat) representa un recurso y se intercambia JSON.
Backdrop-filter
Propiedad CSS que desenfoca lo que hay detrás de un elemento (efecto cristal de la cabecera).
Chatbot basado en reglas
Programa que responde comparando el texto con palabras clave predefinidas; no aprende ni usa IA.
Cliente–servidor
Arquitectura en la que el navegador (cliente) pide datos a un programa remoto (servidor).
Consumo único de palabras
Regla del motor: una palabra del usuario solo puede puntuar una vez por regla.
Design token
Variable CSS con un valor de diseño reutilizable (color, radio, sombra, tipografía).
Distancia de Levenshtein
Número mínimo de ediciones (insertar, borrar, sustituir) para convertir una palabra en otra.
DOM
Representación en árbol del HTML que JavaScript puede leer y modificar.
Fallback
Respuesta alternativa cuando ninguna regla coincide; aquí redirige a las FAQ.
fetch / async-await
API para pedir recursos por HTTP y sintaxis para esperar el resultado sin bloquear.
http.server
Módulo de la biblioteca estándar de Python para crear servidores HTTP sencillos.
i18n
Internacionalización: separar los textos del código para soportar varios idiomas.
JSON
Formato de texto para datos estructurados; el mismo en JavaScript y Python (json.loads).
KHDA
Autoridad reguladora de los colegios privados de Dubái.
localStorage
Almacén clave-valor del navegador que persiste entre sesiones.
Media query
Regla CSS condicional, p. ej. @media (max-width: 720px).
Normalización
Transformar texto a una forma estándar (minúsculas, sin acentos) para comparar con fiabilidad.
Prioridad / bonificación de especificidad
Valor de una regla que se suma a su puntuación para que lo específico gane a lo genérico.
Responsive design
Diseño que se adapta al tamaño de pantalla.
Set
Colección de valores únicos (JS Set, Python set).
SVG
Gráficos vectoriales escalables; formato de los iconos de la web.
Template literal
Cadena JS con comillas invertidas que permite insertar variables: `Hola ${nombre}`. En Python: f-strings.
unittest
Módulo de pruebas unitarias de la biblioteca estándar de Python.