Spaces:
Running
Running
Download site/app.js from SMLBuilder/SML-Tree-Explorer: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/SMLBuilder/SML-Tree-Explorer/resolve/main/site/app.js
- Command line
-
hf download hf://spaces/SMLBuilder/SML-Tree-Explorer/site/app.js
-
curl -L -o app.js https://huggingface.co/spaces/SMLBuilder/SML-Tree-Explorer/resolve/main/site/app.js
10.7 kB
| ; | |
| 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(); | |