@@ -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>
808835function 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+ }
814949function 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