afip / static /index.html
josephrw's picture
Upload static/index.html with huggingface_hub
ed63b0f verified
Raw
History Blame Contribute Delete
25.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AFIP v3 — Acoustic Flatulence Intelligence Platform</title>
<style>
:root{--bg:#05080f;--panel:#0d1117;--border:#21262d;--text:#c9d1d9;--muted:#8b949e;--accent:#58a6ff;--accent2:#3fb950;--red:#f85149;--orange:#d29922;--mono:'SF Mono',ui-monospace,monospace;}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--mono);background:var(--bg);color:var(--text);line-height:1.6;overflow-x:hidden}
a{color:var(--accent);text-decoration:none}
@keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}
@keyframes scanline{0%{transform:translateY(-100%)}100%{transform:translateY(100%)}}
@keyframes glow{0%,100%{box-shadow:0 0 8px rgba(88,166,255,.15)}50%{box-shadow:0 0 20px rgba(88,166,255,.35)}}
@keyframes type{from{width:0}to{width:100%}}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
@keyframes spin{to{transform:rotate(360deg)}}
/* Header */
header{background:var(--panel);border-bottom:1px solid var(--border);padding:14px 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:200}
header h1{font-size:13px;font-weight:700;color:var(--accent2);letter-spacing:2px;text-transform:uppercase}
header .badge{font-size:10px;background:var(--border);padding:3px 10px;border-radius:4px;color:var(--muted);letter-spacing:.5px}
/* Hero Landing */
.hero{min-height:40vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:40px 20px;border-bottom:1px solid var(--border);position:relative;overflow:hidden}
.hero::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 50% 50%,rgba(88,166,255,.04) 0%,transparent 70%);pointer-events:none}
.hero h2{font-size:32px;font-weight:700;margin-bottom:12px;color:var(--text);letter-spacing:-1px}
.hero .typewriter{font-size:14px;color:var(--muted);max-width:600px;margin:0 auto 28px;overflow:hidden;white-space:nowrap;border-right:2px solid var(--accent2);animation:type 3s steps(60,end),blink .8s step-end infinite;animation-fill-mode:forwards}
.hero .stats{display:flex;gap:32px;justify-content:center;flex-wrap:wrap;margin-bottom:24px}
.hero .stat{text-align:center}
.hero .stat .num{font-size:28px;font-weight:700;color:var(--accent)}
.hero .stat .lbl{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.btn-primary{background:var(--accent);color:#000;border:none;padding:12px 28px;font-family:var(--mono);font-size:13px;font-weight:700;border-radius:6px;cursor:pointer;transition:all .2s}
.btn-primary:hover{background:#79b8ff;transform:translateY(-1px)}
.btn-secondary{background:transparent;color:var(--accent);border:1px solid var(--accent);padding:10px 20px;font-family:var(--mono);font-size:12px;border-radius:6px;cursor:pointer;transition:all .2s}
.btn-secondary:hover{background:rgba(88,166,255,.08)}
/* Layout */
.container{max-width:1300px;margin:0 auto;padding:24px;display:none;animation:fadeIn .5s ease}
.container.visible{display:grid;grid-template-columns:300px 1fr;gap:24px}
@media(max-width:900px){.container.visible{grid-template-columns:1fr}}
/* Sidebar */
.sidebar{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:20px;height:fit-content;position:sticky;top:70px}
.sidebar h3{font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:16px;letter-spacing:1.5px}
.sidebar .btn{width:100%;padding:10px 14px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-family:var(--mono);font-size:12px;cursor:pointer;border-radius:6px;margin-bottom:8px;transition:all .15s;text-align:left;display:flex;align-items:center;gap:8px}
.sidebar .btn:hover{border-color:var(--accent);color:var(--accent)}
.sidebar .btn.rec{background:var(--red);border-color:var(--red);color:#fff;animation:pulse 1.2s infinite}
.sidebar .status{margin-top:16px;padding-top:14px;border-top:1px solid var(--border);font-size:11px;color:var(--muted)}
.sidebar .status .row{display:flex;justify-content:space-between;margin-bottom:6px}
.sidebar .status .val{color:var(--text)}
.sidebar .status .val.live{color:var(--accent2)}
/* Main */
.main{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:24px}
.tabs{display:flex;gap:6px;margin-bottom:20px;border-bottom:1px solid var(--border);padding-bottom:12px}
.tab{padding:7px 16px;font-family:var(--mono);font-size:11px;background:transparent;border:1px solid transparent;color:var(--muted);cursor:pointer;border-radius:6px;text-transform:uppercase;letter-spacing:.5px;transition:all .15s}
.tab.active{background:var(--bg);border-color:var(--border);color:var(--text)}
.tab:hover{color:var(--text)}
.section{display:none}
.section.active{display:block;animation:fadeIn .4s ease}
/* Dropzone */
.dropzone{border:2px dashed var(--border);border-radius:12px;padding:56px 24px;text-align:center;cursor:pointer;transition:all .25s;margin-bottom:20px;position:relative}
.dropzone:hover{border-color:var(--accent);background:rgba(88,166,255,.02);animation:glow 2s infinite}
.dropzone.dragover{border-color:var(--accent2);background:rgba(63,185,80,.03)}
.dropzone .icon{font-size:36px;margin-bottom:12px;opacity:.6}
.dropzone h3{font-size:15px;margin-bottom:6px}
.dropzone p{font-size:11px;color:var(--muted)}
/* Loading */
.loading{display:none;text-align:center;padding:48px;color:var(--muted);font-size:12px}
.loading.active{display:flex;flex-direction:column;align-items:center;gap:12px}
.spinner{width:18px;height:18px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
/* Error */
.error{display:none;background:rgba(248,81,73,.08);border:1px solid var(--red);color:var(--red);padding:12px 16px;border-radius:8px;font-size:12px;margin-bottom:16px}
.error.active{display:block}
/* Results */
.result-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-bottom:20px}
.r-card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:18px;transition:all .2s}
.r-card:hover{border-color:var(--accent)}
.r-card h4{font-size:10px;text-transform:uppercase;color:var(--muted);margin-bottom:10px;letter-spacing:1.2px;display:flex;align-items:center;gap:8px}
.r-card h4::before{content:'';display:inline-block;width:5px;height:5px;background:var(--accent2);border-radius:50%}
.r-card .metric{font-size:28px;font-weight:700;color:var(--accent);margin-bottom:4px}
.r-card .label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px}
.r-card .value{font-size:11px;color:var(--text);word-break:break-all;font-family:var(--mono)}
.gauge{height:6px;background:var(--border);border-radius:3px;overflow:hidden;margin-top:10px}
.gauge-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:3px;transition:width 1s ease}
/* Canvas */
.canvas-wrap{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:16px}
.canvas-wrap h4{font-size:10px;text-transform:uppercase;color:var(--muted);margin-bottom:10px;letter-spacing:1.2px}
canvas{display:block;width:100%;height:150px;background:var(--bg)}
/* MIDI */
.midi-player{display:flex;gap:10px;align-items:center;margin:16px 0;flex-wrap:wrap}
/* Receipt */
.receipt{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-top:14px;font-size:11px}
.receipt .line{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border)}
.receipt .line:last-child{border-bottom:none}
.receipt .hash{color:var(--accent2);font-family:var(--mono);font-size:10px}
.receipt .lbl{color:var(--muted)}
/* Collapsible */
.collapsible{cursor:pointer;padding:10px 0;border-bottom:1px solid var(--border);font-size:12px;display:flex;justify-content:space-between;align-items:center;transition:color .15s}
.collapsible:hover{color:var(--accent)}
.collapsible::after{content:'+';color:var(--muted);font-size:14px;transition:transform .2s}
.collapsible.open::after{content:'\2212'}
.collapsible-content{display:none;padding:10px 0;font-size:11px;color:var(--muted);animation:fadeIn .3s ease}
.collapsible-content.open{display:block}
.model-row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);font-size:11px}
.model-row .role{color:var(--text);font-size:10px}
/* Tables */
.tbl{width:100%;font-size:11px;border-collapse:collapse}
.tbl th{text-align:left;padding:10px 8px;color:var(--muted);border-bottom:1px solid var(--border);font-weight:400;text-transform:uppercase;letter-spacing:.5px;font-size:10px}
.tbl td{padding:10px 8px;border-bottom:1px solid var(--border);color:var(--text)}
.tbl tr:hover{background:rgba(88,166,255,.03)}
/* Footer */
.footer{text-align:center;padding:30px;color:var(--muted);font-size:11px;border-top:1px solid var(--border);margin-top:40px}
</style>
</head>
<body>
<!-- Landing Hero -->
<div id="landing" class="hero">
<h2>Acoustic Flatulence Intelligence Platform</h2>
<div class="typewriter">36-model ensemble · YIN pitch detection · SMF MIDI generation · SHA-256 receipt ledger · Solana fingerprinting</div>
<div class="stats">
<div class="stat"><div class="num">36</div><div class="lbl">Models</div></div>
<div class="stat"><div class="num">YIN</div><div class="lbl">Pitch Engine</div></div>
<div class="stat"><div class="num">SMF</div><div class="lbl">MIDI Writer</div></div>
<div class="stat"><div class="num">SHA-256</div><div class="lbl">Receipt Chain</div></div>
</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
<button class="btn-primary" onclick="enterApp()">Enter Terminal</button>
<button class="btn-secondary" onclick="demoMock()">Run Mock Analysis</button>
</div>
</div>
<!-- Main App -->
<div id="app" class="container">
<aside class="sidebar">
<h3>Operations</h3>
<button class="btn" id="btnRecord" onclick="toggleRecord()">● Record Audio</button>
<button class="btn" id="btnUpload" onclick="document.getElementById('fileIn').click()">↑ Upload File</button>
<input type="file" id="fileIn" style="display:none" accept="audio/*" onchange="handleFile(this.files[0])">
<button class="btn" onclick="switchTab('registry')">⊞ Model Registry</button>
<button class="btn" onclick="switchTab('history')">◔ History</button>
<button class="btn" onclick="switchTab('leaderboard')">▲ Leaderboard</button>
<button class="btn" onclick="demoMock()">⚡ Mock Demo</button>
<div class="status">
<div class="row"><span>Status</span><span class="val live" id="connStatus">ONLINE</span></div>
<div class="row"><span>Latency</span><span class="val" id="latency">-- ms</span></div>
<div class="row"><span>Models</span><span class="val">36 loaded</span></div>
<div class="row"><span>DB</span><span class="val">afip.db</span></div>
</div>
</aside>
<main class="main">
<div class="tabs">
<button class="tab active" data-tab="analyze" onclick="switchTab('analyze')">Analyze</button>
<button class="tab" data-tab="registry" onclick="switchTab('registry')">Registry</button>
<button class="tab" data-tab="history" onclick="switchTab('history')">History</button>
<button class="tab" data-tab="leaderboard" onclick="switchTab('leaderboard')">Leaderboard</button>
</div>
<!-- Analyze Tab -->
<div id="tab-analyze" class="section active">
<div class="dropzone" id="dropzone" ondragover="event.preventDefault();this.classList.add('dragover')" ondragleave="this.classList.remove('dragover')" ondrop="handleDrop(event)">
<div class="icon">📡</div>
<h3>Drop audio file or click to browse</h3>
<p>WAV · WebM · OGG · MP3 &nbsp;|&nbsp; Max 10MB</p>
</div>
<div class="loading" id="loading"><div class="spinner"></div><div>Running 36-model ensemble + YIN pitch detection + on-chain notarization...</div></div>
<div class="error" id="error"></div>
<div id="results" style="display:none">
<div class="result-grid">
<div class="r-card"><h4>FartScore</h4><div class="metric" id="fartScore">0</div><div class="label">Risk-Adjusted Acoustic Rating</div><div class="gauge"><div class="gauge-fill" id="scoreGauge" style="width:0%"></div></div></div>
<div class="r-card"><h4>Fingerprint</h4><div class="value" id="fingerprint">--</div><div class="label">Solana-Compatible Base58 Hash</div></div>
<div class="r-card"><h4>Duration</h4><div class="metric" id="duration">0.00s</div><div class="label">Acoustic Event Length</div></div>
<div class="r-card"><h4>Notes</h4><div class="metric" id="noteCount">0</div><div class="label">MIDI Segments Extracted</div></div>
</div>
<div class="canvas-wrap"><h4>Waveform</h4><canvas id="waveCanvas" width="900" height="150"></canvas></div>
<div class="canvas-wrap"><h4>Piano Roll</h4><canvas id="pianoCanvas" width="900" height="150"></canvas></div>
<div class="midi-player">
<button class="btn-primary" onclick="playMIDI()">▶ Play MIDI</button>
<button class="btn-secondary" onclick="downloadMIDI()">↓ Download .MID</button>
<span style="font-size:11px;color:var(--muted)" id="midiInfo"></span>
</div>
<div class="r-card" style="margin-top:16px"><h4>Receipt Chain</h4>
<div class="receipt">
<div class="line"><span class="lbl">Previous</span><span class="hash" id="prevReceipt">genesis</span></div>
<div class="line"><span class="lbl">Current</span><span class="hash" id="currReceipt">--</span></div>
</div>
</div>
<div class="r-card" style="margin-top:16px"><h4>36-Model Ensemble Analysis</h4><div id="modelOutputs"></div></div>
<div class="r-card" style="margin-top:16px"><h4>LLM Creative Outputs</h4><div id="llmOutputs"></div></div>
</div>
</div>
<!-- Registry Tab -->
<div id="tab-registry" class="section">
<div class="r-card"><h4>ML Models (30)</h4><div id="mlList" style="max-height:400px;overflow-y:auto"></div></div>
<div class="r-card" style="margin-top:16px"><h4>LLM Models (6)</h4><div id="llmList" style="max-height:300px;overflow-y:auto"></div></div>
</div>
<!-- History Tab -->
<div id="tab-history" class="section">
<table class="tbl"><thead><tr><th>ID</th><th>Time</th><th>Score</th><th>Notes</th><th>Fingerprint</th><th>Receipt</th></tr></thead><tbody id="historyBody"></tbody></table>
</div>
<!-- Leaderboard Tab -->
<div id="tab-leaderboard" class="section">
<table class="tbl"><thead><tr><th>Rank</th><th>Fingerprint</th><th>FartScore</th><th>Notes</th><th>Time</th></tr></thead><tbody id="leaderboardBody"></tbody></table>
</div>
</main>
</div>
<div class="footer">AFIP v3.0.0 — Acoustic Flatulence Intelligence Platform · 36-Model Ensemble · On-Chain Receipts</div>
<script>
const $ = id => document.getElementById(id);
let audioCtx, mediaRecorder, recordedChunks = [], currentMidiUrl = null, currentNotes = [];
function enterApp() {
$('landing').style.display = 'none';
$('app').classList.add('visible');
}
function switchTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.querySelector(`[data-tab="${name}"]`)?.classList.add('active');
$(`tab-${name}`).classList.add('active');
if(name === 'history') loadHistory();
if(name === 'leaderboard') loadLeaderboard();
if(name === 'registry') loadRegistry();
}
function encodeWav(samples, sr) {
const n = samples.length;
const buf = new ArrayBuffer(44 + n * 2);
const v = new DataView(buf);
const ws = (o, s) => { for(let i=0;i<s.length;i++) v.setUint8(o+i, s.charCodeAt(i)); };
ws(0,'RIFF'); v.setUint32(4,36+n*2,true); ws(8,'WAVE');
ws(12,'fmt '); v.setUint32(16,16,true); v.setUint16(20,1,true); v.setUint16(22,1,true);
v.setUint32(24,sr,true); v.setUint32(28,sr*2,true); v.setUint16(32,2,true); v.setUint16(34,16,true);
ws(36,'data'); v.setUint32(40,n*2,true);
for(let i=0;i<n;i++){ let s=Math.max(-1,Math.min(1,samples[i])); v.setInt16(44+i*2,s<0?s*0x8000:s*0x7FFF,true); }
return new Blob([buf],{type:'audio/wav'});
}
function mixToMono(buf) {
const L=buf.getChannelData(0), R=buf.getChannelData(1), out=new Float32Array(L.length);
for(let i=0;i<L.length;i++) out[i]=(L[i]+R[i])/2;
return out;
}
async function toggleRecord() {
const btn = $('btnRecord');
if(btn.classList.contains('rec')) {
mediaRecorder.stop(); btn.classList.remove('rec'); btn.textContent = '● Record Audio'; return;
}
try {
const stream = await navigator.mediaDevices.getUserMedia({audio:true});
audioCtx = new (window.AudioContext||window.webkitAudioContext)();
recordedChunks = [];
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = e => { if(e.data.size) recordedChunks.push(e.data); };
mediaRecorder.onstop = async () => {
const blob = new Blob(recordedChunks,{type:'audio/webm'});
const ab = await blob.arrayBuffer();
const buf = await audioCtx.decodeAudioData(ab);
const mono = buf.numberOfChannels>1 ? mixToMono(buf) : buf.getChannelData(0);
const wav = encodeWav(mono, buf.sampleRate);
handleFile(new File([wav],'recording.wav',{type:'audio/wav'}));
stream.getTracks().forEach(t=>t.stop());
};
mediaRecorder.start();
btn.classList.add('rec'); btn.textContent = '■ Stop Recording';
} catch(e) { showError('Microphone denied: '+e.message); }
}
function handleDrop(e) {
e.preventDefault();
$('dropzone').classList.remove('dragover');
if(e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
}
$('dropzone').addEventListener('click',()=>$('fileIn').click());
function showError(msg) {
$('error').textContent = msg;
$('error').classList.add('active');
setTimeout(()=>$('error').classList.remove('active'), 6000);
}
async function handleFile(file) {
if(!file) return;
$('loading').classList.add('active');
$('results').style.display = 'none';
$('error').classList.remove('active');
const form = new FormData(); form.append('file', file);
const t0 = performance.now();
try {
const r = await fetch('/analyze',{method:'POST',body:form});
$('latency').textContent = Math.round(performance.now()-t0)+' ms';
if(!r.ok) throw new Error(await r.text());
renderResults(await r.json());
} catch(e) { showError(e.message); }
$('loading').classList.remove('active');
}
async function demoMock() {
enterApp();
$('loading').classList.add('active');
$('results').style.display = 'none';
$('error').classList.remove('active');
const t0 = performance.now();
try {
const r = await fetch('/mock');
$('latency').textContent = Math.round(performance.now()-t0)+' ms';
if(!r.ok) throw new Error(await r.text());
renderResults(await r.json());
} catch(e) { showError(e.message); }
$('loading').classList.remove('active');
}
function renderResults(d) {
$('results').style.display = 'block';
$('fartScore').textContent = d.fartscore;
$('scoreGauge').style.width = d.fartscore+'%';
$('fingerprint').textContent = d.fingerprint;
$('duration').textContent = d.duration_sec.toFixed(2)+'s';
$('noteCount').textContent = d.note_count;
$('currReceipt').textContent = d.receipt.substring(0,28)+'...';
$('prevReceipt').textContent = d.prev_receipt ? d.prev_receipt.substring(0,28)+'...' : 'genesis';
currentMidiUrl = d.midi_url;
currentNotes = d.notes || [];
$('midiInfo').textContent = currentMidiUrl ? currentMidiUrl.split('/').pop() : 'No MIDI';
drawWaveform(d.notes || []);
drawPianoRoll(d.notes || []);
// Model outputs
const mdiv = $('modelOutputs'); mdiv.innerHTML = '';
if(d.model_outputs) {
const byTask = {};
d.model_outputs.forEach(o => { (byTask[o.task]=byTask[o.task]||[]).push(o); });
Object.entries(byTask).forEach(([task,items]) => {
const hdr = document.createElement('div'); hdr.className='collapsible'; hdr.textContent=`${task} (${items.length})`;
const body = document.createElement('div'); body.className='collapsible-content';
body.innerHTML = items.map(o=>`<div class="model-row"><span class="role">${o.role}</span><span style="color:var(--muted)">${JSON.stringify(o.result).substring(0,90)}</span></div>`).join('');
hdr.addEventListener('click',()=>{ hdr.classList.toggle('open'); body.classList.toggle('open'); });
mdiv.appendChild(hdr); mdiv.appendChild(body);
});
}
// LLM outputs
const ldiv = $('llmOutputs'); ldiv.innerHTML = '';
if(d.llm_outputs) {
d.llm_outputs.forEach(o => {
const hdr = document.createElement('div'); hdr.className='collapsible'; hdr.textContent=o.role;
const body = document.createElement('div'); body.className='collapsible-content';
body.innerHTML = `<b style="color:var(--text)">Prompt:</b> ${o.prompt}<br><b style="color:var(--text)">Result:</b> ${o.result.generated_text||JSON.stringify(o.result)}`;
hdr.addEventListener('click',()=>{ hdr.classList.toggle('open'); body.classList.toggle('open'); });
ldiv.appendChild(hdr); ldiv.appendChild(body);
});
}
}
function drawWaveform(notes) {
const c=$('waveCanvas'), ctx=c.getContext('2d');
c.width=c.offsetWidth||900; c.height=150;
ctx.fillStyle='#05080f'; ctx.fillRect(0,0,c.width,c.height);
ctx.strokeStyle='rgba(88,166,255,.4)'; ctx.lineWidth=1; ctx.beginPath();
if(!notes.length) { for(let x=0;x<c.width;x++){ctx.moveTo(x,75);ctx.lineTo(x,75+Math.sin(x*.1)*30);} ctx.stroke(); return; }
const maxT=Math.max(...notes.map(n=>n.start+(n.duration||n.dur)),1);
for(let x=0;x<c.width;x++){
const t=(x/c.width)*maxT;
const active=notes.some(n=>n.start<=t && t<=n.start+(n.duration||n.dur));
const y=active? 30+Math.random()*90 : 75+Math.sin(x*.05)*10;
if(x===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
}
ctx.stroke();
}
function drawPianoRoll(notes) {
const c=$('pianoCanvas'), ctx=c.getContext('2d');
c.width=c.offsetWidth||900; c.height=150;
ctx.fillStyle='#05080f'; ctx.fillRect(0,0,c.width,c.height);
if(!notes.length) return;
const maxT=Math.max(...notes.map(n=>n.start+(n.duration||n.dur)),1);
const minN=Math.min(...notes.map(n=>n.midi||n.note),20);
const maxN=Math.max(...notes.map(n=>n.midi||n.note),100);
const nRange=maxN-minN+5;
notes.forEach(n=>{
const x=(n.start/maxT)*c.width;
const w=((n.duration||n.dur)/maxT)*c.width;
const y=c.height-((n.midi||n.note-minN+2)/nRange)*c.height;
const h=c.height/nRange;
ctx.fillStyle='#58a6ff'; ctx.globalAlpha=.75;
ctx.fillRect(x,y,Math.max(w,2),h);
ctx.globalAlpha=1;
});
}
function playMIDI() {
if(!currentNotes.length) return;
const ctx=new AudioContext();
currentNotes.forEach(n=>{
const o=ctx.createOscillator(), g=ctx.createGain();
o.connect(g); g.connect(ctx.destination);
o.type='sine';
o.frequency.value=440*Math.pow(2,((n.midi||n.note)-69)/12);
g.gain.value=(n.velocity||n.vel||70)/127*.3;
o.start(ctx.currentTime+n.start);
g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+n.start+(n.duration||n.dur));
o.stop(ctx.currentTime+n.start+(n.duration||n.dur));
});
}
function downloadMIDI() {
if(currentMidiUrl) window.location=currentMidiUrl;
}
async function loadRegistry() {
try {
const r=await fetch('/registry'); const d=await r.json();
$('mlList').innerHTML=d.ml_models.map(m=>`<div class="model-row"><span class="role">${m.role}</span><span style="color:var(--muted)">${m.id}</span></div>`).join('');
$('llmList').innerHTML=d.llm_models.map(m=>`<div class="model-row"><span class="role">${m.role}</span><span style="color:var(--muted)">${m.id}</span></div>`).join('');
} catch(e){ console.error(e); }
}
async function loadHistory() {
try {
const r=await fetch('/history?limit=20'); const d=await r.json();
$('historyBody').innerHTML=d.map(row=>`<tr><td>${row.id}</td><td>${row.ts?.slice(0,19)||''}</td><td style="color:var(--accent2);font-weight:700">${row.fartscore}</td><td>${row.note_count}</td><td style="font-size:10px">${row.fingerprint?.slice(0,24)}...</td><td style="font-size:10px">${row.receipt_hash?.slice(0,16)}...</td></tr>`).join('');
} catch(e){ console.error(e); }
}
async function loadLeaderboard() {
try {
const r=await fetch('/leaderboard'); const d=await r.json();
$('leaderboardBody').innerHTML=d.map((row,i)=>`<tr><td style="color:var(--accent);font-weight:700">#${i+1}</td><td style="font-size:10px">${row.fingerprint?.slice(0,28)}...</td><td style="color:var(--accent2);font-weight:700">${row.fartscore}</td><td>${row.note_count}</td><td>${row.ts?.slice(0,19)||''}</td></tr>`).join('');
} catch(e){ console.error(e); }
}
// Auto-enter if hash present
if(location.hash==='#app') enterApp();
</script>
</body>
</html>