priyansh-saxena1 commited on
Commit
058b7cd
·
1 Parent(s): 5a79774

feat: add chat interface

Browse files
Files changed (3) hide show
  1. app/main.py +10 -1
  2. app/static/index.html +954 -0
  3. requirements.txt +8 -7
app/main.py CHANGED
@@ -8,7 +8,9 @@ from pydantic import BaseModel
8
  from app.graph import build_graph
9
  from app.schemas import ClinicalBrief
10
  from langgraph.types import Command
11
-
 
 
12
 
13
  class ChatRequest(BaseModel):
14
  session_id: str
@@ -23,6 +25,13 @@ class ChatResponse(BaseModel):
23
 
24
  app = FastAPI(title="Clinical Intake Agent")
25
 
 
 
 
 
 
 
 
26
  graph, checkpointer = build_graph()
27
 
28
 
 
8
  from app.graph import build_graph
9
  from app.schemas import ClinicalBrief
10
  from langgraph.types import Command
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.responses import FileResponse
13
+ import os
14
 
15
  class ChatRequest(BaseModel):
16
  session_id: str
 
25
 
26
  app = FastAPI(title="Clinical Intake Agent")
27
 
28
+ app.mount("/static", StaticFiles(directory="app/static"), name="static")
29
+
30
+ @app.get("/")
31
+ async def root():
32
+ return FileResponse("app/static/index.html")
33
+
34
+
35
  graph, checkpointer = build_graph()
36
 
37
 
app/static/index.html ADDED
@@ -0,0 +1,954 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Clinical Intake Agent</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #f4f6f9;
12
+ --surface: #ffffff;
13
+ --surface-2: #f0f2f5;
14
+ --border: #e2e6ea;
15
+ --text-primary: #1a1d23;
16
+ --text-secondary: #5a6170;
17
+ --text-muted: #9ba3af;
18
+ --accent: #2563eb;
19
+ --accent-hover: #1d4ed8;
20
+ --accent-light: #eff6ff;
21
+ --user-bg: #2563eb;
22
+ --user-text: #ffffff;
23
+ --agent-bg: #ffffff;
24
+ --agent-text: #1a1d23;
25
+ --success: #16a34a;
26
+ --success-light: #f0fdf4;
27
+ --radius: 12px;
28
+ --radius-sm: 6px;
29
+ --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
30
+ --shadow-md: 0 4px 16px rgba(0,0,0,0.08);
31
+ }
32
+
33
+ body {
34
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
35
+ background: var(--bg);
36
+ color: var(--text-primary);
37
+ height: 100vh;
38
+ display: flex;
39
+ flex-direction: column;
40
+ font-size: 14px;
41
+ line-height: 1.5;
42
+ }
43
+
44
+ /* Header */
45
+ header {
46
+ background: var(--surface);
47
+ border-bottom: 1px solid var(--border);
48
+ padding: 0 24px;
49
+ height: 60px;
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: space-between;
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ .header-left {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 12px;
60
+ }
61
+
62
+ .logo-mark {
63
+ width: 32px;
64
+ height: 32px;
65
+ background: var(--accent);
66
+ border-radius: var(--radius-sm);
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ }
71
+
72
+ .logo-mark svg {
73
+ width: 18px;
74
+ height: 18px;
75
+ stroke: white;
76
+ fill: none;
77
+ stroke-width: 2;
78
+ stroke-linecap: round;
79
+ stroke-linejoin: round;
80
+ }
81
+
82
+ .header-title {
83
+ font-size: 15px;
84
+ font-weight: 600;
85
+ color: var(--text-primary);
86
+ letter-spacing: -0.01em;
87
+ }
88
+
89
+ .header-subtitle {
90
+ font-size: 12px;
91
+ color: var(--text-muted);
92
+ }
93
+
94
+ .status-badge {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 6px;
98
+ font-size: 12px;
99
+ color: var(--text-secondary);
100
+ padding: 4px 10px;
101
+ border: 1px solid var(--border);
102
+ border-radius: 20px;
103
+ background: var(--surface);
104
+ }
105
+
106
+ .status-dot {
107
+ width: 7px;
108
+ height: 7px;
109
+ border-radius: 50%;
110
+ background: #d1d5db;
111
+ transition: background 0.3s;
112
+ }
113
+
114
+ .status-dot.active { background: var(--success); }
115
+ .status-dot.thinking {
116
+ background: var(--accent);
117
+ animation: pulse 1.2s infinite;
118
+ }
119
+
120
+ @keyframes pulse {
121
+ 0%, 100% { opacity: 1; }
122
+ 50% { opacity: 0.4; }
123
+ }
124
+
125
+ /* Main layout */
126
+ .main {
127
+ flex: 1;
128
+ display: flex;
129
+ overflow: hidden;
130
+ gap: 0;
131
+ }
132
+
133
+ /* Chat panel */
134
+ .chat-panel {
135
+ flex: 1;
136
+ display: flex;
137
+ flex-direction: column;
138
+ overflow: hidden;
139
+ min-width: 0;
140
+ }
141
+
142
+ /* Progress bar */
143
+ .progress-bar {
144
+ background: var(--surface);
145
+ border-bottom: 1px solid var(--border);
146
+ padding: 12px 24px;
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 16px;
150
+ flex-shrink: 0;
151
+ }
152
+
153
+ .progress-steps {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 0;
157
+ flex: 1;
158
+ }
159
+
160
+ .step {
161
+ display: flex;
162
+ align-items: center;
163
+ flex: 1;
164
+ }
165
+
166
+ .step-label {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 6px;
170
+ font-size: 12px;
171
+ font-weight: 500;
172
+ color: var(--text-muted);
173
+ white-space: nowrap;
174
+ transition: color 0.3s;
175
+ }
176
+
177
+ .step-num {
178
+ width: 22px;
179
+ height: 22px;
180
+ border-radius: 50%;
181
+ border: 1.5px solid var(--border);
182
+ display: flex;
183
+ align-items: center;
184
+ justify-content: center;
185
+ font-size: 11px;
186
+ font-weight: 600;
187
+ color: var(--text-muted);
188
+ background: var(--surface);
189
+ transition: all 0.3s;
190
+ }
191
+
192
+ .step.active .step-label { color: var(--accent); }
193
+ .step.active .step-num {
194
+ border-color: var(--accent);
195
+ background: var(--accent-light);
196
+ color: var(--accent);
197
+ }
198
+
199
+ .step.done .step-label { color: var(--success); }
200
+ .step.done .step-num {
201
+ border-color: var(--success);
202
+ background: var(--success-light);
203
+ color: var(--success);
204
+ }
205
+
206
+ .step-connector {
207
+ flex: 1;
208
+ height: 1px;
209
+ background: var(--border);
210
+ margin: 0 8px;
211
+ min-width: 20px;
212
+ }
213
+
214
+ /* Messages area */
215
+ .messages {
216
+ flex: 1;
217
+ overflow-y: auto;
218
+ padding: 24px;
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 12px;
222
+ scroll-behavior: smooth;
223
+ }
224
+
225
+ .messages::-webkit-scrollbar { width: 4px; }
226
+ .messages::-webkit-scrollbar-track { background: transparent; }
227
+ .messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
228
+
229
+ /* Message bubbles */
230
+ .message {
231
+ display: flex;
232
+ gap: 10px;
233
+ max-width: 680px;
234
+ animation: fadeIn 0.2s ease;
235
+ }
236
+
237
+ @keyframes fadeIn {
238
+ from { opacity: 0; transform: translateY(4px); }
239
+ to { opacity: 1; transform: translateY(0); }
240
+ }
241
+
242
+ .message.user {
243
+ align-self: flex-end;
244
+ flex-direction: row-reverse;
245
+ }
246
+
247
+ .message.agent { align-self: flex-start; }
248
+
249
+ .avatar {
250
+ width: 30px;
251
+ height: 30px;
252
+ border-radius: 8px;
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ flex-shrink: 0;
257
+ font-size: 11px;
258
+ font-weight: 700;
259
+ letter-spacing: 0.03em;
260
+ }
261
+
262
+ .message.agent .avatar {
263
+ background: var(--accent-light);
264
+ color: var(--accent);
265
+ border: 1px solid #bfdbfe;
266
+ }
267
+
268
+ .message.user .avatar {
269
+ background: #e0e7ff;
270
+ color: #4338ca;
271
+ }
272
+
273
+ .bubble {
274
+ padding: 10px 14px;
275
+ border-radius: var(--radius);
276
+ font-size: 14px;
277
+ line-height: 1.55;
278
+ max-width: 560px;
279
+ }
280
+
281
+ .message.agent .bubble {
282
+ background: var(--agent-bg);
283
+ color: var(--agent-text);
284
+ border: 1px solid var(--border);
285
+ border-top-left-radius: 4px;
286
+ box-shadow: var(--shadow);
287
+ }
288
+
289
+ .message.user .bubble {
290
+ background: var(--user-bg);
291
+ color: var(--user-text);
292
+ border-top-right-radius: 4px;
293
+ }
294
+
295
+ .typing-indicator {
296
+ display: flex;
297
+ gap: 4px;
298
+ padding: 14px 16px;
299
+ align-items: center;
300
+ }
301
+
302
+ .typing-dot {
303
+ width: 6px;
304
+ height: 6px;
305
+ border-radius: 50%;
306
+ background: var(--text-muted);
307
+ animation: typing 1.2s infinite;
308
+ }
309
+
310
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
311
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
312
+
313
+ @keyframes typing {
314
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
315
+ 30% { transform: translateY(-4px); opacity: 1; }
316
+ }
317
+
318
+ /* Input area */
319
+ .input-area {
320
+ background: var(--surface);
321
+ border-top: 1px solid var(--border);
322
+ padding: 16px 24px;
323
+ flex-shrink: 0;
324
+ }
325
+
326
+ .input-wrapper {
327
+ display: flex;
328
+ align-items: flex-end;
329
+ gap: 10px;
330
+ background: var(--surface-2);
331
+ border: 1px solid var(--border);
332
+ border-radius: var(--radius);
333
+ padding: 8px 8px 8px 14px;
334
+ transition: border-color 0.2s;
335
+ }
336
+
337
+ .input-wrapper:focus-within {
338
+ border-color: var(--accent);
339
+ background: var(--surface);
340
+ }
341
+
342
+ textarea {
343
+ flex: 1;
344
+ border: none;
345
+ background: transparent;
346
+ resize: none;
347
+ font-family: inherit;
348
+ font-size: 14px;
349
+ color: var(--text-primary);
350
+ line-height: 1.5;
351
+ max-height: 120px;
352
+ min-height: 22px;
353
+ outline: none;
354
+ padding: 1px 0;
355
+ }
356
+
357
+ textarea::placeholder { color: var(--text-muted); }
358
+ textarea:disabled { opacity: 0.5; cursor: not-allowed; }
359
+
360
+ .send-btn {
361
+ width: 34px;
362
+ height: 34px;
363
+ border-radius: 8px;
364
+ border: none;
365
+ background: var(--accent);
366
+ color: white;
367
+ cursor: pointer;
368
+ display: flex;
369
+ align-items: center;
370
+ justify-content: center;
371
+ flex-shrink: 0;
372
+ transition: background 0.2s, transform 0.1s;
373
+ }
374
+
375
+ .send-btn:hover:not(:disabled) { background: var(--accent-hover); }
376
+ .send-btn:active:not(:disabled) { transform: scale(0.95); }
377
+ .send-btn:disabled { background: var(--border); cursor: not-allowed; }
378
+
379
+ .send-btn svg {
380
+ width: 16px;
381
+ height: 16px;
382
+ stroke: currentColor;
383
+ fill: none;
384
+ stroke-width: 2;
385
+ stroke-linecap: round;
386
+ stroke-linejoin: round;
387
+ }
388
+
389
+ .input-hint {
390
+ font-size: 11px;
391
+ color: var(--text-muted);
392
+ margin-top: 8px;
393
+ text-align: right;
394
+ }
395
+
396
+ /* Brief panel */
397
+ .brief-panel {
398
+ width: 360px;
399
+ min-width: 320px;
400
+ background: var(--surface);
401
+ border-left: 1px solid var(--border);
402
+ display: flex;
403
+ flex-direction: column;
404
+ overflow: hidden;
405
+ flex-shrink: 0;
406
+ }
407
+
408
+ .brief-header {
409
+ padding: 16px 20px;
410
+ border-bottom: 1px solid var(--border);
411
+ display: flex;
412
+ align-items: center;
413
+ justify-content: space-between;
414
+ }
415
+
416
+ .brief-header h2 {
417
+ font-size: 13px;
418
+ font-weight: 600;
419
+ color: var(--text-primary);
420
+ letter-spacing: 0.02em;
421
+ text-transform: uppercase;
422
+ }
423
+
424
+ .brief-badge {
425
+ font-size: 11px;
426
+ font-weight: 600;
427
+ padding: 2px 8px;
428
+ border-radius: 4px;
429
+ background: var(--surface-2);
430
+ color: var(--text-muted);
431
+ }
432
+
433
+ .brief-badge.complete {
434
+ background: var(--success-light);
435
+ color: var(--success);
436
+ }
437
+
438
+ .brief-content {
439
+ flex: 1;
440
+ overflow-y: auto;
441
+ padding: 16px 20px;
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: 16px;
445
+ }
446
+
447
+ .brief-content::-webkit-scrollbar { width: 4px; }
448
+ .brief-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
449
+
450
+ .brief-empty {
451
+ display: flex;
452
+ flex-direction: column;
453
+ align-items: center;
454
+ justify-content: center;
455
+ height: 100%;
456
+ gap: 10px;
457
+ color: var(--text-muted);
458
+ text-align: center;
459
+ padding: 32px;
460
+ }
461
+
462
+ .brief-empty svg {
463
+ width: 40px;
464
+ height: 40px;
465
+ stroke: var(--border);
466
+ fill: none;
467
+ stroke-width: 1.5;
468
+ stroke-linecap: round;
469
+ stroke-linejoin: round;
470
+ }
471
+
472
+ .brief-empty p { font-size: 13px; line-height: 1.6; }
473
+
474
+ .brief-section { display: flex; flex-direction: column; gap: 8px; }
475
+
476
+ .brief-section-title {
477
+ font-size: 11px;
478
+ font-weight: 700;
479
+ letter-spacing: 0.06em;
480
+ text-transform: uppercase;
481
+ color: var(--text-muted);
482
+ padding-bottom: 6px;
483
+ border-bottom: 1px solid var(--border);
484
+ }
485
+
486
+ .cc-value {
487
+ font-size: 15px;
488
+ font-weight: 600;
489
+ color: var(--text-primary);
490
+ padding: 10px 12px;
491
+ background: var(--accent-light);
492
+ border-radius: var(--radius-sm);
493
+ border-left: 3px solid var(--accent);
494
+ }
495
+
496
+ .hpi-grid {
497
+ display: flex;
498
+ flex-direction: column;
499
+ gap: 4px;
500
+ }
501
+
502
+ .hpi-row {
503
+ display: flex;
504
+ gap: 8px;
505
+ padding: 5px 0;
506
+ border-bottom: 1px solid var(--surface-2);
507
+ }
508
+
509
+ .hpi-row:last-child { border-bottom: none; }
510
+
511
+ .hpi-key {
512
+ font-size: 11px;
513
+ font-weight: 600;
514
+ color: var(--text-muted);
515
+ text-transform: uppercase;
516
+ letter-spacing: 0.04em;
517
+ width: 80px;
518
+ flex-shrink: 0;
519
+ padding-top: 1px;
520
+ }
521
+
522
+ .hpi-val {
523
+ font-size: 13px;
524
+ color: var(--text-primary);
525
+ flex: 1;
526
+ }
527
+
528
+ .ros-system { display: flex; flex-direction: column; gap: 4px; }
529
+
530
+ .ros-system-name {
531
+ font-size: 12px;
532
+ font-weight: 600;
533
+ color: var(--text-secondary);
534
+ text-transform: capitalize;
535
+ }
536
+
537
+ .ros-findings { display: flex; flex-wrap: wrap; gap: 4px; }
538
+
539
+ .finding-tag {
540
+ font-size: 11px;
541
+ padding: 2px 8px;
542
+ border-radius: 4px;
543
+ font-weight: 500;
544
+ }
545
+
546
+ .finding-tag.positive {
547
+ background: #fef2f2;
548
+ color: #dc2626;
549
+ border: 1px solid #fecaca;
550
+ }
551
+
552
+ .finding-tag.negative {
553
+ background: var(--success-light);
554
+ color: var(--success);
555
+ border: 1px solid #bbf7d0;
556
+ }
557
+
558
+ .brief-timestamp {
559
+ font-size: 11px;
560
+ color: var(--text-muted);
561
+ text-align: right;
562
+ padding-top: 8px;
563
+ border-top: 1px solid var(--border);
564
+ }
565
+
566
+ /* Reset button */
567
+ .reset-btn {
568
+ margin: 0 20px 16px;
569
+ padding: 8px;
570
+ background: var(--surface);
571
+ border: 1px solid var(--border);
572
+ border-radius: var(--radius-sm);
573
+ color: var(--text-secondary);
574
+ font-family: inherit;
575
+ font-size: 12px;
576
+ font-weight: 500;
577
+ cursor: pointer;
578
+ transition: all 0.2s;
579
+ flex-shrink: 0;
580
+ }
581
+
582
+ .reset-btn:hover {
583
+ border-color: var(--accent);
584
+ color: var(--accent);
585
+ background: var(--accent-light);
586
+ }
587
+
588
+ @media (max-width: 768px) {
589
+ .brief-panel { display: none; }
590
+ }
591
+ </style>
592
+ </head>
593
+ <body>
594
+
595
+ <header>
596
+ <div class="header-left">
597
+ <div class="logo-mark">
598
+ <svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
599
+ </div>
600
+ <div>
601
+ <div class="header-title">Clinical Intake Agent</div>
602
+ <div class="header-subtitle">Pre-visit patient intake system</div>
603
+ </div>
604
+ </div>
605
+ <div class="status-badge">
606
+ <div class="status-dot" id="statusDot"></div>
607
+ <span id="statusText">Initializing</span>
608
+ </div>
609
+ </header>
610
+
611
+ <div class="main">
612
+ <div class="chat-panel">
613
+ <div class="progress-bar">
614
+ <div class="progress-steps">
615
+ <div class="step" id="step-intake">
616
+ <div class="step-label">
617
+ <div class="step-num">1</div>
618
+ Chief Complaint
619
+ </div>
620
+ </div>
621
+ <div class="step-connector"></div>
622
+ <div class="step" id="step-hpi">
623
+ <div class="step-label">
624
+ <div class="step-num">2</div>
625
+ History
626
+ </div>
627
+ </div>
628
+ <div class="step-connector"></div>
629
+ <div class="step" id="step-ros">
630
+ <div class="step-label">
631
+ <div class="step-num">3</div>
632
+ Systems Review
633
+ </div>
634
+ </div>
635
+ <div class="step-connector"></div>
636
+ <div class="step" id="step-done">
637
+ <div class="step-label">
638
+ <div class="step-num">4</div>
639
+ Summary
640
+ </div>
641
+ </div>
642
+ </div>
643
+ </div>
644
+
645
+ <div class="messages" id="messages"></div>
646
+
647
+ <div class="input-area">
648
+ <div class="input-wrapper">
649
+ <textarea
650
+ id="input"
651
+ placeholder="Type your response..."
652
+ rows="1"
653
+ autocomplete="off"
654
+ spellcheck="false"
655
+ ></textarea>
656
+ <button class="send-btn" id="sendBtn" disabled>
657
+ <svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
658
+ </button>
659
+ </div>
660
+ <div class="input-hint">Press Enter to send &nbsp;&middot;&nbsp; Shift+Enter for new line</div>
661
+ </div>
662
+ </div>
663
+
664
+ <div class="brief-panel">
665
+ <div class="brief-header">
666
+ <h2>Clinical Brief</h2>
667
+ <span class="brief-badge" id="briefBadge">Pending</span>
668
+ </div>
669
+ <div class="brief-content" id="briefContent">
670
+ <div class="brief-empty">
671
+ <svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
672
+ <p>The clinical brief will appear here once the intake is complete.</p>
673
+ </div>
674
+ </div>
675
+ <button class="reset-btn" id="resetBtn">Start New Session</button>
676
+ </div>
677
+ </div>
678
+
679
+ <script>
680
+ const messagesEl = document.getElementById('messages');
681
+ const inputEl = document.getElementById('input');
682
+ const sendBtn = document.getElementById('sendBtn');
683
+ const statusDot = document.getElementById('statusDot');
684
+ const statusText = document.getElementById('statusText');
685
+ const briefContent = document.getElementById('briefContent');
686
+ const briefBadge = document.getElementById('briefBadge');
687
+ const resetBtn = document.getElementById('resetBtn');
688
+
689
+ let sessionId = 'session_' + Math.random().toString(36).slice(2, 11);
690
+ let isWaiting = false;
691
+ let isComplete = false;
692
+
693
+ const STEPS = { intake: 1, hpi: 2, ros: 3, brief_generator: 4, done: 4 };
694
+
695
+ function setStatus(state) {
696
+ if (state === 'thinking') {
697
+ statusDot.className = 'status-dot thinking';
698
+ statusText.textContent = 'Processing';
699
+ } else if (state === 'ready') {
700
+ statusDot.className = 'status-dot active';
701
+ statusText.textContent = 'Ready';
702
+ } else if (state === 'complete') {
703
+ statusDot.className = 'status-dot active';
704
+ statusText.textContent = 'Intake complete';
705
+ } else {
706
+ statusDot.className = 'status-dot';
707
+ statusText.textContent = 'Offline';
708
+ }
709
+ }
710
+
711
+ function updateProgress(nodeState) {
712
+ const stepMap = { intake: 'step-intake', hpi: 'step-hpi', ros: 'step-ros', done: 'step-done', brief_generator: 'step-done' };
713
+ const order = ['step-intake', 'step-hpi', 'step-ros', 'step-done'];
714
+ const current = stepMap[nodeState] || 'step-intake';
715
+ const currentIdx = order.indexOf(current);
716
+
717
+ order.forEach((id, idx) => {
718
+ const el = document.getElementById(id);
719
+ el.className = 'step';
720
+ if (idx < currentIdx) el.classList.add('done');
721
+ else if (idx === currentIdx) el.classList.add('active');
722
+ });
723
+ }
724
+
725
+ function addMessage(role, text) {
726
+ const wrap = document.createElement('div');
727
+ wrap.className = `message ${role}`;
728
+
729
+ const avatar = document.createElement('div');
730
+ avatar.className = 'avatar';
731
+ avatar.textContent = role === 'agent' ? 'AI' : 'PT';
732
+
733
+ const bubble = document.createElement('div');
734
+ bubble.className = 'bubble';
735
+ bubble.textContent = text;
736
+
737
+ wrap.appendChild(avatar);
738
+ wrap.appendChild(bubble);
739
+ messagesEl.appendChild(wrap);
740
+ messagesEl.scrollTop = messagesEl.scrollHeight;
741
+ return wrap;
742
+ }
743
+
744
+ function showTyping() {
745
+ const wrap = document.createElement('div');
746
+ wrap.className = 'message agent';
747
+ wrap.id = 'typing';
748
+
749
+ const avatar = document.createElement('div');
750
+ avatar.className = 'avatar';
751
+ avatar.textContent = 'AI';
752
+
753
+ const bubble = document.createElement('div');
754
+ bubble.className = 'bubble typing-indicator';
755
+ for (let i = 0; i < 3; i++) {
756
+ const dot = document.createElement('div');
757
+ dot.className = 'typing-dot';
758
+ bubble.appendChild(dot);
759
+ }
760
+
761
+ wrap.appendChild(avatar);
762
+ wrap.appendChild(bubble);
763
+ messagesEl.appendChild(wrap);
764
+ messagesEl.scrollTop = messagesEl.scrollHeight;
765
+ }
766
+
767
+ function removeTyping() {
768
+ const el = document.getElementById('typing');
769
+ if (el) el.remove();
770
+ }
771
+
772
+ function renderBrief(brief) {
773
+ const hpiLabels = {
774
+ onset: 'Onset', location: 'Location', duration: 'Duration',
775
+ character: 'Character', severity: 'Severity',
776
+ aggravating: 'Aggravating', relieving: 'Relieving'
777
+ };
778
+
779
+ let html = `
780
+ <div class="brief-section">
781
+ <div class="brief-section-title">Chief Complaint</div>
782
+ <div class="cc-value">${escHtml(brief.chief_complaint)}</div>
783
+ </div>
784
+ <div class="brief-section">
785
+ <div class="brief-section-title">History of Present Illness</div>
786
+ <div class="hpi-grid">
787
+ `;
788
+
789
+ for (const [key, label] of Object.entries(hpiLabels)) {
790
+ const val = brief.hpi[key] || 'Not specified';
791
+ html += `
792
+ <div class="hpi-row">
793
+ <div class="hpi-key">${label}</div>
794
+ <div class="hpi-val">${escHtml(val)}</div>
795
+ </div>
796
+ `;
797
+ }
798
+
799
+ html += `</div></div>`;
800
+
801
+ if (brief.ros && Object.keys(brief.ros).length > 0) {
802
+ html += `<div class="brief-section"><div class="brief-section-title">Review of Systems</div>`;
803
+ for (const [system, findings] of Object.entries(brief.ros)) {
804
+ html += `<div class="ros-system"><div class="ros-system-name">${escHtml(system)}</div><div class="ros-findings">`;
805
+ findings.forEach(f => {
806
+ const isNeg = f.toLowerCase().startsWith('no ') || f.toLowerCase().includes('none');
807
+ html += `<span class="finding-tag ${isNeg ? 'negative' : 'positive'}">${escHtml(f)}</span>`;
808
+ });
809
+ html += `</div></div>`;
810
+ }
811
+ html += `</div>`;
812
+ }
813
+
814
+ const ts = brief.generated_at ? new Date(brief.generated_at).toLocaleString() : '';
815
+ if (ts) html += `<div class="brief-timestamp">Generated ${ts}</div>`;
816
+
817
+ briefContent.innerHTML = html;
818
+ briefBadge.textContent = 'Complete';
819
+ briefBadge.className = 'brief-badge complete';
820
+ }
821
+
822
+ function escHtml(str) {
823
+ return String(str)
824
+ .replace(/&/g, '&amp;')
825
+ .replace(/</g, '&lt;')
826
+ .replace(/>/g, '&gt;')
827
+ .replace(/"/g, '&quot;');
828
+ }
829
+
830
+ async function sendMessage(text) {
831
+ if (!text.trim() || isWaiting || isComplete) return;
832
+
833
+ isWaiting = true;
834
+ sendBtn.disabled = true;
835
+ inputEl.disabled = true;
836
+ setStatus('thinking');
837
+
838
+ addMessage('user', text);
839
+ showTyping();
840
+
841
+ try {
842
+ const res = await fetch('/chat', {
843
+ method: 'POST',
844
+ headers: { 'Content-Type': 'application/json' },
845
+ body: JSON.stringify({ session_id: sessionId, message: text })
846
+ });
847
+
848
+ if (!res.ok) throw new Error(`Server error ${res.status}`);
849
+ const data = await res.json();
850
+
851
+ removeTyping();
852
+ addMessage('agent', data.reply);
853
+ updateProgress(data.state);
854
+
855
+ if (data.state === 'done' && data.brief) {
856
+ renderBrief(data.brief);
857
+ isComplete = true;
858
+ setStatus('complete');
859
+ inputEl.disabled = true;
860
+ sendBtn.disabled = true;
861
+ inputEl.placeholder = 'Intake complete. Start a new session to begin again.';
862
+ } else {
863
+ setStatus('ready');
864
+ inputEl.disabled = false;
865
+ inputEl.focus();
866
+ sendBtn.disabled = false;
867
+ }
868
+
869
+ } catch (err) {
870
+ removeTyping();
871
+ addMessage('agent', 'An error occurred. Please try again.');
872
+ setStatus('ready');
873
+ inputEl.disabled = false;
874
+ sendBtn.disabled = false;
875
+ }
876
+
877
+ isWaiting = false;
878
+ }
879
+
880
+ async function initSession() {
881
+ setStatus('thinking');
882
+ showTyping();
883
+
884
+ try {
885
+ const res = await fetch('/chat', {
886
+ method: 'POST',
887
+ headers: { 'Content-Type': 'application/json' },
888
+ body: JSON.stringify({ session_id: sessionId, message: 'hello' })
889
+ });
890
+
891
+ const data = await res.json();
892
+ removeTyping();
893
+ addMessage('agent', data.reply);
894
+ updateProgress(data.state);
895
+ setStatus('ready');
896
+ sendBtn.disabled = false;
897
+ inputEl.focus();
898
+ } catch {
899
+ removeTyping();
900
+ addMessage('agent', 'Could not connect to the server. Please refresh.');
901
+ setStatus('offline');
902
+ }
903
+ }
904
+
905
+ sendBtn.addEventListener('click', () => {
906
+ const text = inputEl.value.trim();
907
+ if (text) { inputEl.value = ''; autoResize(); sendMessage(text); }
908
+ });
909
+
910
+ inputEl.addEventListener('keydown', e => {
911
+ if (e.key === 'Enter' && !e.shiftKey) {
912
+ e.preventDefault();
913
+ const text = inputEl.value.trim();
914
+ if (text) { inputEl.value = ''; autoResize(); sendMessage(text); }
915
+ }
916
+ });
917
+
918
+ inputEl.addEventListener('input', autoResize);
919
+
920
+ function autoResize() {
921
+ inputEl.style.height = 'auto';
922
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
923
+ }
924
+
925
+ resetBtn.addEventListener('click', () => {
926
+ sessionId = 'session_' + Math.random().toString(36).slice(2, 11);
927
+ messagesEl.innerHTML = '';
928
+ briefContent.innerHTML = `
929
+ <div class="brief-empty">
930
+ <svg viewBox="0 0 24 24" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
931
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
932
+ <polyline points="14 2 14 8 20 8"/>
933
+ <line x1="16" y1="13" x2="8" y2="13"/>
934
+ <line x1="16" y1="17" x2="8" y2="17"/>
935
+ <polyline points="10 9 9 9 8 9"/>
936
+ </svg>
937
+ <p>The clinical brief will appear here once the intake is complete.</p>
938
+ </div>`;
939
+ briefBadge.textContent = 'Pending';
940
+ briefBadge.className = 'brief-badge';
941
+ inputEl.value = '';
942
+ inputEl.placeholder = 'Type your response...';
943
+ inputEl.disabled = false;
944
+ isComplete = false;
945
+ isWaiting = false;
946
+ updateProgress('intake');
947
+ initSession();
948
+ });
949
+
950
+ updateProgress('intake');
951
+ initSession();
952
+ </script>
953
+ </body>
954
+ </html>
requirements.txt CHANGED
@@ -1,7 +1,8 @@
1
- langgraph>=0.2.0
2
- fastapi>=0.115.0
3
- uvicorn>=0.32.0
4
- pydantic>=2.9.0
5
- pytest>=8.3.0
6
- httpx>=0.27.0
7
- pytest-asyncio>=0.24.0
 
 
1
+ langgraph
2
+ fastapi
3
+ uvicorn
4
+ pydantic
5
+ pytest
6
+ httpx
7
+ pytest-asyncio
8
+ aiofiles