Narodak / index.html
simikkk's picture
Update index.html
e1a08bf verified
Raw
History Blame Contribute Delete
58.2 kB
<!DOCTYPE html>
<html lang="cs" class="dark">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>TriážAI · Urgentní příjem</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"surface-variant": "#32353c",
"surface": "#10131a",
"outline": "#8c909f",
"on-error-container": "#ffdad6",
"tertiary": "#ffb786",
"on-surface-variant": "#c2c6d6",
"primary-container": "#4d8eff",
"surface-container": "#1d2027",
"on-secondary": "#293040",
"error-container": "#93000a",
"outline-variant": "#424754",
"on-error": "#690005",
"on-tertiary-container": "#461f00",
"secondary": "#c0c6db",
"on-primary": "#002e6a",
"surface-container-high": "#272a31",
"secondary-container": "#404758",
"on-tertiary": "#502400",
"on-secondary-fixed-variant": "#404758",
"tertiary-container": "#df7412",
"inverse-on-surface": "#2e3038",
"error": "#ffb4ab",
"inverse-surface": "#e1e2ec",
"on-surface": "#e1e2ec",
"background": "#10131a",
"surface-dim": "#10131a",
"on-background": "#e1e2ec",
"surface-container-highest": "#32353c",
"surface-container-lowest": "#0b0e15",
"on-primary-fixed": "#001a42",
"primary": "#adc6ff",
"on-secondary-container": "#aeb5c9",
"surface-container-low": "#191b23",
"surface-bright": "#363941",
},
borderRadius: {
DEFAULT: "0.125rem", lg: "0.25rem", xl: "0.5rem", full: "0.75rem"
},
fontFamily: {
mono: ["IBM Plex Mono", "monospace"],
sans: ["IBM Plex Sans", "sans-serif"],
}
}
}
}
</script>
<style>
body { background-color: #0a0e1a; color: #e1e2ec; font-family: "IBM Plex Sans", sans-serif; }
.live-dot { width:8px;height:8px;background:#ffb4ab;border-radius:50%;display:inline-block;animation:pulse-red 2s infinite; }
@keyframes pulse-red {
0%{transform:scale(.95);box-shadow:0 0 0 0 rgba(255,180,171,.7)}
70%{transform:scale(1);box-shadow:0 0 0 6px rgba(255,180,171,0)}
100%{transform:scale(.95);box-shadow:0 0 0 0 rgba(255,180,171,0)}
}
.scrollbar::-webkit-scrollbar{width:4px}
.scrollbar::-webkit-scrollbar-track{background:#10131a}
.scrollbar::-webkit-scrollbar-thumb{background:#424754;border-radius:10px}
.triage-badge { font-family: "IBM Plex Mono", monospace; font-size:10px; font-weight:700; letter-spacing:.05em; }
.mono { font-family: "IBM Plex Mono", monospace; }
.priority-p1 { border-left-color: #ff4444; }
.priority-p2 { border-left-color: #ff8c00; }
.priority-p3 { border-left-color: #f5c518; }
.priority-p4 { border-left-color: #4caf50; }
.priority-p5 { border-left-color: #2196f3; }
.badge-p1 { background:#7f1d1d;color:#fecaca; }
.badge-p2 { background:#7c2d12;color:#fed7aa; }
.badge-p3 { background:#713f12;color:#fef08a; }
.badge-p4 { background:#14532d;color:#bbf7d0; }
.badge-p5 { background:#1e3a5f;color:#bae6fd; }
.thinking-dots span { animation: blink 1.4s infinite; display:inline-block; }
.thinking-dots span:nth-child(2) { animation-delay:.2s; }
.thinking-dots span:nth-child(3) { animation-delay:.4s; }
@keyframes blink { 0%,80%,100%{opacity:0} 40%{opacity:1} }
.tab-active { color:#adc6ff; border-bottom:2px solid #adc6ff; font-weight:700; }
.tab-inactive { color:#8c909f; border-bottom:2px solid transparent; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance:none; }
input[type=number] { -moz-appearance:textfield; }
</style>
</head>
<body class="overflow-hidden">
<!-- ═══════════════════ HEADER ═══════════════════ -->
<header class="bg-surface-container border-b border-outline-variant flex justify-between items-center w-full px-4 h-14 fixed top-0 z-50">
<div class="flex items-center gap-3">
<span class="live-dot"></span>
<span class="mono text-xs font-bold tracking-widest text-on-surface uppercase">TriážAI · Urgentní příjem</span>
</div>
<div class="flex items-center gap-6">
<!-- API KEY INPUT -->
<div class="flex items-center gap-2">
<span class="mono text-[10px] text-outline uppercase">OpenRouter API Key</span>
<input id="api-key-input" type="password" placeholder="sk-or-v1-..."
class="bg-surface-dim border border-outline-variant rounded px-2 py-1 mono text-xs text-on-surface w-52 focus:border-primary outline-none"/>
</div>
<span class="mono text-xs text-outline tabular-nums" id="live-clock">00:00:00</span>
</div>
</header>
<!-- ═══════════════════ LAYOUT ═══════════════════ -->
<div class="flex h-screen pt-14">
<!-- Sidebar -->
<aside class="fixed left-0 top-14 bottom-0 z-40 bg-surface-container-low border-r border-outline-variant w-56 flex flex-col">
<div class="p-3 border-b border-outline-variant">
<span class="mono text-[10px] text-outline uppercase tracking-tighter">Navigace</span>
</div>
<nav class="flex-1 py-2">
<button onclick="setView('recepce')" id="nav-recepce"
class="w-full text-left px-4 py-3 flex items-center gap-3 bg-secondary-container text-on-secondary-container border-l-4 border-primary mono text-xs font-bold">
<span class="material-symbols-outlined text-sm">person_add</span>RECEPCE
</button>
<button onclick="setView('doktor')" id="nav-doktor"
class="w-full text-left px-4 py-3 flex items-center gap-3 text-on-surface-variant hover:bg-surface-variant mono text-xs transition-colors">
<span class="material-symbols-outlined text-sm">stethoscope</span>DOKTOR
</button>
</nav>
<!-- Stats -->
<div class="p-3 border-t border-outline-variant space-y-2">
<div class="flex justify-between items-center">
<span class="mono text-[10px] text-outline uppercase">Celkem čeká</span>
<span class="mono text-xs font-bold text-on-surface" id="stat-total">0</span>
</div>
<div class="flex justify-between items-center">
<span class="mono text-[10px] text-outline uppercase">Kritičtí (P1)</span>
<span class="mono text-xs font-bold text-red-400" id="stat-p1">0</span>
</div>
<div class="flex justify-between items-center">
<span class="mono text-[10px] text-outline uppercase">Urgentní (P2)</span>
<span class="mono text-xs font-bold text-orange-400" id="stat-p2">0</span>
</div>
</div>
<div class="p-3 border-t border-outline-variant">
<div class="flex items-center gap-2">
<div class="w-7 h-7 rounded-full bg-outline-variant flex items-center justify-center mono text-[10px] font-bold">RS</div>
<div>
<div class="mono text-[10px] text-on-surface">Reception_01</div>
<div class="mono text-[9px] text-green-400 uppercase">● Online</div>
</div>
</div>
</div>
</aside>
<!-- Main -->
<main class="flex-1 ml-56 overflow-hidden flex flex-col">
<!-- ═══ RECEPCE VIEW ═══ -->
<div id="view-recepce" class="flex-1 flex gap-3 p-3 overflow-hidden">
<!-- FORM PANEL -->
<div class="flex-1 overflow-y-auto scrollbar pr-1">
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<div class="flex items-center justify-between mb-4 border-b border-outline-variant pb-3">
<h2 class="mono text-xs text-primary uppercase tracking-widest flex items-center gap-2">
<span class="material-symbols-outlined text-sm">person_add</span>NOVÝ PACIENT
</h2>
<span class="mono text-[10px] text-outline" id="patient-uid">UID: generuje se...</span>
</div>
<!-- DATASET LOADER -->
<div class="bg-surface-dim border border-outline-variant rounded-lg p-3 mb-4 space-y-2">
<div class="flex items-center justify-between">
<span class="mono text-[10px] text-tertiary uppercase tracking-wider flex items-center gap-2">
<span class="material-symbols-outlined text-sm">dataset</span>NAČÍST ZE SANDBOX DAT
</span>
<span class="mono text-[9px] text-outline" id="dataset-status">načítám soubory...</span>
</div>
<div class="flex gap-2">
<select id="dataset-patient-select"
class="flex-1 bg-surface-container border border-outline-variant rounded px-2 py-2 mono text-xs text-on-surface outline-none">
<option value="">— vyber ID pacienta z fronty —</option>
</select>
<button onclick="loadPatientFromDataset()"
class="px-3 py-2 bg-surface-container-high border border-outline-variant rounded mono text-[10px] text-on-surface hover:bg-surface-variant transition-colors">
NAČÍST
</button>
</div>
<button onclick="startNewManualPatient()"
class="w-full py-2 bg-surface-container-high border border-dashed border-outline-variant rounded mono text-[9px] text-outline uppercase hover:text-on-surface hover:border-primary transition-colors">
+ Nový pacient (mimo data — vyplnit ručně)
</button>
<div class="flex gap-2">
<button onclick="lookupHistory()"
class="flex-1 py-2 bg-surface-container-high border border-outline-variant rounded mono text-[9px] text-outline uppercase hover:text-on-surface transition-colors">
📋 Dohledat anamnézu
</button>
<button onclick="lookupEpidemiology()"
class="flex-1 py-2 bg-surface-container-high border border-outline-variant rounded mono text-[9px] text-outline uppercase hover:text-on-surface transition-colors">
🦠 Dohledat epidemiologii
</button>
</div>
<div id="dataset-lookup-result" class="hidden mono text-[10px] text-tertiary bg-surface-container-high border border-outline-variant rounded p-2"></div>
<div id="dataset-burden" class="mono text-[9px] text-outline">Zátěž pacienta (počet otázek): <span id="burden-count">0</span></div>
</div>
<!-- FORM -->
<div class="space-y-4" id="patient-form">
<!-- Základní info -->
<div class="grid grid-cols-2 gap-3">
<div class="col-span-2">
<label class="mono text-[10px] text-outline mb-1 block uppercase">Celé jméno</label>
<input id="f-name" type="text" placeholder="Jan Novák"
class="w-full bg-surface-dim border border-outline-variant rounded px-3 py-2 mono text-sm text-on-surface focus:border-primary outline-none"/>
</div>
<div>
<label class="mono text-[10px] text-outline mb-1 block uppercase">Věk</label>
<input id="f-age" type="number" placeholder="45" min="0" max="120"
class="w-full bg-surface-dim border border-outline-variant rounded px-3 py-2 mono text-sm text-on-surface focus:border-primary outline-none"/>
</div>
<div>
<label class="mono text-[10px] text-outline mb-1 block uppercase">Pohlaví</label>
<select id="f-gender"
class="w-full bg-surface-dim border border-outline-variant rounded px-3 py-2 mono text-sm text-on-surface focus:border-primary outline-none appearance-none">
<option>Muž</option><option>Žena</option><option>Neuvedeno</option>
</select>
</div>
</div>
<!-- Symptomy -->
<div>
<label class="mono text-[10px] text-outline mb-1 block uppercase">Hlavní obtíže / Symptomy *</label>
<textarea id="f-symptoms" rows="3" placeholder="Popište hlavní důvod návštěvy..."
class="w-full bg-surface-dim border border-outline-variant rounded px-3 py-2 mono text-sm text-on-surface focus:border-primary outline-none resize-none"></textarea>
</div>
<!-- Vitální funkce -->
<div>
<h3 class="mono text-[10px] text-tertiary mb-2 uppercase tracking-wider flex items-center gap-2">
<span class="material-symbols-outlined text-sm">monitoring</span>VITÁLNÍ FUNKCE (volitelné)
</h3>
<div class="grid grid-cols-3 gap-2">
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<label class="mono text-[9px] text-outline block uppercase mb-1">TK Sys (mmHg)</label>
<input id="f-bp-sys" type="number" placeholder="120"
class="w-full bg-transparent border-none p-0 mono text-lg font-bold text-on-surface focus:ring-0 outline-none"/>
</div>
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<label class="mono text-[9px] text-outline block uppercase mb-1">TK Dia (mmHg)</label>
<input id="f-bp-dia" type="number" placeholder="80"
class="w-full bg-transparent border-none p-0 mono text-lg font-bold text-on-surface focus:ring-0 outline-none"/>
</div>
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<label class="mono text-[9px] text-outline block uppercase mb-1">Puls (/min)</label>
<input id="f-hr" type="number" placeholder="72"
class="w-full bg-transparent border-none p-0 mono text-lg font-bold text-on-surface focus:ring-0 outline-none"/>
</div>
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<label class="mono text-[9px] text-outline block uppercase mb-1">SpO₂ (%)</label>
<input id="f-spo2" type="number" placeholder="98"
class="w-full bg-transparent border-none p-0 mono text-lg font-bold text-on-surface focus:ring-0 outline-none"/>
</div>
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<label class="mono text-[9px] text-outline block uppercase mb-1">Teplota (°C)</label>
<input id="f-temp" type="number" placeholder="36.6" step="0.1"
class="w-full bg-transparent border-none p-0 mono text-lg font-bold text-on-surface focus:ring-0 outline-none"/>
</div>
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<label class="mono text-[9px] text-outline block uppercase mb-1">GCS (3–15)</label>
<input id="f-gcs" type="number" placeholder="15" min="3" max="15"
class="w-full bg-transparent border-none p-0 mono text-lg font-bold text-on-surface focus:ring-0 outline-none"/>
</div>
</div>
</div>
<!-- Anamnéza -->
<div>
<label class="mono text-[10px] text-outline mb-1 block uppercase">Relevantní anamnéza (volitelné)</label>
<textarea id="f-history" rows="2" placeholder="Chronická onemocnění, léky, alergie..."
class="w-full bg-surface-dim border border-outline-variant rounded px-3 py-2 mono text-sm text-on-surface focus:border-primary outline-none resize-none"></textarea>
</div>
<!-- AI Result Panel -->
<div id="ai-result" class="hidden">
<div class="border border-outline-variant rounded-lg overflow-hidden">
<div class="bg-surface-variant px-4 py-2 flex items-center justify-between">
<span class="mono text-[10px] text-outline uppercase tracking-wider flex items-center gap-2">
<span class="material-symbols-outlined text-sm">psychology</span>AI TRIÁŽ · VÝSLEDEK
</span>
<div class="flex items-center gap-2">
<span id="ai-model-tag" class="mono text-[9px] text-outline"></span>
<span id="ai-priority-badge" class="triage-badge px-2 py-0.5 rounded"></span>
</div>
</div>
<div class="p-4 space-y-3">
<div class="flex items-center gap-3">
<div>
<div class="mono text-[10px] text-outline uppercase mb-0.5">Doporučená priorita</div>
<div id="ai-priority-text" class="mono text-2xl font-bold"></div>
</div>
<div class="flex-1 border-l border-outline-variant pl-3">
<div class="mono text-[10px] text-outline uppercase mb-0.5">Čekací lhůta</div>
<div id="ai-timeframe" class="mono text-sm text-tertiary font-bold"></div>
</div>
</div>
<div>
<div class="mono text-[10px] text-outline uppercase mb-1">Zdůvodnění agenta</div>
<div id="ai-reasoning" class="text-sm text-on-surface-variant leading-relaxed"></div>
</div>
<div id="ai-flags-container" class="hidden">
<div class="mono text-[10px] text-outline uppercase mb-1">⚠ Varovné signály</div>
<div id="ai-flags" class="text-sm text-amber-400"></div>
</div>
<div class="border-t border-outline-variant pt-3">
<div class="mono text-[10px] text-amber-400 uppercase mb-2">Konečné rozhodnutí je na lékaři.</div>
<button onclick="confirmAndQueue()"
class="w-full py-3 bg-primary text-on-primary mono text-xs font-bold rounded-lg flex items-center justify-center gap-2 active:scale-[0.98] transition-transform">
<span class="material-symbols-outlined text-sm">check_circle</span>POTVRDIT A ZAŘADIT DO FRONTY
</button>
</div>
</div>
</div>
</div>
<!-- Loading state -->
<div id="ai-loading" class="hidden">
<div class="border border-outline-variant rounded-lg p-6 flex flex-col items-center gap-3">
<div class="thinking-dots mono text-primary text-2xl">
<span>·</span><span>·</span><span>·</span>
</div>
<div class="mono text-[10px] text-outline uppercase">AI agent analyzuje stav pacienta</div>
<div id="loading-step" class="mono text-xs text-on-surface-variant text-center"></div>
</div>
</div>
<!-- Error state -->
<div id="ai-error" class="hidden">
<div class="border border-error rounded-lg p-4 bg-error-container">
<div class="mono text-[10px] text-error uppercase mb-1">Chyba</div>
<div id="ai-error-text" class="text-sm text-on-error-container"></div>
</div>
</div>
<!-- Submit -->
<button onclick="submitToTriage()"
class="w-full py-4 bg-secondary-container text-on-secondary-container mono text-xs font-bold rounded-lg flex items-center justify-center gap-2 hover:bg-surface-variant transition-colors border border-outline-variant active:scale-[0.98]">
<span class="material-symbols-outlined text-sm">send_and_archive</span>ODESLAT K AI TRIÁŽI
</button>
</div>
</div>
</div>
<!-- QUEUE PANEL -->
<div class="w-72 flex flex-col gap-3">
<div class="bg-surface-container border border-outline-variant rounded-lg flex flex-col" style="max-height:calc(100vh - 4rem - 1.5rem)">
<div class="p-3 border-b border-outline-variant flex items-center justify-between flex-shrink-0">
<h2 class="mono text-[10px] text-on-surface uppercase tracking-widest flex items-center gap-2">
<span class="material-symbols-outlined text-sm">queue</span>FRONTA
</h2>
<span id="queue-count-badge" class="mono text-[9px] bg-primary text-on-primary px-2 py-0.5 rounded-full">0</span>
</div>
<div id="queue-list" class="flex-1 overflow-y-auto scrollbar p-2 space-y-2">
<div class="text-center py-8 text-outline mono text-[10px] uppercase">
Žádní pacienti ve frontě
</div>
</div>
<div class="p-2 border-t border-outline-variant flex-shrink-0">
<button onclick="setView('doktor')"
class="w-full py-2 bg-surface-container-high border border-outline-variant mono text-[10px] text-outline rounded hover:text-on-surface transition-colors uppercase">
Přejít na pohled doktora →
</button>
</div>
</div>
</div>
</div>
<!-- ═══ DOKTOR VIEW ═══ -->
<div id="view-doktor" class="hidden flex-1 overflow-hidden flex flex-col">
<div class="border-b border-outline-variant px-4 flex items-center gap-4 bg-surface-container flex-shrink-0">
<button onclick="setDoctorTab('queue')" id="dtab-queue"
class="tab-active py-3 px-2 mono text-[10px] uppercase tracking-wider flex items-center gap-1.5">
<span class="material-symbols-outlined text-sm">list_alt</span>Fronta
</button>
<button onclick="setDoctorTab('detail')" id="dtab-detail"
class="tab-inactive py-3 px-2 mono text-[10px] uppercase tracking-wider flex items-center gap-1.5">
<span class="material-symbols-outlined text-sm">person</span>Detail pacienta
</button>
</div>
<!-- Doctor Queue Tab -->
<div id="dtab-queue-view" class="flex-1 overflow-y-auto scrollbar p-4">
<div id="doctor-queue" class="space-y-2">
<div class="text-center py-12 text-outline mono text-[10px] uppercase">Fronta je prázdná</div>
</div>
</div>
<!-- Doctor Detail Tab -->
<div id="dtab-detail-view" class="hidden flex-1 overflow-y-auto scrollbar p-4">
<div id="detail-empty" class="text-center py-12 text-outline mono text-[10px] uppercase">
Vyberte pacienta z fronty
</div>
<div id="detail-content" class="hidden space-y-4">
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<div class="flex items-start justify-between mb-4">
<div>
<div id="d-name" class="mono text-xl font-bold text-on-surface uppercase"></div>
<div id="d-meta" class="mono text-xs text-outline mt-1"></div>
</div>
<div id="d-badge" class="triage-badge px-3 py-1 rounded-lg text-sm"></div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<div class="mono text-[10px] text-outline uppercase mb-1">Symptomy</div>
<div id="d-symptoms" class="text-sm text-on-surface-variant"></div>
</div>
<div>
<div class="mono text-[10px] text-outline uppercase mb-1">Anamnéza</div>
<div id="d-history" class="text-sm text-on-surface-variant"></div>
</div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<h3 class="mono text-[10px] text-outline uppercase mb-3 flex items-center gap-2">
<span class="material-symbols-outlined text-sm">monitoring</span>Vitální funkce
</h3>
<div id="d-vitals" class="grid grid-cols-3 gap-2"></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<h3 class="mono text-[10px] text-outline uppercase mb-3 flex items-center gap-2">
<span class="material-symbols-outlined text-sm">psychology</span>Analýza AI agenta
</h3>
<div id="d-reasoning" class="text-sm text-on-surface-variant leading-relaxed mb-3"></div>
<div id="d-flags-wrap" class="hidden mb-3">
<div class="mono text-[10px] text-amber-400 uppercase mb-1">⚠ Varovné signály</div>
<div id="d-flags" class="text-sm text-amber-400"></div>
</div>
<div class="mono text-[10px] text-red-400 uppercase p-2 border border-red-900 rounded bg-red-900/20">
⚠ Výstup AI je pouze doporučení. Rozhodnutí přísluší lékaři.
</div>
</div>
<div class="flex gap-3">
<button onclick="dischargePatient()"
class="flex-1 py-3 bg-surface-container-high border border-outline-variant mono text-xs font-bold text-outline rounded-lg hover:text-on-surface transition-colors">
ODEPSAT / OŠETŘEN
</button>
<button onclick="escalatePatient()"
class="flex-1 py-3 bg-error-container text-error mono text-xs font-bold rounded-lg hover:opacity-80 transition-opacity">
ESKALOVAT LÉKAŘI
</button>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- ═══════════════════ SCRIPTS ═══════════════════ -->
<script>
// ═══════════════════════════════════════
// STATE
// ═══════════════════════════════════════
let queue = [];
let currentAIResult = null;
let selectedPatientId = null;
// ═══════════════════════════════════════
// SANDBOX DATASET (patients_presentation.json / patients_anamnesis.json /
// epidemiology.csv / practice_scenario.json) — soubory musí ležet ve stejné
// složce jako index.html (např. v rootu HF Static Space).
// ═══════════════════════════════════════
let DATASET = {
presentations: {}, // id -> presentation object
anamnesis: {}, // id -> anamnesis object
epidemiology: [], // list of {datum, ARI_vyskyt_na_100k, uroven_vlny}
queueIds: [], // practice_scenario.json fronta
};
let currentDatasetPatientId = null; // ID aktuálně vybraného pacienta z datasetu
let patientBurden = 0; // počet "otázek" položených pacientovi (ask_patient ekvivalent)
function parseCSV(text) {
const lines = text.trim().split(/\r?\n/);
const headers = lines[0].split(',');
return lines.slice(1).map(line => {
const cells = line.split(',');
const row = {};
headers.forEach((h, i) => row[h.trim()] = cells[i]?.trim());
return row;
});
}
async function loadDataset() {
const statusEl = document.getElementById('dataset-status');
try {
const [presRes, anamRes, epiRes, scenarioRes] = await Promise.all([
fetch('patients_presentation.json'),
fetch('patients_anamnesis.json'),
fetch('epidemiology.csv'),
fetch('practice_scenario.json'),
]);
if (!presRes.ok || !anamRes.ok || !epiRes.ok || !scenarioRes.ok) {
throw new Error('Některý datový soubor nebyl nalezen (zkontrolujte, že jsou ve stejné složce jako index.html).');
}
const presList = await presRes.json();
DATASET.presentations = {};
presList.forEach(p => DATASET.presentations[p.id] = p);
DATASET.anamnesis = await anamRes.json();
DATASET.epidemiology = parseCSV(await epiRes.text());
const scenario = await scenarioRes.json();
DATASET.queueIds = scenario.fronta || [];
const select = document.getElementById('dataset-patient-select');
select.innerHTML = '<option value="">— vyber ID pacienta z fronty —</option>' +
DATASET.queueIds.map(id => {
const p = DATASET.presentations[id];
const label = p ? `${id} · ${p.hlavni_obtiz} (${p.vek} let)` : id;
return `<option value="${id}">${label}</option>`;
}).join('');
statusEl.textContent = `načteno: ${DATASET.queueIds.length} pacientů`;
statusEl.className = 'mono text-[9px] text-green-400';
} catch (err) {
statusEl.textContent = 'chyba: ' + err.message;
statusEl.className = 'mono text-[9px] text-red-400';
}
}
function loadPatientFromDataset() {
const id = document.getElementById('dataset-patient-select').value;
if (!id) return;
const p = DATASET.presentations[id];
if (!p) { showError('Pacient s tímto ID nebyl v presentation datech nalezen.'); return; }
currentDatasetPatientId = id;
hideDatasetLookupResult();
document.getElementById('f-name').value = `${id} (sandbox)`;
document.getElementById('f-age').value = p.vek ?? '';
document.getElementById('f-gender').value = p.pohlavi === 'Ž' ? 'Žena' : (p.pohlavi === 'M' ? 'Muž' : 'Neuvedeno');
document.getElementById('f-symptoms').value = `${p.hlavni_obtiz}${(p.symptomy || []).join(', ')}`;
const vit = p.vitaly || {};
if (vit.TK) {
const [sys, dia] = String(vit.TK).split('/');
document.getElementById('f-bp-sys').value = sys || '';
document.getElementById('f-bp-dia').value = dia || '';
}
document.getElementById('f-hr').value = vit.TF ?? '';
document.getElementById('f-spo2').value = vit.SpO2 ?? '';
document.getElementById('f-temp').value = vit.teplota ?? '';
document.getElementById('f-gcs').value = '';
// Anamnéza se NEnačítá automaticky — musí se aktivně "dohledat" (viz README: zátěž pacienta)
document.getElementById('f-history').value = '';
showNotification(`Načten pacient ${id} z presentation dat (vidí každý). Anamnézu dohledejte tlačítkem.`);
}
function hideDatasetLookupResult() {
document.getElementById('dataset-lookup-result').classList.add('hidden');
}
function showDatasetLookupResult(html) {
const el = document.getElementById('dataset-lookup-result');
el.innerHTML = html;
el.classList.remove('hidden');
}
// get_history(id) — NEzatěžuje pacienta, jen dohledání v záznamech
function lookupHistory() {
if (!currentDatasetPatientId) { showError('Nejdřív načtěte pacienta z datasetu.'); return; }
const anam = DATASET.anamnesis[currentDatasetPatientId];
if (!anam) {
showDatasetLookupResult('Anamnéza nenalezena v dostupných záznamech.');
return;
}
const factors = (anam.rizikove_faktory || []).join(', ') || 'žádné uvedené';
document.getElementById('f-history').value =
`Rizikové faktory: ${factors}. Předchozí návštěvy: ${anam.predchozi_navstevy ?? 0}.`;
showDatasetLookupResult(
`<b>get_history(${currentDatasetPatientId})</b> — rizikové faktory: ${factors} · ` +
`předchozích návštěv: ${anam.predchozi_navstevy ?? 0} (nezatěžuje pacienta)`
);
}
// get_epidemiology(datum) — NEzatěžuje pacienta
function lookupEpidemiology() {
if (!currentDatasetPatientId) { showError('Nejdřív načtěte pacienta z datasetu.'); return; }
const p = DATASET.presentations[currentDatasetPatientId];
const datum = p?.datum;
const row = DATASET.epidemiology.find(r => r.datum === datum) ||
{ datum, ARI_vyskyt_na_100k: '150', uroven_vlny: 'nízká' };
showDatasetLookupResult(
`<b>get_epidemiology(${datum || '?'})</b> — výskyt ARI: ${row.ARI_vyskyt_na_100k}/100k · ` +
`úroveň vlny: <b>${row.uroven_vlny}</b> (nezatěžuje pacienta)`
);
// uložíme do skrytého stavu pro prompt
currentEpidemiologyContext = row;
}
// ask_patient ekvivalent — pokud chcete simulovat zátěž pacienta při manuálním zadávání
function bumpPatientBurden() {
patientBurden++;
document.getElementById('burden-count').textContent = patientBurden;
}
let currentEpidemiologyContext = null;
loadDataset();
const PRIORITY_META = {
P1: { label: "P1 · OKAMŽITĚ", timeframe: "Okamžitá péče (< 1 min)", color: "badge-p1", borderClass: "priority-p1", textColor: "text-red-400" },
P2: { label: "P2 · URGENTNÍ", timeframe: "Do 15 minut", color: "badge-p2", borderClass: "priority-p2", textColor: "text-orange-400" },
P3: { label: "P3 · NALÉHAVÉ", timeframe: "Do 30 minut", color: "badge-p3", borderClass: "priority-p3", textColor: "text-yellow-400" },
P4: { label: "P4 · MÉNĚ NALÉHAVÉ", timeframe: "Do 60–120 minut", color: "badge-p4", borderClass: "priority-p4", textColor: "text-green-400" },
P5: { label: "P5 · NEURGENTNÍ", timeframe: "Může čekat (> 2 hod)", color: "badge-p5", borderClass: "priority-p5", textColor: "text-blue-400" },
};
// ═══════════════════════════════════════
// CLOCK & UID
// ═══════════════════════════════════════
function updateClock() {
const now = new Date();
document.getElementById('live-clock').textContent =
[now.getHours(), now.getMinutes(), now.getSeconds()]
.map(n => String(n).padStart(2, '0')).join(':');
}
setInterval(updateClock, 1000);
updateClock();
function generateUID() {
const now = new Date();
const datePart = now.toISOString().slice(0,10).replace(/-/g,'');
const rand = String(Math.floor(Math.random() * 9000) + 1000);
return `${datePart}-${rand}`;
}
document.getElementById('patient-uid').textContent = 'UID: ' + generateUID();
// ═══════════════════════════════════════
// VIEW SWITCHING
// ═══════════════════════════════════════
function setView(view) {
document.getElementById('view-recepce').classList.toggle('hidden', view !== 'recepce');
document.getElementById('view-recepce').classList.toggle('flex', view === 'recepce');
document.getElementById('view-doktor').classList.toggle('hidden', view !== 'doktor');
document.getElementById('view-doktor').classList.toggle('flex', view === 'doktor');
const navRec = document.getElementById('nav-recepce');
const navDoc = document.getElementById('nav-doktor');
if (view === 'recepce') {
navRec.className = 'w-full text-left px-4 py-3 flex items-center gap-3 bg-secondary-container text-on-secondary-container border-l-4 border-primary mono text-xs font-bold';
navDoc.className = 'w-full text-left px-4 py-3 flex items-center gap-3 text-on-surface-variant hover:bg-surface-variant mono text-xs transition-colors';
} else {
navDoc.className = 'w-full text-left px-4 py-3 flex items-center gap-3 bg-secondary-container text-on-secondary-container border-l-4 border-primary mono text-xs font-bold';
navRec.className = 'w-full text-left px-4 py-3 flex items-center gap-3 text-on-surface-variant hover:bg-surface-variant mono text-xs transition-colors';
renderDoctorQueue();
}
}
function setDoctorTab(tab) {
const queueView = document.getElementById('dtab-queue-view');
const detailView = document.getElementById('dtab-detail-view');
const queueTab = document.getElementById('dtab-queue');
const detailTab = document.getElementById('dtab-detail');
if (tab === 'queue') {
queueView.classList.remove('hidden'); detailView.classList.add('hidden');
queueTab.className = 'tab-active py-3 px-2 mono text-[10px] uppercase tracking-wider flex items-center gap-1.5';
detailTab.className = 'tab-inactive py-3 px-2 mono text-[10px] uppercase tracking-wider flex items-center gap-1.5';
renderDoctorQueue();
} else {
detailView.classList.remove('hidden'); queueView.classList.add('hidden');
detailTab.className = 'tab-active py-3 px-2 mono text-[10px] uppercase tracking-wider flex items-center gap-1.5';
queueTab.className = 'tab-inactive py-3 px-2 mono text-[10px] uppercase tracking-wider flex items-center gap-1.5';
}
}
// ═══════════════════════════════════════
// AI TRIAGE AGENT (OpenRouter)
// ═══════════════════════════════════════
// ═══════════════════════════════════════
// OPENROUTER — POUZE FREE MODELY.
// Seznam se natahuje dynamicky z /api/v1/models a filtruje na pricing.prompt==0
// && pricing.completion==0, takže appka nikdy nepoužije placený model, ani
// kdyby se OpenRouter nabídka modelů změnila.
// ═══════════════════════════════════════
const FALLBACK_FREE_MODELS = [
// ručně daný záložní seznam pro případ, že by /models endpoint nebyl dostupný
'meta-llama/llama-3.3-70b-instruct:free',
'deepseek/deepseek-chat-v3-0324:free',
'google/gemini-2.0-flash-exp:free',
'mistralai/mistral-small-3.1-24b-instruct:free',
'meta-llama/llama-3.1-8b-instruct:free',
'qwen/qwen-2.5-72b-instruct:free',
];
let _freeModelsCache = null;
async function getFreeModels(apiKey) {
if (_freeModelsCache) return _freeModelsCache;
try {
const res = await fetch('https://openrouter.ai/api/v1/models', {
headers: { 'Authorization': `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error('models endpoint ' + res.status);
const json = await res.json();
const free = (json.data || [])
.filter(m => {
const p = m.pricing || {};
return parseFloat(p.prompt || '0') === 0 && parseFloat(p.completion || '0') === 0;
})
.map(m => m.id)
// preferuj větší/known modely napřed, ale je to jedno, jsou všechny free
.sort((a, b) => (a.includes(':free') ? 0 : 1) - (b.includes(':free') ? 0 : 1));
if (free.length === 0) throw new Error('žádné free modely nevráceny');
_freeModelsCache = free;
return free;
} catch (e) {
// fallback na ruční seznam, pokud /models nejde dotázat
_freeModelsCache = FALLBACK_FREE_MODELS;
return FALLBACK_FREE_MODELS;
}
}
async function callOpenRouterWithFallback(prompt, apiKey) {
const models = await getFreeModels(apiKey);
let lastError = null;
for (const model of models) {
try {
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': window.location.href,
'X-Title': 'TriazAI MVP',
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }],
temperature: 0.1,
max_tokens: 1500,
}),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
lastError = new Error(err?.error?.message || `HTTP ${response.status} (model: ${model})`);
continue; // zkus další free model ve fallback řadě
}
const data = await response.json();
if (!data.choices || !data.choices.length) {
lastError = new Error(`Prázdná odpověď od modelu ${model}`);
continue;
}
return { data, usedModel: model };
} catch (err) {
lastError = err;
// síťová chyba u tohoto modelu, zkus další
}
}
throw lastError || new Error('Žádný free model nebyl dostupný.');
}
async function submitToTriage() {
const apiKey = document.getElementById('api-key-input').value.trim();
if (!apiKey) {
showError('Zadejte prosím OpenRouter API klíč v pravém horním rohu.');
return;
}
const symptoms = document.getElementById('f-symptoms').value.trim();
if (!symptoms) {
showError('Vyplňte prosím alespoň symptomy pacienta.');
return;
}
// Build patient data
const patient = {
name: document.getElementById('f-name').value.trim() || 'Neznámý',
age: document.getElementById('f-age').value || '?',
gender: document.getElementById('f-gender').value,
symptoms,
bpSys: document.getElementById('f-bp-sys').value,
bpDia: document.getElementById('f-bp-dia').value,
hr: document.getElementById('f-hr').value,
spo2: document.getElementById('f-spo2').value,
temp: document.getElementById('f-temp').value,
gcs: document.getElementById('f-gcs').value,
history: document.getElementById('f-history').value.trim(),
};
// Show loading
document.getElementById('ai-result').classList.add('hidden');
document.getElementById('ai-error').classList.add('hidden');
document.getElementById('ai-loading').classList.remove('hidden');
const loadingSteps = [
'Čtu vitální funkce...',
'Porovnávám se standardem ČSAIM...',
'Vyhodnocuji rizikové faktory...',
'Přiřazuji prioritu podle MTS/CTAS...',
];
let stepIdx = 0;
const stepInterval = setInterval(() => {
document.getElementById('loading-step').textContent = loadingSteps[stepIdx % loadingSteps.length];
stepIdx++;
}, 800);
try {
const prompt = buildTriagePrompt(patient);
const { data, usedModel } = await callOpenRouterWithFallback(prompt, apiKey);
const text = data.choices?.[0]?.message?.content || '';
const result = parseAIResponse(text, patient);
currentAIResult = { ...result, patient, uid: generateUID(), arrivalTime: new Date(), aiModel: usedModel };
// Show result
clearInterval(stepInterval);
document.getElementById('ai-loading').classList.add('hidden');
renderAIResult(currentAIResult);
} catch (err) {
clearInterval(stepInterval);
document.getElementById('ai-loading').classList.add('hidden');
showError(`Chyba při volání AI: ${err.message}`);
}
}
function buildTriagePrompt(p) {
const vitals = [];
if (p.bpSys && p.bpDia) vitals.push(`TK: ${p.bpSys}/${p.bpDia} mmHg`);
if (p.hr) vitals.push(`Puls: ${p.hr}/min`);
if (p.spo2) vitals.push(`SpO₂: ${p.spo2}%`);
if (p.temp) vitals.push(`Teplota: ${p.temp}°C`);
if (p.gcs) vitals.push(`GCS: ${p.gcs}/15`);
const epiLine = currentEpidemiologyContext
? `\n- Epidemiologický kontext: výskyt ARI ${currentEpidemiologyContext.ARI_vyskyt_na_100k}/100k, úroveň vlny: ${currentEpidemiologyContext.uroven_vlny}`
: '';
return `Jsi triážní AI agent na urgentním příjmu české nemocnice. Používáš Manchester Triage System (MTS) a platné české standardy triáže.
PACIENT:
- Jméno: ${p.name}
- Věk: ${p.age} let, Pohlaví: ${p.gender}
- Symptomy: ${p.symptoms}
- Vitální funkce: ${vitals.length ? vitals.join(', ') : 'nezměřeny'}
- Anamnéza: ${p.history || 'neuvedena'}${epiLine}
Proveď triáž a vrať POUZE JSON v tomto přesném formátu (bez markdown backticks):
{
"priority": "P1" nebo "P2" nebo "P3" nebo "P4" nebo "P5",
"mts_category": "název kategorie MTS česky",
"reasoning": "STRUČNĚ 1-2 věty (max. 250 znaků) vysvětlující, co konkrétně rozhodlo o prioritě",
"red_flags": ["varovný signál 1", "varovný signál 2"] nebo [],
"undertriage_risk": "nízké" nebo "střední" nebo "vysoké",
"recommended_action": "konkrétní doporučení pro příjmovou sestru/recepci"
}
Definice priorit:
P1 = okamžitá hrozba života (< 1 min)
P2 = urgentní, nestabilní stav (< 15 min)
P3 = naléhavé, může se zhoršit (< 30 min)
P4 = méně naléhavé, stabilní (< 120 min)
P5 = neurgentní, může čekat (> 2 hod)
DŮLEŽITÉ: Věnuj zvláštní pozornost atypickým příznakům infarktu (zejména u žen a diabetiků). Nízká SpO₂ (<94%), GCS<15, TK>180/110 nebo <90/60 jsou vždy minimálně P2.`;
}
function extractField(text, field) {
// Vytáhne hodnotu "field": "..." i z neuzavřeného/oseknutého JSON.
const re = new RegExp(`"${field}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`, 'i');
const m = text.match(re);
if (!m) return null;
return m[1].replace(/\\"/g, '"').replace(/\\n/g, ' ').trim();
}
function extractArrayField(text, field) {
const re = new RegExp(`"${field}"\\s*:\\s*\\[([^\\]]*)\\]`, 'i');
const m = text.match(re);
if (!m) return [];
return m[1]
.split(',')
.map(s => s.trim().replace(/^"|"$/g, ''))
.filter(Boolean);
}
function parseAIResponse(text, patient) {
// Strip markdown if present
const clean = text.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
try {
const json = JSON.parse(clean);
return {
priority: json.priority || 'P3',
mts_category: json.mts_category || '',
reasoning: json.reasoning || 'Automatická triáž.',
red_flags: Array.isArray(json.red_flags) ? json.red_flags : [],
undertriage_risk: json.undertriage_risk || 'nízké',
recommended_action: json.recommended_action || '',
};
} catch (e) {
// Model nejčastěji odpověď utne uprostřed JSONu (limit tokenů) nebo přidá
// text okolo. Místo zobrazení syrového odseknutého textu zkusíme pole
// vytáhnout regexem jedno po druhém — to funguje i na neuzavřený JSON.
const reasoning = extractField(clean, 'reasoning');
const mtsCategory = extractField(clean, 'mts_category');
const undertriage = extractField(clean, 'undertriage_risk');
const recommendedAction = extractField(clean, 'recommended_action');
const redFlags = extractArrayField(clean, 'red_flags');
const priorityMatch = clean.match(/"priority"\s*:\s*"(P[1-5])"/i) || clean.match(/\bP[1-5]\b/);
return {
priority: priorityMatch ? priorityMatch[1] || priorityMatch[0] : 'P3',
mts_category: mtsCategory || '',
reasoning: reasoning
? reasoning + (reasoning.length > 0 && !/[.!?]$/.test(reasoning) ? '…' : '')
: 'Odpověď AI nebylo možné plně zpracovat (pravděpodobně odseknuta na limitu délky) — ověřte ručně.',
red_flags: redFlags,
undertriage_risk: undertriage || 'střední',
recommended_action: recommendedAction || 'Ověřte ručně.',
};
}
}
function renderAIResult(result) {
const meta = PRIORITY_META[result.priority] || PRIORITY_META['P3'];
document.getElementById('ai-priority-badge').textContent = meta.label;
document.getElementById('ai-priority-badge').className = `triage-badge px-2 py-0.5 rounded ${meta.color}`;
document.getElementById('ai-priority-text').textContent = meta.label;
document.getElementById('ai-priority-text').className = `mono text-2xl font-bold ${meta.textColor}`;
document.getElementById('ai-timeframe').textContent = meta.timeframe;
document.getElementById('ai-reasoning').textContent = result.reasoning;
if (result.mts_category) {
document.getElementById('ai-reasoning').textContent =
`[${result.mts_category}] ${result.reasoning}`;
}
if (result.aiModel) {
document.getElementById('ai-model-tag').textContent = result.aiModel.replace(':free', '');
}
if (result.red_flags && result.red_flags.length > 0) {
document.getElementById('ai-flags-container').classList.remove('hidden');
document.getElementById('ai-flags').textContent = result.red_flags.join(' · ');
} else {
document.getElementById('ai-flags-container').classList.add('hidden');
}
if (result.undertriage_risk === 'vysoké') {
document.getElementById('ai-flags-container').classList.remove('hidden');
const existing = document.getElementById('ai-flags').textContent;
document.getElementById('ai-flags').textContent = `⚡ Vysoké riziko podhodnocení! ${existing}`;
}
document.getElementById('ai-result').classList.remove('hidden');
}
function showError(msg) {
document.getElementById('ai-error-text').textContent = msg;
document.getElementById('ai-error').classList.remove('hidden');
}
// ═══════════════════════════════════════
// QUEUE MANAGEMENT
// ═══════════════════════════════════════
function confirmAndQueue() {
if (!currentAIResult) return;
const patient = {
...currentAIResult,
id: Date.now(),
status: 'waiting', // waiting | in_progress | done
};
queue.push(patient);
// Sort: P1 first, P5 last, then by arrivalTime
queue.sort((a, b) => {
const pA = parseInt(a.priority[1]);
const pB = parseInt(b.priority[1]);
if (pA !== pB) return pA - pB;
return a.arrivalTime - b.arrivalTime;
});
updateStats();
renderSidebarQueue();
resetForm();
currentAIResult = null;
// Flash notification
showNotification(`${patient.patient.name} zařazen jako ${patient.priority}`);
}
function startNewManualPatient() {
resetForm();
document.getElementById('f-name').focus();
showNotification('Nový pacient mimo dataset — vyplňte formulář ručně.');
}
function resetForm() {
['f-name','f-age','f-symptoms','f-history','f-bp-sys','f-bp-dia','f-hr','f-spo2','f-temp','f-gcs'].forEach(id => {
document.getElementById(id).value = '';
});
document.getElementById('ai-result').classList.add('hidden');
document.getElementById('ai-error').classList.add('hidden');
document.getElementById('patient-uid').textContent = 'UID: ' + generateUID();
document.getElementById('dataset-patient-select').value = '';
currentDatasetPatientId = null;
currentEpidemiologyContext = null;
hideDatasetLookupResult();
}
function updateStats() {
const waiting = queue.filter(p => p.status !== 'done');
document.getElementById('stat-total').textContent = waiting.length;
document.getElementById('stat-p1').textContent = waiting.filter(p => p.priority === 'P1').length;
document.getElementById('stat-p2').textContent = waiting.filter(p => p.priority === 'P2').length;
document.getElementById('queue-count-badge').textContent = waiting.length;
}
function renderSidebarQueue() {
const container = document.getElementById('queue-list');
const waiting = queue.filter(p => p.status !== 'done');
if (waiting.length === 0) {
container.innerHTML = '<div class="text-center py-8 text-outline mono text-[10px] uppercase">Žádní pacienti ve frontě</div>';
return;
}
container.innerHTML = waiting.map((p, i) => {
const meta = PRIORITY_META[p.priority] || PRIORITY_META['P3'];
const waitMins = Math.floor((new Date() - p.arrivalTime) / 60000);
return `<div onclick="openPatientDetail(${p.id})"
class="bg-surface-dim border-l-4 ${meta.borderClass} border border-outline-variant p-2 cursor-pointer hover:bg-surface-variant transition-colors rounded-sm">
<div class="flex justify-between items-center mb-1">
<span class="triage-badge ${meta.color} px-1.5 py-0.5 rounded">${p.priority}</span>
<span class="mono text-[9px] text-outline">${waitMins}m čeká</span>
</div>
<div class="mono text-xs font-bold text-on-surface uppercase">${p.patient.name}</div>
<div class="mono text-[9px] text-outline">${p.patient.age}l / ${p.patient.gender}</div>
<div class="${meta.textColor} text-[10px] leading-tight mt-1 truncate">${p.patient.symptoms.slice(0,50)}</div>
</div>`;
}).join('');
}
function renderDoctorQueue() {
const container = document.getElementById('doctor-queue');
const waiting = queue.filter(p => p.status !== 'done');
if (waiting.length === 0) {
container.innerHTML = '<div class="text-center py-12 text-outline mono text-[10px] uppercase">Fronta je prázdná</div>';
return;
}
container.innerHTML = waiting.map((p, i) => {
const meta = PRIORITY_META[p.priority] || PRIORITY_META['P3'];
const waitMins = Math.floor((new Date() - p.arrivalTime) / 60000);
const flagText = p.red_flags?.length ? p.red_flags.join(' · ') : '';
return `<div onclick="openPatientDetail(${p.id})"
class="bg-surface-container border-l-4 ${meta.borderClass} border border-outline-variant p-4 cursor-pointer hover:bg-surface-variant transition-colors rounded-sm flex items-start gap-4">
<div class="text-center flex-shrink-0 w-10">
<div class="mono text-xs font-bold text-outline uppercase">#${i+1}</div>
<div class="${meta.textColor} mono text-2xl font-bold">${p.priority}</div>
<div class="mono text-[9px] text-outline">${waitMins}m</div>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-2 mb-1">
<div class="mono text-base font-bold text-on-surface uppercase truncate">${p.patient.name}</div>
<span class="triage-badge ${meta.color} px-2 py-0.5 rounded flex-shrink-0">${meta.timeframe}</span>
</div>
<div class="mono text-[10px] text-outline mb-2">${p.patient.age} let · ${p.patient.gender}</div>
<div class="text-sm text-on-surface-variant mb-2 line-clamp-2">${p.patient.symptoms}</div>
${flagText ? `<div class="mono text-[10px] text-amber-400">⚠ ${flagText}</div>` : ''}
${p.undertriage_risk === 'vysoké' ? '<div class="mono text-[10px] text-red-400 mt-1">⚡ Vysoké riziko podhodnocení</div>' : ''}
</div>
<div class="flex-shrink-0">
<span class="material-symbols-outlined text-outline-variant">chevron_right</span>
</div>
</div>`;
}).join('');
}
function openPatientDetail(id) {
const p = queue.find(q => q.id === id);
if (!p) return;
selectedPatientId = id;
const meta = PRIORITY_META[p.priority] || PRIORITY_META['P3'];
document.getElementById('d-name').textContent = p.patient.name;
document.getElementById('d-meta').textContent = `${p.patient.age} let · ${p.patient.gender} · Příchod: ${p.arrivalTime.toLocaleTimeString('cs-CZ')} · UID: ${p.uid}`;
document.getElementById('d-badge').textContent = meta.label;
document.getElementById('d-badge').className = `triage-badge px-3 py-1 rounded-lg text-sm ${meta.color}`;
document.getElementById('d-symptoms').textContent = p.patient.symptoms || '—';
document.getElementById('d-history').textContent = p.patient.history || 'Neuvedena';
document.getElementById('d-reasoning').textContent = p.reasoning || '—';
if (p.mts_category) {
document.getElementById('d-reasoning').textContent = `[${p.mts_category}] ${p.reasoning}`;
}
// Vitals
const vitalDefs = [
{ label: 'TK', value: (p.patient.bpSys && p.patient.bpDia) ? `${p.patient.bpSys}/${p.patient.bpDia}` : '—', unit: 'mmHg' },
{ label: 'Puls', value: p.patient.hr || '—', unit: '/min' },
{ label: 'SpO₂', value: p.patient.spo2 || '—', unit: '%' },
{ label: 'Teplota', value: p.patient.temp || '—', unit: '°C' },
{ label: 'GCS', value: p.patient.gcs || '—', unit: '/15' },
];
document.getElementById('d-vitals').innerHTML = vitalDefs.map(v => `
<div class="bg-surface-dim border border-outline-variant rounded p-2">
<div class="mono text-[9px] text-outline uppercase">${v.label}</div>
<div class="mono text-lg font-bold text-on-surface">${v.value}</div>
<div class="mono text-[9px] text-outline">${v.unit}</div>
</div>`).join('');
// Flags
if (p.red_flags && p.red_flags.length > 0) {
document.getElementById('d-flags-wrap').classList.remove('hidden');
document.getElementById('d-flags').textContent = p.red_flags.join(' · ');
} else {
document.getElementById('d-flags-wrap').classList.add('hidden');
}
document.getElementById('detail-empty').classList.add('hidden');
document.getElementById('detail-content').classList.remove('hidden');
// Switch to doctor view and detail tab
setView('doktor');
setDoctorTab('detail');
}
function dischargePatient() {
if (!selectedPatientId) return;
const p = queue.find(q => q.id === selectedPatientId);
if (p) p.status = 'done';
selectedPatientId = null;
document.getElementById('detail-empty').classList.remove('hidden');
document.getElementById('detail-content').classList.add('hidden');
updateStats();
renderDoctorQueue();
}
function escalatePatient() {
if (!selectedPatientId) return;
const p = queue.find(q => q.id === selectedPatientId);
if (p) {
p.priority = 'P1';
p.reasoning = '[ESKALOVÁNO LÉKAŘEM] ' + (p.reasoning || '');
}
updateStats();
renderDoctorQueue();
setDoctorTab('queue');
selectedPatientId = null;
}
// ═══════════════════════════════════════
// NOTIFICATION TOAST
// ═══════════════════════════════════════
function showNotification(msg) {
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 bg-surface-container-high border border-outline-variant rounded-lg px-4 py-3 mono text-xs text-on-surface z-50 shadow-lg';
toast.innerHTML = `<span class="material-symbols-outlined text-sm text-green-400 align-middle mr-2">check_circle</span>${msg}`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// ═══════════════════════════════════════
// REFRESH QUEUE WAIT TIMES every 30s
// ═══════════════════════════════════════
setInterval(() => {
renderSidebarQueue();
if (!document.getElementById('dtab-queue-view').classList.contains('hidden')) {
renderDoctorQueue();
}
}, 30000);
// Init
setView('recepce');
</script>
</body>
</html>