Skip to content

Commit 621abf1

Browse files
committed
Add BBQS Live Demo tab to Transform UI
New "BBQS Live Demo" tab (default) in the Transform view lets users pick a real BBQS entity (Investigator, Dataset, Device, FundedProject, Software), choose a target schema, and see field-level mapping results instantly — entirely in-browser using the registry's alignment edges. Mirrors the Python transform_record() algorithm: - Finds BBQS source classes from allClasses - Follows ALIGNED_TO edges to target classes (distance <= 0.6) - Case-insensitive property name matching - Shows mapped fields table (with value + via-alignment annotation), unmapped field chips, and a "Download mapped JSON" button Sample data comes from the BBQS bbqssampledataset_1.jsonld fixture. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXeTeJsXisxTRXay6EoBSQ
1 parent 1df9ff2 commit 621abf1

1 file changed

Lines changed: 141 additions & 3 deletions

File tree

index.html

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,39 @@ <h5 id="g-detail-name" style="color:var(--teal);font-size:13px"></h5>
254254
<div id="view-transform" class="view">
255255
<div class="transform-panel">
256256
<div class="query-tabs" style="margin-bottom:20px">
257-
<button class="active" onclick="trTabSwitch('browser',this)">In-browser CSV</button>
257+
<button onclick="trTabSwitch('demo',this)" class="active">BBQS Live Demo</button>
258+
<button onclick="trTabSwitch('browser',this)">In-browser CSV</button>
258259
<button onclick="trTabSwitch('api',this)">API reference</button>
259260
</div>
260261

262+
<!-- BBQS Live Demo -->
263+
<div id="tr-tab-demo">
264+
<p style="color:var(--text-dim);font-size:12.5px;margin-bottom:20px;line-height:1.6">
265+
Transform real BBQS entities to other schemas using live alignment data from the registry. Runs entirely in your browser.
266+
</p>
267+
268+
<div class="transform-section">
269+
<h3>1. Pick a BBQS entity</h3>
270+
<div id="tr-demo-cards" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-top:4px"></div>
271+
</div>
272+
273+
<div class="transform-section" id="tr-demo-step2" style="display:none">
274+
<h3>2. Target schema</h3>
275+
<div class="builder-row">
276+
<label>Convert to</label>
277+
<select id="tr-demo-tgt" onchange="trRunDemo()"></select>
278+
</div>
279+
<div id="tr-demo-record" style="margin-top:12px;background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;padding:10px 14px;font-family:var(--mono);font-size:11px;color:var(--text-dim);white-space:pre-wrap;line-height:1.6"></div>
280+
</div>
281+
282+
<div class="transform-section" id="tr-demo-result" style="display:none">
283+
<h3>3. Result</h3>
284+
<div id="tr-demo-result-inner"></div>
285+
</div>
286+
</div>
287+
261288
<!-- In-browser CSV transform -->
262-
<div id="tr-tab-browser">
289+
<div id="tr-tab-browser" style="display:none">
263290
<p style="color:var(--text-dim);font-size:12.5px;margin-bottom:20px;line-height:1.6">Upload a CSV whose columns match one schema, and convert to another. Uses stored alignments to auto-map columns. Runs entirely in your browser — no data leaves your machine.</p>
264291

265292
<div class="transform-section">
@@ -554,7 +581,7 @@ <h2>Register a Schema</h2>
554581
if(pr.status==="fulfilled"&&pr.value.ok)provenance=await pr.value.json();
555582
renderConceptsSrc();renderConcepts();renderDiffSelects();renderGraph();renderProvenance();
556583
renderHistList("schemas");renderHistList("classes");renderHistList("properties");
557-
initQuery();initTransform();
584+
initQuery();initTransform();initDemoCards();
558585
}catch(e){document.getElementById("status").innerHTML='<span class="status-dot err"></span>Failed to load';}
559586
}
560587

@@ -808,10 +835,121 @@ <h4>${displayName} <span class="ver-badge">v${v.version}</span></h4>
808835
function trTabSwitch(tab,btn){
809836
document.querySelectorAll("#view-transform .query-tabs button").forEach(b=>b.classList.remove("active"));
810837
btn.classList.add("active");
838+
document.getElementById("tr-tab-demo").style.display=tab==="demo"?"block":"none";
811839
document.getElementById("tr-tab-browser").style.display=tab==="browser"?"block":"none";
812840
document.getElementById("tr-tab-api").style.display=tab==="api"?"block":"none";
813841
}
842+
843+
// ---------------------------------------------------------------------------
844+
// BBQS Live Demo
845+
// ---------------------------------------------------------------------------
846+
const BBQS_ENTITIES=[
847+
{type:"Investigator",icon:"👤",record:{name:"Laura Yenisa Cabrera Trujillo",email:"lcabrera@psu.edu",identifier:"0000-0002-1234-5678",affiliation:"Penn State University",researchAreas:"neuroethics, DBS"}},
848+
{type:"Dataset",icon:"📊",record:{identifier:"DANDI:000123",name:"EMBER Dandiset 000123 — Human iEEG during mood tasks",species:"Human",modality:"iEEG",license:"CC-BY-4.0",url:"https://dandi.emberarchive.org/dandiset/000123"}},
849+
{type:"Device",icon:"🔬",record:{name:"Neuropixels 2.0",modality:"electrophysiology",channels:384,commonIssues:"drift correction, spike sorting artifacts"}},
850+
{type:"FundedProject",icon:"🏛️",record:{identifier:"1R61MH138612",name:"SeeMe: Sensing Emotion in Mood Episodes",funding:"NIMH",species:"Human",leadInstitution:"Stony Brook University"}},
851+
{type:"Software",icon:"💻",record:{name:"bbqs-spike-sorter",programmingLanguage:"Python",license:"MIT",codeRepository:"https://github.com/brain-bbqs/spike-sorter"}},
852+
];
853+
let trDemoEntity=null;
854+
855+
function initDemoCards(){
856+
const wrap=document.getElementById("tr-demo-cards");
857+
if(!wrap)return;
858+
wrap.innerHTML=BBQS_ENTITIES.map((e,i)=>`
859+
<div onclick="trSelectEntity(${i})" id="tr-demo-card-${i}" style="cursor:pointer;border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px;background:var(--surface);transition:border-color .15s">
860+
<div style="font-size:20px;margin-bottom:6px">${e.icon}</div>
861+
<div style="font-size:11.5px;font-weight:600;color:var(--text)">${e.type}</div>
862+
<div style="font-size:10.5px;color:var(--muted);margin-top:3px">${Object.keys(e.record).length} fields</div>
863+
</div>`).join("");
864+
// populate target select
865+
const sel=document.getElementById("tr-demo-tgt");
866+
if(sel){sel.innerHTML="";allSources.filter(s=>s.label!=="bbqs").forEach(s=>{const o=document.createElement("option");o.value=s.label;o.textContent=s.label;sel.appendChild(o);});}
867+
}
868+
869+
function trSelectEntity(idx){
870+
BBQS_ENTITIES.forEach((_,i)=>{
871+
const card=document.getElementById("tr-demo-card-"+i);
872+
if(card)card.style.borderColor=i===idx?"var(--teal)":"var(--border)";
873+
});
874+
trDemoEntity=BBQS_ENTITIES[idx];
875+
document.getElementById("tr-demo-step2").style.display="block";
876+
document.getElementById("tr-demo-record").textContent=JSON.stringify(trDemoEntity.record,null,2);
877+
trRunDemo();
878+
}
879+
880+
function trRunDemo(){
881+
if(!trDemoEntity)return;
882+
const tgt=document.getElementById("tr-demo-tgt").value;
883+
const record=trDemoEntity.record;
884+
885+
// Mirror Python transform_record(): find BBQS classes, follow alignment edges
886+
// to target classes, match property names case-insensitively.
887+
const srcClasses=allClasses.filter(c=>(c.sources||[c.source]).includes("bbqs"));
888+
const tgtClassMap={};
889+
allClasses.filter(c=>(c.sources||[c.source]).includes(tgt)).forEach(c=>{tgtClassMap[c.uid]=c;});
890+
891+
const propMap={};
892+
for(const srcC of srcClasses){
893+
for(const aln of (srcC.alignments||[])){
894+
if(aln.distance>0.6)continue;
895+
const tgtC=tgtClassMap[aln.target_uid];
896+
if(!tgtC)continue;
897+
const tgtProps={};
898+
(tgtC.properties||[]).forEach(p=>{tgtProps[p.name.toLowerCase()]=p.name;});
899+
for(const srcP of (srcC.properties||[])){
900+
const sl=srcP.name.toLowerCase();
901+
if(tgtProps[sl])propMap[sl]={target:tgtProps[sl],via:`${srcC.name}${aln.target_name}`,distance:aln.distance};
902+
}
903+
}
904+
}
905+
906+
const mapped={},unmapped=[],details=[];
907+
Object.entries(record).forEach(([field,value])=>{
908+
const m=propMap[field.toLowerCase()];
909+
if(m){mapped[m.target]=value;details.push({src:field,tgt:m.target,value,via:m.via,distance:m.distance});}
910+
else unmapped.push(field);
911+
});
912+
913+
const hasAlignments=Object.keys(propMap).length>0;
914+
const mappedRows=details.map(d=>`
915+
<tr>
916+
<td style="padding:7px 10px;font-family:var(--mono);font-size:11px">${d.src}</td>
917+
<td style="padding:7px 10px;color:var(--teal)">→</td>
918+
<td style="padding:7px 10px;font-family:var(--mono);font-size:11px">${d.tgt}</td>
919+
<td style="padding:7px 10px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;color:var(--text-dim)">${String(d.value)}</td>
920+
<td style="padding:7px 10px;font-size:10px;color:var(--muted)">${d.via} <span style="color:var(--border-soft)">(d=${d.distance.toFixed(2)})</span></td>
921+
</tr>`).join("");
922+
923+
const unmappedHtml=unmapped.length?`
924+
<div style="margin-top:14px">
925+
<div style="font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-bottom:6px">Unmapped fields (no alignment to ${tgt})</div>
926+
<div style="display:flex;flex-wrap:wrap;gap:6px">
927+
${unmapped.map(f=>`<span style="background:var(--bg-elev);border:1px solid var(--border);border-radius:4px;padding:2px 8px;font-family:var(--mono);font-size:11px;color:var(--muted)">${f}</span>`).join("")}
928+
</div>
929+
</div>`:"";
930+
931+
const warningHtml=!hasAlignments?`<div style="color:var(--yellow,#fbbf24);font-size:12px;padding:10px 0">No alignment edges found between bbqs and ${tgt}. Run the pipeline to compute cross-schema edges.</div>`:"";
932+
933+
document.getElementById("tr-demo-result").style.display="block";
934+
document.getElementById("tr-demo-result-inner").innerHTML=warningHtml+(mappedRows?`
935+
<table style="width:100%;border-collapse:collapse;font-size:12px">
936+
<thead><tr style="border-bottom:1px solid var(--border)">
937+
<th style="padding:6px 10px;text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">bbqs field</th>
938+
<th style="padding:6px 10px"></th>
939+
<th style="padding:6px 10px;text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">${tgt} field</th>
940+
<th style="padding:6px 10px;text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">value</th>
941+
<th style="padding:6px 10px;text-align:left;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em">via alignment</th>
942+
</tr></thead>
943+
<tbody>${mappedRows}</tbody>
944+
</table>`:`<div style="color:var(--muted);font-size:12.5px;padding:8px 0">No property-level mappings found for ${trDemoEntity.type}${tgt}.</div>`)+unmappedHtml+
945+
`<div style="margin-top:14px;display:flex;gap:8px">
946+
<button class="btn" onclick="dlJSON(${JSON.stringify(mapped)},'bbqs-to-${tgt}.json')" style="font-size:11.5px;padding:6px 12px">Download mapped JSON</button>
947+
</div>`;
948+
}
814949
function initTransform(){
950+
// Demo tab is shown by default; hide the others
951+
document.getElementById("tr-tab-browser").style.display="none";
952+
document.getElementById("tr-tab-api").style.display="none";
815953
const src=document.getElementById("tr-src"),tgt=document.getElementById("tr-tgt");
816954
src.innerHTML="";tgt.innerHTML="";
817955
allSources.forEach((s,i)=>{

0 commit comments

Comments
 (0)