SMLBuilder's picture
Publish traceable conversation explorer and standalone Python decoder
f425b0b verified
Raw History Blame Contribute Delete
10.7 kB
'use strict';
const $ = id => document.getElementById(id);
const state = {catalog: null, doc: null, trace: null, id: null, conversation: 0, path: [], limit: 60, request: 0, busy: false};
const categoryLabel = value => ({Kids_Brain:'AAC conversations', Children:'Children', PythonTraining:'Python', SwiftTraining:'Swift', COBOLTraining:'COBOL', CobaltTraining:'Cybersecurity', cannabis:'Cannabis'}[value] || value);
const make = (tag, className, text) => {const el = document.createElement(tag); if (className) el.className = className; if (text !== undefined) el.textContent = text; return el;};
const sizeLabel = bytes => bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.ceil(bytes / 1000)} KB`;
function showError(error) {$('error').textContent = error.message || String(error); $('error').hidden = false;}
function busy(on, message='Loading and verifying the source…') {
state.busy = on; $('loading').hidden = !on; $('loading').textContent = message;
$('conversation-body').setAttribute('aria-busy', String(on));
$('reset').disabled = on || !state.trace; $('back').disabled = on || !state.path.length;
$('conversation-select').disabled = on;
document.querySelectorAll('.choice, .path button').forEach(b => b.disabled = on);
}
async function api(route, params) {
const response = await fetch(`/api/${route}${params ? '?' + new URLSearchParams(params) : ''}`);
const data = await response.json();
if (!response.ok) throw new Error(data.error || `Could not load this source (${response.status}).`);
return data;
}
function renderLibrary() {
const search = $('search').value.toLowerCase().trim(), category = $('category').value;
const rows = state.catalog.files.filter(r => (!category || r.category === category) && (!search || `${r.title} ${r.path} ${categoryLabel(r.category)}`.toLowerCase().includes(search)));
const featured = new Set(state.catalog.featured);
rows.sort((a,b) => Number(featured.has(b.id)) - Number(featured.has(a.id)) || a.title.localeCompare(b.title));
$('results-caption').textContent = `${rows.length.toLocaleString()} files${search || category ? ' found' : ' across ' + new Set(rows.map(r=>r.category)).size + ' collections'}`;
const list = $('library-list'); list.replaceChildren();
for (const row of rows.slice(0, state.limit)) {
const button = make('button', 'library-item' + (row.id === state.id ? ' selected' : ''));
button.setAttribute('aria-pressed', String(row.id === state.id));
button.append(make('span', 'file-title', row.title));
const meta = make('span', 'file-category'); meta.append(make('span', '', categoryLabel(row.category)), make('span', '', sizeLabel(row.bytes)));
button.append(meta); button.title = row.path; button.addEventListener('click', () => loadSource(row.id)); list.append(button);
}
if (rows.length > state.limit) {const more = make('button', 'show-more', `Show more (${rows.length - state.limit})`); more.onclick = () => {state.limit += 60; renderLibrary();}; list.append(more);}
if (!rows.length) list.append(make('p', 'demo-note', 'No files match. Try a different topic or collection.'));
}
function traceParams(path=state.path) {return {id:state.id, conversation:state.conversation, path:path.join(',')};}
async function loadSource(id, conversation=0, path=[]) {
const request = ++state.request; busy(true); $('error').hidden = true;
try {
const doc = await api('document', {id});
const trace = doc.conversations.length ? await api('trace', {id, conversation, path:path.join(',')}) : null;
if (request !== state.request) return;
Object.assign(state, {doc, trace, id, conversation, path}); renderLibrary(); renderDocument(); renderTrace(); saveLocation();
} catch(error) {if (request === state.request) showError(error);}
finally {if (request === state.request) busy(false);}
}
async function navigate(path) {
const request = ++state.request; busy(true, 'Following the recorded branch…'); $('error').hidden = true;
try {
const trace = await api('trace', traceParams(path));
if (request !== state.request) return;
state.path = path; state.trace = trace; renderTrace(); saveLocation();
const body = $('conversation-body'); const target = $('choices-heading'); body.scrollTop = Math.max(0, target.offsetTop - body.offsetTop - body.clientHeight / 2);
} catch(error) {if (request === state.request) showError(error);}
finally {if (request === state.request) busy(false);}
}
function renderDocument() {
const {source, conversations} = state.doc;
$('topic-category').textContent = categoryLabel(source.category).toUpperCase(); $('topic-title').textContent = source.title;
$('source-repo').textContent = source.repo; $('revision').textContent = source.revision;
$('source-link').href = source.source_url; $('sha256').textContent = source.verified_sha256;
$('verified').textContent = '✓ Source content verified';
const select = $('conversation-select'); select.replaceChildren();
conversations.forEach(c => {const opt = make('option', '', `${c.index + 1}. ${c.title}`); opt.value=c.index; select.append(opt);});
select.value = state.conversation; $('conversation-select-wrap').hidden = conversations.length < 2;
$('unsupported').hidden = !!state.trace; $('conversation-body').scrollTop = 0;
}
function inspectTurn(index) {
const turn = state.trace?.history[index];
$('turn-select').value = String(index); $('pointer').textContent = turn?.pointer || '—';
$('metadata').textContent = JSON.stringify(turn?.metadata || {}, null, 2);
}
function renderTrace() {
const trace = state.trace, transcript = $('transcript'), choices = $('choices'), path = $('path');
transcript.replaceChildren(); choices.replaceChildren(); path.replaceChildren(); $('turn-select').replaceChildren();
$('unsupported').hidden = !!trace; $('end').hidden = !trace?.terminal; $('choices-heading').hidden = !trace || trace.terminal;
$('export').hidden = !trace; $('share').hidden = !trace;
$('schema').textContent = trace?.schema || 'Training file / unsupported format';
$('branch-path').textContent = trace ? 'root' + state.path.map(i => ` → ${i}`).join('') : '—';
$('turn-count').textContent = `${trace?.history.length || 0} recorded turns`;
$('latency').textContent = trace ? `${trace.decode_ms} ms · server` : '';
$('choice-count').textContent = trace ? `${trace.options.length} available` : '';
if (!trace) {inspectTurn(-1); return;}
const crumb = (label, length) => {const button = make('button', '', label); button.title = length ? `Return to selection ${length}` : 'Return to root'; button.onclick = () => navigate(state.path.slice(0,length)); path.append(button);};
crumb('root', 0); state.path.forEach((n,i) => {path.append(make('span', '', '→')); crumb(String(n), i+1);});
trace.history.forEach((turn, index) => {
const item = make('article', 'message' + (turn.kind === 'response' || (trace.linear && index%2) ? ' response' : ''));
item.append(make('p', 'speaker', `${turn.role} · turn ${index+1}`));
const bubble = make('button', 'bubble'); bubble.setAttribute('aria-label', `Inspect turn ${index+1}: ${turn.text}`);
if (turn.emoji) bubble.append(make('span','emoji',turn.emoji)); bubble.append(document.createTextNode(turn.text));
bubble.onclick = () => inspectTurn(index); item.append(bubble);
const meta = turn.metadata;
for (const key of ['code_example','cobol_example','jcl_example','commands']) {
if (typeof meta[key] === 'string' && meta[key]) {const details=make('details','turn-code'); details.append(make('summary','',key.replaceAll('_',' ')),make('pre','',meta[key])); item.append(details);}
}
transcript.append(item);
const option=make('option','',`${index+1}. ${turn.role}: ${turn.text.slice(0,70)}`); option.value=index; $('turn-select').append(option);
});
trace.options.forEach(option => {
const button=make('button','choice'); button.append(make('span','choice-number',String(option.index + 1)));
const copy=make('span','choice-copy',`${option.emoji ? option.emoji+' ' : ''}${option.text}`);
if (option.metadata.intent) copy.append(make('span','intent',String(option.metadata.intent).replaceAll('_',' ')));
button.append(copy); button.title=`Source branch ${option.index} · ${option.pointer}`; button.onclick=()=>navigate([...state.path,option.index]); choices.append(button);
});
$('choices-heading').firstElementChild.textContent = trace.linear ? 'NEXT RECORDED MESSAGE' : 'CHOOSE A REPLY';
$('export').href = '/api/export?' + new URLSearchParams(traceParams());
inspectTurn(trace.history.length-1);
}
function saveLocation() {
const fragment = new URLSearchParams(traceParams()).toString();
try {history.replaceState(null, '', '#' + fragment);} catch (_) { /* The explorer also works inside a restricted embed. */ }
}
async function start() {
try {
state.catalog = await api('catalog'); $('catalog-count').textContent = state.catalog.count.toLocaleString();
const categories=[...new Set(state.catalog.files.map(r=>r.category))].sort((a,b)=>categoryLabel(a).localeCompare(categoryLabel(b)));
for(const c of categories){const o=make('option','',categoryLabel(c));o.value=c;$('category').append(o);}
renderLibrary();
const link = new URLSearchParams(location.hash.slice(1));
const id = link.get('id') || state.catalog.default_id;
const conversation = Number(link.get('conversation') || 0);
const pathText=link.get('path') || ''; const path=pathText ? pathText.split(',').map(Number) : [];
if (!state.catalog.files.some(r=>r.id===id) || !Number.isInteger(conversation) || conversation<0 || path.some(n=>!Number.isInteger(n)||n<0)) throw new Error('This shared path is invalid. Select a conversation from the library to start.');
await loadSource(id,conversation,path);
} catch(error) {showError(error);busy(false);}
}
$('search').addEventListener('input',()=>{state.limit=60;renderLibrary();});
$('category').addEventListener('change',()=>{state.limit=60;renderLibrary();});
$('conversation-select').addEventListener('change',event=>loadSource(state.id,Number(event.target.value)));
$('turn-select').addEventListener('change',event=>inspectTurn(Number(event.target.value)));
$('back').onclick=()=>navigate(state.path.slice(0,-1)); $('reset').onclick=()=>navigate([]);
$('share').onclick=async()=>{
const url = 'https://smlbuilder-sml-tree-explorer.hf.space/#' + new URLSearchParams(traceParams());
try {await navigator.clipboard.writeText(url); $('share').textContent='Link copied';setTimeout(()=>$('share').textContent='Copy a link to this path',1800);}
catch(_){$('error').textContent='Link to this path: '+url;$('error').hidden=false;}
};
start();