| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>RustVital‑AMD | Zero‑Trust Medical AI</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <style> |
| .pii-highlight { background-color: #fee2e2; padding: 0 2px; border-radius: 3px; font-weight: bold; } |
| .agent-card { transition: all 0.3s ease; } |
| .agent-active { box-shadow: 0 0 12px rgba(139, 92, 246, 0.5); } |
| #gpu-gauge { transition: width 0.5s ease; } |
| #rehydrated-output { white-space: pre-wrap; } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen flex flex-col items-center p-4"> |
| <div class="max-w-4xl w-full"> |
| <div id="device-banner" class="bg-purple-100 text-purple-800 px-4 py-2 rounded-lg mb-2 text-sm text-center font-medium"></div> |
| <script> |
| fetch('/status') |
| .then(r=>r.json()) |
| .then(s=>{ |
| document.getElementById('device-banner').textContent = |
| `Running on ${s.device} – Model: ${s.model}`; |
| }); |
| </script> |
|
|
| <div class="bg-white rounded-2xl shadow-xl p-6 mb-6"> |
| <div class="flex items-center gap-3 mb-4"> |
| <span class="text-4xl">🏥</span> |
| <h1 class="text-2xl font-bold text-gray-800">RustVital‑AMD</h1> |
| </div> |
| <p class="text-gray-500 mb-4">Zero‑trust medical triage with on‑chain audit</p> |
|
|
| |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Scenario</label> |
| <select id="scenario-select" onchange="loadScenario()" class="w-full border rounded-lg p-2"> |
| <option value="custom">Custom Note</option> |
| <option value="cardiac">🚨 Cardiac Emergency (Dr. Chen’s Night Shift)</option> |
| <option value="pediatric">👶 Pediatric Trauma + DICOM</option> |
| </select> |
| </div> |
|
|
| <div class="flex flex-col md:flex-row gap-4"> |
| <div class="flex-1"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Original Note</label> |
| <textarea id="patient-note" rows="5" |
| class="w-full border border-gray-300 rounded-lg p-3 focus:ring-2 focus:ring-purple-500 focus:border-transparent" |
| placeholder="Enter patient note...">Patient John Smith, 45 yo, chest pain</textarea> |
| </div> |
| <div id="redacted-preview" class="flex-1 hidden"> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Redacted (PII removed)</label> |
| <div id="redacted-text" class="bg-gray-100 p-3 rounded-lg text-sm font-mono"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="mt-4 hidden" id="gpu-section"> |
| <div class="flex items-center justify-between text-sm"> |
| <span>GPU Utilization</span> |
| <span id="gpu-pct">0%</span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-3 mt-1"> |
| <div id="gpu-gauge" class="bg-purple-600 h-3 rounded-full" style="width: 0%"></div> |
| </div> |
| </div> |
|
|
| <button onclick="runTriage()" id="start-btn" |
| class="mt-4 w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold py-3 rounded-lg transition"> |
| Start Triage |
| </button> |
| </div> |
|
|
| |
| <div id="agent-progress" class="hidden mb-6 space-y-2"> |
| <div class="agent-card bg-white p-3 rounded-xl shadow" id="card-shield">🛡️ Shield <span class="text-sm text-gray-500" id="shield-status">Waiting...</span></div> |
| <div class="agent-card bg-white p-3 rounded-xl shadow" id="card-triage">🧠 Triage <span class="text-sm text-gray-500" id="triage-status">Waiting...</span></div> |
| <div class="agent-card bg-white p-3 rounded-xl shadow" id="card-audit">⛓️ Audit <span class="text-sm text-gray-500" id="audit-status">Waiting...</span></div> |
| </div> |
|
|
| |
| <div id="rehydrated-container" class="hidden bg-white rounded-2xl shadow-xl p-6 mb-6"> |
| <h3 class="font-semibold text-gray-700 mb-2">Clinician’s View (Rehydrated)</h3> |
| <div id="rehydrated-output" class="bg-green-50 p-3 rounded-lg text-lg"></div> |
| </div> |
|
|
| <div id="result" class="space-y-4"></div> |
| </div> |
|
|
| <script> |
| const scenarios = { |
| cardiac: "Patient John Morrison, 67 yo, MRN 847291A, SSN 123-45-6789. Presents with acute substernal chest pain radiating to left arm. History of HTN, DM2. ECG shows ST elevation in leads II, III, aVF. Troponin pending.", |
| pediatric: "Patient Jane Doe, 7yo female, MRN 293-B, Parents: Michael & Sarah Doe, Phone 555-123-4567. Fell from tree, complaining of left upper quadrant pain. CT shows grade III splenic laceration. Vitals: HR 120, BP 90/60." |
| }; |
| |
| let piiMap = []; |
| |
| function loadScenario() { |
| const sel = document.getElementById('scenario-select').value; |
| if (scenarios[sel]) { |
| document.getElementById('patient-note').value = scenarios[sel]; |
| } |
| } |
| |
| function highlightPlaceholders(text) { |
| return text.replace(/\[([A-Z_]+)_(\d+)\]/g, '<span class="pii-highlight">[$1_$2]</span>'); |
| } |
| |
| function rehydrateText(text) { |
| let result = text; |
| piiMap.forEach(p => { |
| const regex = new RegExp(p.placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); |
| result = result.replace(regex, p.original); |
| }); |
| return result; |
| } |
| |
| async function runTriage() { |
| const note = document.getElementById('patient-note').value; |
| document.getElementById('agent-progress').classList.remove('hidden'); |
| document.getElementById('gpu-section').classList.remove('hidden'); |
| document.getElementById('rehydrated-container').classList.remove('hidden'); |
| document.getElementById('start-btn').disabled = true; |
| document.getElementById('result').innerHTML = ''; |
| document.getElementById('rehydrated-output').innerText = ''; |
| piiMap = []; |
| |
| const eventSource = new EventSource(`/triage/stream?patient_note=${encodeURIComponent(note)}`); |
| |
| eventSource.onmessage = (event) => { |
| const data = JSON.parse(event.data); |
| if (data.agent === 'Shield' && data.status === 'completed') { |
| document.getElementById('card-shield').classList.add('agent-active'); |
| document.getElementById('shield-status').textContent = `${data.pii_map.length} PII entities redacted`; |
| piiMap = data.pii_map; |
| } else if (data.agent === 'Triage' && data.status === 'started') { |
| document.getElementById('gpu-gauge').style.width = data.gpu_util + '%'; |
| document.getElementById('gpu-pct').textContent = data.gpu_util + '%'; |
| } else if (data.agent === 'Triage' && data.token) { |
| |
| let displayToken = data.token; |
| piiMap.forEach(p => { |
| displayToken = displayToken.replace(new RegExp(p.placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), p.original); |
| }); |
| document.getElementById('rehydrated-output').innerText += displayToken; |
| |
| document.getElementById('gpu-gauge').style.width = '78%'; |
| document.getElementById('gpu-pct').textContent = '78%'; |
| } else if (data.agent === 'Triage' && data.status === 'completed') { |
| document.getElementById('card-triage').classList.add('agent-active'); |
| document.getElementById('triage-status').textContent = 'Inference complete'; |
| } else if (data.agent === 'Audit' && data.status === 'completed') { |
| document.getElementById('card-audit').classList.add('agent-active'); |
| document.getElementById('audit-status').textContent = 'On-chain tx confirmed'; |
| eventSource.close(); |
| fetchFinalResult(note); |
| } |
| }; |
| |
| eventSource.onerror = () => { |
| eventSource.close(); |
| fetchFinalResult(note); |
| }; |
| } |
| |
| async function fetchFinalResult(note) { |
| const resp = await fetch('/triage', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({patient_note: note, consent_hash: 'abc123'}) |
| }); |
| const data = await resp.json(); |
| document.getElementById('redacted-preview').classList.remove('hidden'); |
| document.getElementById('redacted-text').innerHTML = highlightPlaceholders(data.redacted_prompt); |
| document.getElementById('result').innerHTML = ` |
| <div class="bg-white rounded-2xl shadow-xl p-6 space-y-4"> |
| <div class="text-sm text-purple-700 bg-purple-50 px-3 py-1 rounded-full inline-block"> |
| ${data.device_info} · Model: Qwen2.5-${data.model_used} |
| </div> |
| <div><h3 class="font-semibold">Triage Result</h3> |
| <div class="bg-purple-50 p-3 rounded-lg text-lg font-medium">${data.triage_result}</div> |
| </div> |
| <div><h3 class="font-semibold">PII Redaction Map</h3> |
| <ul class="list-disc list-inside text-sm">${data.pii_map.map(p => `<li>🔴 <strong>${p.original}</strong> → <code>${p.placeholder}</code></li>`).join('')}</ul> |
| </div> |
| <div><h3 class="font-semibold">Redaction Proof (Schnorr Signature)</h3> |
| <code class="text-xs bg-gray-100 p-2 rounded block">${data.redaction_proof}</code> |
| </div> |
| <div><h3 class="font-semibold">On‑Chain Audit</h3> |
| <p>CID: <code>${data.cid}</code></p> |
| <p>Transaction: <a href="https://sepolia.basescan.org/tx/${data.transaction_hash}" target="_blank" class="text-purple-600 underline">${data.transaction_hash}</a></p> |
| </div> |
| </div> |
| `; |
| } |
| </script> |
| </body> |
| </html> |
|
|