rustvital-amd / src /ui.rs
brainworm2024's picture
Final live AMD GPU integration, audit fix
74f2b46
use chrono::SecondsFormat;
use crate::{models::TriageRecord, pipeline::orchestrator::TriageOutcome};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TriageViewModel {
pub record_id: String,
pub patient_id: String,
pub reason: String,
pub redacted_note: String,
pub pii_map: Vec<crate::shield::redact::PiiMatch>,
pub triage_result: String,
pub model_used: String,
pub device_used: String,
pub cid: String,
pub transaction_hash: String,
pub redaction_proof: crate::pipeline::proof::RedactionProof,
pub consortium_attestations: Vec<crate::models::ConsortiumAttestation>,
pub enrichment: crate::models::MedicalEnrichment,
pub degraded_reason: Option<String>,
pub created_at: String,
}
impl TriageViewModel {
pub fn from_outcome(outcome: TriageOutcome) -> Self {
let record = outcome.record;
Self {
record_id: record.record_id.as_str().to_string(),
patient_id: record.patient_id.as_str().to_string(),
reason: record.reason,
redacted_note: record.redacted_note,
pii_map: record.pii_map,
triage_result: record.triage_result,
model_used: record.model_used,
device_used: record.device_used,
cid: record.cid,
transaction_hash: record.transaction_hash,
redaction_proof: record.redaction_proof,
consortium_attestations: record.consortium_attestations,
enrichment: record.enrichment,
degraded_reason: record.degraded_reason,
created_at: record.created_at.to_rfc3339_opts(SecondsFormat::Secs, true),
}
}
}
pub fn render_dashboard_fragment(records: &[TriageRecord]) -> String {
if records.is_empty() {
return r#"<div class='rounded-2xl border border-dashed border-slate-300 p-6 text-slate-500'>No triage history yet. Run a case and it appears here instantly.</div>"#.to_string();
}
let rows = records.iter().rev().take(12).map(render_history_row).collect::<String>();
format!(r#"
<div class='overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm'>
<div class='border-b border-slate-200 px-5 py-4'>
<div class='text-xs uppercase tracking-[0.25em] text-slate-400'>Live Memory</div>
<h3 class='mt-1 text-lg font-semibold text-slate-900'>Recent triage decisions</h3>
</div>
<div class='divide-y divide-slate-100'>
{}
</div>
</div>
"#, rows)
}
fn render_history_row(record: &TriageRecord) -> String {
format!(r#"
<div class='grid gap-3 px-5 py-4 md:grid-cols-4'>
<div>
<div class='text-xs text-slate-400'>Record</div>
<div class='font-mono text-sm text-slate-900'>{}</div>
</div>
<div>
<div class='text-xs text-slate-400'>Patient</div>
<div class='text-sm font-medium text-slate-900'>{}</div>
</div>
<div>
<div class='text-xs text-slate-400'>Decision</div>
<div class='text-sm text-slate-700'>{}</div>
</div>
<div>
<div class='text-xs text-slate-400'>Audit</div>
<div class='text-sm text-slate-700'>CID {}</div>
</div>
</div>
"#, record.record_id.as_str(), escape_html(record.patient_id.as_str()), escape_html(&record.triage_result), escape_html(&record.cid))
}
pub fn render_triage_fragment(vm: &TriageViewModel) -> String {
let pii_rows = if vm.pii_map.is_empty() {
"<li class='text-slate-500'>No PHI patterns detected by the deterministic shield.</li>".to_string()
} else {
vm.pii_map.iter().map(|item| {
format!(r#"<li class='flex gap-2'><span class='text-rose-500'>●</span><span><strong>{}</strong> → <code class='rounded bg-slate-100 px-1.5 py-0.5'>{}</code></span></li>"#, escape_html(&item.original), escape_html(&item.placeholder))
}).collect::<String>()
};
let attestation_rows = vm.consortium_attestations.iter().map(|a| {
format!(r#"<div class='rounded-2xl border border-slate-200 bg-slate-50 p-3'><div class='font-medium text-slate-900'>{}</div><div class='text-xs text-slate-500'>{}</div><div class='mt-2 font-mono text-xs text-slate-600 break-all'>{}</div></div>"#, escape_html(&a.hospital), escape_html(&a.signature_status), escape_html(&a.attestation_hash))
}).collect::<String>();
let pubmed_rows = if vm.enrichment.pubmed_hits.is_empty() {
"<div class='text-sm text-slate-500'>No PubMed results matched this case.</div>".to_string()
} else {
vm.enrichment.pubmed_hits.iter().map(|hit| {
format!(r#"<a class='block rounded-2xl border border-slate-200 p-3 hover:bg-slate-50' href='{}' target='_blank' rel='noreferrer'><div class='text-xs text-slate-400'>PubMed {}</div><div class='text-sm font-medium text-slate-900'>{}</div></a>"#, escape_html(&hit.url), escape_html(&hit.pmid), escape_html(&hit.title))
}).collect::<String>()
};
let degraded = vm.degraded_reason.as_ref().map(|reason| format!(r#"<div class='rounded-2xl border border-amber-200 bg-amber-50 p-4 text-amber-900'><div class='font-semibold'>Backend degraded mode</div><div class='text-sm mt-1'>{}</div></div>"#, escape_html(reason))).unwrap_or_default();
format!(r#"
<div class='space-y-6 rounded-3xl border border-slate-200 bg-white p-6 shadow-xl'>
<div class='flex flex-wrap items-center justify-between gap-3'>
<div>
<div class='text-xs uppercase tracking-[0.25em] text-slate-400'>Triage Result</div>
<h3 class='mt-1 text-2xl font-semibold text-slate-900'>Case {}</h3>
</div>
<div class='rounded-full bg-emerald-50 px-3 py-1 text-sm font-medium text-emerald-700'>{}</div>
</div>
{}
<div class='grid gap-4 lg:grid-cols-2'>
<div class='rounded-3xl bg-slate-50 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>Clinical synthesis</div>
<p class='whitespace-pre-wrap text-sm leading-7 text-slate-700'>{}</p>
</div>
<div class='space-y-4 rounded-3xl bg-slate-50 p-4'>
<div>
<div class='mb-2 text-sm font-semibold text-slate-900'>Redacted note</div>
<pre class='whitespace-pre-wrap rounded-2xl bg-slate-950 p-4 text-xs leading-6 text-emerald-200'>{}</pre>
</div>
<div>
<div class='mb-2 text-sm font-semibold text-slate-900'>Redaction proof</div>
<div class='break-all rounded-2xl bg-slate-950 p-4 font-mono text-xs text-sky-200'>{}</div>
<div class='mt-2 text-xs text-slate-500'>Verified: {}</div>
</div>
</div>
</div>
<div class='grid gap-4 lg:grid-cols-2'>
<div class='rounded-3xl border border-slate-200 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>PII shield map</div>
<ul class='space-y-2 text-sm text-slate-700'>{}</ul>
</div>
<div class='rounded-3xl border border-slate-200 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>Consortium attestations</div>
<div class='grid gap-3'>{}</div>
</div>
</div>
<div class='grid gap-4 lg:grid-cols-2'>
<div class='rounded-3xl border border-slate-200 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>PubMed enrichment</div>
<div class='space-y-3'>{}</div>
</div>
<div class='rounded-3xl border border-slate-200 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>On-chain audit</div>
<div class='space-y-2 text-sm text-slate-700'>
<div><span class='text-slate-400'>CID:</span> <span class='font-mono break-all'>{}</span></div>
<div><span class='text-slate-400'>Tx:</span> <span class='font-mono break-all'>{}</span></div>
<div><span class='text-slate-400'>Model:</span> {} · {}</div>
<div><span class='text-slate-400'>Created:</span> {}</div>
</div>
</div>
</div>
</div>
"#, escape_html(&vm.record_id), escape_html(&vm.reason), degraded, escape_html(&vm.triage_result), escape_html(&vm.redacted_note), escape_html(&vm.redaction_proof.proof), vm.redaction_proof.verified, pii_rows, attestation_rows, pubmed_rows, escape_html(&vm.cid), escape_html(&vm.transaction_hash), escape_html(&vm.model_used), escape_html(&vm.device_used), escape_html(&vm.created_at))
}
pub fn render_dicom_fragment(result: &crate::pipeline::dicom::DicomRedactionResult) -> String {
let fields = if result.report.detected_fields.is_empty() {
"<li class='text-slate-500'>No structured DICOM fields were parsed in this demo file.</li>".to_string()
} else {
result.report.detected_fields.iter().map(|field| format!(r#"<li>{}</li>"#, escape_html(field))).collect::<String>()
};
format!(r#"
<div class='space-y-4 rounded-3xl border border-slate-200 bg-white p-5 shadow-lg'>
<div>
<div class='text-xs uppercase tracking-[0.25em] text-slate-400'>DICOM Shield</div>
<h3 class='mt-1 text-xl font-semibold text-slate-900'>{}</h3>
</div>
<div class='grid gap-4 lg:grid-cols-2'>
<div class='rounded-2xl bg-slate-50 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>Parsed fields</div>
<ul class='list-disc space-y-1 pl-5 text-sm text-slate-700'>{}</ul>
</div>
<div class='rounded-2xl bg-slate-50 p-4'>
<div class='mb-2 text-sm font-semibold text-slate-900'>Burned-in OCR preview</div>
<div class='whitespace-pre-wrap text-sm text-slate-700'>{}</div>
</div>
</div>
<div>
<div class='mb-2 text-sm font-semibold text-slate-900'>Redacted preview</div>
<pre class='whitespace-pre-wrap rounded-2xl bg-slate-950 p-4 text-xs text-emerald-200'>{}</pre>
</div>
</div>
"#, escape_html(&result.report.filename), fields, escape_html(&result.report.burned_in_text_preview), escape_html(&result.report.redacted_preview))
}
fn escape_html(input: &str) -> String {
html_escape::encode_text(input).to_string()
}