microgpt / index.html
carpelan's picture
Update index.html
1a45c0b verified
Raw
History Blame Contribute Delete
25.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>microgpt-rs</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 720px;
margin: 2rem auto;
padding: 0 1rem;
color: #1a1a1a;
background: #fafafa;
}
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
p.subtitle { color: #666; margin-bottom: 1.5rem; font-size: 0.9rem; }
h2 { font-size: 1.05rem; margin-bottom: 0.5rem; color: #333; }
h3 { font-size: 0.9rem; color: #555; margin-bottom: 0.3rem; }
button {
background: #2563eb;
color: white;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: background 0.15s;
}
button:hover { background: #1d4ed8; }
button:disabled { background: #93b4f5; cursor: not-allowed; }
.mono { font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; }
#info { margin-top: 1rem; color: #555; font-size: 0.85rem; display: none; }
#progress-wrap { margin-top: 0.75rem; display: none; }
#progress-bar { width: 100%; height: 22px; background: #e5e7eb; border-radius: 11px; overflow: hidden; }
#progress-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #2563eb, #7c3aed); border-radius: 11px; transition: width 0.15s; }
#progress-text { display: flex; justify-content: space-between; margin-top: 0.3rem; font-size: 0.8rem; color: #666; }
/* Loss chart */
#chart-wrap { margin-top: 0.75rem; display: none; }
#chart { width: 100%; height: 220px; border-radius: 6px; background: #111; }
/* Sample names timeline */
#samples-wrap { margin-top: 0.75rem; display: none; }
#samples-scroll { display: flex; gap: 0.5rem; overflow-x: auto; padding-bottom: 0.4rem; }
#samples-scroll::-webkit-scrollbar { height: 4px; }
#samples-scroll::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
.sample-card { flex-shrink: 0; background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 0.4rem 0.6rem; font-size: 0.8rem; }
.sample-card .step-label { font-size: 0.7rem; color: #999; margin-bottom: 0.2rem; }
.sample-card .sample-name { color: #3730a3; font-weight: 500; }
/* Generated names */
#names { margin-top: 1rem; display: none; }
#names ul { list-style: none; display: flex; flex-wrap: wrap; gap: 0.5rem; }
#names li { background: #e0e7ff; color: #3730a3; padding: 0.3rem 0.8rem; border-radius: 999px; font-size: 0.95rem; }
/* Prefix explorer */
#explorer-wrap { margin-top: 1.5rem; display: none; }
#prefix-input {
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
font-size: 1.1rem;
padding: 0.5rem 0.75rem;
border: 2px solid #d1d5db;
border-radius: 8px;
width: 100%;
max-width: 300px;
outline: none;
transition: border-color 0.15s;
}
#prefix-input:focus { border-color: #2563eb; }
#prefix-input::placeholder { color: #bbb; }
#prefix-bar-chart { width: 100%; height: 180px; border-radius: 6px; background: #111; margin-top: 0.5rem; }
/* Temperature comparison */
#temp-compare { margin-top: 1.25rem; }
#temp-columns { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.75rem; margin-top: 0.5rem; }
.temp-col {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 0.6rem;
text-align: center;
}
.temp-col .temp-label { font-size: 0.95rem; font-weight: 600; color: #333; }
.temp-col .temp-desc { font-size: 0.72rem; color: #888; margin-bottom: 0.4rem; }
.temp-col canvas { width: 100%; height: 100px; border-radius: 4px; background: #111; }
.temp-col .temp-names { margin-top: 0.4rem; font-size: 0.85rem; text-align: left; }
.temp-col .temp-names div { color: #3730a3; font-weight: 500; padding: 0.1rem 0; }
#regen-btn { margin-top: 0.75rem; font-size: 0.85rem; padding: 0.4rem 1rem; }
/* Dataset section */
#dataset-section { margin-bottom: 1rem; }
#dataset-status { font-size: 0.85rem; color: #555; margin-bottom: 0.5rem; }
#dataset-buttons { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
#dataset-buttons button {
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
background: #f3f4f6;
color: #333;
border: 1px solid #d1d5db;
}
#dataset-buttons button:hover { background: #e5e7eb; }
#dataset-reset { display: none; background: #fef2f2 !important; color: #b91c1c !important; border-color: #fca5a5 !important; }
#dataset-reset:hover { background: #fee2e2 !important; }
#dataset-textarea-wrap { display: none; margin-top: 0.5rem; }
#dataset-textarea {
width: 100%;
height: 120px;
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
font-size: 0.85rem;
padding: 0.5rem;
border: 1px solid #d1d5db;
border-radius: 6px;
resize: vertical;
}
#dataset-textarea::placeholder { color: #bbb; }
#dataset-textarea:focus { outline: none; border-color: #2563eb; }
/* Name scorer */
#scorer-wrap { margin-top: 1.5rem; display: none; }
#scorer-input {
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
font-size: 1.1rem;
padding: 0.5rem 0.75rem;
border: 2px solid #d1d5db;
border-radius: 8px;
width: 100%;
max-width: 300px;
outline: none;
transition: border-color 0.15s;
}
#scorer-input:focus { border-color: #2563eb; }
#scorer-input::placeholder { color: #bbb; }
#scorer-chars { margin-top: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.15rem; }
.scored-char {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 0.3rem 0.35rem 0.2rem;
border-radius: 6px;
min-width: 2rem;
}
.scored-char .char-letter {
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
font-size: 1.4rem;
font-weight: 700;
line-height: 1;
}
.scored-char .char-prob {
font-size: 0.65rem;
opacity: 0.8;
margin-top: 0.15rem;
}
#scorer-summary {
margin-top: 0.5rem;
font-size: 0.82rem;
color: #555;
}
</style>
</head>
<body>
<h1>microgpt-rs</h1>
<p class="subtitle">Character-level GPT trained on names, running in WebAssembly. Based on Andrej Karpathy's <a href="https://gist.github.com/karpathy/8627fe009c40f57531cb18360106ce95" style="color:#2563eb">microgpt</a> (100k steps instead of 1k). <a href="https://github.com/carpelan/microgpt-rs" style="color:#2563eb">Source code</a>.</p>
<div id="dataset-section">
<div id="dataset-status">Dataset: Using default (32,033 names)</div>
<div id="dataset-buttons">
<button id="dataset-upload-btn">Upload .txt file</button>
<button id="dataset-paste-btn">Paste custom data</button>
<button id="dataset-reset">Reset to default</button>
<input type="file" id="dataset-file" accept=".txt,.csv" style="display:none">
</div>
<div id="dataset-textarea-wrap">
<textarea id="dataset-textarea" placeholder="one word per line, a-z only&#10;luna&#10;mochi&#10;bella"></textarea>
</div>
</div>
<button id="btn">Train</button>
<div id="info"></div>
<div id="progress-wrap">
<div id="progress-bar"><div id="progress-fill"></div></div>
<div id="progress-text" class="mono">
<span id="pt-left"></span>
<span id="pt-right"></span>
</div>
</div>
<div id="chart-wrap"><canvas id="chart"></canvas></div>
<div id="samples-wrap">
<h2>Sample names during training</h2>
<div id="samples-scroll"></div>
</div>
<div id="names"><h2>Generated names</h2><ul id="name-list"></ul></div>
<div id="explorer-wrap">
<h2>What comes after ___?</h2>
<label for="prefix-input" style="font-size:0.85rem; color:#555;">Type a prefix:</label>
<input type="text" id="prefix-input" class="mono" placeholder="mar" maxlength="14" autocomplete="off" spellcheck="false">
<canvas id="prefix-bar-chart"></canvas>
<div id="temp-compare">
<h3>How temperature affects these predictions</h3>
<div id="temp-columns">
<div class="temp-col" id="temp-col-0">
<div class="temp-label">T = 0.2</div>
<div class="temp-desc">Conservative &mdash; always picks the obvious choice</div>
<canvas id="temp-chart-0"></canvas>
<div class="temp-names" id="temp-names-0"></div>
</div>
<div class="temp-col" id="temp-col-1">
<div class="temp-label">T = 0.8</div>
<div class="temp-desc">Balanced &mdash; natural variety</div>
<canvas id="temp-chart-1"></canvas>
<div class="temp-names" id="temp-names-1"></div>
</div>
<div class="temp-col" id="temp-col-2">
<div class="temp-label">T = 1.8</div>
<div class="temp-desc">Creative &mdash; wild, unexpected picks</div>
<canvas id="temp-chart-2"></canvas>
<div class="temp-names" id="temp-names-2"></div>
</div>
</div>
<button id="regen-btn">Regenerate</button>
</div>
</div>
<div id="scorer-wrap">
<h2>How surprising is this name?</h2>
<label for="scorer-input" style="font-size:0.85rem; color:#555;">Type a name:</label>
<input type="text" id="scorer-input" class="mono" placeholder="maria" maxlength="14" autocomplete="off" spellcheck="false">
<div id="scorer-chars"></div>
<div id="scorer-summary"></div>
</div>
<script type="module">
import init, { Trainer } from './pkg/microgpt_rs.js';
const $ = id => document.getElementById(id);
const btn = $('btn'), info = $('info');
const progressWrap = $('progress-wrap'), progressFill = $('progress-fill');
const ptLeft = $('pt-left'), ptRight = $('pt-right');
const chartWrap = $('chart-wrap'), canvas = $('chart');
const samplesWrap = $('samples-wrap'), samplesScroll = $('samples-scroll');
const namesDiv = $('names'), nameList = $('name-list');
const explorerWrap = $('explorer-wrap');
const prefixInput = $('prefix-input');
const prefixBarChart = $('prefix-bar-chart');
const regenBtn = $('regen-btn');
const scorerWrap = $('scorer-wrap');
const scorerInput = $('scorer-input');
const scorerChars = $('scorer-chars');
const scorerSummary = $('scorer-summary');
const CHUNK = 500, SAMPLE_INTERVAL = 500;
const VOCAB = 27;
const PREV_COLORS = ['#555', '#444', '#383838'];
let prevRuns = [];
const LABELS = 'abcdefghijklmnopqrstuvwxyz\u00b7'.split('');
const TEMPS = [0.2, 0.8, 1.8];
// ── Loss chart ──
function drawChart(points, total) {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
const W = rect.width, H = rect.height;
const pad = { top: 16, right: 16, bottom: 28, left: 46 };
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, W, H);
if (points.length < 2) return;
const maxLoss = points[0].loss;
const minLoss = Math.min(...points.map(p => p.loss));
const yTop = minLoss * 0.9;
const yBot = maxLoss * 1.05;
const xMax = total;
const toX = step => pad.left + (step / xMax) * plotW;
const toY = loss => pad.top + plotH - ((loss - yTop) / (yBot - yTop)) * plotH;
// Grid
ctx.strokeStyle = '#333'; ctx.lineWidth = 0.5;
ctx.font = '10px system-ui'; ctx.fillStyle = '#888'; ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const v = yTop + (yBot - yTop) * (i / 4), y = toY(v);
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(W - pad.right, y); ctx.stroke();
ctx.fillText(v.toFixed(2), pad.left - 4, y + 3);
}
ctx.textAlign = 'center';
for (const frac of [0, 0.25, 0.5, 0.75, 1.0]) {
const step = Math.round(frac * xMax), x = toX(step);
ctx.beginPath(); ctx.moveTo(x, pad.top); ctx.lineTo(x, H - pad.bottom); ctx.stroke();
ctx.fillText(step >= 1000 ? (step/1000).toFixed(step % 1000 ? 1 : 0) + 'k' : step, x, H - pad.bottom + 14);
}
// Random baseline: ln(27)
const lnVocab = Math.log(27);
if (lnVocab >= yTop && lnVocab <= yBot) {
const by = toY(lnVocab);
ctx.setLineDash([4, 4]);
ctx.strokeStyle = '#ef4444';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(pad.left, by); ctx.lineTo(W - pad.right, by); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#ef4444';
ctx.font = '9px system-ui';
ctx.textAlign = 'left';
ctx.fillText('random baseline (ln 27)', pad.left + 4, by - 4);
}
// Previous runs (ghost lines)
for (let r = 0; r < prevRuns.length; r++) {
const run = prevRuns[r];
ctx.beginPath();
ctx.strokeStyle = PREV_COLORS[Math.min(r, PREV_COLORS.length - 1)];
ctx.lineWidth = 1; ctx.lineJoin = 'round';
for (let i = 0; i < run.length; i++) {
const x = toX(run[i].step), y = toY(run[i].loss);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
// Current loss curve
ctx.beginPath();
ctx.strokeStyle = '#4ade80'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round';
for (let i = 0; i < points.length; i++) {
const x = toX(points[i].step), y = toY(points[i].loss);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
// Current value
const last = points[points.length - 1];
ctx.fillStyle = '#4ade80'; ctx.font = 'bold 11px system-ui'; ctx.textAlign = 'left';
ctx.fillText(last.loss.toFixed(4), Math.min(toX(last.step) + 6, W - pad.right - 40), Math.max(toY(last.loss) + 4, pad.top + 12));
// Axis labels
ctx.fillStyle = '#666'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
ctx.fillText('step', pad.left + plotW / 2, H - 2);
ctx.save(); ctx.translate(10, pad.top + plotH / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('loss', 0, 0); ctx.restore();
}
// ── Softmax with temperature ──
function softmaxWithTemp(logits, temperature) {
const scaled = logits.map(l => l / temperature);
const max = Math.max(...scaled);
const exps = scaled.map(l => Math.exp(l - max));
const sum = exps.reduce((a, b) => a + b);
return exps.map(e => e / sum);
}
// ── Bar chart drawing (shared) ──
function drawBarChart(canvasEl, probs, { highlightTop = 0, showLabels = true, showPct = true } = {}) {
const dpr = window.devicePixelRatio || 1;
const rect = canvasEl.getBoundingClientRect();
canvasEl.width = rect.width * dpr;
canvasEl.height = rect.height * dpr;
const ctx = canvasEl.getContext('2d');
ctx.scale(dpr, dpr);
const W = rect.width, H = rect.height;
const pad = { top: 12, right: 8, bottom: showLabels ? 22 : 6, left: 8 };
const plotW = W - pad.left - pad.right;
const plotH = H - pad.top - pad.bottom;
ctx.fillStyle = '#111';
ctx.fillRect(0, 0, W, H);
const n = VOCAB, gap = 1;
const barW = (plotW - gap * (n - 1)) / n;
const maxProb = Math.max(...probs, 0.01);
// Find top-N indices
const topSet = new Set();
if (highlightTop > 0) {
const indexed = probs.map((p, i) => [p, i]).sort((a, b) => b[0] - a[0]);
for (let i = 0; i < Math.min(highlightTop, n); i++) topSet.add(indexed[i][1]);
}
for (let i = 0; i < n; i++) {
const barH = (probs[i] / maxProb) * plotH;
const x = pad.left + i * (barW + gap);
const y = pad.top + plotH - barH;
ctx.fillStyle = topSet.has(i) ? '#4ade80' : '#3b82f6';
ctx.fillRect(x, y, barW, barH);
if (showLabels) {
ctx.fillStyle = topSet.has(i) ? '#4ade80' : '#888';
ctx.font = `${Math.min(barW + 1, 11)}px monospace`;
ctx.textAlign = 'center';
ctx.fillText(LABELS[i], x + barW / 2, H - pad.bottom + 13);
}
if (showPct && probs[i] > 0.03) {
ctx.fillStyle = '#ccc'; ctx.font = '8px system-ui';
ctx.textAlign = 'center';
ctx.fillText((probs[i] * 100).toFixed(0) + '%', x + barW / 2, y - 2);
}
}
}
// ── Sample names card ──
function addSampleCard(step, namesStr) {
const card = document.createElement('div');
card.className = 'sample-card';
const names = namesStr.split('\n').filter(n => n);
card.innerHTML = `<div class="step-label mono">step ${step}</div>` +
names.map(n => `<div class="sample-name">${n || '\u2014'}</div>`).join('');
samplesScroll.appendChild(card);
samplesScroll.scrollLeft = samplesScroll.scrollWidth;
}
// ── Prefix explorer logic ──
let currentTrainer = null;
let currentLogits = null;
function updatePrefixExplorer() {
if (!currentTrainer) return;
const prefix = prefixInput.value;
const logitsArr = currentTrainer.predict_next(prefix);
currentLogits = Array.from(logitsArr);
// Draw main bar chart with softmax (no temperature)
const probs = softmaxWithTemp(currentLogits, 1.0);
drawBarChart(prefixBarChart, probs, { highlightTop: 5, showLabels: true, showPct: true });
// Draw temperature mini charts
for (let i = 0; i < TEMPS.length; i++) {
const tempProbs = softmaxWithTemp(currentLogits, TEMPS[i]);
const miniCanvas = $('temp-chart-' + i);
drawBarChart(miniCanvas, tempProbs, { highlightTop: 3, showLabels: true, showPct: true });
}
}
function regenerateTemperatureNames() {
if (!currentTrainer) return;
for (let i = 0; i < TEMPS.length; i++) {
const namesStr = currentTrainer.generate_names_with_temperature(TEMPS[i]);
const el = $('temp-names-' + i);
const names = namesStr.split('\n').filter(n => n);
el.innerHTML = names.map(n => `<div>${n || '\u2014'}</div>`).join('');
}
}
// ── Name scorer logic ──
function probColor(p) {
if (p >= 0.3) return '#4ade80';
if (p >= 0.1) return '#a3e635';
if (p >= 0.03) return '#facc15';
if (p >= 0.01) return '#fb923c';
return '#ef4444';
}
function updateScorer() {
if (!currentTrainer) return;
const name = scorerInput.value;
if (!name) {
scorerChars.innerHTML = '';
scorerSummary.textContent = '';
return;
}
const probs = Array.from(currentTrainer.score_name(name));
const letters = name.toLowerCase().replace(/[^a-z]/g, '').split('');
scorerChars.innerHTML = '';
let totalSurprise = 0;
for (let i = 0; i < letters.length && i < probs.length; i++) {
const p = probs[i];
const color = probColor(p);
const span = document.createElement('span');
span.className = 'scored-char';
span.style.background = color + '22';
span.innerHTML =
`<span class="char-letter" style="color:${color}">${letters[i]}</span>` +
`<span class="char-prob" style="color:${color}">${(p * 100).toFixed(1)}%</span>`;
scorerChars.appendChild(span);
totalSurprise += -Math.log(Math.max(p, 1e-10));
}
const n = Math.min(letters.length, probs.length);
if (n > 0) {
const avgSurprise = totalSurprise / n;
const perplexity = Math.exp(avgSurprise);
scorerSummary.textContent =
`Total surprise: ${totalSurprise.toFixed(2)} nats (perplexity: ${perplexity.toFixed(1)})`;
} else {
scorerSummary.textContent = '';
}
}
// ── Dataset handling ──
let customDataset = null;
const datasetStatus = $('dataset-status');
const datasetUploadBtn = $('dataset-upload-btn');
const datasetPasteBtn = $('dataset-paste-btn');
const datasetReset = $('dataset-reset');
const datasetFile = $('dataset-file');
const datasetTextareaWrap = $('dataset-textarea-wrap');
const datasetTextarea = $('dataset-textarea');
function countWords(text) {
return text.split('\n').map(l => l.trim()).filter(l => l.length > 0).length;
}
function setCustomDataset(text) {
const count = countWords(text);
if (count === 0) {
resetDataset();
return;
}
customDataset = text;
datasetStatus.textContent = `Dataset: Custom (${count.toLocaleString()} words)`;
datasetReset.style.display = '';
}
function resetDataset() {
customDataset = null;
datasetStatus.textContent = 'Dataset: Using default (32,033 names)';
datasetReset.style.display = 'none';
datasetTextareaWrap.style.display = 'none';
datasetTextarea.value = '';
}
datasetUploadBtn.addEventListener('click', () => datasetFile.click());
datasetFile.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
setCustomDataset(reader.result);
datasetTextareaWrap.style.display = 'none';
};
reader.readAsText(file);
datasetFile.value = '';
});
datasetPasteBtn.addEventListener('click', () => {
const show = datasetTextareaWrap.style.display === 'none';
datasetTextareaWrap.style.display = show ? 'block' : 'none';
if (show) datasetTextarea.focus();
});
datasetTextarea.addEventListener('input', () => {
const text = datasetTextarea.value;
if (text.trim()) {
setCustomDataset(text);
} else {
resetDataset();
}
});
datasetReset.addEventListener('click', resetDataset);
// ── Main ──
await init();
btn.addEventListener('click', () => {
btn.disabled = true;
btn.textContent = 'Training\u2026';
progressWrap.style.display = 'block';
progressFill.style.width = '0%';
chartWrap.style.display = 'block';
samplesWrap.style.display = 'block';
samplesScroll.innerHTML = '';
namesDiv.style.display = 'none';
nameList.innerHTML = '';
explorerWrap.style.display = 'none';
scorerWrap.style.display = 'none';
const seed = BigInt(Date.now()) ^ BigInt(Math.floor(Math.random() * 2**32));
console.log('Training with seed:', seed.toString());
const trainer = customDataset ? new Trainer(customDataset, seed) : new Trainer(undefined, seed);
const total = trainer.total_steps();
if (prevRuns.length >= 3) prevRuns.shift();
if (prevRuns._current && prevRuns._current.length > 0) prevRuns.push(prevRuns._current);
const lossHistory = [];
prevRuns._current = lossHistory;
let lastSampleStep = 0;
info.textContent = trainer.info() + `, seed=${seed}`;
info.style.display = 'block';
const t0 = performance.now();
function tick() {
const msg = trainer.step_batch(CHUNK);
const parts = msg.split(' ');
const step = parseInt(parts[0]);
const pct = parseInt(parts[2]);
const loss = parseFloat(parts[3]);
const lr = parts[4];
const elapsed = (performance.now() - t0) / 1000;
const sps = (step / elapsed).toFixed(0);
const eta = ((total - step) / (step / elapsed)).toFixed(1);
progressFill.style.width = pct + '%';
ptLeft.textContent = `Step ${step}/${total} (${pct}%) loss=${loss.toFixed(4)} lr=${lr}`;
ptRight.textContent = `${sps} steps/s ETA ${eta}s`;
lossHistory.push({ step, loss });
drawChart(lossHistory, total);
// Sample names every SAMPLE_INTERVAL steps
if (step - lastSampleStep >= SAMPLE_INTERVAL) {
const sampleNames = trainer.sample_names();
addSampleCard(step, sampleNames);
lastSampleStep = step;
}
if (!trainer.done()) {
setTimeout(tick, 0);
} else {
progressFill.style.width = '100%';
ptLeft.textContent = 'Generating names\u2026';
ptRight.textContent = '';
setTimeout(() => {
const names = trainer.generate_names();
const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
ptLeft.textContent = `Done in ${elapsed}s`;
namesDiv.style.display = 'block';
nameList.innerHTML = names.split('\n').filter(n => n)
.map(n => `<li>${n}</li>`).join('');
// Show prefix explorer + temperature comparison
currentTrainer = trainer;
explorerWrap.style.display = 'block';
prefixInput.value = '';
updatePrefixExplorer();
regenerateTemperatureNames();
prefixInput.addEventListener('input', updatePrefixExplorer);
regenBtn.addEventListener('click', regenerateTemperatureNames);
// Show name scorer
scorerWrap.style.display = 'block';
scorerInput.value = '';
scorerChars.innerHTML = '';
scorerSummary.textContent = '';
scorerInput.addEventListener('input', updateScorer);
btn.disabled = false;
btn.textContent = 'Train again';
}, 0);
}
}
setTimeout(tick, 0);
});
</script>
</body>
</html>