File size: 10,949 Bytes
4a90885
 
 
 
 
 
 
 
 
eff14fc
 
 
 
4a90885
 
 
eff14fc
4a90885
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eff14fc
 
 
 
 
 
 
 
 
 
4a90885
 
 
f21e0aa
4a90885
 
 
 
 
 
 
 
 
eff14fc
 
 
 
 
 
 
 
 
 
 
 
 
f21e0aa
4a90885
 
 
eff14fc
 
 
 
 
 
 
 
 
 
 
 
 
4a90885
 
 
 
eff14fc
fbc77a0
eff14fc
 
 
 
 
 
 
 
 
 
 
 
4a90885
 
 
 
eff14fc
 
 
 
 
 
 
 
 
f21e0aa
 
eff14fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f21e0aa
eff14fc
 
f21e0aa
eff14fc
 
 
 
 
 
 
 
 
 
 
 
f21e0aa
4a90885
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<!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>

            <!-- Scenario Selector -->
            <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>

            <!-- GPU Meter -->
            <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>

        <!-- Agent Progress Cards -->
        <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>

        <!-- Rehydrated output (clinical view) -->
        <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) {
                    // rehydrate token and display
                    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;
                    // update GPU gauge (to simulate high usage)
                    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>