PeterPinetree commited on
Commit
61905b1
·
1 Parent(s): 82b13dc

Update index.html

Browse files

Update index.html to support Qwen3-0.6B ONNX (local + Hub fallback) with progress bar

- Integrated Transformers.js v3 for model/tokenizer loading
- Added Qwen3-0.6B int8 ONNX model under assets/models/qwen with Hub fallback
- Preserved distilgpt2 as secondary option
- Implemented download progress indicator via progress_callback
- Fixed invalid CSS grid units (0.35fr / 1fr)
- Retained semantic neighborhood map, auto-switching JSONs for Qwen vs distilgpt2
- Updated UI wiring for deterministic next-token prediction (greedy Top-K)

Files changed (1) hide show
  1. index.html +380 -491
index.html CHANGED
@@ -2,522 +2,411 @@
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Next Token Predictor</title>
7
  <style>
8
- :root{
9
- --bg:#0b0f14; --text:#fff; --muted:#9aa4b2; --accent:#38bdf8; --border:#1f2a3a;
10
- --chip:#111827; --chip-border:#263246; --chip-hover:#1a2434;
11
- --mono: ui-monospace,Menlo,Consolas,monospace; --sans: system-ui, -apple-system,"Segoe UI", Roboto, Arial;
12
- }
13
- *{box-sizing:border-box}
14
- body{margin:0;background:radial-gradient(1000px 600px at 50% -80px,#0c162a 15%,#081019 40%,var(--bg) 68%);color:var(--text);font-family:var(--sans)}
15
- .wrap{max-width:1100px;margin:0 auto;padding:16px}
16
- h1{margin:.2rem 0 .25rem;font-size:2.1rem;color:var(--accent)}
17
- .sub{color:var(--muted);margin:0 0 .8rem}
18
-
19
- /* Main two-column (Left: Top-10, Right: Map) */
20
- .grid{display:grid;gap:12px;grid-template-columns:0.35fr 0.65fr}
21
- @media (max-width:900px){.grid{grid-template-columns:1fr}.row{flex-wrap:wrap}}
22
-
23
- .row{display:flex;gap:.6rem;align-items:center}
24
- .card{background:linear-gradient(180deg,#0c1624,#0a1220);border:1px solid var(--border);border-radius:14px;padding:12px}
25
- select,input{border-radius:10px;border:1px solid var(--border);background:#0a1220;color:var(--text);padding:.6rem .8rem;outline:none}
26
- select:focus,input:focus{border-color:var(--accent)}
27
- #status{color:var(--muted);font-size:.9rem}
28
-
29
- /* Token chips (neighbors) */
30
- .tokens{display:flex;gap:.4rem;flex-wrap:wrap}
31
- .chip{border:1px solid var(--chip-border);background:var(--chip);padding:.35rem .5rem;border-radius:10px;font-family:var(--mono);color:var(--text);}
32
-
33
- /* Top-10 list */
34
- #topk{display:flex;flex-direction:column;gap:.4rem;padding-right:4px}
35
- .k{
36
- padding:.45rem .6rem;border-radius:10px;background:#102133;border:1px solid #1c2b44;
37
- font-family:var(--mono);cursor:pointer;color:var(--text);
38
- display:flex;align-items:center;justify-content:space-between;width:100%;text-align:left;
39
- }
40
- .k:hover{border-color:var(--accent)}
41
- .note{color:var(--muted);font-size:.82rem}
42
-
43
- /* Neighborhood viewer */
44
- #emb .panel{
45
- display:grid;
46
- grid-template-columns:minmax(0,1fr) 260px; /* map grows, sidebar fixed */
47
- gap:12px;
48
- align-items:start;
49
- }
50
- /* Responsive canvas – take full width of its grid cell, keep aspect */
51
- #scatter{
52
- width:100%;
53
- height:auto;
54
- aspect-ratio:4/3; /* desktop aspect */
55
- border-radius:10px;
56
- background:#09121d;border:1px solid var(--border)
57
- }
58
- #nbrs{align-content:flex-start}
59
-
60
- /* Mobile layout: stack map on top; neighbors in one column */
61
- @media (max-width:700px){
62
- #emb .panel{
63
- grid-template-columns:1fr; /* stack */
64
- grid-template-rows:auto auto;
65
- }
66
- #scatter{ aspect-ratio:1/1; } /* square-ish map on phones */
67
- #nbrs{
68
- display:grid; /* single-column neighbors */
69
- grid-template-columns:1fr;
70
- gap:.5rem;
71
- }
72
- .chip{ width:100%; } /* chips fill width neatly */
73
- }
74
-
75
- .legend{display:flex;gap:10px;align-items:center;margin:.25rem 0 .5rem}
76
- .dot{width:10px;height:10px;border-radius:50%}
77
- .all{background:#1a2a3a}
78
- .target{background:#22d3ee}
79
- .nb{background:#93c5fd}
80
- .warn{color:#ffd79a}
81
- .footer{margin-top:18px;text-align:center;color:var(--muted);font-size:.9rem}
82
- .footer a{color:#8fd6ff;text-decoration:none}
83
- .err{margin-top:8px;background:#1f2937;border:1px solid #374151;color:#ffb4b4;padding:8px 10px;border-radius:10px;display:none}
84
  </style>
 
 
 
 
 
 
 
 
 
 
85
  </head>
 
86
  <body>
87
- <main class="wrap">
88
- <h1>Next Token Predictor</h1>
89
- <div class="sub">Type a sentence to see the AI’s next-token guesses. Click to add a token, or hover to find similar ones.</div>
90
-
91
- <section class="card">
92
- <div class="row" style="gap:12px">
93
- <div class="row">
94
- <label style="margin-right:.5rem">Model</label>
95
- <select id="model">
96
- <option value="distilgpt2">distilgpt2</option>
97
- <option value="qwen3" selected>Qwen3-0.6B</option>
98
- </select>
99
- </div>
100
- <input id="text" placeholder="Enter your text here..." style="flex:1;min-width:240px" />
101
- <div id="status">Loading…</div>
102
  </div>
103
- <div id="error" class="err"></div>
104
- </section>
105
-
106
- <section class="grid">
107
- <article class="card">
108
- <h3 style="margin:.2rem 0 .6rem">Top-10 next tokens</h3>
109
- <div id="topk"></div>
110
- </article>
111
-
112
- <article id="emb" class="card">
113
- <h3 style="margin:.2rem 0 .6rem">Semantic neighborhood</h3>
114
- <div class="legend">
115
- <div class="dot all"></div><div class="note">All tokens</div>
116
- <div class="dot nb"></div><div class="note">Similar tokens</div>
117
- <div class="dot target"></div><div class="note">Your token</div>
118
- </div>
119
- <div class="panel">
120
- <canvas id="scatter" width="600" height="520"></canvas>
121
- <div>
122
- <div class="note" id="embInfo">Hover a suggestion to explore.</div>
123
- <div id="nbrs" class="tokens" style="margin-top:.5rem"></div>
124
  </div>
125
- </div>
126
- <div class="note" style="margin-top:.6rem">
127
- Each dot is a token. Nearby dots have similar meanings. The bright dot is your chosen token.
128
- Percentages show how closely each neighbor relates — higher means more similar.
129
- </div>
130
- </article>
131
- </section>
132
-
133
- <div class="footer">
134
- Built by Peter Adams • Powered in your browser by <a href="https://xenova.github.io/transformers.js/" target="_blank" rel="noreferrer">Transformers.js</a>.
135
- </div>
136
- </main>
137
-
138
- <script type="module">
139
- // ---- Robust ESM loader ----
140
- async function loadTransformers() {
141
- const urls = [
142
- 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.2/+esm',
143
- 'https://esm.run/@huggingface/transformers@3.7.2',
144
- 'https://esm.sh/@huggingface/transformers@3.7.2',
145
- ];
146
- for (const u of urls) {
147
- try {
148
- const m = await import(u);
149
- if (m?.env && m.AutoTokenizer && m.AutoModelForCausalLM) return m;
150
- } catch {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
- throw new Error('Failed to load @huggingface/transformers (ESM).');
153
- }
154
-
155
- const tf = await loadTransformers();
156
- const DEVICE = (navigator.gpu ? 'webgpu' : 'wasm');
157
- tf.env.useBrowserCache = true;
158
- tf.env.allowRemoteModels = true;
159
- tf.env.allowLocalModels = false;
160
-
161
- // UI refs
162
- const $ = s => document.querySelector(s);
163
- const textIn = $('#text');
164
- const statusEl = $('#status');
165
- const topkEl = $('#topk');
166
- const nbrsEl = $('#nbrs');
167
- const embInfo = $('#embInfo');
168
- const canvas = $('#scatter');
169
- const ctx = canvas.getContext('2d');
170
- const errBox = $('#error');
171
- const modelSel = $('#model');
172
-
173
- // Model registry
174
- const MODELS = {
175
- distilgpt2: {
176
- id: 'Xenova/distilgpt2',
177
- emb: {
178
- coords: 'assets/embeddings/pca_top5k_coords.json',
179
- neigh : 'assets/embeddings/neighbors_top5k_k40.json'
180
- },
181
- label: 'distilgpt2'
182
- },
183
- qwen3: {
184
- id: 'onnx-community/Qwen3-0.6B-ONNX',
185
- emb: {
186
- coords: 'assets/embeddings/qwen_pca_top5k_coords.json',
187
- neigh : 'assets/embeddings/qwen_neighbors_top5k_k40.json'
188
  },
189
- label: 'Qwen3-0.6B'
190
- }
191
- };
192
-
193
- // State
194
- let tokenizer = null, model = null;
195
- let currentModel = 'qwen3';
196
- let busy = false, flight = 0, warmed = false;
197
- const EmbCache = {};
198
- let lastHoveredId = null; // remember last hovered token for redraws on resize
199
- let stickyId = null; // when you click/tap, persist this after predict()
200
-
201
- function setStatus(m){ statusEl.textContent = m; }
202
- function showError(e){ errBox.style.display='block'; errBox.textContent = e?.message || String(e); }
203
- function clearError(){ errBox.style.display='none'; errBox.textContent=''; }
204
-
205
- // Load model
206
- async function loadModel(modelKey){
207
- if (!MODELS[modelKey]) {
208
- throw new Error(`Unknown model key "${modelKey}"`);
209
- }
210
-
211
- currentModel = modelKey;
212
- const conf = MODELS[modelKey];
213
-
214
- // Reset click/hover + UI when switching models
215
- lastHoveredId = null; // preview state
216
- stickyId = null; // clicked selection state
217
- embInfo.textContent = 'Hover a suggestion to explore.';
218
- nbrsEl.innerHTML = '';
219
- try { ctx.clearRect(0, 0, canvas.width, canvas.height); } catch {}
220
-
221
- // (Optional) mark the emb cache for this model as needing a redraw
222
- if (EmbCache[currentModel]) {
223
- EmbCache[currentModel].baseDrawn = false;
224
- }
225
-
226
- clearError();
227
- setStatus(`Loading ${conf.label} tokenizer…`);
228
- tokenizer = await tf.AutoTokenizer.from_pretrained(conf.id);
229
-
230
- setStatus(`Loading ${conf.label} model…`);
231
- model = await tf.AutoModelForCausalLM.from_pretrained(conf.id, { device: DEVICE });
232
-
233
- setStatus('Ready.');
234
- warmed = false;
235
- }
236
-
237
-
238
- // Helpers
239
- const softmax = arr => { const m=Math.max(...arr); const exps=arr.map(x=>Math.exp(x-m)); const s=exps.reduce((a,b)=>a+b,0); return exps.map(x=>x/s); };
240
- const topK = (probs, k) => probs.map((p,i)=>[p,i]).sort((a,b)=>b[0]-a[0]).slice(0,k);
241
- function normalizeText(x){ if (x==null) return ''; if (typeof x==='string') return x; if (Array.isArray(x)) return x.map(v=>String(v??'')).join(''); if (typeof x==='object'&&'text'in x) return normalizeText(x.text); return String(x); }
242
- async function tokenize(text){ text=normalizeText(text||textIn?.value||''); if(!text.trim()) text=' '; const enc=await tokenizer(text,{add_special_tokens:false}); tokenize.lastEnc=enc; return enc; }
243
- function decodeId(id){ try{return tokenizer.decode([id],{skip_special_tokens:false,clean_up_tokenization_spaces:false});}catch{return '';} }
244
-
245
- // Predict
246
- async function predict(){
247
- if (!tokenizer || !model) return;
248
- if (busy) return;
249
- busy = true; clearError();
250
- const myFlight = ++flight;
251
-
252
- try {
253
- const enc = tokenize.lastEnc ?? await tokenize();
254
- const out = await model(enc);
255
- if (myFlight !== flight) return;
256
-
257
- const [ , T, V ] = out.logits.dims;
258
- const start = (T - 1) * V;
259
- const last = Array.from(out.logits.data.slice(start, start + V));
260
- const probs = softmax(last);
261
- const k = topK(probs, 10);
262
-
263
- topkEl.innerHTML = '';
264
- for (const [p, i] of k) {
265
- let tok = decodeId(i);
266
- if (tok === '') {
267
- tok = tokenizer.id_to_token ? (tokenizer.id_to_token(i) ?? '(special/space)') : '(special/space)';
268
  }
269
-
270
- const btn = document.createElement('button');
271
- btn.className = 'k';
272
- btn.innerHTML = `<span>${tok}</span><span>${(p * 100).toFixed(1)}%</span>`;
273
-
274
- // Hover/pen = preview (desktop keeps working)
275
- const preview = () => { lastHoveredId = i; stickyId = null; drawNeighborhood(i); };
276
- btn.onmouseenter = preview;
277
- btn.onpointerenter = preview;
278
-
279
- // Click/tap = append + lock neighborhood
280
- btn.onclick = async () => {
281
- // show neighborhood immediately for mobile
282
- lastHoveredId = i;
283
- stickyId = i;
284
- drawNeighborhood(i);
285
-
286
- // append token and re-run prediction
287
- const cur = normalizeText(textIn.value);
288
- textIn.value = cur + tok;
289
- await tokenize(textIn.value);
290
- await predict();
291
- };
292
-
293
- topkEl.appendChild(btn);
294
- }
295
-
296
- // If a token was clicked, keep its neighborhood visible after re-render
297
- if (stickyId != null) {
298
- drawNeighborhood(stickyId);
299
  }
300
-
301
- if (!warmed) { warmed = true; setStatus('Ready.'); }
302
- } catch (e) {
303
- console.error(e); showError(e); setStatus('Error');
304
- } finally {
305
- busy = false;
306
- }
307
- }
308
-
309
- // ===== Embedding Viewer (robust key resolution) =====
310
- function getEmbState(){
311
- if(!EmbCache[currentModel]) EmbCache[currentModel]={coords:null,neigh:null,keySet:null,keyMode:null,normIndex:null,baseDrawn:false};
312
- return EmbCache[currentModel];
313
- }
314
-
315
- function normalizePiece(s){
316
- return (s || '')
317
- .replaceAll('▁', ' ')
318
- .replaceAll('Ġ', ' ')
319
- .replace(/\s+/g,' ')
320
- .trim()
321
- .toLowerCase();
322
- }
323
- function detectKeyMode(coords){
324
- const keys = Object.keys(coords);
325
- const numeric = keys.length && keys.every(k => String(+k) === k);
326
- return numeric ? 'id' : 'token';
327
- }
328
-
329
- async function ensureEmbeddings(){
330
- const emb=getEmbState();
331
- if(emb.coords && emb.neigh && emb.keySet) return emb;
332
-
333
- const files=MODELS[currentModel].emb;
334
- emb.coords = await fetch(files.coords).then(r=>r.json());
335
- emb.neigh = await fetch(files.neigh ).then(r=>r.json());
336
- emb.keyMode = detectKeyMode(emb.coords);
337
- emb.keySet = new Set(Object.keys(emb.coords));
338
- emb.baseDrawn = false;
339
-
340
- emb.normIndex = new Map();
341
- if (emb.keyMode === 'token') {
342
- for (const k of emb.keySet) {
343
- const nk = normalizePiece(k);
344
- if (!emb.normIndex.has(nk)) emb.normIndex.set(nk, k);
345
  }
346
- }
347
- // Ensure canvas backing store matches CSS size
348
- resizeCanvas(true);
349
- return emb;
350
- }
351
-
352
- function idToCandidates(id){
353
- const c = [];
354
- c.push(String(id));
355
- try {
356
- if (tokenizer.id_to_token) {
357
- const piece = tokenizer.id_to_token(id);
358
- if (piece) {
359
- c.push(piece);
360
- const deSp = piece.replace(/^▁/, ' ').replace(/^Ġ/, ' ');
361
- c.push(deSp);
362
- if (!piece.startsWith(' ')) c.push(' ' + piece);
363
- if (!deSp.startsWith(' ')) c.push(' ' + deSp);
364
- c.push(piece.toLowerCase(), deSp.toLowerCase());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  }
 
 
 
366
  }
367
- } catch {}
368
- try {
369
- const dec = decodeId(id);
370
- if (dec) {
371
- c.push(dec);
372
- if (!dec.startsWith(' ')) c.push(' ' + dec);
373
- c.push(dec.toLowerCase());
374
- c.push('▁' + dec.replace(/^\s/,''));
375
- c.push('Ġ' + dec.replace(/^\s/,''));
376
  }
377
- } catch {}
378
- return Array.from(new Set(c));
379
- }
380
 
381
- function resolveCoordKey(emb, id){
382
- for (const k of idToCandidates(id)) {
383
- if (emb.keySet.has(k)) return k;
384
- }
385
- if (emb.keyMode === 'id') return null;
 
 
 
 
 
 
 
 
386
 
387
- const base = (tokenizer.id_to_token?.(id)) || decodeId(id) || '';
388
- const norm = normalizePiece(base);
389
- if (norm && emb.normIndex?.has(norm)) return emb.normIndex.get(norm);
 
 
390
 
391
- if (norm && emb.normIndex) {
392
- let candidate = null, candLen = Infinity;
393
- for (const [nk, original] of emb.normIndex.entries()) {
394
- if (nk.includes(norm) || norm.includes(nk)) {
395
- if (nk.length < candLen) { candidate = original; candLen = nk.length; }
396
  }
397
  }
398
- if (candidate) return candidate;
399
- }
400
- return null;
401
- }
402
-
403
- function getNeighborList(emb, coordKey, id){
404
- const N = emb.neigh?.neighbors || {};
405
- let list = N[coordKey];
406
- if (!list) list = N[String(id)];
407
- if (!list) list = N[id];
408
- if (!list) {
409
- for (const k of idToCandidates(id)) { if (N[k]) { list = N[k]; break; } }
410
- }
411
- return Array.isArray(list) ? list : [];
412
- }
413
-
414
- function mapNeighborEntry(emb, entry){
415
- const [nid, sim] = entry;
416
- if (typeof nid === 'string' && emb.keySet.has(nid)) return [nid, sim];
417
- const maybe = typeof nid === 'number' ? nid : +nid;
418
- if (!Number.isNaN(maybe)) {
419
- const k = resolveCoordKey(emb, maybe);
420
- if (k) return [k, sim];
421
- }
422
- if (typeof nid === 'string') {
423
- const nk = normalizePiece(nid);
424
- const hit = emb.normIndex?.get(nk);
425
- if (hit) return [hit, sim];
426
- }
427
- return null;
428
- }
429
-
430
- function getBounds(coords){ const pts=Object.values(coords); let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity; for(const [x,y] of pts){ if(x<minX)minX=x; if(y<minY)minY=y; if(x>maxX)maxX=x; if(y>maxY)maxY=y; } return {minX,minY,maxX,maxY}; }
431
- function makeToXY(coords){
432
- const {minX,minY,maxX,maxY}=getBounds(coords);
433
- const pad=18, w=canvas.width-pad*2, h=canvas.height-pad*2;
434
- return ([x,y])=>{const nx=(x-minX)/(maxX-minX); const ny=(y-minY)/(maxY-minY); return [pad+nx*w, pad+(1-ny)*h];};
435
- }
436
- function drawBase(emb,toXY){
437
- ctx.clearRect(0,0,canvas.width,canvas.height);
438
- ctx.fillStyle='#1a2a3a';
439
- for(const k in emb.coords){
440
- const [x,y]=toXY(emb.coords[k]);
441
- ctx.beginPath(); ctx.arc(x,y,2,0,Math.PI*2); ctx.fill();
442
- }
443
- emb.baseDrawn=true;
444
- }
445
-
446
- async function drawNeighborhood(tokenId){
447
- const emb = await ensureEmbeddings();
448
-
449
- const key = resolveCoordKey(emb, tokenId);
450
- if (!key) {
451
- embInfo.innerHTML = '<span class="warn">Neighborhood unavailable for this token (not in the current map).</span>';
452
- nbrsEl.innerHTML = '';
453
- if (!emb.baseDrawn) { const toXY = makeToXY(emb.coords); drawBase(emb, toXY); }
454
- return;
455
- }
456
 
457
- const toXY = makeToXY(emb.coords);
458
- drawBase(emb, toXY);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
 
460
- const targetXY = toXY(emb.coords[key]);
461
- const rawList = getNeighborList(emb, key, tokenId);
462
- const list = rawList.map(e => mapNeighborEntry(emb, e)).filter(Boolean);
463
 
464
- // neighbors
465
- ctx.fillStyle = '#93c5fd';
466
- for (const [nk] of list){
467
- const pt = emb.coords[nk];
468
- if (!pt) continue;
469
- const [x,y] = toXY(pt);
470
- ctx.beginPath(); ctx.arc(x, y, 3.4, 0, Math.PI*2); ctx.fill();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  }
472
- // target
473
- ctx.fillStyle = '#22d3ee';
474
- ctx.beginPath(); ctx.arc(targetXY[0], targetXY[1], 4.8, 0, Math.PI*2); ctx.fill();
475
-
476
- // chips
477
- nbrsEl.innerHTML = '';
478
- embInfo.textContent = 'Nearest neighbors:';
479
- for (const [nk, sim] of list.slice(0,18)){
480
- const label = (String(+nk) === nk)
481
- ? (decodeId(+nk) || (tokenizer.id_to_token ? tokenizer.id_to_token(+nk) : String(nk)))
482
- : nk.replace(/^▁/,' ').replace(/^Ġ/,' ');
483
- const b = document.createElement('div');
484
- b.className = 'chip';
485
- b.textContent = `${label} ${(sim*100).toFixed(1)}%`;
486
- nbrsEl.appendChild(b);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  }
488
- }
489
-
490
- // ===== Responsive canvas (keeps drawing buffer in sync with CSS size) =====
491
- function resizeCanvas(force=false){
492
- const dpr = Math.min(2, window.devicePixelRatio || 1);
493
- const rect = canvas.getBoundingClientRect();
494
- const w = Math.max(1, Math.round(rect.width * dpr));
495
- const h = Math.max(1, Math.round(rect.height * dpr));
496
- if (force || canvas.width !== w || canvas.height !== h){
497
- canvas.width = w;
498
- canvas.height = h;
499
- const emb = getEmbState();
500
- emb.baseDrawn = false;
501
- if (emb.coords){
502
- if (lastHoveredId != null) { drawNeighborhood(lastHoveredId); }
503
- else { const toXY = makeToXY(emb.coords); drawBase(emb, toXY); }
504
  }
505
  }
506
- }
507
- window.addEventListener('resize', () => resizeCanvas(false));
508
- // ==========================================================================
509
-
510
- // Events
511
- let debounceId;
512
- ['input','change'].forEach(ev=>{ textIn.addEventListener(ev,()=>{ clearTimeout(debounceId); debounceId=setTimeout(async()=>{await tokenize(textIn.value); predict();},150); }); });
513
- modelSel.addEventListener('change',async e=>{ const key=e.target.value; setStatus(`Switching to ${MODELS[key].label}…`); await loadModel(key); await tokenize(textIn.value??''); await predict(); });
514
-
515
- // Kickoff (Qwen default)
516
- await loadModel('qwen3');
517
- modelSel.value = 'qwen3';
518
- await tokenize(textIn.value??'');
519
- resizeCanvas(true); // ensure correct backing store before first draw
520
- await predict();
521
- </script>
 
 
 
 
 
 
522
  </body>
523
  </html>
 
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Next-Token Predictor</title>
7
  <style>
8
+ :root { color-scheme: dark; }
9
+ body { margin:0; font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Arial,sans-serif; background:#0b1220; color:#e6f1ff; }
10
+ header { position:sticky; top:0; z-index:5; display:flex; gap:12px; align-items:center; padding:12px 16px; background:#0e1629; border-bottom:1px solid #1c2945; }
11
+ h1 { font-size:16px; font-weight:600; margin:0; letter-spacing:.2px; }
12
+ main { padding:14px; }
13
+ .grid { display:grid; gap:14px; grid-template-columns: 0.35fr 0.65fr; } /* fixed 'fr' spacing */
14
+ @media (max-width: 1000px){ .grid { grid-template-columns:1fr; } }
15
+
16
+ .card { background:#0e162b; border:1px solid #1c2945; border-radius:14px; padding:12px; }
17
+ .muted { color:#9ab0d0; }
18
+ .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
19
+ .grow { flex:1 1 auto; }
20
+
21
+ .input, .select, .btn { border-radius:10px; border:1px solid #22365e; background:#111a33; color:#e6f1ff; }
22
+ .input { width:100%; min-height:120px; padding:10px 12px; resize:vertical; }
23
+ .select { padding:8px 10px; }
24
+ .btn { padding:8px 12px; cursor:pointer; background:#102041; }
25
+ .btn:disabled { opacity:.6; cursor:not-allowed; }
26
+ .err { color:#ffb4c0; }
27
+
28
+ .barwrap { flex:1; height:6px; background:#16233d; border-radius:4px; overflow:hidden; }
29
+ .bar { height:100%; width:0%; background:#38bdf8; transition:width .15s linear; }
30
+ .status { min-width:140px; font-variant-numeric: tabular-nums; }
31
+
32
+ .klist { display:grid; gap:8px; }
33
+ .tokrow { display:grid; grid-template-columns: 1fr auto; gap:8px; align-items:center; padding:8px 10px; border-radius:10px; background:#0f1930; border:1px solid #22365e; cursor:pointer; }
34
+ .tok { font-family: ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:14px; }
35
+
36
+ .panel-title { display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
37
+ canvas { display:block; width:100%; height:420px; background:#0b1327; border:1px solid #1c2945; border-radius:12px; }
38
+
39
+ .inline { display:inline-flex; gap:8px; align-items:center; }
40
+ .small { font-size:12px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </style>
42
+
43
+ <!-- Transformers.js (v3) -->
44
+ <script type="module">
45
+ import {
46
+ env,
47
+ AutoTokenizer,
48
+ AutoModelForCausalLM
49
+ } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0";
50
+ window.HF = { env, AutoTokenizer, AutoModelForCausalLM };
51
+ </script>
52
  </head>
53
+
54
  <body>
55
+ <header>
56
+ <h1>Next-Token Predictor</h1>
57
+ <div class="row grow" style="margin-left:auto;">
58
+ <div id="status" class="status muted">Loading…</div>
59
+ <div class="barwrap"><div id="bar" class="bar"></div></div>
 
 
 
 
 
 
 
 
 
 
60
  </div>
61
+ </header>
62
+
63
+ <main>
64
+ <div class="grid">
65
+ <!-- LEFT: Prompt + controls -->
66
+ <section class="card">
67
+ <div class="row" style="justify-content:space-between; margin-bottom:8px;">
68
+ <div class="inline">
69
+ <span class="muted small">Model:</span>
70
+ <select id="model" class="select">
71
+ <option value="qwen" selected>Qwen3-0.6B (local int8 → Hub fallback)</option>
72
+ <option value="distilgpt2">distilgpt2 (local → Hub fallback)</option>
73
+ </select>
74
+ </div>
75
+ <div class="inline">
76
+ <label class="inline"><input id="hidePunc" type="checkbox" /> <span class="muted small">Hide punctuation-only</span></label>
77
+ <button id="demo" class="btn">Run “Twinkle” demo</button>
78
+ </div>
 
 
 
79
  </div>
80
+
81
+ <textarea id="text" class="input" placeholder="Type a prompt… (e.g., Twinkle, twinkle, little )"></textarea>
82
+
83
+ <div class="row" style="justify-content:space-between; margin-top:8px;">
84
+ <div class="muted small">Deterministic (greedy) next-token from last position.</div>
85
+ <div class="inline">
86
+ <label class="inline">
87
+ <span class="muted small">Top-K:</span>
88
+ <select id="topk" class="select">
89
+ <option>5</option><option selected>10</option><option>20</option><option>30</option>
90
+ </select>
91
+ </label>
92
+ <button id="predictBtn" class="btn">Predict</button>
93
+ </div>
94
+ </div>
95
+
96
+ <div id="error" class="err" style="margin-top:6px;"></div>
97
+ </section>
98
+
99
+ <!-- RIGHT: Top-K + Semantic Neighborhood -->
100
+ <section class="card">
101
+ <div class="panel-title">
102
+ <div class="muted">Top candidates for the <b>next</b> token</div>
103
+ <div id="time" class="muted small"></div>
104
+ </div>
105
+ <div id="klist" class="klist" style="margin-bottom:10px;"></div>
106
+
107
+ <div class="panel-title" style="margin-top:10px;">
108
+ <div class="muted">Semantic Neighborhood</div>
109
+ <div class="muted small" id="embStatus">Loading map…</div>
110
+ </div>
111
+ <canvas id="embCanvas" width="1024" height="680"></canvas>
112
+ </section>
113
+ </div>
114
+ </main>
115
+
116
+ <!-- App code -->
117
+ <script type="module">
118
+ const { env, AutoTokenizer, AutoModelForCausalLM } = window.HF;
119
+
120
+ /* ---------- Environment tuning ---------- */
121
+ env.useBrowserCache = true;
122
+ env.backends.onnx.wasm.proxy = true;
123
+ env.backends.onnx.wasm.numThreads = Math.min(
124
+ 4, Math.max(1, Math.floor((navigator.hardwareConcurrency || 4)/2))
125
+ );
126
+
127
+ /* ---------- DOM ---------- */
128
+ const $ = (s) => document.querySelector(s);
129
+ const statusEl = $('#status'), barEl = $('#bar'), errEl = $('#error');
130
+ const textEl = $('#text'), klistEl = $('#klist'), timeEl = $('#time');
131
+ const modelSel = $('#model'), topkSel = $('#topk'), predictBtn = $('#predictBtn');
132
+ const demoBtn = $('#demo'), hidePunc = $('#hidePunc');
133
+ const embCanvas = $('#embCanvas'), embCtx = embCanvas.getContext('2d');
134
+ const embStatus = $('#embStatus');
135
+
136
+ /* ---------- Progress ---------- */
137
+ function setStatus(t){ if(statusEl) statusEl.textContent = t; }
138
+ function onProgress(evt){
139
+ if (!barEl) return;
140
+ if (evt.status === 'downloading' && evt.loaded && evt.total){
141
+ const pct = Math.max(1, Math.floor((evt.loaded/evt.total)*100));
142
+ barEl.style.width = pct + "%"; setStatus(`Downloading ${evt.file || "model"}… ${pct}%`);
143
+ } else if (evt.status === 'ready'){
144
+ barEl.style.width = "100%"; setStatus("Ready");
145
+ } else if (evt.status){
146
+ setStatus(evt.status);
147
+ }
148
  }
149
+ function setErr(e){ errEl.textContent = e || ""; }
150
+
151
+ /* ---------- Model registry ---------- */
152
+ const MODELS = {
153
+ qwen: {
154
+ local: "assets/models/qwen",
155
+ remote: "onnx-community/Qwen3-0.6B-ONNX",
156
+ dtype: "int8",
157
+ emb: {
158
+ coords: "assets/embeddings/qwen_pca_top5k_coords.json",
159
+ nbrs: "assets/embeddings/qwen_neighbors_top5k_k40.json"
160
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  },
162
+ distilgpt2: {
163
+ local: "assets/models/distilgpt2",
164
+ remote: "Xenova/distilgpt2",
165
+ dtype: undefined,
166
+ emb: {
167
+ coords: "assets/embeddings/pca_top5k_coords.json",
168
+ nbrs: "assets/embeddings/neighbors_top5k_k40.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  }
171
+ };
172
+
173
+ /* ---------- Embedding viewer ---------- */
174
+ const Emb = (() => {
175
+ let coordsPath = "", nbrsPath = "";
176
+ let points = []; // [{t,x,y}]
177
+ let index = new Map(); // token -> {x,y,t}
178
+ let neighbors = new Map(); // token -> [token,...]
179
+ let baseDrawn = false;
180
+
181
+ function setSources(modelKey){
182
+ coordsPath = MODELS[modelKey].emb.coords;
183
+ nbrsPath = MODELS[modelKey].emb.nbrs;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  }
185
+
186
+ async function load(){
187
+ baseDrawn = false; index.clear(); points = []; neighbors.clear();
188
+ embStatus.textContent = "Loading map…";
189
+
190
+ const c = await fetch(coordsPath).then(r=>r.json());
191
+ if (Array.isArray(c)) {
192
+ for (const row of c){
193
+ if (Array.isArray(row) && row.length >= 3){
194
+ const [x,y,t] = row; pushPoint(t,x,y);
195
+ } else if (row && typeof row === 'object'){
196
+ const t = row.t ?? row.token ?? row[2];
197
+ const x = row.x ?? row[0]; const y = row.y ?? row[1];
198
+ pushPoint(t,x,y);
199
+ }
200
+ }
201
+ } else if (c && typeof c === 'object'){
202
+ if (Array.isArray(c.tokens)) {
203
+ for (const row of c.tokens){ pushPoint(row.token ?? row.t, row.x, row.y); }
204
+ } else {
205
+ for (const [t,xy] of Object.entries(c)){
206
+ if (Array.isArray(xy) && xy.length>=2) pushPoint(t, xy[0], xy[1]);
207
+ }
208
+ }
209
+ }
210
+
211
+ const n = await fetch(nbrsPath).then(r=>r.json());
212
+ if (n && typeof n === 'object'){
213
+ if (Array.isArray(n.items)) {
214
+ for (const it of n.items){ if (it?.t && Array.isArray(it.n)) neighbors.set(it.t, it.n); }
215
+ } else {
216
+ for (const [t, arr] of Object.entries(n)){ if (Array.isArray(arr)) neighbors.set(t, arr); }
217
+ }
218
  }
219
+
220
+ drawBase(); baseDrawn = true;
221
+ embStatus.textContent = `${index.size.toLocaleString()} tokens`;
222
  }
223
+
224
+ function pushPoint(t,x,y){
225
+ if (typeof t !== 'string') return;
226
+ if (typeof x !== 'number' || typeof y !== 'number') return;
227
+ const p = { t, x, y }; points.push(p); index.set(t, p);
 
 
 
 
228
  }
 
 
 
229
 
230
+ function bounds(){
231
+ let xmin=Infinity,xmax=-Infinity,ymin=Infinity,ymax=-Infinity;
232
+ for (const p of points){ if(p.x<xmin)xmin=p.x; if(p.x>xmax)xmax=p.x; if(p.y<ymin)ymin=p.y; if(p.y>ymax)ymax=p.y; }
233
+ if (!isFinite(xmin)) { xmin=0;xmax=1;ymin=0;ymax=1; }
234
+ return { xmin,xmax,ymin,ymax };
235
+ }
236
+ function toCanvas(x,y){
237
+ const pad=24, w=embCanvas.width, h=embCanvas.height;
238
+ const {xmin,xmax,ymin,ymax} = bounds();
239
+ const tx = pad + (x - xmin) / Math.max(1e-9,(xmax - xmin)) * (w - pad*2);
240
+ const ty = pad + (1 - (y - ymin) / Math.max(1e-9,(ymax - ymin))) * (h - pad*2);
241
+ return [tx,ty];
242
+ }
243
 
244
+ function drawBase(){
245
+ const ctx = embCtx;
246
+ ctx.clearRect(0,0,embCanvas.width, embCanvas.height);
247
+ ctx.fillStyle = "#0b1327";
248
+ ctx.fillRect(0,0,embCanvas.width, embCanvas.height);
249
 
250
+ ctx.fillStyle = "#5f7aa5";
251
+ for (const p of points){
252
+ const [x,y] = toCanvas(p.x,p.y);
253
+ ctx.fillRect(x, y, 2, 2);
 
254
  }
255
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
+ function highlight(token, alsoLabel=true){
258
+ if (!baseDrawn) drawBase();
259
+ const ctx = embCtx;
260
+ const base = index.get(token);
261
+ if (!base) return;
262
+
263
+ const nbrs = neighbors.get(token) || [];
264
+ ctx.strokeStyle = "#38bdf8"; ctx.lineWidth = 1;
265
+ const [bx,by] = toCanvas(base.x, base.y);
266
+ for (const nTok of nbrs){
267
+ const p = index.get(nTok); if (!p) continue;
268
+ const [x,y] = toCanvas(p.x,p.y);
269
+ ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(x,y); ctx.stroke();
270
+ ctx.fillStyle = "#9bd7ff"; ctx.fillRect(x-2, y-2, 4, 4);
271
+ }
272
+ ctx.fillStyle = "#ffd166"; ctx.beginPath(); ctx.arc(bx, by, 5, 0, Math.PI*2); ctx.fill();
273
+
274
+ if (alsoLabel){
275
+ ctx.fillStyle = "#e6f1ff";
276
+ ctx.font = "12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
277
+ ctx.fillText(showToken(token), bx+8, by-8);
278
+ }
279
+ }
280
+
281
+ return { setSources, load, drawBase, highlight };
282
+ })();
283
+
284
+ /* ---------- Core model state ---------- */
285
+ let tokenizer=null, model=null, currentKey=modelSel.value;
286
 
287
+ async function loadModel(key){
288
+ const cfg = MODELS[key];
289
+ currentKey = key;
290
 
291
+ Emb.setSources(key);
292
+ await Emb.load().catch(()=>{ embStatus.textContent = "Map failed to load"; });
293
+
294
+ setErr(""); setStatus("Loading tokenizer…"); barEl.style.width = "0%";
295
+ try {
296
+ tokenizer = await AutoTokenizer.from_pretrained(cfg.local, { progress_callback: onProgress });
297
+ } catch {
298
+ tokenizer = await AutoTokenizer.from_pretrained(cfg.remote, { progress_callback: onProgress });
299
+ }
300
+
301
+ setStatus("Loading model…");
302
+ try {
303
+ model = await AutoModelForCausalLM.from_pretrained(cfg.local, { dtype: cfg.dtype, progress_callback: onProgress });
304
+ } catch {
305
+ model = await AutoModelForCausalLM.from_pretrained(cfg.remote, { dtype: cfg.dtype, progress_callback: onProgress });
306
+ }
307
+
308
+ setStatus("Warming up…");
309
+ const enc = await tokenizer(" ", { add_special_tokens:false });
310
+ await model(enc.input_ids, { attention_mask: enc.attention_mask });
311
+ setStatus("Ready");
312
+ }
313
+
314
+ /* ---------- Next-token logic ---------- */
315
+ function showToken(s){
316
+ if (s === "\n") return "⏎";
317
+ if (s.trim() === "") return `␣${s.length>1 ? "×"+s.length : ""}`;
318
+ return s;
319
+ }
320
+ const PUNC_ONLY = /^[\s.,;:!?—-]+$/;
321
+
322
+ async function greedyNext(text, topK=10){
323
+ if (!tokenizer || !model) throw new Error("Model not loaded yet.");
324
+ const enc = await tokenizer(text || " ", { add_special_tokens:false });
325
+ const t0 = performance.now();
326
+ const out = await model(enc.input_ids, { attention_mask: enc.attention_mask });
327
+ const dt = (performance.now() - t0) | 0;
328
+
329
+ const last = out.logits[out.logits.length - 1];
330
+ const m = Math.max(...last);
331
+ const exps = last.map(v => Math.exp(v - m));
332
+ const Z = exps.reduce((a,b)=>a+b,0);
333
+ const probs = exps.map(v => v/Z);
334
+
335
+ const idx = probs.map((p,i)=>[p,i]).sort((a,b)=>b[0]-a[0]).slice(0, topK);
336
+
337
+ const rows = [];
338
+ for (const [p, i] of idx){
339
+ const tok = await tokenizer.decode([i], { skip_special_tokens:false });
340
+ rows.push({ token: tok, p, id:i });
341
+ }
342
+ return { rows, dt };
343
  }
344
+
345
+ function renderRows(rows){
346
+ klistEl.innerHTML = "";
347
+ const hide = hidePunc.checked;
348
+
349
+ for (const r of rows){
350
+ if (hide && PUNC_ONLY.test(r.token)) continue;
351
+
352
+ const row = document.createElement('div');
353
+ row.className = 'tokrow';
354
+ const left = document.createElement('div');
355
+ left.className = 'tok';
356
+ left.textContent = showToken(r.token);
357
+ const right = document.createElement('div');
358
+ right.className = 'muted small';
359
+ right.textContent = (r.p*100).toFixed(2) + "%";
360
+ row.appendChild(left); row.appendChild(right);
361
+
362
+ row.addEventListener('click', () => {
363
+ textEl.value = (textEl.value || "") + r.token;
364
+ predict();
365
+ });
366
+ row.addEventListener('mouseenter', () => {
367
+ Emb.drawBase();
368
+ Emb.highlight(r.token);
369
+ });
370
+
371
+ klistEl.appendChild(row);
372
+ }
373
  }
374
+
375
+ async function predict(){
376
+ try{
377
+ setErr(""); predictBtn.disabled = true;
378
+ const topK = parseInt(topkSel.value,10) || 10;
379
+ const { rows, dt } = await greedyNext(textEl.value, topK);
380
+ renderRows(rows);
381
+ timeEl.textContent = `+${dt} ms`;
382
+ }catch(e){
383
+ console.error(e); setErr(String(e?.message || e));
384
+ }finally{
385
+ predictBtn.disabled = false;
 
 
 
 
386
  }
387
  }
388
+
389
+ /* ---------- UI wiring ---------- */
390
+ predictBtn.addEventListener('click', predict);
391
+ textEl.addEventListener('input', (() => { let to; return () => { clearTimeout(to); to = setTimeout(predict, 250); }; })());
392
+ hidePunc.addEventListener('change', predict);
393
+ topkSel.addEventListener('change', predict);
394
+ demoBtn.addEventListener('click', () => {
395
+ textEl.value = "Twinkle, twinkle, little "; // trailing space matters
396
+ predict();
397
+ });
398
+ modelSel.addEventListener('change', async (e) => {
399
+ await loadModel(e.target.value);
400
+ predict();
401
+ });
402
+
403
+ /* ---------- Boot ---------- */
404
+ (async function init(){
405
+ await loadModel(modelSel.value); // defaults to 'qwen'
406
+ if (!textEl.value) textEl.value = "Twinkle, twinkle, little ";
407
+ await predict();
408
+ })();
409
+ </script>
410
  </body>
411
  </html>
412
+