| <!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; } |
| |
| |
| #chart-wrap { margin-top: 0.75rem; display: none; } |
| #chart { width: 100%; height: 220px; border-radius: 6px; background: #111; } |
| |
| |
| #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; } |
| |
| |
| #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; } |
| |
| |
| #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; } |
| |
| |
| #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 { 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; } |
| |
| |
| #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 luna mochi 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 — 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 — 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 — 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]; |
| |
| |
| 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; |
| |
| |
| 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); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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(); |
| } |
| |
| |
| 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(); |
| |
| |
| 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)); |
| |
| |
| 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(); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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); |
| |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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); |
| |
| |
| const probs = softmaxWithTemp(currentLogits, 1.0); |
| drawBarChart(prefixBarChart, probs, { highlightTop: 5, showLabels: true, showPct: true }); |
| |
| |
| 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(''); |
| } |
| } |
| |
| |
| 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 = ''; |
| } |
| } |
| |
| |
| 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); |
| |
| |
| 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); |
| |
| |
| 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(''); |
| |
| |
| currentTrainer = trainer; |
| explorerWrap.style.display = 'block'; |
| prefixInput.value = ''; |
| updatePrefixExplorer(); |
| regenerateTemperatureNames(); |
| |
| prefixInput.addEventListener('input', updatePrefixExplorer); |
| regenBtn.addEventListener('click', regenerateTemperatureNames); |
| |
| |
| 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> |
|
|