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.

HTML5CSS3JavaScript ES2022JSONPython 3.12 Sin frameworks12 idiomasResponsive

¿Qué problema resuelve la aplicación?

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.

Cifras del proyecto (leídas en vivo de los datos)

8preguntas frecuentes (JSON)
15documentos en la lista
57reglas del chatbot
86intenciones totales (reglas + FAQ + documentos + pasos)
1000+palabras clave
28colegios en el mapa
9grupos de búsqueda
—líneas de código
12idiomas (2 nativos + 10 automáticos)

Criterios de éxito (requisitos del cliente)

  • 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.

HTML5Estructura y semántica
  • Etiquetas semánticas: header, nav, main, section, article, figure, footer.
  • details/summary para acordeones sin JavaScript.
  • Atributos data-* para traducciones, iconos y acciones.
CSS3Identidad visual y responsive
  • Variables CSS (design tokens) de la marca.
  • Grid, Flexbox, clamp(), backdrop-filter.
  • Media queries en 1024, 900, 720 y 480 px.
JavaScriptInteracción y lógica (ES2022)
  • Módulos por componente (init() / render()).
  • Clase RuleBasedChatbot con campos estáticos.
  • fetch, async/await, AbortController, eventos personalizados.
JSONDatos separados del código
  • 6 archivos bilingües: FAQ, documentos, currículos, pasos, reglas, palabras clave.
  • Editables sin tocar código.
  • Validados por un script Python.
Python 3Servidor, API y pruebas
  • 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áticoModo servidor Python
    Cómo se arrancaCualquier servidor de archivos (Live Server, python -m http.server, Netlify…)python server.py o doble clic en start-server.bat
    Motor del chatbotJavaScript en el navegador (chatbot.js)Python en el servidor (/api/chat), con JavaScript como respaldo
    BúsquedaJavaScriptJavaScript (y /api/search disponible)
    Validación de datosManual (python python/data_validator.py)También en vivo: /api/validate
    DetecciónAl 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

    1. index.html enlaza styles.css y los scripts en orden: icons.js → i18n.js → translate.js → chatbot.js → app.js.
    2. Al dispararse DOMContentLoaded: applyTranslations() traduce los textos estáticos y renderIcons() sustituye los <i data-icon> por SVG.
    3. Nav.init() activa el menú, el resaltado de sección y el menú de idiomas (Translator.init()).
    4. loadData() descarga los seis JSON en paralelo y los guarda en DATA.
    5. Cada módulo ejecuta init() → registra eventos y llama a render().
    6. ChatUI.detectPython() comprueba si existe la API y elige el motor.
    7. Si cambia el idioma, setLanguage() emite languagechange y todos los módulos vuelven a renderizar.

    5Identidad de marca y diseño visual

    Logotipo de EnrolUAE: medallón azul marino con doble anillo dorado, el Burj Khalifa central flanqueado por dos arcos protectores y un libro abierto en la base

    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.

    Tamaños fluidos con clamp(): h1 { font-size: clamp(2.2rem, 4.2vw + 0.6rem, 3.6rem); }.

    Logotipo vectorial y cursor

    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.

    Componentes de interfaz reutilizables

    :root {
      --navy-900: #0b2a3a;   /* color principal de marca */
      --gold-600: #c9a227;   /* acento premium */
      --gold-gradient: linear-gradient(135deg, var(--gold-700), var(--gold-300));
      --radius: 18px; --shadow: 0 10px 30px rgba(11, 42, 58, 0.12);
    }
    .btn-gold { background: var(--gold-gradient); color: var(--navy-900); }
    .hero-overlay { background: linear-gradient(90deg, rgba(11,42,58,.92), rgba(11,42,58,.15)); }

    6Diseño responsive

    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écnicaDóndeEfecto
    meta viewportindex.htmlEl móvil usa su ancho real.
    CSS Grid con repeat().feature-grid, .stepper, .gallery5 → 3 → 2 → 1 columnas según el ancho.
    Hero fotográfico con degradado.hero-overlayEn escritorio oscurece la izquierda (texto); en móvil oscurece la mitad inferior y deja la foto arriba.
    width: min(100%, Npx)ImágenesNunca más anchas que el contenedor.
    Tablas apiladas.compare-table en < 720 pxCada celda muestra su cabecera con attr(data-label); sin scroll horizontal.
    Menú hamburguesa.nav-toggle en < 900 pxNavegación e idiomas en un panel vertical.
    Objetivos táctiles ≥ 44 px.btn, .chip, casillasTodo 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.

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((e) => { if (e.isIntersecting)
        links.forEach((a) => a.classList.toggle("is-active", a.getAttribute("href") === "#" + e.target.id)); });
    }, { rootMargin: "-40% 0px -55% 0px" });
    sections.forEach((s) => observer.observe(s));

    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.

    applies(doc, profile) {
      const rules = doc.rules || {};
      if (rules.always) return true;
      return Object.entries(rules).every(([key, allowed]) => allowed.includes(profile[key]));
    }

    Demo en vivo: motor de reglas de documentos

    8.5 Currículos, comparador y calculadora de curso

    data/curricula.jsonapp.js › Curricula

    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)

    data/chatbot-rules.jsonjs/chatbot.jspython/chatbot_engine.pyapp.js › ChatUI

    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.

    1. Multipregunta: «¿qué documentos necesito y cuánto cuestan las cuotas?» se divide por «y / además / ?», se responde cada parte y se unen numeradas.
    2. 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.
    3. 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).
    4. 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.
    5. Edad → curso: «mi hijo tiene siete años» → FS/Year, KG/Grade y ciclo francés con la regla del 31 de agosto.
    6. Reglas por palabras clave (el motor v2, intacto), con contexto y nivel FAQ.
    7. Aclaración: dos candidatos igual de débiles de categorías distintas → «¿te refieres a A o a B?» con chips.
    8. 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:

    1. Normalizar: minúsculas, sin acentos ni signos.
    2. 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.
    3. Contexto: si nada coincide y el mensaje es corto, se reintenta con la pregunta anterior («¿y el británico?»).
    4. Nivel FAQ: si sigue sin coincidencia, se puntúan las reglas generadas desde las FAQ.
    5. Small talk: saludos, gracias, adiós, ayuda, «¿quién eres?».
    6. Fallback: respuesta alternativa con enlace a las FAQ y sugerencias.
    7. 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.
    filtered() {
      const f = this.filters, q = normalise(f.q);
      return this.schools.filter((s) =>
        (f.curriculum === "all" || s.curriculum.includes(f.curriculum)) &&
        (f.rating === "all" || s.khda.rating === f.rating) &&
        s.fees.min <= f.maxFee && (!f.spanish || s.spanish.offered) &&
        (!q || normalise(s.name + " " + s.area).includes(q)));
    }
    // marcador: color por currículo + iniciales, seleccionado en dorado
    L.marker([s.lat, s.lng], { icon: this.markerIcon(s, s.id === this.selectedId) })
      .bindPopup(`<strong>${s.name}</strong>…`).on("click", () => this.select(s.id));
    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.

    API REST

    Método y rutaEntradaSalida
    GET /api/health—{ ok, engine, python, rules, intents, keywords, uptime }
    POST /api/chat{ "text": "…", "session": "id", "reset": false }Respuesta completa del motor (tipo, regla, puntuación, confianza, respuesta ES/EN, enlace, relacionadas)
    GET /api/search?q=textogrupos 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.

    ClaveContenidoMódulo
    enroluae-lang"es" | "en"i18n.js
    enroluae-mtidioma de traducción automática activotranslate.js
    enroluae-steps-doneids de pasos completadosProcess
    enroluae-docs-checkedids de documentos marcadosDocuments
    enroluae-docs-profilelas 4 respuestas del personalizadorDocuments
    enroluae-chat-sessionid de sesión para la API PythonChatUI
    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).
    • ARIA: role="progressbar" + aria-valuenow, aria-expanded (menús), aria-selected (pestañas), role="menu" / menuitemradio (idiomas), aria-live (chat, resultados, insignia de motor).
    • 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:

    NivelHerramientaQué compruebaResultado
    Unitariounittest (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 v427/27 ✔
    Datosdata_validator.pyJSON válidos, bilingües completos, ids únicos, enlaces existentes, ≥ 8 FAQ, ≥ 8 documentos, ≥ 3 currículos, ≥ 15 reglas, ≥ 4 grupos de búsqueda0 errores ✔
    Extremo a extremoPlaywright (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.
    Copiado