Spaces:
Running
Running
ananya173147
Clean up task3 and task8 simulations — only imperfect where reasoning warrants it
35a2bd3 | <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 ; } | |
| .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 & 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 & 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 & 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } | |
| // ─── 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> | |