| <!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 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"> |
| |
| <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> |
|
|
| |
| <div class="flex h-screen pt-14"> |
|
|
| |
| <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> |
| |
| <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 class="flex-1 ml-56 overflow-hidden flex flex-col"> |
|
|
| |
| <div id="view-recepce" class="flex-1 flex gap-3 p-3 overflow-hidden"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="space-y-4" id="patient-form"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <script> |
| |
| |
| |
| let queue = []; |
| let currentAIResult = null; |
| let selectedPatientId = null; |
| |
| |
| |
| |
| |
| |
| let DATASET = { |
| presentations: {}, |
| anamnesis: {}, |
| epidemiology: [], |
| queueIds: [], |
| }; |
| let currentDatasetPatientId = null; |
| let patientBurden = 0; |
| |
| 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 = ''; |
| |
| 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'); |
| } |
| |
| |
| 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)` |
| ); |
| } |
| |
| |
| 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)` |
| ); |
| |
| currentEpidemiologyContext = row; |
| } |
| |
| |
| 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" }, |
| }; |
| |
| |
| |
| |
| 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(); |
| |
| |
| |
| |
| 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'; |
| } |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const FALLBACK_FREE_MODELS = [ |
| |
| '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) |
| |
| .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) { |
| |
| _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; |
| } |
| |
| 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; |
| |
| } |
| } |
| 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; |
| } |
| |
| |
| 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(), |
| }; |
| |
| |
| 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 }; |
| |
| |
| 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) { |
| |
| 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) { |
| |
| 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) { |
| |
| |
| |
| 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'); |
| } |
| |
| |
| |
| |
| function confirmAndQueue() { |
| if (!currentAIResult) return; |
| |
| const patient = { |
| ...currentAIResult, |
| id: Date.now(), |
| status: 'waiting', |
| }; |
| |
| queue.push(patient); |
| |
| 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; |
| |
| |
| 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}`; |
| } |
| |
| |
| 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(''); |
| |
| |
| 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'); |
| |
| |
| 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; |
| } |
| |
| |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| setInterval(() => { |
| renderSidebarQueue(); |
| if (!document.getElementById('dtab-queue-view').classList.contains('hidden')) { |
| renderDoctorQueue(); |
| } |
| }, 30000); |
| |
| |
| setView('recepce'); |
| </script> |
| </body> |
| </html> |