'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();