Spaces:
Running
Running
Download app.js from Hugdrigo/modelguard-observatory: direct link, hf CLI and curl.
- Browser
- Download file 29 kB
-
https://huggingface.co/spaces/Hugdrigo/modelguard-observatory/resolve/main/app.js
- Command line
-
hf download hf://spaces/Hugdrigo/modelguard-observatory/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Hugdrigo/modelguard-observatory/resolve/main/app.js
29 kB
| /* ModelGuard Observatory — static, in-browser research interface. MIT. */ | |
| (function(){'use strict'; | |
| const E=window.ModelGuard; | |
| const MODEL_NAMES={nb:'Multinomial Naive Bayes',word:'Word TF-IDF',char:'Character TF-IDF',hybrid:'Hybrid TF-IDF'}; | |
| const VIEWS={overview:'Overview',drift:'Drift detection',slices:'Slice analysis',errors:'Error explorer',regression:'Model regression',gates:'Release gates',registry:'Run registry',export:'Export & governance'}; | |
| const state={rows:[],source:'Bundled synthetic-curated evaluation artifacts',model:'hybrid',split:'challenge',view:'overview',segment:'language',errorFilter:'all',candidate:'nb',threshold:{minChallengeN:50,minChallengeAccuracy:.75,maxGeneralizationGap:.22,minSliceN:10,minSliceAccuracy:.58,maxLengthPSI:.5},manifest:null,sort:'accuracy'}; | |
| const $=(id)=>document.getElementById(id); | |
| const esc=s=>String(s??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"').replaceAll("'",'''); | |
| const pc=x=>x==null||!Number.isFinite(x)?'N/A':(100*x).toFixed(1)+'%'; | |
| const n3=x=>x==null||!Number.isFinite(x)?'N/A':x.toFixed(3); | |
| const gap=x=>x==null||!Number.isFinite(x)?'N/A':(x*100).toFixed(1)+' pp'; | |
| const label=s=>({nb:'Naive Bayes',word:'Word TF-IDF',char:'Character TF-IDF',hybrid:'Hybrid TF-IDF'}[s]||s); | |
| const rowSet=(model=state.model,split=state.split)=>E.select(state.rows,model,split); | |
| const describe=(title,description,kicker='EVALUATION LAB')=>`<div class="section-title"><div><div class="panel-kicker">${esc(kicker)}</div><h2>${esc(title)}</h2></div><p>${esc(description)}</p></div>`; | |
| function notify(msg,type='info'){const node=$('notice');node.hidden=false;node.className='notice'+(type==='error'?' error':'');node.textContent=msg;} | |
| function clearNotice(){const n=$('notice');n.hidden=true;n.textContent='';} | |
| function bar(name,value,alternate=false){const x=Math.max(0,Math.min(100,(value||0)*100));return `<div class="barline"><label>${esc(name)}</label><div class="bartrack"><div class="barfill ${alternate?'alt':''}" style="width:${x}%"></div></div><b>${pc(value)}</b></div>`;} | |
| function stat(label,value,hint='',cls=''){return `<div class="metric"><small>${esc(label)}</small><strong class="${cls}">${esc(value)}</strong><span>${esc(hint)}</span></div>`;} | |
| function rowTable(headers,rows){return `<div class="table-wrap"><table class="data-table"><thead><tr>${headers.map(h=>`<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table></div>`;} | |
| const txt=(row)=>esc((row.text||'No text supplied').slice(0,240)); | |
| function renderOverview(){const all=state.rows,s=E.select(all,state.model,state.split),base=E.select(all,state.model,'iid'),chal=E.select(all,state.model,'challenge'),now=E.metrics(s),b=E.metrics(base),c=E.metrics(chal),comparison=E.compare(all,state.model),g=E.gate(all,state.model,state.threshold),slices=E.segments(s,'language'),audit=E.audit(all); | |
| const summary=describe('Evaluation overview','Snapshot-based monitoring of an academic rhetorical-intent classifier. Controlled IID and novel-pattern challenge are not chronological production runs.'); | |
| let vals=`<div class="metric-grid">${stat('Selected evaluation accuracy',pc(now.accuracy),`${now.n} labeled examples`,now.accuracy>=.75?'good':'warn')}${stat('Macro F1',pc(now.macroF1),'Equal class weighting')}${stat('Generalization gap',gap(comparison.gap),'IID − challenge','warn')}${stat('Release checks',g.checks.filter(x=>x.pass).length+'/'+g.checks.length, g.pass?'Exploratory pass':'Needs review',g.pass?'good':'warn')}</div>`; | |
| let modelRows=E.METHODS.map(m=>{const x=E.compare(all,m),sel=state.model===m;return `<tr><td>${sel?'<span class="tag good">SELECTED</span> ':''}${esc(label(m))}</td><td>${pc(x.iid.accuracy)}</td><td>${pc(x.challenge.accuracy)}</td><td>${gap(x.gap)}</td><td>${x.challenge.n}</td></tr>`;}); | |
| return summary+vals+`<div class="content-grid"><section class="panel"><div class="panel-kicker">EXPERIMENT DASHBOARD</div><h3>Generalization across evaluation splits</h3>${E.METHODS.map(m=>{const c=E.compare(all,m);return `<div style="margin:13px 0"><div class="subtle">${esc(label(m))}</div>${bar('Controlled IID',c.iid.accuracy)}${bar('Novel-pattern',c.challenge.accuracy,true)}</div>`;}).join('')}<div class="legend"><span><i class="dot"></i>Controlled</span><span><i class="dot alt"></i>Challenge</span></div></section><section class="panel"><div class="panel-kicker">RESEARCH WARNINGS</div><h3>What the snapshots actually show</h3><div class="chip-row"><span class="chip">${esc(state.source)}</span><span class="chip">${audit.clean?'No duplicate IDs across splits':'Data integrity warning'}</span></div><p>The challenging examples intentionally change wording patterns. These benchmark scores are diagnostic, not guaranteed estimates of real-world performance.</p><div class="hint">${g.pass?'All configured exploratory gates passed. This is not a production certificate.':`${g.checks.filter(x=>!x.pass).length} configured release check(s) failed. Open Release gates to investigate. No overrides are applied automatically.`}</div><p>The length-distribution PSI compares text-length histograms; the language and label comparisons use Jensen–Shannon divergence. None identifies the cause of a change by itself.</p></section></div><section class="panel"><div class="panel-kicker">VERIFIED DATASET ARTIFACTS</div><h3>Four-model comparison</h3>${rowTable(['Model','Controlled accuracy','Challenge accuracy','Difference','Challenge n'],modelRows)}<div class="divider"></div><div class="panel-kicker">SELECTED SPLIT: ${esc(state.split.toUpperCase())}</div><h3>Accuracy by language</h3>${slices.map(x=>bar(`${x.name} (n=${x.n})`,x.accuracy)).join('')}</section>`; | |
| } | |
| function renderDrift(){const a=rowSet(state.model,'iid'),b=rowSet(state.model,'challenge'),d=E.drift(a,b);const lenText=['0–4','5–9','10–14','15–19','20–29','30–49','50+'];const max=Math.max(1,...d.baselineLength,...d.candidateLength);return describe('Drift detection','Compare observed properties between controlled and challenge evaluation splits. This is dataset shift, not evidence of live production drift.', 'DATA SHIFT & DISTRIBUTION DIAGNOSTICS')+`<div class="metric-grid">${stat('Text-length PSI',n3(d.lengthPSI),'Bins: word types, not tokens',d.lengthPSI>.25?'warn':'good')}${stat('Language JSD',n3(d.languageJSD),'Range 0–1 bits')}${stat('True-label JSD',n3(d.truthJSD),'Range 0–1 bits')}${stat('Sample volume',`${a.length} → ${b.length}`,'Controlled → challenge')}</div><div class="content-grid"><section class="panel"><div class="panel-kicker">WORD-TYPE DISTRIBUTION</div><h3>Length-bin comparison</h3><div class="legend"><span><i class="dot"></i>Controlled</span><span><i class="dot alt"></i>Challenge</span></div><div class="dual-bars">${lenText.map((l,i)=>`<div class="dual-item"><span class="subtle">${l}</span><div class="tracks"><div class="bartrack"><div class="barfill" style="width:${d.baselineLength[i]/max*100}%"></div></div><div class="bartrack"><div class="barfill alt" style="width:${d.candidateLength[i]/max*100}%"></div></div></div></div>`).join('')}</div><div class="hint">A high PSI indicates a shift in this particular distribution. Thresholds are user-defined heuristics; PSI is sensitive to binning and smoothing, especially on small samples.</div></section><section class="panel"><div class="panel-kicker">LANGUAGE COVERAGE</div><h3>Distribution by language</h3>${rowTable(['Language','Controlled','Challenge'],d.languageCounts.map(x=>`<tr><td>${esc(x.name)}</td><td>${x.baseline}</td><td>${x.candidate}</td></tr>`))}<div class="divider"></div><h3>Interpreting these statistics</h3><p><b>JSD</b> is symmetric divergence measured in bits (0 means equal distributions). <b>PSI</b> compares binned frequencies with a numerical smoothing term. Neither is a formal significance test here.</p><p>The challenge set is small and curated. Unequal split sizes alone should not be interpreted as an alarming production change.</p></section></div>`;} | |
| function renderSlices(){const splits=E.segments(rowSet(),state.segment);const select=`<div class="filters"><label for="sliceBy" class="subtle">Group by</label><select id="sliceBy"><option value="language" ${state.segment==='language'?'selected':''}>Language</option><option value="domain" ${state.segment==='domain'?'selected':''}>Domain</option><option value="truth" ${state.segment==='truth'?'selected':''}>True label</option></select></div>`;return describe('Slice analysis','Expose weak subgroups rather than relying only on aggregate accuracy. Slice results are descriptive and small samples are clearly marked.')+select+`<div class="content-grid"><section class="panel"><div class="panel-kicker">${esc(state.segment.toUpperCase())} DISAGGREGATION</div><h3>Accuracy by subgroup</h3>${splits.map(s=>bar(`${s.name} · ${s.n}`,s.accuracy)).join('')}</section><section class="panel"><div class="panel-kicker">INTERPRETATION</div><h3>Do not certify fairness from this view</h3><p>Language and academic domain are task categories here, not protected demographic groups. This view identifies variation but does not validate fairness, equity or performance in real deployment.</p><div class="hint">Any slice with fewer than 10 examples should be treated as highly unstable. Compare supporting counts before ranking models.</div></section></div><section class="panel"><h3>Full slice statistics</h3>${rowTable(['Slice','n','Accuracy','Macro F1','Correct','Reliability'],splits.map(s=>`<tr><td>${esc(s.name)}</td><td>${s.n}</td><td>${pc(s.accuracy)}</td><td>${pc(s.macroF1)}</td><td>${s.correct}</td><td>${s.n<10?'<span class="tag warn">small sample</span>':'<span class="tag">descriptive</span>'}</td></tr>`))}</section>`;} | |
| function renderErrors(){const rows=rowSet(),mt=E.metrics(rows),errorRows=rows.filter(x=>x.pred!==x.truth&&(state.errorFilter==='all'||x.truth===state.errorFilter)).slice(0,50),max=Math.max(1,...Object.values(mt.confusion).flatMap(x=>Object.values(x)));const legend=E.LABELS.map(l=>`<div class="matrix-label">${esc(l.slice(0,6))}</div>`).join('');const conf=E.LABELS.map(truth=>`<div style="display:grid;grid-template-columns:95px repeat(6,minmax(0,1fr));gap:5px;align-items:center;margin:5px 0"><div class="matrix-label" style="text-align:left">${esc(truth)}</div>${E.LABELS.map(pred=>{const n=mt.confusion[truth][pred],p=n/max,alpha=(.12+.72*p).toFixed(3);return `<div class="heat" style="background:rgba(${pred===truth?'68,198,172':'229,127,136'},${alpha})" title="${truth} → ${pred}: ${n}">${n}</div>`;}).join('')}</div>`).join('');return describe('Error explorer','Inspect exact misclassifications with original text, true labels and predicted labels. Full text comes only from the bundled synthetic dataset.')+`<div class="metric-grid">${stat('Evaluated',mt.n,'Labeled observations')}${stat('Correct',mt.correct||0,'Exact label matches','good')}${stat('Errors',mt.errors,'Misclassifications','warn')}${stat('Macro F1',pc(mt.macroF1),'Six rhetorical classes')}</div><section class="panel"><div class="panel-kicker">CONFUSION MATRIX</div><h3>Truth (rows) vs prediction (columns)</h3><div style="overflow:auto"><div style="min-width:640px"><div style="display:grid;grid-template-columns:95px repeat(6,minmax(0,1fr));gap:5px"><span></span>${legend}</div>${conf}</div></div><p class="subtle">Green cells indicate correct predictions; red cells indicate mistakes. Counts, not percentages.</p></section><section class="panel" style="margin-top:14px"><div class="panel-kicker">ERROR RECORDS</div><h3>Individual examples</h3><div class="filters"><label for="errorBy" class="subtle">Filter true label</label><select id="errorBy"><option value="all">All labels</option>${E.LABELS.map(l=>`<option value="${l}" ${state.errorFilter===l?'selected':''}>${l}</option>`).join('')}</select></div>${errorRows.length?rowTable(['ID','Language','True','Predicted','Original synthetic example'],errorRows.map(r=>`<tr><td class="nowrap">${esc(r.id)}</td><td>${esc(r.language)}</td><td><span class="tag">${esc(r.truth)}</span></td><td><span class="tag bad">${esc(r.pred)}</span></td><td>${txt(r)}</td></tr>`)):'<div class="empty">No misclassifications for this filter.</div>'}</section>`;} | |
| function renderRegression(){const selected=rowSet(state.model,'challenge');const other=state.candidate===state.model?'nb':state.candidate;const available=E.select(state.rows,state.model,'challenge').length&&E.select(state.rows,other,'challenge').length;if(!available)return describe('Model regression laboratory','Paired comparisons require challenge predictions for two models.')+'<div class="empty">Load compatible predictions for at least two models with matching example IDs before comparing them. You can restore the bundled benchmark under Export & governance.</div>';const p=E.pairedComparison(state.rows,state.model,other,'challenge',500,2026);const a=E.metrics(selected),b=E.metrics(rowSet(other,'challenge'));return describe('Model regression laboratory','Compare two methods on identical challenge examples. A paired bootstrap describes uncertainty for this small curated set, not general-purpose statistical significance.')+`<section class="panel"><div class="filters"><label for="compareMethod" class="subtle">Compare ${esc(label(state.model))} against</label><select id="compareMethod">${E.METHODS.filter(x=>x!==state.model).map(m=>`<option value="${m}" ${other===m?'selected':''}>${esc(label(m))}</option>`).join('')}</select></div><div class="metric-grid">${stat('Reference model',pc(a.accuracy),label(state.model))}${stat('Comparator',pc(b.accuracy),label(other))}${stat('Paired difference',gap(p.delta), 'Comparator − reference')}${stat('Paired n',p.n,'Matched example IDs')}</div><div class="content-grid"><div class="panel"><div class="panel-kicker">PAIRED BOOTSTRAP</div><h3>Difference interval</h3><span class="small-number">${gap(p.ciLow)} to ${gap(p.ciHigh)}</span><p>500 resamples · deterministic seed 2026 · percentile interval over matched challenge examples.</p><p class="hint">This interval is descriptive, sensitive to the synthetic examples, and cannot establish population-level superiority.</p></div><div class="panel"><div class="panel-kicker">MODEL DISAGREEMENTS</div><h3>Paired outcomes</h3>${bar('Comparator fixes',p.bWins/p.n)}${bar('Comparator breaks',p.aWins/p.n,true)}<p>${p.bWins} examples corrected by comparator, ${p.aWins} examples that the reference got right but comparator missed.</p></div></div></section>`;} | |
| function renderGates(){const g=E.gate(state.rows,state.model,state.threshold);return describe('Research release gates','Adjust explicit quality thresholds and inspect every pass/fail result. This is an exploratory engineering checklist, not a production approval or safety certification.')+`<section class="panel"><div class="panel-kicker">GOVERNANCE POLICY</div><h3>Threshold configuration</h3><div class="grid-form"><label class="field">Minimum challenge accuracy (%)<input id="tAccuracy" type="number" min="0" max="100" step="1" value="${state.threshold.minChallengeAccuracy*100}"></label><label class="field">Maximum generalization gap (pp)<input id="tGap" type="number" min="0" max="100" step="1" value="${state.threshold.maxGeneralizationGap*100}"></label><label class="field">Maximum length PSI<input id="tPSI" type="number" min="0.01" max="100" step=".05" value="${state.threshold.maxLengthPSI}"></label><label class="field">Minimum slice accuracy (%)<input id="tSlice" type="number" min="0" max="100" step="1" value="${state.threshold.minSliceAccuracy*100}"></label><label class="field">Minimum challenge examples<input id="tCount" type="number" min="1" max="10000" step="1" value="${state.threshold.minChallengeN}"></label><label class="field">Minimum slice examples<input id="tSliceN" type="number" min="1" max="10000" step="1" value="${state.threshold.minSliceN}"></label></div><div class="report-actions"><button class="btn btn-primary" id="applyGates">Apply thresholds</button><button class="btn btn-outline" id="resetGates">Restore research defaults</button></div><p class="subtle">Thresholds do not change measured results. Relaxing thresholds may produce a PASS badge without improving the model.</p></section><section class="panel" style="margin-top:14px"><div class="panel-kicker">AUDIT RESULT</div><h3>${g.pass?'<span class="tag good">CONFIGURED CHECKS PASS</span>':'<span class="tag bad">CONFIGURED CHECKS FAIL</span>'} · ${g.checks.filter(c=>c.pass).length} of ${g.checks.length} passed</h3>${rowTable(['Rule','Outcome','Recorded value'],g.checks.map(c=>`<tr><td>${esc(c.label)}</td><td><span class="tag ${c.pass?'good':'bad'}">${c.pass?'PASS':'FAIL'}</span></td><td>${esc(c.detail)}</td></tr>`))}<p class="hint">A passing research gate is neither an ethical, legal, fairness, robustness nor operational safety certification. External domain validation is required.</p></section>`;} | |
| function renderRegistry(){const manifest=state.manifest||{},audit=E.audit(state.rows),records=E.METHODS.flatMap(m=>['iid','challenge'].map(s=>{const mt=E.metrics(rowSet(m,s));return `<tr><td>${esc(label(m))}</td><td>${s==='iid'?'Controlled IID':'Challenge'}</td><td>${mt.n}</td><td>${pc(mt.accuracy)}</td><td>${pc(mt.macroF1)}</td><td>deterministic</td></tr>`}));return describe('Run registry & lineage','Inspect the underlying evaluation runs, their provenance, source licenses and dataset-integrity checks. A snapshot registry, not real-time production telemetry.')+`<div class="metric-grid">${stat('Prediction rows',audit.total,'Four methods × two splits')}${stat('Duplicate keys',audit.duplicates,'split + method + sample ID')}${stat('Cross-split ID overlap',audit.splitIdOverlap,'Should be zero')}${stat('Integrity review',audit.clean?'PASS':'CHECK',audit.clean?'No structural anomalies':'See issues',audit.clean?'good':'warn')}</div><div class="content-grid"><section class="panel"><div class="panel-kicker">ARTIFACT LINEAGE</div><h3>Experiment manifest</h3><div class="codebox">${esc(JSON.stringify(manifest,null,2))}</div></section><section class="panel"><div class="panel-kicker">REPRODUCIBILITY & LIMITS</div><h3>What is and is not being measured</h3><p>Predictions come from retraining four browser-compatible baselines with the original 842-row synthetic training split. No third-party model APIs, personal records, real-time inference logs or remote monitoring are used.</p><p>Data license: CC BY 4.0. Application code license: MIT. The dataset and baseline methods are deliberately small-scale research resources.</p><div class="hint">Risk: synthetic template-family data may exaggerate performance on easy examples. Do not use the app's numbers for high-stakes deployment decisions.</div></section></div><section class="panel"><div class="panel-kicker">RECORDED RUNS</div><h3>Evaluation ledger</h3>${rowTable(['Method','Split','Samples','Accuracy','Macro F1','Reproduction'],records)}</section>`;} | |
| function buildReport(){const a=E.compare(state.rows,state.model),d=a.drift,gate=E.gate(state.rows,state.model,state.threshold),slices=E.segments(rowSet(),'language');let lines=['# ModelGuard Observatory — Research report','','**Provenance:** '+state.source,'**Dataset:** Academic Rhetorical Intent Mini v1.1, synthetic-curated','**Evaluation type:** Static IID versus novel-pattern challenge snapshots (NOT temporal production monitoring)','**Method:** '+label(state.model),'**Visible split:** '+state.split,'','## Evaluation','',`- Controlled accuracy: ${pc(a.iid.accuracy)} (n=${a.iid.n})`,`- Challenge accuracy: ${pc(a.challenge.accuracy)} (n=${a.challenge.n})`,`- Generalization gap: ${gap(a.gap)}`,'','## Shift diagnostics','',`- Token-type count PSI: ${n3(d.lengthPSI)}`,`- Language JSD: ${n3(d.languageJSD)} bits`,`- True-label JSD: ${n3(d.truthJSD)} bits`,'','## Language slices',''];for(const s of slices)lines.push(`- ${s.name}: n=${s.n}; accuracy=${pc(s.accuracy)}; macro-F1=${pc(s.macroF1)}`);lines.push('','## Exploratory release gates','',...gate.checks.map(c=>`- ${c.pass?'PASS':'FAIL'}: ${c.label}. ${c.detail}`),'','## Limitations','', '- Dataset synthetic-curated; narrow academic rhetorical-intent task.','- Comparison across intentionally different dataset splits, not a timestamped stream.','- PSI and JSD measure descriptive shift, not causes or statistical significance.','- Thresholds are researcher configurable and cannot certify safety, bias mitigation or production readiness.','- Model performance on this dataset does not establish external reliability.','',`SHA256: ${state.manifest?.evaluationSha256||'unknown'}`);return lines.join('\n');} | |
| function download(name,contents,mime){const blob=new Blob([contents],{type:mime});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);notify(name+' downloaded.');} | |
| function renderExport(){return describe('Export & governance','Download underlying predictions, metrics and methodology. You may also import a compatible CSV or JSON snapshot for entirely local auditing.')+`<div class="content-grid"><section class="panel"><div class="panel-kicker">REPRODUCIBLE DELIVERABLES</div><h3>Download an audit package</h3><div class="report-actions"><button class="btn btn-primary" data-download="report">↧ Research brief (.md)</button><button class="btn btn-outline" data-download="metrics">↧ Metrics (.json)</button><button class="btn btn-outline" data-download="rows">↧ Predictions (.csv)</button><button class="btn btn-outline" data-download="session">↧ Session (.json)</button></div><p>Exported data includes model names, split labels and source provenance. CSV cells are escaped to reduce spreadsheet-formula injection risk.</p></section><section class="panel"><div class="panel-kicker">LOCAL DATA ONLY</div><h3>Import your evaluation snapshot</h3><p>Load a JSON array, <span class="code-inline">{'{ "rows": [...] }'}</span> object, or CSV with the required fields: <b>id, split, method, truth, pred</b>. Optional: language, domain, tokenCount, text.</p><input id="inputFile" type="file" accept=".json,.csv,application/json,text/csv" class="control" style="width:100%"><div class="report-actions"><button class="btn btn-primary" id="importBtn">Import local file</button><button class="btn btn-outline" id="restoreBtn">Restore bundled evidence</button><button class="btn btn-outline" data-download="template">↧ CSV template</button></div><p class="subtle">Imported files stay in your browser session, are not uploaded to this app, and are not saved across page reloads. Do not import sensitive personal records.</p></section></div><section class="panel"><div class="panel-kicker">RESPONSIBLE EVALUATION</div><h3>Guardrails and citations</h3><div class="two-col"><p><b>Methodological boundaries.</b> Drift, class-level disparity and release gates are diagnostics. A scientist must review external tests, documentation, sampling assumptions, and the consequences of errors.</p><p><b>Governance boundaries.</b> No automatic retraining, decisions affecting individuals, fairness certification, background telemetry or credentials. The Space runs on the client and fetches only its own local research artifacts.</p></div><div class="chip-row"><span class="chip">Dataset CC BY 4.0</span><span class="chip">Code MIT</span><span class="chip">Local-only custom import</span><span class="chip">Deterministic baseline replay</span></div></section>`;} | |
| function render(){clearNotice();const content=({overview:renderOverview,drift:renderDrift,slices:renderSlices,errors:renderErrors,regression:renderRegression,gates:renderGates,registry:renderRegistry,export:renderExport}[state.view]||renderOverview)();$('panel').innerHTML=content;$('crumb').textContent=VIEWS[state.view]||'Overview';document.querySelectorAll('[data-view]').forEach(btn=>{btn.classList.toggle('active',btn.dataset.view===state.view);btn.setAttribute('aria-current',btn.dataset.view===state.view?'page':'false');});$('sourceLabel').textContent=state.source.startsWith('Bundled')?'SYNTHETIC-CURATED DATA':'USER IMPORT · LOCAL ONLY';$('heroN').textContent=state.rows.length.toLocaleString('en-US');bindView();} | |
| function performDownload(type){const m=state.model,s=state.split,comp=E.compare(state.rows,m),g=E.gate(state.rows,m,state.threshold);if(type==='report')return download('modelguard_research_report.md',buildReport(),'text/markdown;charset=utf-8');if(type==='metrics')return download('modelguard_metrics.json',JSON.stringify({generatedUTC:new Date().toISOString(),provenance:state.source,method:m,split:s,metrics:comp,gate:g,manifest:state.manifest},null,2),'application/json');if(type==='session')return download('modelguard_session.json',JSON.stringify({schemaVersion:'1.0',provenance:state.source,model:m,split:s,thresholds:state.threshold,rows:state.rows},null,2),'application/json');if(type==='rows')return download('modelguard_predictions.csv',E.csv(state.rows,['split','method','id','language','domain','truth','pred','tokenCount','text']),'text/csv;charset=utf-8');if(type==='template')return download('modelguard_import_template.csv',E.csv([{id:'sample-001',split:'iid',method:'hybrid',truth:'objective',pred:'objective',language:'en',domain:'academic',tokenCount:14,text:'Fictional example only'}],['id','split','method','truth','pred','language','domain','tokenCount','text']),'text/csv;charset=utf-8');} | |
| async function importLocal(){const f=$('inputFile')?.files?.[0];if(!f)return notify('Choose a JSON or CSV file first.','error');if(f.size>12*1024*1024)return notify('File exceeds the 12 MB local import limit.','error');try{const text=await f.text();const candidate=f.name.toLowerCase().endsWith('.csv')?E.parseCsv(text):JSON.parse(text);const rows=E.parseRows(candidate);const inspection=E.audit(rows);if(inspection.duplicates)throw Error('Duplicate row identifiers are not allowed.');state.rows=rows;state.manifest={source:'User-provided local snapshot',recordCount:rows.length,importedFile:f.name,limitations:'User-supplied records are unverified; this is local-only evaluation.'};state.source='Imported '+f.name;state.view='overview';render();notify('Imported '+rows.length+' rows. Provenance is user supplied; results are not independently verified.');}catch(e){notify('Import failed: '+e.message,'error');}} | |
| async function restoreBundled(){try{const [d,m]=await Promise.all([fetch('evaluation_snapshots.json').then(x=>{if(!x.ok)throw Error('Missing data file');return x.json();}),fetch('benchmark_manifest.json').then(x=>{if(!x.ok)throw Error('Missing manifest');return x.json();})]);state.rows=E.parseRows(d);state.manifest=m;state.source='Bundled synthetic-curated evaluation artifacts';state.view='overview';render();notify('Bundled benchmark restored.');}catch(e){notify('Could not restore: '+e.message,'error');}} | |
| function bindView(){const q=(id,ev,fn)=>{const x=$(id);if(x)x.addEventListener(ev,fn);};q('sliceBy','change',e=>{state.segment=e.target.value;render();});q('errorBy','change',e=>{state.errorFilter=e.target.value;render();});q('compareMethod','change',e=>{state.candidate=e.target.value;render();});q('applyGates','click',()=>{const int=(id)=>Number($(id).value),th={minChallengeAccuracy:int('tAccuracy')/100,maxGeneralizationGap:int('tGap')/100,maxLengthPSI:int('tPSI'),minSliceAccuracy:int('tSlice')/100,minChallengeN:int('tCount'),minSliceN:int('tSliceN')};if(Object.entries(th).some(([k,v])=>!Number.isFinite(v)||v<0||((k==='minChallengeAccuracy'||k==='maxGeneralizationGap'||k==='minSliceAccuracy')&&v>1)||((k==='minChallengeN'||k==='minSliceN')&&(!Number.isInteger(v)||v<1))))return notify('Use valid positive thresholds (percent values from 0 to 100).','error');state.threshold=th;render();notify('Thresholds updated. Measurements were not changed.');});q('resetGates','click',()=>{state.threshold={minChallengeN:50,minChallengeAccuracy:.75,maxGeneralizationGap:.22,minSliceN:10,minSliceAccuracy:.58,maxLengthPSI:.5};render();});q('importBtn','click',importLocal);q('restoreBtn','click',restoreBundled);document.querySelectorAll('[data-download]').forEach(x=>x.addEventListener('click',()=>performDownload(x.dataset.download)));} | |
| function bindGlobal(){document.querySelectorAll('[data-view]').forEach(btn=>btn.addEventListener('click',()=>{state.view=btn.dataset.view;render();document.getElementById('panel').scrollIntoView({behavior:'smooth',block:'start'});}));$('methodSelect').addEventListener('change',e=>{state.model=e.target.value;if(state.candidate===state.model)state.candidate=E.METHODS.find(x=>x!==state.model);render();});$('splitSelect').addEventListener('change',e=>{state.split=e.target.value;render();});$('reportBtn').addEventListener('click',()=>performDownload('report'));} | |
| async function init(){bindGlobal();await restoreBundled();clearNotice();} | |
| init().catch(e=>{console.error(e);$('panel').innerHTML=`<div class="empty">Could not load the evaluation artifacts: ${esc(e.message)}. Check that evaluation_snapshots.json and benchmark_manifest.json are in the Space root.</div>`;}); | |
| })(); | |