clinKriya / ui /index.html
ananya173147
Clean up task3 and task8 simulations — only imperfect where reasoning warrants it
35a2bd3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>clinKriya PCP Clinic — Primary Care EHR</title>
<style>
:root {
--bg: #f5f7fa; --surface: #ffffff; --surface2: #f0f4f8; --surface3: #e8edf3;
--border: #d0dae6; --text: #1a2f4a; --muted: #6d8fad; --muted2: #9bb3ca;
--blue: #4d9de0; --green: #2ecc71; --red: #e84855; --yellow: #ffbe0b;
--purple: #9b59b6; --teal: #00c9a7; --orange: #f0883e;
--accent: #1565c0; --accent2: #1976d2;
--review: #00c9a7; --document: #f59e0b; --sign: #818cf8;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 13px; line-height: 1.5; overflow: hidden; height: 100vh; }
/* ── Layout ── */
.shell { display: grid; grid-template-rows: 52px 1fr; height: 100vh; }
.content { display: grid; grid-template-columns: 300px 1fr; overflow: hidden; }
/* ── Header ── */
header {
background: linear-gradient(90deg, #0a1628 0%, #0f2244 100%);
border-bottom: 2px solid var(--accent);
display: flex; align-items: center; padding: 0 20px; gap: 14px;
}
.logo { display: flex; align-items: center; gap: 10px; }
.logo-icon { width: 32px; height: 32px; background: linear-gradient(135deg,#1565c0,#4d9de0); border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 17px; box-shadow: 0 0 12px rgba(77,157,224,.4); }
.logo-name { font-size: 16px; font-weight: 800; letter-spacing: -.3px; color: #e8f4ff; }
.logo-sub { font-size: 10px; color: var(--muted); letter-spacing: .5px; text-transform: uppercase; }
.header-center { margin-left: 20px; display: flex; align-items: center; gap: 6px; }
.dept-badge { background: rgba(77,157,224,.12); border: 1px solid rgba(77,157,224,.3); border-radius: 4px; padding: 3px 10px; font-size: 11px; font-weight: 700; color: var(--blue); letter-spacing: .3px; }
.header-pill { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.pill { background: rgba(255,255,255,.06); border: 1px solid var(--border); border-radius: 20px; padding: 3px 10px; font-size: 11px; font-weight: 600; color: var(--muted); display: flex; align-items: center; gap: 5px; }
.dot { width: 6px; height: 6px; border-radius: 50%; }
.dot-green { background: var(--green); animation: pulse 2s infinite; }
.dot-red { background: var(--red); }
.dot-yellow { background: var(--yellow); animation: pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.4} }
/* ── Sidebar ── */
.sidebar { background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
.sidebar-section { padding: 14px 14px 10px; border-bottom: 1px solid var(--border); }
.sidebar-section:last-child { border-bottom: none; }
.section-title { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .8px; margin-bottom: 10px; }
/* Patient queue */
.type-tabs { display: flex; gap: 4px; margin-bottom: 8px; flex-wrap: wrap; }
.ttab { background: transparent; border: 1px solid var(--border); border-radius: 5px; padding: 3px 8px; font-size: 11px; font-weight: 600; color: var(--muted); cursor: pointer; transition: all .15s; }
.ttab:hover { border-color: var(--blue); color: var(--blue); }
.ttab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
select.task-select {
width: 100%; background: var(--surface2); border: 1px solid var(--border);
border-radius: 6px; color: var(--text); font-size: 12px; padding: 7px 8px;
outline: none; cursor: pointer; appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%236d8fad'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 8px center; padding-right: 24px;
}
select.task-select:focus { border-color: var(--accent); }
/* Patient preview card */
.task-preview { margin-top: 8px; background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 10px; display: none; }
.task-preview.visible { display: block; }
.pt-preview-name { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 3px; }
.pt-preview-demos { font-size: 11px; color: var(--muted); margin-bottom: 5px; }
.pt-preview-mrn { font-family: monospace; font-size: 10px; color: var(--blue); font-weight: 700; }
.pt-preview-visit { display: inline-block; margin-top: 5px; font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 700; }
.pt-preview-reason { font-size: 11px; color: var(--muted); margin-top: 5px; line-height: 1.4; }
.allergy-chip { display: inline-flex; align-items: center; gap: 3px; background: rgba(232,72,85,.12); border: 1px solid rgba(232,72,85,.3); border-radius: 4px; padding: 1px 6px; font-size: 10px; font-weight: 700; color: var(--red); margin-top: 4px; }
.btn { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; padding: 8px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; border: none; transition: all .15s; margin-top: 8px; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--accent2); }
.btn-primary:disabled { background: var(--muted2); cursor: not-allowed; opacity: .6; }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
.btn-outline:hover { border-color: var(--blue); color: var(--blue); }
/* Current encounter panel */
.session-status { display: flex; flex-direction: column; gap: 8px; }
.stat-row { display: flex; justify-content: space-between; align-items: center; }
.stat-label { font-size: 11px; color: var(--muted); }
.stat-val { font-size: 12px; font-weight: 700; }
.steps-bar { background: var(--border); border-radius: 3px; height: 5px; overflow: hidden; margin-top: 2px; }
.steps-fill { height: 100%; background: linear-gradient(90deg,var(--accent),var(--blue)); border-radius: 3px; transition: width .3s; }
.status-chip { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 10px; }
.status-running { background: rgba(77,157,224,.15); color: var(--blue); }
.status-completed { background: rgba(46,204,113,.15); color: var(--green); }
.status-error { background: rgba(232,72,85,.15); color: var(--red); }
/* Visit score */
.reward-big { text-align: center; padding: 12px 0 8px; }
.reward-num { font-size: 36px; font-weight: 800; line-height: 1; }
.reward-sub { font-size: 11px; color: var(--muted); margin-top: 3px; }
.reward-comps { display: flex; flex-direction: column; gap: 7px; margin-top: 10px; }
.rc-row { display: flex; flex-direction: column; gap: 2px; }
.rc-header { display: flex; justify-content: space-between; font-size: 11px; }
.rc-name { color: var(--muted); }
.rc-val { font-weight: 700; }
.rc-track { background: var(--border); border-radius: 3px; height: 5px; overflow: hidden; }
.rc-fill { height: 100%; border-radius: 3px; transition: width .8s ease; }
/* Scoring rubric */
.reward-model { overflow: visible; }
/* FHIR APIs tab */
.fhir-tab { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.fhir-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 10px; color: var(--muted); }
.fhir-fn-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; }
.fhir-fn-name { font-family: monospace; font-size: 12px; font-weight: 700; color: var(--blue); margin-bottom: 4px; }
.fhir-fn-desc { font-size: 11px; color: var(--muted); line-height: 1.5; }
.fhir-fn-params { margin-top: 6px; display: flex; flex-wrap: wrap; gap: 4px; }
.fhir-param { font-size: 10px; font-family: monospace; background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; color: var(--text); }
.rm-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid var(--border); }
.rm-row:last-child { border-bottom: none; }
.rm-icon { width: 22px; text-align: center; font-size: 14px; flex-shrink: 0; }
.rm-info { flex: 1; }
.rm-name { font-size: 11px; font-weight: 600; }
.rm-desc { font-size: 10px; color: var(--muted); }
.rm-range { font-size: 10px; font-weight: 700; white-space: nowrap; font-family: monospace; }
/* ── Main panel ── */
.main { display: flex; flex-direction: column; overflow: hidden; }
.tab-bar { display: flex; background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 16px; gap: 0; flex-shrink: 0; }
.tab { padding: 11px 14px; font-size: 12px; font-weight: 500; color: var(--muted); cursor: pointer; border-bottom: 2px solid transparent; transition: all .15s; white-space: nowrap; }
.tab:hover { color: var(--text); }
.tab.active { color: var(--blue); border-bottom-color: var(--blue); }
/* ── Patient banner (replaces task card) ── */
.pt-banner {
background: linear-gradient(135deg, #e8f3ff 0%, #f0f7ff 100%);
border-bottom: 1px solid var(--border);
padding: 0; flex-shrink: 0;
}
.pt-banner-empty { display: flex; align-items: center; gap: 10px; color: var(--muted); font-size: 13px; padding: 16px 18px; }
.pt-banner-content { display: none; }
.pt-banner-content.visible { display: block; }
/* Patient demographics bar */
.pt-demo-bar {
display: flex; align-items: center; gap: 0;
background: rgba(21,101,192,.15); border-bottom: 1px solid rgba(77,157,224,.2);
padding: 8px 18px; flex-wrap: wrap; gap: 0;
}
.pt-name-big { font-size: 15px; font-weight: 800; color: #1a3a6b; margin-right: 14px; }
.pt-demo-sep { color: var(--muted2); margin: 0 8px; }
.pt-demo-item { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 4px; margin-right: 14px; }
.pt-demo-item b { color: var(--text); font-weight: 600; }
.pt-allergy { background: rgba(232,72,85,.15); border: 1px solid rgba(232,72,85,.3); border-radius: 4px; padding: 1px 7px; font-size: 10px; font-weight: 700; color: #ff6b7a; margin-left: auto; }
.pt-status-bar { display: flex; align-items: center; gap: 10px; padding: 8px 18px; }
.pt-visit-type { display: flex; align-items: center; gap: 6px; }
.pt-visit-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.pt-visit-chip { font-size: 11px; font-weight: 700; padding: 2px 10px; border-radius: 4px; }
.pt-reason { font-size: 12px; color: var(--muted); flex: 1; }
.pt-banner-status { margin-left: auto; display: flex; align-items: center; gap: 8px; }
/* Clinical context toggle */
.clin-ctx-toggle { display: flex; align-items: center; gap: 6px; padding: 6px 18px; cursor: pointer; user-select: none; color: var(--muted); font-size: 11px; border-top: 1px solid var(--border); }
.clin-ctx-toggle:hover { color: var(--blue); background: rgba(77,157,224,.04); }
.clin-ctx-body { padding: 10px 18px; background: var(--surface2); border-top: 1px solid var(--border); font-family: monospace; font-size: 10px; color: var(--muted); max-height: 140px; overflow-y: auto; white-space: pre-wrap; display: none; }
.clin-ctx-body.open { display: block; }
/* ── Encounter notes (trace) ── */
.session-pane { display: flex; flex-direction: column; overflow: hidden; flex: 1; }
.trace { flex: 1; overflow-y: auto; padding: 14px 18px; display: flex; flex-direction: column; gap: 10px; }
.trace::-webkit-scrollbar { width: 4px; }
.trace::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.trace-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; color: var(--muted); }
.trace-empty-icon { font-size: 40px; opacity: .3; }
/* Encounter note messages */
.tmsg { display: flex; flex-direction: column; gap: 3px; }
.tmsg-header { display: flex; align-items: center; gap: 8px; }
.tmsg-role { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .7px; }
.tmsg-step { font-size: 10px; color: var(--muted2); }
.tmsg-body { border-radius: 7px; border: 1px solid var(--border); overflow: hidden; }
/* Chart Review (GET) */
.msg-get .tmsg-role { color: var(--review); }
.msg-get .tmsg-body { background: rgba(0,201,167,.05); border-color: rgba(0,201,167,.2); }
/* Order/Document (POST) */
.msg-post .tmsg-role { color: var(--document); }
.msg-post .tmsg-body { background: rgba(245,158,11,.05); border-color: rgba(245,158,11,.2); }
/* Sign & Close (FINISH) */
.msg-finish .tmsg-role { color: var(--sign); }
.msg-finish .tmsg-body { background: rgba(129,140,248,.06); border-color: rgba(129,140,248,.25); }
/* EHR response */
.msg-response .tmsg-role { color: var(--muted2); }
.msg-response .tmsg-body { background: var(--surface2); }
/* Env/system message */
.msg-env .tmsg-role { color: var(--muted); }
.msg-env .tmsg-body { background: var(--surface2); }
.action-line { display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px; }
.action-verb { font-weight: 800; font-size: 11px; padding: 2px 8px; border-radius: 4px; flex-shrink: 0; font-family: monospace; }
.verb-get { background: rgba(0,201,167,.18); color: var(--review); }
.verb-post { background: rgba(245,158,11,.18); color: var(--document); }
.verb-finish { background: rgba(129,140,248,.18); color: var(--sign); }
.action-url { font-family: monospace; font-size: 11px; color: var(--text); word-break: break-all; }
.action-body-pre { margin: 0 12px 8px; background: rgba(0,0,0,.04); border: 1px solid var(--border); border-radius: 5px; padding: 8px; font-family: monospace; font-size: 10px; color: var(--text); white-space: pre-wrap; }
.fhir-resource { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; font-weight: 700; padding: 1px 7px; border-radius: 10px; background: var(--surface3); border: 1px solid var(--border); color: var(--muted); font-family: monospace; }
/* EHR response */
.resp-toggle { display: flex; align-items: center; gap: 6px; padding: 5px 12px; font-size: 10px; color: var(--muted); cursor: pointer; border-top: 1px solid var(--border); user-select: none; }
.resp-toggle:hover { background: rgba(0,0,0,.03); color: var(--text); }
.resp-body { padding: 8px 12px; font-family: monospace; font-size: 10px; color: var(--muted); white-space: pre-wrap; border-top: 1px solid var(--border); max-height: 220px; overflow-y: auto; display: none; }
.resp-body.open { display: block; }
.resp-summary { font-size: 11px; color: var(--muted); padding: 6px 12px; font-weight: 600; }
/* Clinical table (FHIR data formatted) */
.clin-section { padding: 8px 12px 10px; }
.clin-table { width: 100%; border-collapse: collapse; font-size: 11px; }
.clin-table th { text-align: left; font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; padding: 4px 8px; border-bottom: 1px solid var(--border); }
.clin-table td { padding: 5px 8px; border-bottom: 1px solid rgba(30,64,104,.15); color: var(--text); vertical-align: top; }
.clin-table tr:last-child td { border-bottom: none; }
.clin-val { font-weight: 700; color: var(--blue); font-family: monospace; }
.clin-date { color: var(--muted); font-size: 10px; white-space: nowrap; }
.clin-empty { padding: 10px 12px; color: var(--muted); font-size: 11px; font-style: italic; }
/* Patient FHIR card */
.pt-fhir-card { padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; }
.pt-fhir-name { font-size: 13px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
.pt-fhir-row { display: flex; gap: 8px; font-size: 11px; }
.pt-fhir-row span:first-child { color: var(--muted); min-width: 60px; }
.pt-fhir-row span:last-child { color: var(--text); font-weight: 600; }
/* FINISH answer */
.finish-answer { padding: 8px 12px; }
.finish-label { font-size: 10px; color: var(--muted); margin-bottom: 4px; }
.finish-vals { display: flex; flex-wrap: wrap; gap: 4px; }
.finish-val { background: rgba(129,140,248,.12); border: 1px solid rgba(129,140,248,.3); border-radius: 5px; padding: 3px 10px; font-family: monospace; font-size: 12px; font-weight: 700; color: var(--sign); }
/* Reward card in trace */
.reward-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-top: 4px; }
.reward-card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
.reward-card-val { font-size: 28px; font-weight: 800; }
.reward-card-label { font-size: 11px; color: var(--muted); }
.reward-card-status { margin-left: auto; }
.reward-bars { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.rbar { display: flex; flex-direction: column; gap: 3px; }
.rbar-header { display: flex; justify-content: space-between; font-size: 10px; }
.rbar-name { color: var(--muted); }
.rbar-val { font-weight: 700; }
.rbar-track { background: var(--border); border-radius: 3px; height: 5px; overflow: hidden; }
.rbar-fill { height: 100%; border-radius: 3px; }
/* ── Clinical action panel ── */
.action-panel { background: var(--surface); border-top: 1px solid var(--border); padding: 8px 16px; flex-shrink: 0; }
.action-panel-title { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.action-panel-title h3 { font-size: 11px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.action-panel-title .step-badge { font-size: 11px; color: var(--blue); font-weight: 700; }
.quick-section { display: none; }
.action-form { display: none; }
/* Clinical workflow progress */
.workflow-bar { display: flex; align-items: center; gap: 0; margin-bottom: 0; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
.wf-step { flex: 1; padding: 5px 8px; text-align: center; font-size: 10px; font-weight: 600; color: var(--muted); transition: all .3s; border-right: 1px solid var(--border); }
.wf-step:last-child { border-right: none; }
.wf-step.active { background: rgba(77,157,224,.15); color: var(--blue); }
.wf-step.done { background: rgba(46,204,113,.1); color: var(--green); }
/* Quick chart lookup buttons */
.quick-section { margin-bottom: 10px; }
.quick-label { font-size: 10px; color: var(--muted); font-weight: 700; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }
.quick-btns { display: flex; flex-wrap: wrap; gap: 5px; }
.qbtn { background: var(--surface2); border: 1px solid var(--border); border-radius: 5px; padding: 4px 10px; font-size: 11px; font-weight: 600; cursor: pointer; color: var(--muted); transition: all .15s; display: flex; align-items: center; gap: 4px; }
.qbtn:hover { background: rgba(77,157,224,.06); }
.qbtn:disabled { opacity: .4; cursor: not-allowed; }
.qbtn-get { border-color: rgba(0,201,167,.3); color: var(--review); }
.qbtn-get:hover { border-color: var(--review); background: rgba(0,201,167,.06); }
.qbtn-post { border-color: rgba(245,158,11,.3); color: var(--document); }
.qbtn-post:hover { border-color: var(--document); background: rgba(245,158,11,.06); }
.qbtn-finish { border-color: rgba(129,140,248,.3); color: var(--sign); }
.qbtn-finish:hover { border-color: var(--sign); background: rgba(129,140,248,.08); }
/* Action form */
.action-form { display: grid; grid-template-columns: auto 1fr; gap: 8px; align-items: start; }
.action-type-btns { display: flex; flex-direction: column; gap: 4px; }
.atype-btn { width: 72px; padding: 6px 0; border-radius: 5px; font-size: 11px; font-weight: 700; cursor: pointer; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); transition: all .15s; text-align: center; }
.atype-btn.sel-get { background: rgba(0,201,167,.12); border-color: var(--review); color: var(--review); }
.atype-btn.sel-post { background: rgba(245,158,11,.12); border-color: var(--document); color: var(--document); }
.atype-btn.sel-finish { background: rgba(129,140,248,.12); border-color: var(--sign); color: var(--sign); }
.action-inputs { display: flex; flex-direction: column; gap: 6px; }
.input-row { display: flex; align-items: center; gap: 6px; }
.fhir-prefix { font-family: monospace; font-size: 10px; color: var(--muted); white-space: nowrap; background: var(--surface2); border: 1px solid var(--border); border-right: none; border-radius: 5px 0 0 5px; padding: 6px 8px; }
input.url-input, textarea.body-input { background: var(--surface2); border: 1px solid var(--border); border-radius: 5px; color: var(--text); font-size: 12px; outline: none; font-family: monospace; transition: border .15s; }
input.url-input { flex: 1; padding: 6px 8px; border-radius: 0 5px 5px 0; }
input.url-input:focus, textarea.body-input:focus { border-color: var(--accent); }
.answer-input { background: var(--surface2); border: 1px solid var(--border); border-radius: 5px; color: var(--text); font-size: 12px; padding: 6px 8px; outline: none; font-family: monospace; width: 100%; }
.answer-input:focus { border-color: var(--accent); }
textarea.body-input { width: 100%; padding: 6px 8px; resize: vertical; min-height: 56px; max-height: 120px; font-size: 11px; }
.field-label { font-size: 10px; color: var(--muted); font-weight: 600; margin-bottom: 2px; }
.send-row { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.btn-send { background: var(--accent); color: #fff; border: none; border-radius: 6px; padding: 7px 18px; font-size: 12px; font-weight: 700; cursor: pointer; transition: background .15s; }
.btn-send:hover { background: var(--accent2); }
.btn-send:disabled { background: var(--muted2); cursor: not-allowed; }
.btn-send.sign-mode { background: #4f46e5; }
.btn-send.sign-mode:hover { background: #6366f1; }
.send-hint { font-size: 11px; color: var(--muted); }
.error-msg { font-size: 11px; color: var(--red); margin-top: 4px; }
/* ── Performance dashboard ── */
.overview-tab { flex: 1; overflow-y: auto; padding: 20px; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 14px; align-content: start; }
.ov-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 18px; }
.ov-card h3 { font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .8px; margin-bottom: 14px; }
/* Leaderboard */
.lb-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.lb-table th { text-align: left; font-size: 10px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; padding: 5px 10px; border-bottom: 2px solid var(--border); }
.lb-table th.right { text-align: right; }
.lb-table td { padding: 8px 10px; border-bottom: 1px solid rgba(30,64,104,.1); vertical-align: middle; }
.lb-table tr:last-child td { border-bottom: none; }
.lb-table tr:hover td { background: rgba(77,157,224,.04); }
.lb-rank { font-size: 13px; font-weight: 800; color: var(--muted2); width: 28px; }
.lb-rank.gold { color: #f5a623; }
.lb-rank.silver { color: #9baab8; }
.lb-rank.bronze { color: #c47a3a; }
.lb-model { font-weight: 700; color: var(--text); }
.lb-model-sub { font-size: 10px; color: var(--muted); font-weight: 400; margin-top: 1px; }
.lb-bar-cell { width: 130px; }
.lb-bar-wrap { display: flex; align-items: center; gap: 7px; }
.lb-bar-track { flex: 1; height: 6px; background: var(--surface3); border-radius: 3px; overflow: hidden; }
.lb-bar-fill { height: 100%; border-radius: 3px; }
.lb-pct { font-size: 11px; font-weight: 700; white-space: nowrap; min-width: 38px; text-align: right; }
.lb-task-chips { display: flex; gap: 5px; }
.lb-chip { font-size: 10px; font-weight: 600; padding: 2px 7px; border-radius: 4px; white-space: nowrap; }
.lb-chip-ok { background: rgba(46,204,113,.12); color: var(--green); }
.lb-chip-mid { background: rgba(255,190,11,.12); color: #b8860b; }
.lb-chip-low { background: rgba(232,72,85,.12); color: var(--red); }
.big-num { font-size: 44px; font-weight: 800; line-height: 1; }
.big-sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
.arch-rows { display: flex; flex-direction: column; gap: 0; }
.arch-row { display: flex; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border); }
.arch-row:last-child { border-bottom: none; }
.arch-icon { width: 26px; font-size: 16px; flex-shrink: 0; }
.arch-title { font-size: 12px; font-weight: 600; }
.arch-desc { font-size: 11px; color: var(--muted); margin-top: 1px; }
.perf-rows { display: flex; flex-direction: column; gap: 10px; }
.perf-row { display: flex; flex-direction: column; gap: 4px; }
.perf-header { display: flex; justify-content: space-between; }
.perf-name { font-size: 12px; font-weight: 600; }
.perf-score { font-size: 12px; font-weight: 700; }
.perf-sub { font-size: 10px; color: var(--muted); }
.perf-bar { height: 7px; background: var(--border); border-radius: 4px; overflow: hidden; }
.perf-fill { height: 100%; border-radius: 4px; }
/* scrollbar */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.hidden { display: none !important; }
.env-text { padding: 8px 12px; font-size: 12px; color: var(--muted); }
</style>
</head>
<body>
<div class="shell">
<!-- ── Header ── -->
<header>
<div class="logo">
<div class="logo-icon">🏥</div>
<div>
<div class="logo-name">clinKriya PCP Clinic</div>
<div class="logo-sub">Primary Care EHR</div>
</div>
</div>
<div class="header-center">
<span class="dept-badge">Family Medicine · Outpatient</span>
</div>
<div class="header-pill">
<div class="pill"><div class="dot dot-green"></div>FHIR Connected</div>
<div class="pill" id="server-status"><div class="dot dot-yellow" id="server-dot"></div><span id="server-label">Connecting…</span></div>
</div>
</header>
<div class="content">
<!-- ── SIDEBAR ── -->
<div class="sidebar">
<!-- Patient Queue -->
<div class="sidebar-section">
<div class="section-title">Patient Queue</div>
<div class="type-tabs" id="type-tabs">
<button class="ttab active" onclick="setTypeFilter('all',this)">All</button>
<button class="ttab" onclick="setTypeFilter('task3',this)">Vital Signs</button>
<button class="ttab" onclick="setTypeFilter('task5',this)">CT Follow-up</button>
<button class="ttab" onclick="setTypeFilter('task7',this)">QTc Review</button>
<button class="ttab" onclick="setTypeFilter('task8',this)">Referral</button>
<button class="ttab" onclick="setTypeFilter('task10',this)">Lab Review</button>
</div>
<select class="task-select" id="task-select" onchange="onTaskSelect()">
<option value="">— select a patient encounter —</option>
</select>
<div class="task-preview" id="task-preview">
<div class="pt-preview-name" id="prev-name"></div>
<div class="pt-preview-demos" id="prev-demos"></div>
<div class="pt-preview-mrn" id="prev-mrn"></div>
<span class="pt-preview-visit" id="prev-type"></span>
<div class="pt-preview-reason" id="prev-reason"></div>
<div class="allergy-chip" id="prev-allergy">⚠ NKA</div>
</div>
<button class="btn btn-primary" id="start-btn" onclick="startSession()" disabled>📂 Open Chart</button>
</div>
<!-- Current Encounter -->
<div class="sidebar-section" id="session-section">
<div class="section-title">Current Encounter</div>
<div class="session-status">
<div class="stat-row"><span class="stat-label">Patient</span><span class="stat-val" id="ss-task"></span></div>
<div class="stat-row"><span class="stat-label">Visit Status</span><span class="status-chip status-running" id="ss-status"></span></div>
<div class="stat-row"><span class="stat-label">Actions</span><span class="stat-val" id="ss-steps">0 / 8</span></div>
<div class="steps-bar"><div class="steps-fill" id="ss-steps-bar" style="width:0%"></div></div>
</div>
<button class="btn btn-outline" id="reset-btn" style="margin-top:10px" onclick="resetSession()">↺ Close &amp; New Encounter</button>
</div>
<!-- Scoring Rubric -->
<div class="sidebar-section">
<div class="section-title">Scoring Rubric</div>
<div class="reward-model">
<div class="rm-row"><div class="rm-icon"></div><div class="rm-info"><div class="rm-name">Clinical Accuracy</div><div class="rm-desc">Correct orders & documentation</div></div><div class="rm-range" style="color:var(--green)">0.0–0.4</div></div>
<div class="rm-row"><div class="rm-icon">🏗</div><div class="rm-info"><div class="rm-name">Order Structure</div><div class="rm-desc">Right endpoint + resource type</div></div><div class="rm-range" style="color:var(--blue)">0.0–0.2</div></div>
<div class="rm-row"><div class="rm-icon">🧑‍⚕️</div><div class="rm-info"><div class="rm-name">Patient Match</div><div class="rm-desc">Correct MRN in order</div></div><div class="rm-range" style="color:var(--purple)">0.0–0.1</div></div>
<div class="rm-row"><div class="rm-icon"></div><div class="rm-info"><div class="rm-name">Efficiency</div><div class="rm-desc">Fewer actions = higher bonus</div></div><div class="rm-range" style="color:var(--yellow)">0.0–0.1</div></div>
<div class="rm-row"><div class="rm-icon">🏁</div><div class="rm-info"><div class="rm-name">Visit Completion</div><div class="rm-desc">Bonus for signing off</div></div><div class="rm-range" style="color:var(--teal)">+0.05</div></div>
<div class="rm-row"><div class="rm-icon">⚠️</div><div class="rm-info"><div class="rm-name">Redundancy</div><div class="rm-desc">Penalty per unnecessary action</div></div><div class="rm-range" style="color:var(--red)">−0.1</div></div>
<div class="rm-row"><div class="rm-icon">🚫</div><div class="rm-info"><div class="rm-name">Format Error</div><div class="rm-desc">Invalid action format</div></div><div class="rm-range" style="color:var(--red)">−0.1</div></div>
</div>
</div>
</div><!-- /sidebar -->
<!-- ── MAIN PANEL ── -->
<div class="main">
<div class="tab-bar">
<div class="tab active" id="tab-session" onclick="showTab('session',this)">🩺 Patient Encounter</div>
<div class="tab" id="tab-fhir" onclick="showTab('fhir',this)">🔧 FHIR APIs</div>
<div class="tab" id="tab-overview" onclick="showTab('overview',this)">📊 Performance Dashboard</div>
</div>
<!-- ENCOUNTER PANE -->
<div class="session-pane" id="pane-session">
<!-- Patient banner -->
<div class="pt-banner" id="pt-banner">
<div class="pt-banner-empty" id="card-empty">
<span style="font-size:24px;opacity:.3">🏥</span>
<span>Select a patient from the queue and click <strong>Open Chart</strong> to begin</span>
</div>
<div class="pt-banner-content" id="card-content">
<div class="pt-demo-bar">
<span class="pt-name-big" id="card-pt-name"></span>
<span class="pt-demo-item"><b id="card-dob"></b></span>
<span class="pt-demo-sep">·</span>
<span class="pt-demo-item">Age <b id="card-age"></b></span>
<span class="pt-demo-sep">·</span>
<span class="pt-demo-item">MRN <b id="card-mrn" style="font-family:monospace"></b></span>
<span class="pt-allergy" id="card-allergy">⚠ NKA</span>
</div>
<div class="pt-status-bar">
<div class="pt-visit-type">
<span class="pt-visit-label">Visit Type</span>
<span class="pt-visit-chip" id="card-type" style="margin-left:6px"></span>
</div>
<span class="pt-reason" id="card-instr"></span>
<div class="pt-banner-status">
<span class="status-chip status-running" id="card-status">Visit Active</span>
</div>
</div>
</div>
</div>
<!-- Encounter notes -->
<div class="trace" id="trace">
<div class="trace-empty" id="trace-empty">
<div class="trace-empty-icon">📋</div>
<div>Open a patient chart to begin documenting the encounter</div>
</div>
</div>
<!-- Clinical actions panel -->
<div class="action-panel" id="action-panel">
<div class="action-panel-title">
<h3>Clinical Actions</h3>
<span class="step-badge" id="ap-step"></span>
<span class="send-hint" id="ap-hint" style="margin-left:auto">Open a chart to begin</span>
</div>
<!-- Workflow progress bar -->
<div class="workflow-bar" id="workflow-bar">
<div class="wf-step" id="wf-review">📋 Chart Review</div>
<div class="wf-step" id="wf-order">📝 Order / Document</div>
<div class="wf-step" id="wf-sign">✅ Sign &amp; Close</div>
</div>
<!-- Quick chart lookup buttons -->
<div class="quick-section" id="quick-section">
<div class="quick-label">Quick Chart Lookups</div>
<div class="quick-btns" id="quick-btns"></div>
</div>
<!-- Action form -->
<div class="action-form">
<div class="action-type-btns">
<div class="field-label" style="text-align:center;margin-bottom:4px">Action</div>
<button class="atype-btn sel-get" id="atype-get" onclick="setActionType('GET')">📋 Review</button>
<button class="atype-btn" id="atype-post" onclick="setActionType('POST')">📝 Document</button>
<button class="atype-btn" id="atype-finish" onclick="setActionType('FINISH')">✅ Sign</button>
</div>
<div class="action-inputs">
<!-- GET / POST: URL field -->
<div id="url-field">
<div class="field-label">FHIR Resource Path</div>
<div class="input-row">
<div class="fhir-prefix">…/fhir/</div>
<input class="url-input" id="url-input" type="text" placeholder="Observation?patient=S1234567&code=A1C">
</div>
</div>
<!-- POST body -->
<div id="body-field" class="hidden">
<div class="field-label">Order / Documentation Payload (JSON)</div>
<textarea class="body-input" id="body-input" placeholder='{"resourceType":"Observation","status":"final",...}'></textarea>
</div>
<!-- FINISH answer -->
<div id="answer-field" class="hidden">
<div class="field-label">Clinical findings / answer (comma-separated)</div>
<input class="answer-input" id="answer-input" type="text" placeholder='e.g. controlled or 9.1%, 2023-08-01'>
</div>
<div class="send-row">
<button class="btn-send" id="send-btn" onclick="sendAction()" disabled>Submit</button>
<div class="error-msg hidden" id="action-error"></div>
</div>
</div>
</div>
</div>
</div>
<!-- PERFORMANCE DASHBOARD -->
<!-- FHIR APIs PANE -->
<div class="fhir-tab hidden" id="pane-fhir">
<div class="fhir-empty" id="fhir-empty">
<span style="font-size:32px;opacity:.3">🔧</span>
<div>Loading FHIR API tools…</div>
</div>
<div id="fhir-fn-list" style="display:none;flex-direction:column;gap:10px"></div>
</div>
<div class="overview-tab hidden" id="pane-overview">
<!-- SOTA Leaderboard -->
<div class="ov-card" style="grid-column:1/-1">
<h3>Model Leaderboard — clinKriya Benchmark (90 tasks)</h3>
<table class="lb-table">
<thead>
<tr>
<th>#</th>
<th>Model</th>
<th class="lb-bar-cell">Overall Score</th>
<th>Metric</th>
<th class="right">Avg Steps</th>
</tr>
</thead>
<tbody>
<tr>
<td class="lb-rank gold">1</td>
<td><div class="lb-model">DeepSeek-V3</div><div class="lb-model-sub">deepseek-ai · reasoning</div></td>
<td class="lb-bar-cell"><div class="lb-bar-wrap"><div class="lb-bar-track"><div class="lb-bar-fill" style="width:80%;background:#2ecc71"></div></div><span class="lb-pct" style="color:#2ecc71">80.00%</span></div></td>
<td><span class="lb-chip lb-chip-ok">Success Rate</span></td>
<td style="text-align:right;font-family:monospace;font-weight:700">4.82</td>
</tr>
<tr>
<td class="lb-rank silver">2</td>
<td><div class="lb-model">Gemini 1.5 Pro</div><div class="lb-model-sub">Google · multimodal</div></td>
<td class="lb-bar-cell"><div class="lb-bar-wrap"><div class="lb-bar-track"><div class="lb-bar-fill" style="width:78.89%;background:#4d9de0"></div></div><span class="lb-pct" style="color:#4d9de0">78.89%</span></div></td>
<td><span class="lb-chip lb-chip-ok">Success Rate</span></td>
<td style="text-align:right;font-family:monospace;font-weight:700">5.02</td>
</tr>
<tr>
<td class="lb-rank bronze">3</td>
<td><div class="lb-model">Claude 3.5 Sonnet v2</div><div class="lb-model-sub">Anthropic · instruction-tuned</div></td>
<td class="lb-bar-cell"><div class="lb-bar-wrap"><div class="lb-bar-track"><div class="lb-bar-fill" style="width:75.56%;background:#9b59b6"></div></div><span class="lb-pct" style="color:#9b59b6">75.56%</span></div></td>
<td><span class="lb-chip lb-chip-ok">Success Rate</span></td>
<td style="text-align:right;font-family:monospace;font-weight:700">4.53</td>
</tr>
<tr>
<td class="lb-rank">4</td>
<td><div class="lb-model">GPT-5.2</div><div class="lb-model-sub">OpenAI · chat</div></td>
<td class="lb-bar-cell"><div class="lb-bar-wrap"><div class="lb-bar-track"><div class="lb-bar-fill" style="width:64.44%;background:#f0883e"></div></div><span class="lb-pct" style="color:#f0883e">64.44%</span></div></td>
<td><span class="lb-chip lb-chip-ok">Success Rate</span></td>
<td style="text-align:right;font-family:monospace;font-weight:700">5.56</td>
</tr>
<tr id="lb-baseline-row" style="display:none">
<td class="lb-rank">5</td>
<td><div class="lb-model">Qwen3-1.7B (clinKriya)</div><div class="lb-model-sub">RL fine-tuned · GRPO baseline</div></td>
<td class="lb-bar-cell"><div class="lb-bar-wrap"><div class="lb-bar-track"><div class="lb-bar-fill" id="lb-baseline-bar" style="background:#ffbe0b"></div></div><span class="lb-pct" id="lb-baseline-pct" style="color:#ffbe0b"></span></div></td>
<td><span class="lb-chip lb-chip-mid">Reward Score</span></td>
<td id="lb-baseline-tasks" style="text-align:right;font-family:monospace;font-weight:700"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div><!-- /main -->
</div><!-- /content -->
</div><!-- /shell -->
<script>
// ─── State ────────────────────────────────────────────────────────────────
const FHIR_BASE = 'http://localhost:8080/fhir/';
const TASK_META = {
task3: { label: 'Vital Signs Documentation', color: '#4d9de0', visitColor: 'rgba(77,157,224,.15)', desc: 'Nurse documentation — record blood pressure reading' },
task5: { label: 'CT Oncology Follow-up', color: '#f0883e', visitColor: 'rgba(240,136,62,.12)', desc: 'Confirm kidney malignancy — order CT & IR referral if scan overdue' },
task7: { label: 'QTc Interval Review', color: '#e84855', visitColor: 'rgba(232,72,85,.12)', desc: 'Check QTc interval — discontinue QT-prolonging meds & order ECG if prolonged' },
task8: { label: 'Specialist Referral', color: '#2ecc71', visitColor: 'rgba(46,204,113,.15)', desc: 'Create orthopedic surgery referral order' },
task10: { label: 'Chronic Disease Review', color: '#9b59b6', visitColor: 'rgba(155,89,182,.15)', desc: 'HbA1c lab review and potential reorder for diabetes management' },
};
let allTasks = [], filteredTasks = [], typeFilter = 'all';
let selectedTask = null, sessionActive = false, sessionDone = false;
let currentStepNumber = 0, maxSteps = 8, currentActionType = 'GET';
let traceSteps = [], episodeReward = null;
// ─── Patient info — uses real FHIR demographics returned by /api/tasks ────
function getPatientInfo(mrn, taskType, task) {
// Use real FHIR data when available (task object has patient_name, patient_dob, patient_gender)
const realName = task && task.patient_name ? task.patient_name : null;
const realDob = task && task.patient_dob ? task.patient_dob : null;
const realGender = task && task.patient_gender ? task.patient_gender : null;
let name = realName || mrn;
let dob = '—';
let age = '—';
let gender = realGender ? realGender.charAt(0).toUpperCase() : '—';
if (realDob) {
const d = new Date(realDob + 'T00:00:00');
dob = d.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
age = new Date().getFullYear() - d.getFullYear();
}
// Deterministic allergy from MRN seed (cosmetic only)
let seed = 0;
for (let i = 0; i < mrn.length; i++) seed = ((seed * 31) + mrn.charCodeAt(i)) & 0xffff;
const allergies = ['NKDA','Penicillin','Sulfa','Aspirin','Codeine','Latex'];
const allergy = allergies[seed % 6];
return { name, gender, dob, age, mrn, allergy };
}
// ─── Init ─────────────────────────────────────────────────────────────────
async function init() {
await Promise.all([loadTasks(), loadBaseline(), loadFhirFunctions()]);
checkServer();
}
async function checkServer() {
try { const r = await fetch('/health'); if (r.ok) { setServerStatus('online'); return; } } catch {}
setServerStatus('offline');
}
function setServerStatus(s) {
document.getElementById('server-dot').className = 'dot ' + (s === 'online' ? 'dot-green' : 'dot-red');
document.getElementById('server-label').textContent = s === 'online' ? 'EHR Online' : 'EHR Offline';
}
// ─── FHIR Functions (pre-load on page open) ───────────────────────────────
async function loadFhirFunctions() {
try {
const r = await fetch('/api/functions');
if (!r.ok) return;
const fns = await r.json();
if (fns?.length) renderFhirTab(fns);
} catch {}
}
// ─── Tasks / Patient Queue ────────────────────────────────────────────────
async function loadTasks() {
try { const r = await fetch('/api/tasks'); allTasks = await r.json(); filteredTasks = allTasks; renderTaskSelect(); } catch {}
}
function setTypeFilter(f, el) {
typeFilter = f;
document.querySelectorAll('.ttab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
filteredTasks = f === 'all' ? allTasks : allTasks.filter(t => t.task_type === f);
renderTaskSelect();
}
function renderTaskSelect() {
const sel = document.getElementById('task-select');
const prev = sel.value;
sel.innerHTML = '<option value="">— select a patient encounter —</option>' +
filteredTasks.map(t => {
const pt = getPatientInfo(t.eval_MRN, t.task_type, t);
const meta = TASK_META[t.task_type] || {};
return `<option value="${t.index}">${pt.name} (${pt.age}${pt.gender}) — ${meta.label || t.task_type}</option>`;
}).join('');
if (filteredTasks.find(t => t.index == prev)) sel.value = prev;
onTaskSelect();
}
function onTaskSelect() {
const idx = parseInt(document.getElementById('task-select').value);
selectedTask = isNaN(idx) ? null : allTasks.find(t => t.index === idx) || null;
const preview = document.getElementById('task-preview');
const startBtn = document.getElementById('start-btn');
if (!selectedTask) { preview.classList.remove('visible'); startBtn.disabled = true; return; }
const meta = TASK_META[selectedTask.task_type] || {};
const pt = getPatientInfo(selectedTask.eval_MRN, selectedTask.task_type, selectedTask);
document.getElementById('prev-name').textContent = pt.name;
document.getElementById('prev-demos').textContent = `${pt.dob} · ${pt.age}y · ${pt.gender}`;
document.getElementById('prev-mrn').textContent = `MRN: ${pt.mrn}`;
const typeEl = document.getElementById('prev-type');
typeEl.textContent = meta.label || selectedTask.task_type;
typeEl.style.background = meta.visitColor || 'rgba(77,157,224,.15)';
typeEl.style.color = meta.color || '#888';
document.getElementById('prev-reason').textContent = selectedTask.instruction.substring(0, 100) + (selectedTask.instruction.length > 100 ? '…' : '');
const allergyEl = document.getElementById('prev-allergy');
allergyEl.textContent = pt.allergy === 'NKDA' ? '✓ NKDA' : `⚠ Allergy: ${pt.allergy}`;
allergyEl.style.color = pt.allergy === 'NKDA' ? 'var(--teal)' : 'var(--red)';
allergyEl.style.background = pt.allergy === 'NKDA' ? 'rgba(0,201,167,.1)' : 'rgba(232,72,85,.1)';
allergyEl.style.borderColor = pt.allergy === 'NKDA' ? 'rgba(0,201,167,.3)' : 'rgba(232,72,85,.3)';
preview.classList.add('visible');
startBtn.disabled = false;
}
// ─── Session ──────────────────────────────────────────────────────────────
async function startSession() {
if (!selectedTask) return;
document.getElementById('start-btn').disabled = true;
clearTrace();
sessionActive = true; sessionDone = false; currentStepNumber = 0; episodeReward = null;
document.getElementById('send-btn').disabled = false;
document.getElementById('ap-hint').textContent = '';
buildQuickButtons();
updateSessionPanel();
showPatientBanner(selectedTask);
updateWorkflowBar('review');
try {
const r = await fetch('/api/reset', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({task_index: selectedTask.index}) });
if (!r.ok) throw new Error(await r.text());
const obs = await r.json();
handleObservation(obs, 'reset');
autoRunSession();
} catch(e) {
appendEnvMessage(`Error opening chart: ${e.message}`, true);
document.getElementById('start-btn').disabled = false;
sessionActive = false;
}
}
function resetSession() {
clearTrace(); sessionActive = false; sessionDone = false; currentStepNumber = 0; episodeReward = null;
document.getElementById('card-empty').classList.remove('hidden');
document.getElementById('card-content').classList.add('visible');
document.getElementById('card-content').classList.remove('visible');
document.getElementById('card-empty').style.display = '';
document.getElementById('card-content').style.display = 'none';
document.getElementById('send-btn').disabled = true;
document.getElementById('ap-hint').textContent = 'Open a chart to begin';
document.getElementById('start-btn').disabled = selectedTask ? false : true;
updateSessionPanel();
updateWorkflowBar(null);
}
function handleObservation(obs, context, tracked) {
const observation = obs.observation || obs;
const reward = obs.reward, done = obs.done;
currentStepNumber = observation.step_number ?? currentStepNumber;
maxSteps = observation.max_steps ?? maxSteps;
if (context === 'reset') {
if (observation.available_functions?.length) renderFhirTab(observation.available_functions);
} else {
const resp = observation.response_text || '', err = observation.error;
if (err) appendEnvMessage(`⚠ ${err}`, true);
else if (resp) appendFhirResponse(resp);
}
const status = observation.task_status || 'running';
updateSessionPanel(status);
if (done || status !== 'running') {
sessionDone = true;
document.getElementById('send-btn').disabled = true;
document.getElementById('ap-hint').textContent = 'Visit closed';
const statusEl = document.getElementById('card-status');
statusEl.textContent = status === 'completed' ? 'Visit Signed' : status === 'task_limit_reached' ? 'Time Limit Reached' : 'Visit Ended';
statusEl.className = 'status-chip ' + (status === 'completed' ? 'status-completed' : 'status-error');
updateWorkflowBar('done');
if (reward !== undefined && reward !== null) showReward(reward, status, tracked || null);
}
}
// ─── Actions ──────────────────────────────────────────────────────────────
function setActionType(t) {
currentActionType = t;
const labels = { GET: '📋 Review', POST: '📝 Document', FINISH: '✅ Sign' };
['GET','POST','FINISH'].forEach(type => {
const el = document.getElementById(`atype-${type.toLowerCase()}`);
el.className = `atype-btn${t === type ? ` sel-${t.toLowerCase()}` : ''}`;
el.textContent = labels[type];
});
document.getElementById('url-field').classList.toggle('hidden', t === 'FINISH');
document.getElementById('body-field').classList.toggle('hidden', t !== 'POST');
document.getElementById('answer-field').classList.toggle('hidden', t !== 'FINISH');
const sendBtn = document.getElementById('send-btn');
sendBtn.textContent = t === 'FINISH' ? 'Sign & Close Visit' : t === 'POST' ? 'Submit Order' : 'Pull Chart Data';
sendBtn.className = 'btn-send' + (t === 'FINISH' ? ' sign-mode' : '');
if (t === 'GET') updateWorkflowBar('review');
else if (t === 'POST') updateWorkflowBar('order');
else updateWorkflowBar('sign');
}
async function sendAction() {
if (!sessionActive || sessionDone || autoRunning) return;
document.getElementById('action-error').classList.add('hidden');
let url = '', body = null, answer = null, rawResponse = '';
if (currentActionType === 'GET') {
const path = document.getElementById('url-input').value.trim();
if (!path) { showError('Enter a FHIR resource path'); return; }
url = FHIR_BASE + path; rawResponse = `GET ${url}`;
} else if (currentActionType === 'POST') {
const path = document.getElementById('url-input').value.trim();
const bodyStr = document.getElementById('body-input').value.trim();
if (!path) { showError('Enter a FHIR resource path'); return; }
if (!bodyStr) { showError('Enter order payload'); return; }
try { body = JSON.parse(bodyStr); } catch { showError('Invalid JSON in payload'); return; }
url = FHIR_BASE + path; rawResponse = `POST ${url}\n${bodyStr}`;
} else {
const ansStr = document.getElementById('answer-input').value.trim();
answer = ansStr ? ansStr.split(',').map(s => s.trim()).filter(Boolean) : [];
rawResponse = `FINISH(${JSON.stringify(answer)})`;
}
appendAgentAction(currentActionType, url, body, answer, rawResponse);
document.getElementById('send-btn').disabled = true;
try {
const r = await fetch('/api/step', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ action: { action_type: currentActionType, url, body, answer, raw_response: rawResponse } })
});
if (!r.ok) throw new Error(await r.text());
const result = await r.json();
handleObservation(result, 'step');
if (!sessionDone) document.getElementById('send-btn').disabled = false;
} catch(e) {
appendEnvMessage(`Error: ${e.message}`, true);
document.getElementById('send-btn').disabled = false;
}
}
function showError(msg) {
const e = document.getElementById('action-error');
e.textContent = msg; e.classList.remove('hidden');
}
// ─── Quick chart lookup buttons ───────────────────────────────────────────
function buildQuickButtons() {
if (!selectedTask) return;
const mrn = selectedTask.eval_MRN, type = selectedTask.task_type;
const container = document.getElementById('quick-btns');
const gets = [
{ label: '🪪 Demographics', path: `Patient?identifier=${mrn}` },
{ label: '🧪 Lab Results', path: `Observation?patient=${mrn}&_sort=-date&_count=50` },
{ label: '💊 Active Meds', path: `MedicationRequest?patient=${mrn}&status=active` },
{ label: '📋 Problem List', path: `Condition?patient=${mrn}` },
{ label: '🔬 Procedures', path: `Procedure?patient=${mrn}` },
];
if (type === 'task10') gets.splice(2, 0, { label: '🩸 HbA1c History', path: `Observation?patient=${mrn}&code=A1C&_count=5000` });
if (type === 'task3') gets.splice(2, 0, { label: '❤️ Vital Signs', path: `Observation?patient=${mrn}&category=vital-signs&_sort=-date` });
const getHtml = gets.map(g =>
`<button class="qbtn qbtn-get" onclick="prefillGet('${g.path}')" title="${g.path}">${g.label}</button>`
).join('');
let postHtml = '';
const dt = selectedTask.context?.match(/\d{4}-\d{2}-\d{2}T[\d:+]+/)?.[0] || new Date().toISOString();
if (type === 'task3') {
const bp = JSON.stringify({ resourceType:'Observation', status:'final', category:[{coding:[{system:'http://terminology.hl7.org/CodeSystem/observation-category',code:'vital-signs'}]}], code:{text:'Blood pressure',coding:[{code:'BP'}]}, effectiveDateTime:dt, valueString:'118/77 mmHg', subject:{reference:`Patient/${mrn}`} }, null, 2);
postHtml = `<button class="qbtn qbtn-post" onclick="prefillPost('Observation',${escAttr(bp)})">📝 Document BP Reading</button>`;
}
if (type === 'task8') {
const ref = JSON.stringify({ resourceType:'ServiceRequest', status:'active', intent:'order', priority:'stat', code:{coding:[{system:'http://snomed.info/sct',code:'306181000000106',display:'Referral to orthopedic surgeon'}]}, subject:{reference:`Patient/${mrn}`}, authoredOn:dt, note:[{text:'Situation: acute ACL tear. Background: traumatic injury. Assessment: surgical evaluation needed. Recommendation: orthopedic surgery consultation.'}] }, null, 2);
postHtml = `<button class="qbtn qbtn-post" onclick="prefillPost('ServiceRequest',${escAttr(ref)})">📝 Place Ortho Referral</button>`;
}
if (type === 'task10') {
const lab = JSON.stringify({ resourceType:'ServiceRequest', status:'active', intent:'order', priority:'stat', code:{coding:[{system:'http://loinc.org',code:'4548-4',display:'Hemoglobin A1c/Hemoglobin.total in Blood'}]}, subject:{reference:`Patient/${mrn}`}, authoredOn:dt }, null, 2);
postHtml = `<button class="qbtn qbtn-post" onclick="prefillPost('ServiceRequest',${escAttr(lab)})">📝 Order HbA1c Lab</button>`;
}
const finishHtml = `<button class="qbtn qbtn-finish" onclick="prefillFinish()">✅ Sign &amp; Close Visit</button>`;
container.innerHTML = getHtml + postHtml + finishHtml;
}
function escAttr(s) { return "'" + s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n') + "'"; }
function prefillGet(path) { setActionType('GET'); document.getElementById('url-input').value = path; }
function prefillPost(resource, bodyStr) { setActionType('POST'); document.getElementById('url-input').value = resource; document.getElementById('body-input').value = bodyStr.replace(/\\n/g,'\n'); }
function prefillFinish() { setActionType('FINISH'); document.getElementById('answer-input').focus(); }
// ─── Encounter notes rendering ────────────────────────────────────────────
function clearTrace() {
traceSteps = [];
document.getElementById('trace').innerHTML = '<div class="trace-empty" id="trace-empty"><div class="trace-empty-icon">📋</div><div>Open a patient chart to begin documenting the encounter</div></div>';
}
function hideTraceEmpty() { const e = document.getElementById('trace-empty'); if (e) e.remove(); }
function appendAgentAction(type, url, body, answer, raw) {
hideTraceEmpty();
const step = ++traceSteps.length;
const cls = type === 'GET' ? 'msg-get' : type === 'POST' ? 'msg-post' : 'msg-finish';
const verbCls = type === 'GET' ? 'verb-get' : type === 'POST' ? 'verb-post' : 'verb-finish';
let resource = '';
try { resource = url.replace(FHIR_BASE,'').split('?')[0]; } catch {}
// Clinical action labels
const roleLabels = { GET: '📋 Chart Review', POST: '📝 Order Placed', FINISH: '✅ Visit Signed' };
const verbLabels = { GET: 'REVIEW', POST: 'ORDER', FINISH: 'SIGN' };
let inner = '';
if (type === 'FINISH') {
inner = `<div class="action-line"><span class="action-verb ${verbCls}">SIGN OFF</span>
<div class="finish-vals">${(answer||[]).map(v=>`<span class="finish-val">${esc(v)}</span>`).join('')}</div></div>`;
} else {
// Clinical resource label
const resourceLabels = { Observation:'Lab / Vitals', Condition:'Problem List', MedicationRequest:'Medications', Patient:'Demographics', ServiceRequest:'Order', Procedure:'Procedures' };
const rLabel = resourceLabels[resource] || resource;
inner = `<div class="action-line">
<span class="action-verb ${verbCls}">${verbLabels[type]}</span>
${rLabel ? `<span class="fhir-resource">🏥 ${esc(rLabel)}</span>` : ''}
<span class="action-url" style="color:var(--muted);font-size:10px">${esc(url.replace(FHIR_BASE,''))}</span>
</div>`;
if (body) {
const clinNote = getClinicalNote(body);
if (clinNote) inner += `<div style="padding:4px 12px 8px;font-size:11px;color:var(--text)">${clinNote}</div>`;
inner += `<pre class="action-body-pre">${esc(JSON.stringify(body,null,2))}</pre>`;
}
}
const div = document.createElement('div');
div.className = `tmsg ${cls}`;
div.innerHTML = `
<div class="tmsg-header">
<span class="tmsg-role">${roleLabels[type]}</span>
<span class="tmsg-step">Action ${step} of ${maxSteps}</span>
</div>
<div class="tmsg-body">${inner}</div>`;
document.getElementById('trace').appendChild(div);
scrollTrace();
updateSessionPanel();
}
function getClinicalNote(body) {
if (!body) return '';
if (body.resourceType === 'Observation' && body.valueString) return `📊 Documenting: <b>${esc(body.valueString)}</b> for patient <code>${esc((body.subject?.reference||'').replace('Patient/',''))}</code>`;
if (body.resourceType === 'ServiceRequest') {
const code = body.code?.coding?.[0]?.display || body.code?.text || '';
return `📋 Ordering: <b>${esc(code)}</b> — priority <b>${esc(body.priority||'')}</b>`;
}
return '';
}
function appendFhirResponse(text) {
const id = `resp-${traceSteps.length}`;
let parsed = null, displayText = text;
const prefix = 'Here is the response from the GET request:\n';
const suffix = '. Please call FINISH';
const prefixIdx = text.indexOf(prefix);
if (prefixIdx !== -1) {
const after = text.substring(prefixIdx + prefix.length);
const suffixIdx = after.lastIndexOf(suffix);
displayText = suffixIdx !== -1 ? after.substring(0, suffixIdx) : after;
}
try { parsed = JSON.parse(displayText); } catch {}
// Build clinical table if possible
const clinicalHtml = parsed ? renderClinicalBundle(parsed) : null;
const entries = parsed?.entry?.length ?? 0;
const rtype = parsed?.resourceType;
let summaryText = '';
if (rtype === 'Bundle') {
const typeLabels = { Observation:'observations', Condition:'conditions', MedicationRequest:'medications', Patient:'patient record', Procedure:'procedures', ServiceRequest:'orders' };
const firstRtype = parsed.entry?.[0]?.resource?.resourceType;
const tLabel = typeLabels[firstRtype] || 'records';
summaryText = entries === 0 ? `No ${tLabel} on file` : `${entries} ${tLabel} retrieved`;
} else if (rtype) { summaryText = rtype; }
const prettyText = parsed ? JSON.stringify(parsed, null, 2) : displayText;
const shortText = prettyText.length > 2000 ? prettyText.substring(0, 2000) + '\n… (truncated)' : prettyText;
const div = document.createElement('div');
div.className = 'tmsg msg-response';
div.innerHTML = `
<div class="tmsg-header"><span class="tmsg-role">📊 EHR Response</span></div>
<div class="tmsg-body">
${summaryText ? `<div class="resp-summary">${esc(summaryText)}</div>` : ''}
${clinicalHtml ? `<div class="clin-section">${clinicalHtml}</div>` : ''}
<div class="resp-toggle" onclick="toggleResp(this)">▶ Show raw FHIR</div>
<pre class="resp-body" id="${id}">${esc(shortText)}</pre>
</div>`;
document.getElementById('trace').appendChild(div);
scrollTrace();
}
// ─── Clinical FHIR table renderers ────────────────────────────────────────
function renderClinicalBundle(parsed) {
if (!parsed || parsed.resourceType !== 'Bundle') return null;
const entries = (parsed.entry || []).map(e => e.resource).filter(Boolean);
if (!entries.length) return '<div class="clin-empty">No records found in EHR</div>';
const rtype = entries[0].resourceType;
if (rtype === 'Observation') return renderObservationTable(entries);
if (rtype === 'Condition') return renderConditionTable(entries);
if (rtype === 'MedicationRequest') return renderMedTable(entries);
if (rtype === 'Patient') return renderPatientCard(entries[0]);
if (rtype === 'Procedure') return renderProcedureTable(entries);
return null;
}
function renderObservationTable(resources) {
const rows = resources.map(r => {
const name = r.code?.text || r.code?.coding?.[0]?.display || r.code?.coding?.[0]?.code || '—';
const value = r.valueQuantity ? `${r.valueQuantity.value} ${r.valueQuantity.unit||''}`.trim()
: r.valueString || r.valueCodeableConcept?.text || '—';
const date = r.effectiveDateTime ? new Date(r.effectiveDateTime).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—';
const status = r.status || '';
const statusCls = status === 'final' ? 'status-completed' : 'status-running';
return `<tr><td>${esc(name)}</td><td class="clin-val">${esc(value)}</td><td class="clin-date">${esc(date)}</td><td><span class="status-chip ${statusCls}">${esc(status)}</span></td></tr>`;
}).join('');
return `<table class="clin-table"><thead><tr><th>Observation</th><th>Result</th><th>Date</th><th>Status</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderConditionTable(resources) {
const rows = resources.map(r => {
const name = r.code?.text || r.code?.coding?.[0]?.display || '—';
const status = r.clinicalStatus?.coding?.[0]?.code || '—';
const date = r.recordedDate ? new Date(r.recordedDate).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—';
const icd = r.code?.coding?.find(c => c.system?.includes('icd'))?.code || '';
return `<tr><td>${esc(name)}${icd ? `<br><span style="font-size:9px;color:var(--muted)">${esc(icd)}</span>` : ''}</td><td><span class="status-chip ${status==='active'?'status-completed':'status-running'}">${esc(status)}</span></td><td class="clin-date">${esc(date)}</td></tr>`;
}).join('');
return `<table class="clin-table"><thead><tr><th>Condition</th><th>Status</th><th>Recorded</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderMedTable(resources) {
const rows = resources.map(r => {
const name = r.medicationCodeableConcept?.text || r.medicationCodeableConcept?.coding?.[0]?.display || '—';
const status = r.status || '—';
const date = r.authoredOn ? new Date(r.authoredOn).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—';
return `<tr><td>${esc(name)}</td><td><span class="status-chip ${status==='active'?'status-completed':'status-running'}">${esc(status)}</span></td><td class="clin-date">${esc(date)}</td></tr>`;
}).join('');
return `<table class="clin-table"><thead><tr><th>Medication</th><th>Status</th><th>Ordered</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function renderPatientCard(r) {
if (!r) return null;
const name = r.name?.[0] ? `${r.name[0].family||''}, ${(r.name[0].given||[]).join(' ')}` : '—';
const dob = r.birthDate ? new Date(r.birthDate + 'T00:00:00').toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—';
const mrn = r.identifier?.[0]?.value || '—';
const gender = r.gender ? r.gender.charAt(0).toUpperCase() + r.gender.slice(1) : '—';
return `<div class="pt-fhir-card">
<div class="pt-fhir-name">👤 ${esc(name)}</div>
<div class="pt-fhir-row"><span>DOB</span><span>${esc(dob)}</span></div>
<div class="pt-fhir-row"><span>Gender</span><span>${esc(gender)}</span></div>
<div class="pt-fhir-row"><span>MRN</span><span style="font-family:monospace">${esc(mrn)}</span></div>
</div>`;
}
function renderProcedureTable(resources) {
const rows = resources.map(r => {
const name = r.code?.text || r.code?.coding?.[0]?.display || '—';
const status = r.status || '—';
const date = r.performedDateTime ? new Date(r.performedDateTime).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—';
return `<tr><td>${esc(name)}</td><td><span class="status-chip ${status==='completed'?'status-completed':'status-running'}">${esc(status)}</span></td><td class="clin-date">${esc(date)}</td></tr>`;
}).join('');
return `<table class="clin-table"><thead><tr><th>Procedure</th><th>Status</th><th>Date</th></tr></thead><tbody>${rows}</tbody></table>`;
}
function appendEnvMessage(text, isError) {
hideTraceEmpty();
const div = document.createElement('div');
div.className = 'tmsg msg-env';
div.innerHTML = `
<div class="tmsg-header"><span class="tmsg-role" style="color:${isError?'var(--red)':'var(--muted)'}">${isError?'⚠ System Alert':'ℹ EHR System'}</span></div>
<div class="tmsg-body"><div class="env-text" style="${isError?'color:var(--red)':''}">${esc(text)}</div></div>`;
document.getElementById('trace').appendChild(div);
scrollTrace();
}
function toggleResp(el) {
const body = el.nextElementSibling;
const open = body.classList.toggle('open');
el.textContent = open ? '▼ Hide raw FHIR' : '▶ Show raw FHIR';
}
function scrollTrace() { const t = document.getElementById('trace'); t.scrollTop = t.scrollHeight; }
// ─── Patient banner ───────────────────────────────────────────────────────
function showPatientBanner(task) {
const pt = getPatientInfo(task.eval_MRN, task.task_type, task);
const meta = TASK_META[task.task_type] || {};
document.getElementById('card-empty').style.display = 'none';
const content = document.getElementById('card-content');
content.style.display = 'block';
document.getElementById('card-pt-name').textContent = pt.name;
document.getElementById('card-dob').textContent = pt.dob;
document.getElementById('card-age').textContent = `${pt.age}y ${pt.gender}`;
document.getElementById('card-mrn').textContent = pt.mrn;
const allergyEl = document.getElementById('card-allergy');
allergyEl.textContent = pt.allergy === 'NKDA' ? '✓ NKDA' : `⚠ ${pt.allergy}`;
allergyEl.style.color = pt.allergy === 'NKDA' ? 'var(--teal)' : '#ff6b7a';
allergyEl.style.background = pt.allergy === 'NKDA' ? 'rgba(0,201,167,.15)' : 'rgba(232,72,85,.15)';
allergyEl.style.borderColor = pt.allergy === 'NKDA' ? 'rgba(0,201,167,.3)' : 'rgba(232,72,85,.3)';
const typeEl = document.getElementById('card-type');
typeEl.textContent = meta.label || task.task_type;
typeEl.style.background = meta.visitColor || 'rgba(77,157,224,.15)';
typeEl.style.color = meta.color || '#888';
document.getElementById('card-instr').textContent = task.instruction;
document.getElementById('card-status').textContent = 'Visit Active';
document.getElementById('card-status').className = 'status-chip status-running';
}
// ─── FHIR APIs tab renderer ───────────────────────────────────────────────
function renderFhirTab(fns) {
// Deduplicate by function name
const seen = new Set();
fns = fns.filter(fn => {
const name = fn.name || fn.function?.name || '';
if (seen.has(name)) return false;
seen.add(name);
return true;
});
document.getElementById('fhir-empty').style.display = 'none';
const list = document.getElementById('fhir-fn-list');
list.style.display = 'flex';
list.innerHTML = fns.map(fn => {
const name = fn.name || fn.function?.name || '—';
const desc = fn.description || fn.function?.description || '';
const params = fn.parameters?.properties || fn.function?.parameters?.properties || {};
const paramKeys = Object.keys(params);
return `<div class="fhir-fn-card">
<div class="fhir-fn-name">${esc(name)}</div>
<div class="fhir-fn-desc">${esc(desc.substring(0, 180))}${desc.length > 180 ? '…' : ''}</div>
${paramKeys.length ? `<div class="fhir-fn-params">${paramKeys.map(k => `<span class="fhir-param">${esc(k)}</span>`).join('')}</div>` : ''}
</div>`;
}).join('');
}
// ─── Workflow progress bar ────────────────────────────────────────────────
function updateWorkflowBar(phase) {
const steps = ['review','order','sign'];
steps.forEach(s => {
const el = document.getElementById(`wf-${s}`);
if (!el) return;
el.className = 'wf-step';
if (phase === 'done') el.className = 'wf-step done';
else if (s === phase) el.className = 'wf-step active';
else if (steps.indexOf(s) < steps.indexOf(phase)) el.className = 'wf-step done';
});
}
// ─── Session panel ────────────────────────────────────────────────────────
function updateSessionPanel(status) {
if (!selectedTask) return;
const pt = getPatientInfo(selectedTask.eval_MRN, selectedTask.task_type, selectedTask);
document.getElementById('ss-task').textContent = pt.name.split(',')[0];
const st = status || (sessionDone ? 'done' : sessionActive ? 'running' : '—');
const chip = document.getElementById('ss-status');
const statusLabels = { running: 'Visit Active', completed: 'Visit Signed', task_limit_reached: 'Time Limit', done: 'Closed' };
chip.textContent = statusLabels[st] || st;
chip.className = 'status-chip ' + (st === 'completed' ? 'status-completed' : st === 'running' ? 'status-running' : 'status-error');
document.getElementById('ss-steps').textContent = `${currentStepNumber} / ${maxSteps}`;
document.getElementById('ss-steps-bar').style.width = `${Math.min(100,(currentStepNumber/maxSteps)*100)}%`;
document.getElementById('ap-step').textContent = sessionActive ? `Action ${currentStepNumber + 1} of ${maxSteps}` : '';
}
// ─── Visit score (reward) ─────────────────────────────────────────────────
function showReward(reward, status, tracked) {
const r = parseFloat(reward);
const comps = tracked ? computeComps(r, tracked) : estimateComps(r, status);
appendRewardCard(r, status, comps, tracked);
}
function appendRewardCard(r, status, comps, tracked) {
const col = r >= 0.4 ? 'var(--green)' : r >= 0.1 ? 'var(--yellow)' : 'var(--red)';
const statusLabel = status === 'completed' ? 'Visit Signed ✓' : status === 'task_limit_reached' ? 'Time Limit Reached' : 'Visit Ended';
const statusCls = status === 'completed' ? 'status-completed' : 'status-error';
const barsHtml = [
{ n: 'Clinical Accuracy', v: comps.correctness, max: 0.4, c: '#2ecc71', p: false },
{ n: 'Order Structure', v: comps.structure, max: 0.2, c: '#4d9de0', p: false },
{ n: 'Patient Match', v: comps.patientMatch, max: 0.1, c: '#9b59b6', p: false },
{ n: 'Efficiency', v: comps.efficiency, max: 0.1, c: '#ffbe0b', p: false },
{ n: 'Visit Completion', v: comps.completion, max: 0.05, c: '#00c9a7', p: false },
{ n: 'Redundancy', v: comps.redundancy, max: 0.2, c: '#e84855', p: true },
{ n: 'Format Error', v: comps.formatError, max: 0.1, c: '#e84855', p: true },
].map(c => `
<div class="rbar">
<div class="rbar-header"><span class="rbar-name">${c.n}</span><span class="rbar-val" style="color:${c.c}">${c.p && c.v < 0 ? '' : c.v > 0 ? '+' : ''}${c.v.toFixed(3)}</span></div>
<div class="rbar-track"><div class="rbar-fill" style="width:${Math.min(100,Math.round(Math.abs(c.v)/c.max*100))}%;background:${c.c}"></div></div>
</div>`).join('');
let detailHtml = '';
if (tracked) {
const extraGets = Math.max(0, tracked.numGETs - 3);
const extraPosts = Math.max(0, tracked.numPOSTs - tracked.expectedPosts);
const items = [];
if (extraGets > 0) items.push(`<span style="color:var(--red)">⚠ ${extraGets} extra chart review${extraGets>1?'s':''} (−${(extraGets*0.05).toFixed(2)} each)</span>`);
if (extraPosts > 0) items.push(`<span style="color:var(--red)">⚠ ${extraPosts} duplicate order${extraPosts>1?'s':''} (−0.10 each)</span>`);
if (items.length) detailHtml = `<div style="font-size:11px;padding:6px 0 2px;border-top:1px solid rgba(255,255,255,.08);margin-top:8px">${items.join('<br>')}</div>`;
}
const div = document.createElement('div');
div.className = 'tmsg';
div.innerHTML = `
<div class="tmsg-header"><span class="tmsg-role" style="color:var(--blue)">🏆 Encounter Complete</span></div>
<div class="reward-card">
<div class="reward-card-header">
<div><div class="reward-card-val" style="color:${col}">${r.toFixed(4)}</div><div class="reward-card-label">Reward Score (–0.3 → 1.0)</div></div>
<div class="reward-card-status"><span class="status-chip ${statusCls}">${statusLabel}</span></div>
</div>
<div class="reward-bars">${barsHtml}</div>
${detailHtml}
</div>`;
document.getElementById('trace').appendChild(div);
scrollTrace();
}
// Compute reward components from tracked simulation data (accurate)
function computeComps(r, tracked) {
const { numGETs, numPOSTs, expectedPosts, calledFINISH } = tracked;
const completion = calledFINISH ? 0.05 : 0;
const extraGets = Math.max(0, numGETs - 3);
const extraPosts = Math.max(0, numPOSTs - expectedPosts);
const redundancy = -(extraGets * 0.05 + extraPosts * 0.1);
// Reverse-engineer earned positives: r = positive - |redundancy| - formatError + completion
// Assume no format error (simulation always produces valid actions)
const earned = r - completion - redundancy; // redundancy is negative so this adds it back
// Split earned across: correctness (0–0.4), structure (0–0.2), patientMatch (0–0.1), efficiency (0–0.1)
// Max total = 0.8; correctness dominates
const correctness = earned >= 0.38 ? 0.4 : earned >= 0.15 ? parseFloat((earned * 0.52).toFixed(3)) : 0;
const remaining1 = Math.max(0, earned - correctness);
const structure = Math.min(0.2, remaining1 * 0.55);
const remaining2 = Math.max(0, remaining1 - structure);
const patientMatch = Math.min(0.1, remaining2 * 0.6);
const efficiency = Math.min(0.1, Math.max(0, remaining2 - patientMatch));
return { correctness, structure, patientMatch, efficiency, completion, redundancy, formatError: 0 };
}
// Fallback estimator for manual play (no tracking data)
function estimateComps(r, status) {
const signed = (status === 'completed') ? 0.05 : 0;
if (r >= 0.6) return { correctness:0.4, structure:0.2, patientMatch:0.1, efficiency:0.08, completion:signed, redundancy:0, formatError:0 };
if (r >= 0.35) return { correctness:0.2, structure:0.15, patientMatch:0.08, efficiency:0.05, completion:signed, redundancy:0, formatError:0 };
if (r >= 0.15) return { correctness:0.05, structure:0.1, patientMatch:0.05, efficiency:0.03, completion:signed, redundancy:-0.1, formatError:0 };
if (r > 0) return { correctness:0, structure:0.08, patientMatch:0.02, efficiency:0.02, completion:signed, redundancy:-0.1, formatError:0 };
return { correctness:0, structure:0.02, patientMatch:0, efficiency:0, completion:0, redundancy:-0.15, formatError:-0.1 };
}
// ─── Performance dashboard ────────────────────────────────────────────────
async function loadBaseline() {
try {
const r = await fetch('/api/baseline-results');
const data = await r.json();
const s = data.summary || {};
const reward = s.avg_reward;
if (reward !== undefined) {
const pct = Math.min(100, Math.round(reward * 100));
document.getElementById('lb-baseline-bar').style.width = pct + '%';
document.getElementById('lb-baseline-pct').textContent = (reward * 100).toFixed(2) + '%';
document.getElementById('lb-baseline-tasks').textContent = s.total_tasks || '—';
document.getElementById('lb-baseline-row').style.display = '';
}
} catch {}
}
// ─── Tabs ─────────────────────────────────────────────────────────────────
function showTab(name, el) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
el.classList.add('active');
document.getElementById('pane-session').classList.toggle('hidden', name !== 'session');
document.getElementById('pane-fhir').classList.toggle('hidden', name !== 'fhir');
document.getElementById('pane-overview').classList.toggle('hidden', name !== 'overview');
}
// ─── Util ─────────────────────────────────────────────────────────────────
function esc(s) { return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
// ─── Auto-run agent session ────────────────────────────────────────────────
let autoRunning = false;
let autoTracked = null; // action counters for reward breakdown
function appendRedundancyWarning(numGETs) {
hideTraceEmpty();
const div = document.createElement('div');
div.className = 'tmsg';
div.style.cssText = 'border-left: 3px solid var(--red); background: rgba(232,72,85,.08);';
div.innerHTML = `
<div class="tmsg-header"><span class="tmsg-role" style="color:var(--red)">⚠ Redundancy Penalty</span></div>
<div class="tmsg-body"><div class="env-text" style="color:var(--red);font-size:12px">Extra chart review (${numGETs} GETs). Each call beyond 3 costs −0.05 reward.</div></div>`;
document.getElementById('trace').appendChild(div);
scrollTrace();
}
function appendExtraPostWarning() {
hideTraceEmpty();
const div = document.createElement('div');
div.className = 'tmsg';
div.style.cssText = 'border-left: 3px solid var(--red); background: rgba(232,72,85,.08);';
div.innerHTML = `
<div class="tmsg-header"><span class="tmsg-role" style="color:var(--red)">⚠ Duplicate Order Penalty</span></div>
<div class="tmsg-body"><div class="env-text" style="color:var(--red);font-size:12px">Order already placed. Duplicate submissions cost −0.10 reward each.</div></div>`;
document.getElementById('trace').appendChild(div);
scrollTrace();
}
function parseFhirBundle(result) {
try {
const obs = result?.observation || result;
let jsonStr = obs?.response_text || '';
const pi = jsonStr.indexOf('Here is the response from the GET request:\n');
if (pi !== -1) jsonStr = jsonStr.substring(pi + 'Here is the response from the GET request:\n'.length);
const si = jsonStr.lastIndexOf('. Please call FINISH');
if (si !== -1) jsonStr = jsonStr.substring(0, si);
return JSON.parse(jsonStr);
} catch { return null; }
}
async function autoRunSession() {
autoRunning = true;
autoTracked = { numGETs: 0, numPOSTs: 0, expectedPosts: 1, calledFINISH: false };
const mrn = selectedTask.eval_MRN;
const type = selectedTask.task_type;
const dt = '2023-11-13T10:15:00+00:00';
const delay = ms => new Promise(r => setTimeout(r, ms !== undefined ? ms : 1800));
const setHint = msg => { document.getElementById('ap-hint').textContent = msg; };
try {
if (type === 'task3') {
// task3: always-action — BP value given in instruction, no lookup needed.
// Agent verifies patient, then documents immediately.
const bpMatch = selectedTask.instruction.match(/(\d{2,3}\/\d{2,3})\s*mmHg/i);
const bpValue = bpMatch ? bpMatch[1] + ' mmHg' : '118/77 mmHg';
setHint('🤖 Agent: verifying patient identity…');
await delay(); await autoStep('GET', `Patient?identifier=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: documenting blood pressure reading…');
await delay();
await autoStep('POST', 'Observation', {
resourceType: 'Observation', status: 'final',
category: [{ coding: [{ system: 'http://hl7.org/fhir/observation-category', code: 'vital-signs', display: 'Vital Signs' }] }],
code: { text: 'BP' },
effectiveDateTime: dt, valueString: bpValue,
subject: { reference: `Patient/${mrn}` }
}); if (sessionDone) return;
setHint('🤖 Agent: signing off…');
await delay(); await autoStep('FINISH', null, null, []);
} else if (type === 'task5') {
// task5: action-required — CT oncology follow-up. Agent over-queries
// procedures with two different calls.
autoTracked.expectedPosts = 2; // CT + IR referral
setHint('🤖 Agent: pulling patient demographics…');
await delay(); await autoStep('GET', `Patient?identifier=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: confirming kidney malignancy diagnosis…');
await delay();
const condResult = await autoStep('GET', `Condition?patient=${mrn}`); if (sessionDone) return;
let hasC642 = false;
const condParsed = parseFhirBundle(condResult);
if (condParsed) {
hasC642 = (condParsed.entry || []).some(e =>
e.resource?.code?.coding?.some(c => c.code === 'C64.2') ||
(e.resource?.code?.text || '').toLowerCase().includes('kidney') ||
(e.resource?.code?.text || '').toLowerCase().includes('malignan') ||
(e.resource?.code?.text || '').toLowerCase().includes('neoplasm')
);
if (!hasC642) hasC642 = (condParsed.total || 0) > 0; // fallback: any condition found
}
if (!hasC642) {
setHint('🤖 Agent: no qualifying diagnosis — signing off…');
await delay(); await autoStep('FINISH', null, null, []); return;
}
setHint('🤖 Agent: checking imaging history for prior CT scans…');
await delay();
const procResult = await autoStep('GET', `Procedure?patient=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: cross-checking CT with CPT code filter…');
await delay(); await autoStep('GET', `Procedure?patient=${mrn}&code=74177`); if (sessionDone) return;
// ↑ 4th GET — redundancy penalty
let ctIsOld = true;
const procParsed = parseFhirBundle(procResult);
if (procParsed) {
const ctEntries = (procParsed.entry || []).filter(e =>
JSON.stringify(e.resource?.code || {}).includes('74177') ||
(e.resource?.code?.text || '').toLowerCase().includes('ct')
);
if (ctEntries.length > 0) {
const d = ctEntries[0].resource?.performedDateTime;
if (d) ctIsOld = (new Date('2023-11-13') - new Date(d)) / (1000*60*60*24) > 90;
}
}
if (ctIsOld) {
setHint('🤖 Agent: CT overdue — ordering CT Abdomen/Pelvis…');
await delay();
await autoStep('POST', 'ServiceRequest', {
resourceType: 'ServiceRequest', status: 'active', intent: 'order',
code: { coding: [{ system: 'http://www.ama-assn.org/go/cpt', code: '74177', display: 'CT Abdomen/Pelvis with IV contrast' }] },
subject: { reference: `Patient/${mrn}` }, authoredOn: dt,
note: [{ text: 'Oncologic follow-up CT for malignant neoplasm of left kidney (C64.2).' }]
}); if (sessionDone) return;
setHint('🤖 Agent: placing Interventional Radiology referral…');
await delay();
await autoStep('POST', 'ServiceRequest', {
resourceType: 'ServiceRequest', status: 'active', intent: 'order',
code: { coding: [{ system: 'http://www.ama-assn.org/go/cpt', code: 'CON417', display: 'Interventional Radiology referral' }] },
subject: { reference: `Patient/${mrn}` }, authoredOn: dt,
note: [{ text: 'Evaluate renal mass for biopsy or ablation.' }]
}); if (sessionDone) return;
setHint('🤖 Agent: signing off…');
await delay(); await autoStep('FINISH', null, null, []);
} else {
autoTracked.expectedPosts = 0;
setHint('🤖 Agent: CT is recent — no action needed, signing off…');
await delay(); await autoStep('FINISH', null, null, []);
}
} else if (type === 'task7') {
// task7: action-required — QTc review. Expects 2 POSTs (med stop + ECG order).
// Agent makes an extra GET (checking ECG history) after seeing prolonged QTc.
autoTracked.expectedPosts = 2;
setHint('🤖 Agent: pulling patient demographics…');
await delay(); await autoStep('GET', `Patient?identifier=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: retrieving latest QTc interval…');
await delay();
const qtcResult = await autoStep('GET', `Observation?patient=${mrn}&code=QTCINTERVAL&_sort=-date&_count=1`); if (sessionDone) return;
let qtcValue = null;
const qtcParsed = parseFhirBundle(qtcResult);
if (qtcParsed) {
const first = qtcParsed.entry?.[0]?.resource;
if (first) qtcValue = first.valueQuantity?.value ?? parseFloat(first.valueString);
}
const isProlonged = qtcValue !== null && qtcValue > 450;
if (isProlonged) {
setHint('🤖 Agent: QTc prolonged — reviewing active medications…');
await delay(); await autoStep('GET', `MedicationRequest?patient=${mrn}&status=active`); if (sessionDone) return;
setHint('🤖 Agent: checking prior ECG history before ordering…');
await delay(); await autoStep('GET', `Observation?patient=${mrn}&code=ECG&_sort=-date`); if (sessionDone) return;
// ↑ 4th GET — redundancy penalty
setHint('🤖 Agent: discontinuing QT-prolonging medication…');
await delay();
await autoStep('POST', 'MedicationRequest', {
resourceType: 'MedicationRequest', status: 'stopped', intent: 'order',
medicationCodeableConcept: { text: 'ondansetron' },
subject: { reference: `Patient/${mrn}` }, authoredOn: dt,
note: [{ text: `Discontinued due to prolonged QTc (${qtcValue} ms > 450 ms threshold).` }]
}); if (sessionDone) return;
setHint('🤖 Agent: ordering follow-up 12-lead ECG…');
await delay();
await autoStep('POST', 'ServiceRequest', {
resourceType: 'ServiceRequest', status: 'active', intent: 'order', priority: 'stat',
code: { coding: [{ system: 'http://snomed.info/sct', code: '445118002', display: '12-lead ECG' }] },
subject: { reference: `Patient/${mrn}` }, authoredOn: dt,
note: [{ text: `Follow-up ECG after discontinuation of QT-prolonging agent. Baseline QTc: ${qtcValue} ms.` }]
}); if (sessionDone) return;
setHint('🤖 Agent: signing off…');
await delay(); await autoStep('FINISH', null, null, [qtcValue !== null ? String(qtcValue) : '']);
} else {
autoTracked.expectedPosts = 0;
setHint('🤖 Agent: QTc within normal limits — no action needed…');
await delay(); await autoStep('FINISH', null, null, [qtcValue !== null ? String(qtcValue) : '']);
}
} else if (type === 'task8') {
// task8: always-action — referral instruction is explicit. Agent checks
// problem list for context then places a single clean order.
setHint('🤖 Agent: reviewing problem list and imaging…');
await delay(); await autoStep('GET', `Condition?patient=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: placing orthopedic surgery referral…');
await delay();
await autoStep('POST', 'ServiceRequest', {
resourceType: 'ServiceRequest', status: 'active', intent: 'order', priority: 'stat',
code: { coding: [{ system: 'http://snomed.info/sct', code: '306181000000106', display: 'Referral to orthopedic surgeon' }] },
subject: { reference: `Patient/${mrn}` }, authoredOn: dt,
note: [{ text: 'Situation: acute left knee injury, Background: radiology report indicates ACL tear. Assessment: ACL tear grade II. Recommendation: request for Orthopedic service to evaluate and provide management recommendations.' }]
}); if (sessionDone) return;
setHint('🤖 Agent: signing off…');
await delay(); await autoStep('FINISH', null, null, []);
} else if (type === 'task10') {
// task10: action-required — A1C review. Agent over-queries labs before deciding.
setHint('🤖 Agent: pulling patient demographics…');
await delay(); await autoStep('GET', `Patient?identifier=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: retrieving HbA1c history…');
await delay();
const obsResult = await autoStep('GET', `Observation?patient=${mrn}&code=A1C&_count=5000`); if (sessionDone) return;
setHint('🤖 Agent: reviewing problem list for diabetes diagnosis…');
await delay(); await autoStep('GET', `Condition?patient=${mrn}`); if (sessionDone) return;
setHint('🤖 Agent: checking other recent metabolic labs…');
await delay(); await autoStep('GET', `Observation?patient=${mrn}&code=GLU&_sort=-date&_count=5`); if (sessionDone) return;
// ↑ 4th GET — redundancy penalty
let answerParts = [];
const obsParsed = parseFhirBundle(obsResult);
if (obsParsed) {
const first = obsParsed.entry?.[0]?.resource;
if (first) {
const val = first.valueQuantity?.value ?? first.valueString;
const date = first.effectiveDateTime?.split('T')[0];
if (val !== undefined) answerParts.push(String(val));
if (date) answerParts.push(date);
}
}
setHint('🤖 Agent: ordering HbA1c recheck…');
await delay();
await autoStep('POST', 'ServiceRequest', {
resourceType: 'ServiceRequest', status: 'active', intent: 'order', priority: 'stat',
code: { coding: [{ system: 'http://loinc.org', code: '4548-4', display: 'Hemoglobin A1c/Hemoglobin.total in Blood' }] },
subject: { reference: `Patient/${mrn}` }, authoredOn: dt
}); if (sessionDone) return;
setHint('🤖 Agent: signing off…');
await delay(); await autoStep('FINISH', null, null, answerParts);
}
} finally {
autoRunning = false;
if (!sessionDone) setHint('');
}
}
async function autoStep(actionType, path, body, answer) {
if (sessionDone) return null;
const FHIR_BASE_URL = 'http://localhost:8080/fhir/';
const url = (actionType !== 'FINISH' && path) ? FHIR_BASE_URL + path : '';
const rawResponse = actionType === 'GET' ? `GET ${url}`
: actionType === 'POST' ? `POST ${url}\n${JSON.stringify(body)}`
: `FINISH(${JSON.stringify(answer)})`;
// Track action counts
if (autoTracked) {
if (actionType === 'GET') autoTracked.numGETs++;
if (actionType === 'POST') autoTracked.numPOSTs++;
if (actionType === 'FINISH') autoTracked.calledFINISH = true;
}
setActionType(actionType);
if (path && actionType !== 'FINISH') document.getElementById('url-input').value = path;
if (body) document.getElementById('body-input').value = JSON.stringify(body, null, 2);
if (answer) document.getElementById('answer-input').value = answer.join(', ');
appendAgentAction(actionType, url, body, answer, rawResponse);
// Show redundancy warnings inline
if (autoTracked) {
if (actionType === 'GET' && autoTracked.numGETs > 3) appendRedundancyWarning(autoTracked.numGETs);
if (actionType === 'POST' && autoTracked.numPOSTs > autoTracked.expectedPosts) appendExtraPostWarning();
}
try {
const r = await fetch('/api/step', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: { action_type: actionType, url, body, answer, raw_response: rawResponse } })
});
if (!r.ok) throw new Error(await r.text());
const result = await r.json();
handleObservation(result, 'step', autoTracked ? { ...autoTracked } : null);
return result;
} catch (e) {
appendEnvMessage(`Error: ${e.message}`, true);
return null;
}
}
init();
</script>
</body>
</html>