valeriow/parallel-constrained-decoding
0
1// Minimal App State2let presets = [];3let activePreset = null;4 5const $ = (id) => document.getElementById(id);6 7const els = {8 presetSelect: $('preset-select'),9 btnRun: $('btn-run'),10 summaryBar: $('summary-bar'),11 sumSpeedup: $('sum-speedup'),12 sumTimes: $('sum-times'),13 14 // Left: Parallel Constrained15 timerParallel: $('timer-parallel'),16 parallelOutput: $('parallel-output'),17 bodyParallel: $('body-parallel'),18 19 // Right: Naive20 timerNaive: $('timer-naive'),21 streamOutput: $('stream-output'),22 bodyNaive: $('body-naive'),23 badgeNaiveHallucinated: $('badge-naive-hallucinated'),24};25 26// Synchronized scrolling for side-by-side comparison27let isSyncingNaive = false;28let isSyncingParallel = false;29 30function setupScrollSync() {31 if (els.bodyNaive && els.bodyParallel) {32 els.bodyNaive.addEventListener('scroll', () => {33 if (isSyncingNaive) return;34 isSyncingParallel = true;35 els.bodyParallel.scrollTop = els.bodyNaive.scrollTop;36 els.bodyParallel.scrollLeft = els.bodyNaive.scrollLeft;37 requestAnimationFrame(() => { isSyncingParallel = false; });38 });39 40 els.bodyParallel.addEventListener('scroll', () => {41 if (isSyncingParallel) return;42 isSyncingNaive = true;43 els.bodyNaive.scrollTop = els.bodyParallel.scrollTop;44 els.bodyNaive.scrollLeft = els.bodyParallel.scrollLeft;45 requestAnimationFrame(() => { isSyncingNaive = false; });46 });47 }48}49 50function escapeHtml(str) {51 if (typeof str !== 'string') str = String(str);52 return str53 .replace(/&/g, '&')54 .replace(/</g, '<')55 .replace(/>/g, '>');56}57 58function escapeAttr(str) {59 if (typeof str !== 'string') str = String(str);60 return str61 .replace(/&/g, '&')62 .replace(/"/g, '"');63}64 65function formatValue(val) {66 if (val === null || val === undefined) {67 return '<span class="json-val-null">null</span>';68 }69 if (typeof val === 'boolean') {70 return `<span class="json-val-bool bool-${val}">${val}</span>`;71 }72 if (typeof val === 'number') {73 return `<span class="json-val-num">${val}</span>`;74 }75 if (typeof val === 'string') {76 return `<span class="json-val-str">"${escapeHtml(val)}"</span>`;77 }78 if (Array.isArray(val)) {79 return `<span class="json-punct">[</span>${val.map(formatValue).join('<span class="json-punct">, </span>')}<span class="json-punct">]</span>`;80 }81 return `<span class="json-val-str">${escapeHtml(JSON.stringify(val))}</span>`;82}83 84// Single-line JSON formatters so keys line up line-for-line across both panels85function formatParallelJson(obj) {86 if (!obj || typeof obj !== 'object') return '<div class="json-row"><span class="json-punct">{ }</span></div>';87 const schemaKeys = activePreset && activePreset.schema ? Object.keys(activePreset.schema) : Object.keys(obj);88 const keys = schemaKeys.filter(k => k in obj);89 Object.keys(obj).forEach(k => {90 if (!keys.includes(k)) keys.push(k);91 });92 93 const lines = ['<div class="json-row"><span class="json-punct">{</span></div>'];94 keys.forEach((key, idx) => {95 const isLast = idx === keys.length - 1;96 const comma = isLast ? '' : '<span class="json-punct">,</span>';97 const val = obj[key];98 99 let inner = '';100 if (val && typeof val === 'object' && 'value' in val && 'prob' in val) {101 const valHtml = formatValue(val.value);102 const probNum = typeof val.prob === 'number' ? val.prob.toFixed(4) : escapeHtml(val.prob);103 inner = ` <span class="json-key">"${escapeHtml(key)}"</span><span class="json-punct">: { </span><span class="json-sub">"value"</span><span class="json-punct">: </span>${valHtml}<span class="json-punct">, </span><span class="json-sub">"prob"</span><span class="json-punct">: </span><span class="json-val-prob">${probNum}</span><span class="json-punct"> }</span>${comma}`;104 } else {105 inner = ` <span class="json-key">"${escapeHtml(key)}"</span><span class="json-punct">: </span>${formatValue(val)}${comma}`;106 }107 108 lines.push(`<div class="json-row" data-key="${escapeHtml(key)}">${inner}</div>`);109 });110 lines.push('<div class="json-row"><span class="json-punct">}</span></div>');111 return lines.join('');112}113 114function formatNaiveJson(obj) {115 if (!obj || typeof obj !== 'object') return '<div class="json-row"><span class="json-punct">{ }</span></div>';116 const schemaKeys = activePreset && activePreset.schema ? Object.keys(activePreset.schema) : Object.keys(obj);117 const keys = schemaKeys.filter(k => k in obj);118 Object.keys(obj).forEach(k => {119 if (!keys.includes(k)) keys.push(k);120 });121 122 const lines = ['<div class="json-row"><span class="json-punct">{</span></div>'];123 keys.forEach((key, idx) => {124 const isLast = idx === keys.length - 1;125 const comma = isLast ? '' : '<span class="json-punct">,</span>';126 const val = obj[key];127 const inner = ` <span class="json-key">"${escapeHtml(key)}"</span><span class="json-punct">: </span>${formatValue(val)}${comma}`;128 lines.push(`<div class="json-row" data-key="${escapeHtml(key)}">${inner}</div>`);129 });130 lines.push('<div class="json-row"><span class="json-punct">}</span></div>');131 return lines.join('');132}133 134function highlightStreamJson(text) {135 if (!text) return '';136 const lines = text.split('\n');137 const rendered = lines.map((line, idx) => {138 const isLastLine = idx === lines.length - 1;139 let lineHtml = escapeHtml(line);140 141 if (lineHtml.includes('":')) {142 lineHtml = lineHtml.replace(/"([^"]+)"(\s*:)/g, '<span class="json-key">"$1"</span>$2');143 } else if (isLastLine && /^(\s*)"([^"]*)$/.test(lineHtml)) {144 lineHtml = lineHtml.replace(/^(\s*)"([^"]*)$/, '$1<span class="json-key">"$2</span>');145 } else if (isLastLine && /^(\s*)"([^"]+)"(\s*)$/.test(lineHtml)) {146 lineHtml = lineHtml.replace(/^(\s*)"([^"]+)"(\s*)$/, '$1<span class="json-key">"$2"</span>$3');147 }148 149 return lineHtml;150 });151 152 return rendered.join('\n');153}154 155function setupRowHoverSync() {156 document.addEventListener('mouseover', (e) => {157 const row = e.target.closest('.json-row');158 if (row && row.dataset.key) {159 const key = row.dataset.key;160 document.querySelectorAll(`.json-row[data-key="${CSS.escape(key)}"]`).forEach(el => {161 el.classList.add('row-hover');162 });163 }164 });165 166 document.addEventListener('mouseout', (e) => {167 const row = e.target.closest('.json-row');168 if (row && row.dataset.key) {169 const key = row.dataset.key;170 document.querySelectorAll(`.json-row[data-key="${CSS.escape(key)}"]`).forEach(el => {171 el.classList.remove('row-hover');172 });173 }174 });175}176 177// Initialize178async function init() {179 setupScrollSync();180 setupRowHoverSync();181 try {182 const res = await fetch('/api/presets');183 presets = await res.json();184 185 els.presetSelect.innerHTML = '';186 presets.forEach((p) => {187 const opt = document.createElement('option');188 opt.value = p.id;189 opt.textContent = `${p.title}`;190 els.presetSelect.appendChild(opt);191 });192 193 if (presets.length > 0) {194 activePreset = presets[0];195 }196 } catch (e) {197 console.error('Error loading presets:', e);198 }199 200 els.presetSelect.addEventListener('change', (e) => {201 activePreset = presets.find(p => p.id === e.target.value);202 reset();203 });204 205 els.btnRun.addEventListener('click', runComparison);206}207 208function reset() {209 els.timerNaive.textContent = '0.0 ms';210 els.timerParallel.textContent = '0.0 ms';211 els.streamOutput.innerHTML = '<span class="placeholder-text">Click "Run Comparison" to start...</span>';212 els.parallelOutput.innerHTML = '<span class="placeholder-text">Click "Run Comparison" to start...</span>';213 els.summaryBar.classList.add('hidden');214 if (els.badgeNaiveHallucinated) {215 els.badgeNaiveHallucinated.classList.add('hidden');216 els.badgeNaiveHallucinated.textContent = '';217 }218}219 220// Execute Parallel Constrained Generation221async function runParallel(payload) {222 els.parallelOutput.innerHTML = '<span class="placeholder-text">// Evaluating parallel forward pass across all schema fields...</span>';223 224 const t0 = performance.now();225 let running = true;226 function tickParallel() {227 if (!running) return;228 const ms = performance.now() - t0;229 els.timerParallel.textContent = `${ms.toFixed(1)} ms`;230 requestAnimationFrame(tickParallel);231 }232 requestAnimationFrame(tickParallel);233 234 try {235 const res = await fetch('/api/run-parallel', {236 method: 'POST',237 headers: { 'Content-Type': 'application/json' },238 body: JSON.stringify(payload)239 });240 const data = await res.json();241 running = false;242 const elapsed = data.elapsed_ms || (performance.now() - t0);243 els.timerParallel.textContent = `${elapsed.toFixed(1)} ms`;244 renderParallel(data);245 return data;246 } catch (err) {247 running = false;248 throw err;249 }250}251 252function renderParallel(data) {253 if (data.parsed_json) {254 els.parallelOutput.innerHTML = formatParallelJson(data.parsed_json);255 }256}257 258// Stream Naive259async function streamNaive(payload) {260 els.streamOutput.textContent = '';261 262 const t0 = performance.now();263 let running = true;264 function tickNaive() {265 if (!running) return;266 const ms = performance.now() - t0;267 els.timerNaive.textContent = `${ms.toFixed(1)} ms`;268 requestAnimationFrame(tickNaive);269 }270 requestAnimationFrame(tickNaive);271 272 let tokenCount = 0;273 let finalResult = null;274 275 try {276 const response = await fetch('/api/stream-naive', {277 method: 'POST',278 headers: { 'Content-Type': 'application/json' },279 body: JSON.stringify(payload)280 });281 282 const reader = response.body.getReader();283 const decoder = new TextDecoder();284 let text = '';285 286 while (true) {287 const { value, done } = await reader.read();288 if (done) break;289 290 const chunk = decoder.decode(value, { stream: true });291 const lines = chunk.split('\n');292 293 for (const line of lines) {294 if (line.startsWith('data: ')) {295 try {296 const ev = JSON.parse(line.substring(6));297 if (ev.type === 'token') {298 text += ev.token;299 tokenCount = ev.token_count;300 els.streamOutput.innerHTML = highlightStreamJson(text);301 els.streamOutput.scrollTop = els.streamOutput.scrollHeight;302 } else if (ev.type === 'done') {303 finalResult = ev.result;304 }305 } catch (e) {}306 }307 }308 }309 } finally {310 running = false;311 }312 313 if (finalResult) {314 els.timerNaive.textContent = `${finalResult.elapsed_ms.toFixed(1)} ms`;315 let parsedObj = finalResult.parsed_json;316 if (parsedObj) {317 els.streamOutput.innerHTML = formatNaiveJson(parsedObj);318 } else {319 try {320 const cleaned = text.substring(text.indexOf('{'), text.lastIndexOf('}') + 1);321 parsedObj = JSON.parse(cleaned);322 els.streamOutput.innerHTML = formatNaiveJson(parsedObj);323 } catch (e) {324 els.streamOutput.innerHTML = highlightStreamJson(text);325 }326 }327 328 // Check for missing / hallucinated fields329 const expectedKeys = activePreset && activePreset.schema ? Object.keys(activePreset.schema) : [];330 let missingCount = 0;331 if (parsedObj && typeof parsedObj === 'object') {332 const parsedKeys = Object.keys(parsedObj);333 const missing = expectedKeys.filter(k => !parsedKeys.includes(k));334 const extra = parsedKeys.filter(k => !expectedKeys.includes(k));335 const invalidEnums = finalResult.invalid_enums || [];336 missingCount = missing.length + extra.length + invalidEnums.length;337 } else {338 missingCount = expectedKeys.length;339 }340 341 if (missingCount > 0 && els.badgeNaiveHallucinated) {342 els.badgeNaiveHallucinated.textContent = `${missingCount} field${missingCount > 1 ? 's' : ''} hallucinated`;343 els.badgeNaiveHallucinated.classList.remove('hidden');344 }345 }346 347 return finalResult;348}349 350// Side-by-Side Comparison Runner351async function runComparison() {352 if (!activePreset) return;353 354 reset();355 els.btnRun.disabled = true;356 els.btnRun.textContent = 'Running...';357 358 const payload = {359 context: activePreset.context,360 schema: activePreset.schema361 };362 363 try {364 // 1. Run Parallel Constrained inference first365 const parallelData = await runParallel(payload);366 367 // 2. Stream Naive right after368 const naiveData = await streamNaive(payload);369 370 // 3. Update summary pill371 if (parallelData && naiveData) {372 const speedup = (naiveData.elapsed_ms / Math.max(parallelData.elapsed_ms, 1.0)).toFixed(1);373 els.sumSpeedup.textContent = `${speedup}x FASTER`;374 els.sumTimes.textContent = `${parallelData.elapsed_ms.toFixed(1)} ms vs ${naiveData.elapsed_ms.toFixed(1)} ms`;375 els.summaryBar.classList.remove('hidden');376 }377 } catch (err) {378 console.error('Run failed:', err);379 } finally {380 els.btnRun.disabled = false;381 els.btnRun.innerHTML = '<span>⚡ Run Comparison</span>';382 }383}384 385document.addEventListener('DOMContentLoaded', init);386 