// ============================================================
// YOUR FIRST LLM — Complete Source Code / Código Fuente Completo
// ============================================================
//
// EN: This is EVERY line of JavaScript running in your browser
// right now. Nothing omitted. Nothing hidden.
// Documented in English and Spanish, line by line.
//
// ES: Este es CADA línea de JavaScript que corre en tu navegador
// ahora mismo. Nada omitido. Nada oculto.
// Documentado en inglés y español, línea a línea.
//
// Structure / Estructura:
// 1. Configuration — corpus, seeds, UI strings
// 2. State variables — global state of the app
// 3. UI helpers — console log, debug panel
// 4. Playback engine — animated step-by-step display
// 5. Language switching — EN ↔ ES
// 6. tokenize() — text → array of words
// 7. buildTable() — corpus → frequency table (THE MODEL)
// 8. weightedChoice() — probabilistic word selection
// 9. generate() — produces text word by word
// 10. runModel() — orchestrates everything
// 11. highlightLines() — code highlight on slider change
// 12. window.onload — initialization
// ============================================================
// ============================================================
// 1. CONFIGURATION / CONFIGURACIÓN
// ============================================================
//
// EN: The corpus is loaded from two external files:
// corpus-en.js → declares global variable CORPUS_EN
// corpus-es.js → declares global variable CORPUS_ES
// Each file contains the full text used to train the model.
// The typeof check prevents crashes if a file fails to load.
//
// ES: El corpus se carga desde dos archivos externos:
// corpus-en.js → declara la variable global CORPUS_EN
// corpus-es.js → declara la variable global CORPUS_ES
// Cada archivo contiene el texto completo para entrenar el modelo.
// El chequeo typeof evita errores si un archivo no carga.
const CORPUS = {
es: typeof CORPUS_ES !== 'undefined' ? CORPUS_ES : '[corpus-es.js not loaded]',
en: typeof CORPUS_EN !== 'undefined' ? CORPUS_EN : '[corpus-en.js not loaded]'
};
// EN: Example seeds shown in error messages when the user's
// seed is not found in the frequency table.
// ES: Semillas de ejemplo mostradas en mensajes de error cuando
// la semilla del usuario no se encuentra en la tabla.
const SEEDS = {
es: ['el proyecto', 'la ciudad', 'elena observó', 'el tiempo'],
en: ['the city', 'martin smiled', 'elena watched', 'the project']
};
// EN: UI strings for each language. Keeping them here means
// we never have hardcoded text scattered through the code —
// all visible text is in one place, easy to update.
// ES: Cadenas de UI para cada idioma. Tenerlas aquí significa
// que nunca tenemos texto fijo disperso en el código —
// todo el texto visible está en un lugar, fácil de actualizar.
const UI = {
es: {
seedPlaceholder: 'el proyecto / la ciudad / elena observó',
seedDefault: 'el proyecto',
corpusLabel: 'corpus: español',
paramChanged: 'PARÁMETRO',
nChanged: 'n-gram cambiado a',
nAffects: '· afecta buildTable()',
wChanged: 'palabras cambiado a',
wAffects: '· afecta el bucle generate()',
},
en: {
seedPlaceholder: 'the city / martin smiled / the project',
seedDefault: 'the city',
corpusLabel: 'corpus: english',
paramChanged: 'PARAM',
nChanged: 'n-gram size changed to',
nAffects: '· affects buildTable()',
wChanged: 'words changed to',
wAffects: '· affects generate() loop',
}
};
// ============================================================
// 2. STATE VARIABLES / VARIABLES DE ESTADO
// ============================================================
//
// EN: These are the global variables that hold the app's state.
// In a larger app you'd use a state management library,
// but for a small tool like this, simple globals work fine.
//
// ES: Estas son las variables globales que guardan el estado
// de la aplicación. En una app más grande usarías una
// librería de gestión de estado, pero para una herramienta
// pequeña como esta, los globals simples funcionan bien.
let currentLang = 'en'; // EN: active language / ES: idioma activo
let table = null; // EN: frequency table built from corpus / ES: tabla de frecuencias construida del corpus
let tokens = []; // EN: tokenized corpus words / ES: palabras tokenizadas del corpus
let modelN = 3; // EN: current n-gram size / ES: tamaño actual del n-grama
// ============================================================
// 3. UI HELPERS / AYUDANTES DE INTERFAZ
// ============================================================
// EN: Returns the current time as a string "HH:MM:SS".
// Used as timestamps in the CRT console.
// ES: Devuelve la hora actual como cadena "HH:MM:SS".
// Usada como timestamps en la consola CRT.
function getTime() {
const d = new Date();
return d.toTimeString().slice(0, 8);
}
// EN: Adds a line to the vintage CRT console at the bottom.
// customTime lets us show the REAL execution time
// (e.g. "+0.002ms") instead of the wall-clock time,
// making the display honest about when each step ran.
// ES: Agrega una línea a la consola CRT vintage del fondo.
// customTime nos permite mostrar el tiempo REAL de ejecución
// (ej. "+0.002ms") en lugar de la hora del reloj,
// haciendo el display honesto sobre cuándo corrió cada paso.
function crtLog(msg, cls = 'crt-ok', customTime = null) {
const inner = document.getElementById('crtInner');
const cursorLine = document.getElementById('crtCursorLine');
const line = document.createElement('div');
line.className = 'crt-line';
const ts = customTime !== null ? customTime : getTime();
line.innerHTML = `<span class="crt-time">${ts}</span>` +
`<span class="crt-prompt">></span>` +
`<span class="crt-msg ${cls}">${msg}</span>`;
inner.insertBefore(line, cursorLine);
inner.scrollTop = inner.scrollHeight; // EN: auto-scroll to bottom / ES: auto-scroll al fondo
}
// EN: Adds a debug event card to the right-side execution trace panel.
// Uses a CSS fade+slide animation for each new event.
// The animation is purely cosmetic — it does NOT represent
// real execution time (see playbackQueue for explanation).
// ES: Agrega una tarjeta de evento al panel de execution trace de la derecha.
// Usa una animación CSS de fade+slide para cada nuevo evento.
// La animación es puramente cosmética — NO representa el tiempo
// real de ejecución (ver playbackQueue para explicación).
function addDebug(badge, badgeClass, title, detail) {
const panel = document.getElementById('debugPanel');
// EN: Clear the "waiting" placeholder on first event.
// ES: Limpiamos el placeholder de "esperando" en el primer evento.
if (panel.querySelector('.debug-placeholder')) panel.innerHTML = '';
const ev = document.createElement('div');
ev.className = 'debug-event';
// EN: Start invisible and offset to the left, then animate in.
// ES: Empezamos invisible y desplazado a la izquierda, luego animamos.
ev.style.opacity = '0';
ev.style.transform = 'translateX(-6px)';
ev.style.transition = 'opacity 0.18s ease, transform 0.18s ease';
ev.innerHTML = `<span class="dbadge ${badgeClass}">${badge}</span>` +
`<div class="debug-content">${title}<br>` +
`<span class="debug-val">${detail}</span></div>`;
panel.appendChild(ev);
panel.scrollTop = panel.scrollHeight;
// EN: requestAnimationFrame ensures the browser has painted the
// initial state before we start the transition.
// Without this, the animation wouldn't be visible.
// ES: requestAnimationFrame asegura que el browser haya pintado
// el estado inicial antes de que iniciemos la transición.
// Sin esto, la animación no sería visible.
requestAnimationFrame(() => {
ev.style.opacity = '1';
ev.style.transform = 'translateX(0)';
});
}
// ============================================================
// 4. PLAYBACK ENGINE / MOTOR DE REPRODUCCIÓN
// ============================================================
//
// EN: IMPORTANT — HONESTY NOTE:
// The entire model (tokenize + buildTable + generate) runs
// in under 1 second — all steps complete before the first
// event appears on screen.
//
// The 420ms delay between events is a PEDAGOGICAL CHOICE,
// not the real execution time. It lets you read each step
// as it appears. Real per-step times are shown in the
// "+0.00Xms" timestamps on GEN steps.
//
// ES: IMPORTANTE — NOTA DE HONESTIDAD:
// Todo el modelo (tokenize + buildTable + generate) corre
// en menos de 1 segundo — todos los pasos se completan
// antes de que aparezca el primer evento en pantalla.
//
// El delay de 420ms entre eventos es una DECISIÓN PEDAGÓGICA,
// no el tiempo real de ejecución. Permite leer cada paso
// mientras aparece. Los tiempos reales por paso se muestran
// en los timestamps "+0.00Xms" de los pasos GEN.
//
// EN: How it works:
// runModel() collects ALL events into a single ordered queue Q,
// then passes Q to playbackQueue(). Each event fires after
// a fixed delay. Debug panel and console are synchronized
// because they share the same queue — no separate timers.
//
// ES: Cómo funciona:
// runModel() recolecta TODOS los eventos en una única cola
// ordenada Q, luego pasa Q a playbackQueue(). Cada evento
// dispara después de un delay fijo. El panel de debug y la
// consola están sincronizados porque comparten la misma cola
// — sin timers separados.
function playbackQueue(queue, output, statusData, elapsed) {
const STEP_MS = 420; // EN: milliseconds between events / ES: milisegundos entre eventos
document.getElementById('debugPanel').innerHTML = '';
// EN: Schedule each event at its own time offset.
// Event 0 fires at 0ms, event 1 at 420ms, event 2 at 840ms...
// ES: Programamos cada evento en su propio offset de tiempo.
// Evento 0 dispara a 0ms, evento 1 a 420ms, evento 2 a 840ms...
queue.forEach((ev, i) => {
setTimeout(() => {
if (ev.type === 'debug') {
addDebug(ev.badge, ev.badgeClass, ev.title, ev.detail);
} else {
crtLog(ev.msg, ev.cls, ev.time || null);
}
}, i * STEP_MS);
});
// EN: After all events have fired, show the output and update
// the status bar. The +200ms gives the last event time to
// finish its animation before the output appears.
// ES: Después de que todos los eventos dispararon, mostramos el output
// y actualizamos la barra de estado. Los +200ms dan tiempo al
// último evento para terminar su animación antes de que aparezca
// el output.
const totalDelay = queue.length * STEP_MS + 200;
setTimeout(() => {
document.getElementById('outputBox').textContent = output;
document.getElementById('statusCorpus').textContent = statusData.corpus;
document.getElementById('statusTokens').textContent = statusData.tokens;
document.getElementById('statusContexts').textContent = statusData.contexts;
document.getElementById('statusTime').textContent = `last run: ${elapsed}s actual`;
setStatus('ready');
}, totalDelay);
}
// EN: Updates the status dot and text in the bottom bar.
// running=true shows an amber pulsing dot.
// running=false shows a green steady dot.
// ES: Actualiza el punto de estado y el texto en la barra inferior.
// running=true muestra un punto ámbar parpadeante.
// running=false muestra un punto verde estático.
function setStatus(text, running = false) {
document.getElementById('statusText').textContent = text;
const dot = document.getElementById('statusDot');
dot.className = 'status-dot ' + (running ? 'status-running' : 'status-ready');
}
// ============================================================
// 5. LANGUAGE SWITCHING / CAMBIO DE IDIOMA
// ============================================================
//
// EN: Switches the active corpus and all UI text between
// English and Spanish. The model is reset so the next
// RUN trains on the newly selected corpus.
//
// Things that change on switch:
// - Active corpus (EN or ES text)
// - Seed input placeholder and default value
// - Theory panel example
// - URL bar display
// - Status bar corpus label
// - Console log message
//
// ES: Cambia el corpus activo y todo el texto de la UI entre
// inglés y español. El modelo se resetea para que el
// próximo RUN entrene con el corpus recién seleccionado.
//
// Cosas que cambian al cambiar:
// - Corpus activo (texto EN o ES)
// - Placeholder y valor por defecto del seed input
// - Ejemplo del panel de teoría
// - Display de la barra de URL
// - Etiqueta de corpus en la barra de estado
// - Mensaje de log en la consola
function switchLang(lang) {
if (lang === currentLang) return; // EN: already active / ES: ya está activo
currentLang = lang;
// EN: Toggle the active class on the EN/ES buttons.
// ES: Alternamos la clase active en los botones EN/ES.
document.getElementById('btnEN').classList.toggle('active', lang === 'en');
document.getElementById('btnES').classList.toggle('active', lang === 'es');
// EN: Update the fake URL bar to reflect the language.
// ES: Actualizamos la barra de URL falsa para reflejar el idioma.
document.querySelector('.url-input').textContent =
lang === 'en'
? 'yourfirstllm.dev/level/1-ngrams'
: 'yourfirstllm.dev/nivel/1-ngramas';
// EN: Swap the theory panel example to match the active corpus.
// ES: Intercambiamos el ejemplo del panel de teoría para coincidir
// con el corpus activo.
const exEl = document.getElementById('theoryExample');
if (exEl) {
exEl.innerHTML = lang === 'en'
? '<span class="tc">"the city"</span> →<br>' +
' was: <span class="tp">9</span><br>' +
' had: <span class="tp">6</span><br>' +
' seemed: <span class="tp">3</span>'
: '<span class="tc">"el proyecto"</span> →<br>' +
' crecía: <span class="tp">8</span><br>' +
' avanzaba: <span class="tp">5</span><br>' +
' era: <span class="tp">4</span>';
}
// EN: Update seed input with language-appropriate defaults.
// ES: Actualizamos el seed input con los valores por defecto del idioma.
const seedInput = document.getElementById('seedInput');
const ui = UI[lang];
seedInput.placeholder = ui.seedPlaceholder;
seedInput.value = ui.seedDefault;
// EN: Clear output and debug panel — they belong to the old corpus.
// ES: Limpiamos el output y el panel de debug — pertenecen al corpus viejo.
document.getElementById('outputBox').innerHTML =
'<span class="output-placeholder">Output will appear here after clicking RUN...</span>';
document.getElementById('debugPanel').innerHTML =
'<div class="debug-placeholder">Run the model to see step-by-step debug info here.</div>';
crtLog(`LANG · switched to ${lang.toUpperCase()} · corpus: ${CORPUS[lang].length.toLocaleString()} chars`, 'crt-info-msg');
document.getElementById('statusCorpus').textContent = ui.corpusLabel;
document.getElementById('statusTokens').textContent = 'tokens: —';
document.getElementById('statusContexts').textContent = 'contexts: —';
document.getElementById('statusTime').textContent = 'last run: —';
setStatus('ready');
// EN: Reset the model. Next RUN will retrain on the new corpus.
// ES: Reseteamos el modelo. El próximo RUN entrenará con el nuevo corpus.
table = null;
tokens = [];
}
// ============================================================
// 6. TOKENIZE / TOKENIZAR
// ============================================================
//
// EN: Converts raw text into an array of lowercase words.
// This is step 1 of every language model pipeline.
//
// "The city was quiet." → ["the","city","was","quiet"]
//
// Why lowercase? So "City" and "city" map to the same token.
// Without this, the model treats them as unrelated words.
//
// Why replace punctuation with spaces (not empty string)?
// "city.The" → split by "." → "city" + "The" ✓
// "city.The" → replace "." with "" → "cityThe" ✗
//
// ES: Convierte texto crudo en un array de palabras en minúsculas.
// Este es el paso 1 de toda tubería de modelo de lenguaje.
//
// "La ciudad estaba quieta." → ["la","ciudad","estaba","quieta"]
//
// ¿Por qué minúsculas? Para que "Ciudad" y "ciudad" sean el mismo token.
// Sin esto, el modelo los trata como palabras no relacionadas.
//
// ¿Por qué reemplazar puntuación por espacios (no cadena vacía)?
// "ciudad.La" → dividir por "." → "ciudad" + "La" ✓
// "ciudad.La" → reemplazar "." con "" → "ciudadLa" ✗
function tokenize(text) {
text = text.toLowerCase();
const signs = [
'.', ',', ';', ':', '!', '?',
'(', ')', '"',
'\u2014', // em dash —
'\u00bf', // ¿
'\u00a1', // ¡
'\n', '\r'
];
signs.forEach(s => {
text = text.split(s).join(' ');
});
// EN: /\s+/ matches one or more whitespace characters.
// .filter(t => t.length > 0) removes empty strings.
// ES: /\s+/ coincide con uno o más caracteres de espacio en blanco.
// .filter(t => t.length > 0) elimina cadenas vacías.
return text.split(/\s+/).filter(t => t.length > 0);
}
// ============================================================
// 7. BUILD FREQUENCY TABLE / CONSTRUIR TABLA DE FRECUENCIAS
// ============================================================
//
// EN: This IS the training phase. We scan every position in
// the token array and record what word follows each context.
//
// The result is a nested object:
// table["the city"] = { "was": 4, "never": 2, "always": 1 }
// table["city was"] = { "quiet": 3, "dark": 1 }
// ...
//
// This table IS the model. It encodes everything the model
// "knows" about the language in the corpus.
//
// The 'n' parameter:
// n=2 → context = 1 word (bigram)
// n=3 → context = 2 words (trigram) ← default, good balance
// n=4 → context = 3 words (more coherent, needs bigger corpus)
// n=5 → context = 4 words (very coherent, needs large corpus)
//
// ES: Esta ES la fase de entrenamiento. Recorremos cada posición
// del array de tokens y registramos qué palabra sigue a cada contexto.
//
// El resultado es un objeto anidado:
// tabla["el proyecto"] = { "crecía": 4, "avanzaba": 2, "era": 1 }
// tabla["proyecto crecía"] = { "más": 3, "lentamente": 1 }
// ...
//
// Esta tabla ES el modelo. Codifica todo lo que el modelo
// "sabe" sobre el lenguaje del corpus.
//
// El parámetro 'n':
// n=2 → contexto = 1 palabra (bigrama)
// n=3 → contexto = 2 palabras (trigrama) ← por defecto, buen balance
// n=4 → contexto = 3 palabras (más coherente, necesita corpus mayor)
// n=5 → contexto = 4 palabras (muy coherente, necesita corpus grande)
function buildTable(tokens, n) {
const table = {};
// EN: Stop at tokens.length - (n-1) so we always have
// a "next word" to record at position i + (n-1).
// ES: Nos detenemos en tokens.length - (n-1) para siempre
// tener una "siguiente palabra" en la posición i + (n-1).
for (let i = 0; i < tokens.length - (n - 1); i++) {
// EN: The context is (n-1) consecutive tokens joined by spaces.
// slice(i, i + n - 1) takes tokens from index i up to (not including) i + n - 1.
// ES: El contexto son (n-1) tokens consecutivos unidos por espacios.
// slice(i, i + n - 1) toma tokens desde el índice i hasta (sin incluir) i + n - 1.
const context = tokens.slice(i, i + n - 1).join(' ');
// EN: The word to predict is the token right after the context window.
// ES: La palabra a predecir es el token justo después de la ventana de contexto.
const nextWord = tokens[i + n - 1];
// EN: Initialize the context entry if we've never seen it before.
// ES: Inicializamos la entrada del contexto si nunca lo vimos antes.
if (!table[context]) table[context] = {};
// EN: (table[context][nextWord] || 0) means:
// "if nextWord exists, use its current count; otherwise start at 0"
// Then add 1 for this occurrence.
// ES: (table[context][nextWord] || 0) significa:
// "si nextWord existe, usa su conteo actual; sino empieza en 0"
// Luego suma 1 por esta ocurrencia.
table[context][nextWord] = (table[context][nextWord] || 0) + 1;
}
return table; // EN: This is the complete trained model / ES: Este es el modelo entrenado completo
}
// ============================================================
// 8. WEIGHTED CHOICE / ELECCIÓN PONDERADA
// ============================================================
//
// EN: Selects a word randomly, but weighted by frequency.
// More frequent words have a proportionally higher chance.
// This is what makes generated text varied — running the
// same seed twice produces different output.
//
// Algorithm: weighted random sampling
// 1. Sum all counts → total
// 2. Pick random r in [0, total)
// 3. Subtract each word's count from r in sequence
// 4. Return the word that first brings r to ≤ 0
//
// Intuition: imagine a number line from 0 to total.
// Each word occupies a segment proportional to its count.
// We pick a random point on the line and return whichever
// word's segment contains that point.
//
// ES: Selecciona una palabra aleatoriamente, pero ponderada por frecuencia.
// Las palabras más frecuentes tienen una chance proporcionalmente mayor.
// Esto es lo que hace que el texto generado sea variado — correr
// la misma semilla dos veces produce output diferente.
//
// Algoritmo: muestreo aleatorio ponderado
// 1. Sumamos todos los conteos → total
// 2. Elegimos r aleatorio en [0, total)
// 3. Restamos el conteo de cada palabra de r en secuencia
// 4. Devolvemos la palabra que primero lleva r a ≤ 0
//
// Intuición: imaginar una línea numérica de 0 a total.
// Cada palabra ocupa un segmento proporcional a su conteo.
// Elegimos un punto aleatorio en la línea y devolvemos
// la palabra cuyo segmento contiene ese punto.
function weightedChoice(map) {
const words = Object.keys(map);
// EN: Accumulate total weight using reduce().
// reduce(fn, 0) starts at 0 and applies fn(accumulator, element).
// ES: Acumulamos el peso total con reduce().
// reduce(fn, 0) empieza en 0 y aplica fn(acumulador, elemento).
const total = words.reduce((sum, word) => sum + map[word], 0);
let r = Math.random() * total; // EN: random point on the line / ES: punto aleatorio en la línea
for (const word of words) {
r -= map[word];
if (r <= 0) return word;
}
// EN: Should never reach here in practice, but floating-point
// arithmetic can produce tiny rounding errors that leave r
// just barely above 0 after all words. Return the last word.
// ES: En práctica nunca debería llegar aquí, pero la aritmética
// de punto flotante puede producir pequeños errores de redondeo
// que dejan r apenas por encima de 0 después de todas las palabras.
// Devolvemos la última palabra.
return words[0];
}
// ============================================================
// 9. GENERATE TEXT / GENERAR TEXTO
// ============================================================
//
// EN: Produces text by chaining weighted choices.
// Each new word becomes part of the context for the next.
// This is a first-order Markov chain over word n-grams.
//
// Why does coherence break down after many words?
// Because the model has no memory beyond the last (n-1)
// words. It can't "remember" the topic or characters from
// earlier in the generation. Each step is statistically
// reasonable, but the sequence can drift.
//
// This is the fundamental difference from GPT:
// GPT's attention mechanism can "look back" thousands of
// tokens, maintaining long-range coherence.
//
// ES: Produce texto encadenando elecciones ponderadas.
// Cada nueva palabra pasa a formar parte del contexto para la siguiente.
// Esta es una cadena de Markov de primer orden sobre n-gramas de palabras.
//
// ¿Por qué la coherencia se rompe después de muchas palabras?
// Porque el modelo no tiene memoria más allá de las últimas (n-1)
// palabras. No puede "recordar" el tema o los personajes de antes
// en la generación. Cada paso es estadísticamente razonable,
// pero la secuencia puede derivar.
//
// Esta es la diferencia fundamental con GPT:
// El mecanismo de atención de GPT puede "mirar atrás" miles de
// tokens, manteniendo coherencia de largo alcance.
function generate(seed, table, n, wordCount) {
// EN: Start with the seed words. These are the user's "prompt".
// ES: Empezamos con las palabras de la semilla. Estas son el "prompt" del usuario.
let result = seed.toLowerCase().split(' ').filter(x => x);
// EN: stepTimes records the real execution time of each word prediction.
// This is displayed honestly in the UI as "+0.00Xms".
// Typical value: 0.001ms to 0.005ms — pure lookup in a JS object.
// ES: stepTimes registra el tiempo real de ejecución de cada predicción.
// Se muestra honestamente en la UI como "+0.00Xms".
// Valor típico: 0.001ms a 0.005ms — pura búsqueda en un objeto JS.
const stepTimes = [];
// EN: We subtract (n-1) from wordCount because the seed already
// provides those first words.
// Example: wordCount=30, n=3, seed has 2 words → 28 new words generated.
// ES: Restamos (n-1) de wordCount porque la semilla ya aporta esas
// primeras palabras.
// Ejemplo: wordCount=30, n=3, semilla tiene 2 palabras → 28 palabras nuevas.
for (let i = 0; i < wordCount - (n - 1); i++) {
// EN: slice(-(n-1)) takes the last (n-1) elements of the array.
// For n=3: takes the last 2 words.
// Negative indices in JS count from the end: -1 is last, -2 is second-to-last.
// ES: slice(-(n-1)) toma los últimos (n-1) elementos del array.
// Para n=3: toma las últimas 2 palabras.
// Los índices negativos en JS cuentan desde el final: -1 es el último.
const context = result.slice(-(n - 1)).join(' ');
// EN: Dead end: this context was never seen in the corpus.
// Can happen when generated words form a sequence that
// didn't exist in training data. We stop cleanly.
// ES: Callejón sin salida: este contexto nunca se vio en el corpus.
// Puede ocurrir cuando las palabras generadas forman una secuencia
// que no existía en los datos de entrenamiento. Paramos limpiamente.
if (!table[context]) break;
// EN: Measure the real time for this single prediction.
// performance.now() is more precise than Date.now() —
// it gives sub-millisecond resolution.
// ES: Medimos el tiempo real de esta única predicción.
// performance.now() es más preciso que Date.now() —
// da resolución sub-milisegundo.
const stepStart = performance.now();
result.push(weightedChoice(table[context]));
stepTimes.push(+(performance.now() - stepStart).toFixed(4));
}
// EN: Return both the text and timing data.
// The object destructuring { text, stepTimes } = generate(...)
// lets the caller access each part independently.
// ES: Devolvemos tanto el texto como los datos de tiempo.
// La desestructuración de objeto { text, stepTimes } = generate(...)
// permite al llamador acceder a cada parte independientemente.
return {
text: result.join(' '),
stepTimes: stepTimes
};
}
// ============================================================
// 10. RUN MODEL — MAIN ORCHESTRATOR / ORQUESTADOR PRINCIPAL
// ============================================================
//
// EN: This is the function called when the user clicks RUN.
// It coordinates every step:
// 1. Read parameters from UI (n, wordCount, seed)
// 2. Tokenize the corpus
// 3. Build the frequency table
// 4. Validate the seed
// 5. Generate text
// 6. Collect all events into queue Q
// 7. Hand Q to playbackQueue() for animated display
//
// Notice that steps 2–6 all happen synchronously and
// instantly. The user sees nothing until playbackQueue
// starts replaying events with 420ms delays.
//
// ES: Esta es la función llamada cuando el usuario hace clic en RUN.
// Coordina cada paso:
// 1. Lee parámetros de la UI (n, wordCount, seed)
// 2. Tokeniza el corpus
// 3. Construye la tabla de frecuencias
// 4. Valida la semilla
// 5. Genera texto
// 6. Recolecta todos los eventos en la cola Q
// 7. Entrega Q a playbackQueue() para la visualización animada
//
// Nótese que los pasos 2–6 ocurren todos sincrónicamente
// e instantáneamente. El usuario no ve nada hasta que
// playbackQueue comienza a reproducir eventos con delays de 420ms.
function runModel() {
// EN: Read the current slider/input values from the DOM.
// ES: Leemos los valores actuales de sliders/inputs del DOM.
const n = parseInt(document.getElementById('nRange').value);
const wordCount = parseInt(document.getElementById('wordsRange').value);
const seed = document.getElementById('seedInput').value.trim();
const corpus = CORPUS[currentLang];
// EN: Basic validation — seed must have at least 2 words.
// ES: Validación básica — la semilla debe tener al menos 2 palabras.
if (!seed || seed.split(' ').filter(x => x).length < 2) {
crtLog('ERR · seed must be at least 2 words', 'crt-err');
return;
}
setStatus('running...', true);
document.getElementById('debugPanel').innerHTML =
'<div class="debug-placeholder">Computing...</div>';
document.getElementById('outputBox').innerHTML =
'<span class="output-placeholder">Computing...</span>';
// EN: t0 marks the start of the entire computation.
// We'll use it to report total elapsed time at the end.
// ES: t0 marca el inicio de toda la computación.
// Lo usaremos para reportar el tiempo total transcurrido al final.
const t0 = performance.now();
// EN: Q is the unified event queue. Both debug events and console
// log events go here in the order they should appear.
// type: 'debug' → goes to the right panel
// type: 'crt' → goes to the console
// ES: Q es la cola de eventos unificada. Tanto los eventos de debug
// como los de consola van aquí en el orden en que deben aparecer.
// type: 'debug' → va al panel derecho
// type: 'crt' → va a la consola
const Q = [];
const pd = (badge, badgeClass, title, detail) =>
Q.push({ type: 'debug', badge, badgeClass, title, detail });
const pc = (msg, cls = 'crt-ok', time = null) =>
Q.push({ type: 'crt', msg, cls, time });
// --- STEP 1: TOKENIZE ---
tokens = tokenize(corpus);
const vocab = new Set(tokens); // EN: unique words / ES: palabras únicas
pd('CORPUS', 'dbadge-init',
`Corpus loaded [${currentLang.toUpperCase()}]`,
`${corpus.length.toLocaleString()} chars`);
pc(`CORPUS OK · ${corpus.length.toLocaleString()} chars [${currentLang.toUpperCase()}]`);
pd('TOKEN', 'dbadge-token',
'Tokenization complete',
`${tokens.length.toLocaleString()} tokens · ${vocab.size.toLocaleString()} unique`);
pc(`TOKEN OK · ${tokens.length.toLocaleString()} tokens · ${vocab.size.toLocaleString()} unique`);
// --- STEP 2: BUILD TABLE ---
modelN = n;
table = buildTable(tokens, n);
const ctxCount = Object.keys(table).length;
pd('TABLE', 'dbadge-table',
`Frequency table built(n=${n})`,
`${ctxCount.toLocaleString()} unique contexts`);
pc(`TABLE OK · n=${n} · ${ctxCount.toLocaleString()} contexts`);
// --- STEP 3: VALIDATE SEED ---
const seedWords = seed.toLowerCase().split(' ').filter(x => x);
const neededWords = n - 1; // EN: context size required / ES: tamaño de contexto requerido
// EN: With n=4 we need 3 words of context (n-1=3).
// If the seed has fewer, we can't look it up in the table.
// ES: Con n=4 necesitamos 3 palabras de contexto (n-1=3).
// Si la semilla tiene menos, no podemos buscarla en la tabla.
if (seedWords.length < neededWords) {
const msg = `Seed needs at least ${neededWords} word${neededWords > 1 ? 's' : ''} for n=${n}. You provided ${seedWords.length}.`;
pd('WARN', 'dbadge-warn', 'Not enough seed words', `need ${neededWords} words for n=${n}`);
pc(`WARN · ${msg}`, 'crt-warn');
document.getElementById('outputBox').innerHTML =
`<span style="color:var(--red)">${msg}</span>`;
setStatus('ready');
return;
}
const seedCtx = seedWords.slice(0, neededWords).join(' ');
if (!table[seedCtx]) {
// EN: The seed context doesn't exist in the table.
// Show the first few valid contexts as suggestions.
// ES: El contexto de la semilla no existe en la tabla.
// Mostramos los primeros contextos válidos como sugerencias.
const validSeeds = Object.keys(table).slice(0, 4).join(' · ');
pd('WARN', 'dbadge-warn',
'Seed not found in corpus',
`"${seedCtx}" not in table for n=${n}`);
pc(`WARN · seed "${seedCtx}" not found · try: ${validSeeds}`, 'crt-warn');
playbackQueue(Q,
`Seed "${seedCtx}" not found for n=${n}. Try: ${validSeeds}`,
{
corpus: `corpus: ${currentLang}`,
tokens: `tokens: ${tokens.length.toLocaleString()}`,
contexts: `contexts: ${ctxCount.toLocaleString()}`
},
'—'
);
return;
}
// --- STEP 4: GENERATE ---
pd('GEN', 'dbadge-gen', `Starting generation`, `seed: "${seedCtx}" · ${wordCount} words`);
pc(`GEN · seed="${seedCtx}" · n=${n} · words=${wordCount}`, 'crt-info-msg');
const result = generate(seedCtx, table, n, wordCount);
const output = result.text;
const stepTimes = result.stepTimes;
const words = output.split(' ');
// EN: Show the first 5 generation steps in detail.
// Each step shows: context → chosen word, probability, candidates, real time.
// ES: Mostramos los primeros 5 pasos de generación en detalle.
// Cada paso muestra: contexto → palabra elegida, probabilidad, candidatos, tiempo real.
for (let i = n - 1; i < Math.min(n + 4, words.length); i++) {
const ctx = words.slice(i - (n - 1), i).join(' ');
const chosen = words[i];
const opts = table[ctx] ? Object.keys(table[ctx]).length : 0;
const prob = table[ctx] && table[ctx][chosen]
? (table[ctx][chosen] / Object.values(table[ctx]).reduce((a, b) => a + b, 0)).toFixed(2)
: '?';
const step = i - (n - 2);
// EN: stepTimes index: step 1 is at index (n-1) - (n-1) = 0, etc.
// ES: Índice de stepTimes: el paso 1 está en el índice (n-1) - (n-1) = 0, etc.
const stepMs = stepTimes[i - (n - 1)] !== undefined
? `+${stepTimes[i - (n - 1)].toFixed(3)}ms`
: '+0.000ms';
pd('GEN', 'dbadge-gen',
`Step ${step}: ctx = "${ctx}"`,
`→ "${chosen}" (p=${prob}, ${opts} candidates) · ${stepMs}`);
pc(`STEP ${step} · "${ctx}" → "${chosen}" · p=${prob}`, 'crt-info-msg', stepMs);
}
if (words.length > n + 4) {
pd('GEN', 'dbadge-gen',
`... ${words.length - (n + 4)} more steps`,
`all computed before playback`);
pc(`... ${words.length - (n + 4)} more steps computed`, 'crt-info-msg');
}
const t1 = performance.now();
const elapsed = ((t1 - t0) / 1000).toFixed(2);
pd('OK', 'dbadge-ok', 'Execution complete', `${words.length} words · ${elapsed}s actual`);
pc(`DONE · ${elapsed}s actual runtime · playback 420ms/step`, 'crt-ok');
// EN: All events are collected. Now hand off to playbackQueue.
// From this point the UI takes over with timed animations.
// ES: Todos los eventos están recolectados. Ahora entregamos a playbackQueue.
// Desde este punto la UI toma el control con animaciones temporizadas.
playbackQueue(Q, output, {
corpus: `corpus: ${currentLang} · ${corpus.length.toLocaleString()} chars`,
tokens: `tokens: ${tokens.length.toLocaleString()}`,
contexts: `contexts: ${ctxCount.toLocaleString()}`
}, elapsed);
}
// ============================================================
// 11. HIGHLIGHT LINES / RESALTAR LÍNEAS
// ============================================================
//
// EN: When the user moves a slider, the relevant lines in the
// "Actual code" panel are highlighted and scrolled into view.
// - N-gram slider → highlights buildTable lines (purple)
// - Words slider → highlights generate() loop (amber)
// The highlight fades after 2 seconds.
//
// ES: Cuando el usuario mueve un slider, las líneas relevantes
// en el panel "Actual code" se resaltan y se hace scroll hasta ellas.
// - Slider N-gram → resalta líneas de buildTable (violeta)
// - Slider Words → resalta el bucle de generate() (ámbar)
// El resaltado desaparece después de 2 segundos.
let highlightTimer = null;
function highlightLines(param) {
// EN: Clear any existing highlight before applying a new one.
// ES: Limpiamos cualquier resaltado existente antes de aplicar uno nuevo.
document.querySelectorAll('.cl').forEach(el => {
el.classList.remove('highlight-line', 'highlight-line-amber');
});
const editor = document.getElementById('codeDisplay');
const ui = UI[currentLang];
if (param === 'n') {
// EN: These IDs correspond to the lines in buildTable that use 'n'.
// ES: Estos IDs corresponden a las líneas de buildTable que usan 'n'.
['codeLine11', 'codeLine13', 'codeLine14', 'codeLine15'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('highlight-line');
});
const target = document.getElementById('codeLine11');
if (target) editor.scrollTo({
top: target.offsetTop - editor.offsetTop - 20,
behavior: 'smooth'
});
crtLog(`${ui.paramChanged} · ${ui.nChanged} ${document.getElementById('nVal').textContent} ${ui.nAffects}`, 'crt-info-msg');
}
if (param === 'words') {
// EN: These IDs correspond to the lines in generate() that use 'words'.
// ES: Estos IDs corresponden a las líneas de generate() que usan 'words'.
['codeLine30', 'codeLine32', 'codeLine33'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('highlight-line-amber');
});
const target = document.getElementById('codeLine30');
if (target) editor.scrollTo({
top: target.offsetTop - editor.offsetTop - 20,
behavior: 'smooth'
});
crtLog(`${ui.paramChanged} · ${ui.wChanged} ${document.getElementById('wordsVal').textContent} ${ui.wAffects}`, 'crt-info-msg');
}
// EN: Auto-remove highlight after 2 seconds.
// ES: Eliminar automáticamente el resaltado después de 2 segundos.
clearTimeout(highlightTimer);
highlightTimer = setTimeout(() => {
document.querySelectorAll('.cl').forEach(el => {
el.classList.remove('highlight-line', 'highlight-line-amber');
});
}, 2000);
}
// ============================================================
// 12. INITIALIZATION / INICIALIZACIÓN
// ============================================================
//
// EN: window.onload fires after the HTML, CSS, and all <script>
// tags have loaded. We use it to log the startup message
// to the CRT console and set the initial status bar text.
//
// ES: window.onload se dispara después de que el HTML, CSS y
// todos los tags <script> se han cargado. Lo usamos para
// registrar el mensaje de inicio en la consola CRT y
// establecer el texto inicial de la barra de estado.
window.onload = () => {
crtLog('YOUR_FIRST_LLM v1.0 initialized', 'crt-ok');
crtLog(
`English corpus loaded · ${CORPUS.en.length.toLocaleString()} chars · press RUN to start`,
'crt-info-msg'
);
document.getElementById('statusCorpus').textContent = 'corpus: english';
};