Saicharan21 commited on
Commit
ee45c95
·
verified ·
1 Parent(s): 1cb580d

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +330 -1186
app.py CHANGED
@@ -9,805 +9,154 @@ from groq import Groq
9
  from PIL import Image
10
  from datetime import datetime
11
  from huggingface_hub import HfApi, hf_hub_download
 
12
 
13
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
14
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
15
  HISTORY_REPO = "Saicharan21/cardiolab-chat-history"
16
- PAPERS_DB_REPO = "Saicharan21/cardiolab-papers-db"
17
- CARDIOLAB_MODEL = "Saicharan21/CardioLab-AI-Model"
18
 
19
- CHAT_MODELS = {
20
- "CardioLab Fine-tuned (SJSU)": "cardiolab",
21
- "Llama 3.3 70B (Best)": "llama-3.3-70b-versatile",
22
- "Llama 3.1 8B (Fast)": "llama-3.1-8b-instant",
23
- "Llama 4 Scout (New)": "meta-llama/llama-4-scout-17b-16e-instruct",
24
- "Llama 4 Maverick": "meta-llama/llama-4-maverick-17b-128e-instruct",
25
- }
26
-
27
- KNOWHOW = (
28
- "MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min cardiac output 80-120mmHg. "
29
- "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60 minutes. "
30
- "NORMAL RANGES: TAT below 8 ng/mL. PF1.2 below 2.0 nmol/L. Free hemoglobin below 20 mg/L. Platelets above 150. "
31
- "HIGH RISK: TAT above 15. PF1.2 above 3.0. Hemoglobin above 50. Platelets below 100. "
32
- "uPAD: Jaffe reaction creatinine picric acid orange-red. Normal 0.6-1.2 mg/dL. CKD above 1.5. "
33
- "Stage2 1.5-3.0. Stage3-4 3.0-6.0. Stage5 above 6.0. "
34
  "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
35
- "PIV: green laser 532nm. Normal velocity 0.5-2.0 m/s. Normal shear below 5 Pa. Risk above 10 Pa. "
36
- "Equipment: Heska Element HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno."
37
- )
38
 
39
  CSS = """
40
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=Space+Grotesk:wght@400;500;700&display=swap');
41
-
42
- body {
43
- background: #050a14 !important;
44
- font-family: Inter, sans-serif !important;
45
- }
46
-
47
- body::before {
48
- content: "";
49
- position: fixed;
50
- top: 0; left: 0; width: 100%; height: 100%;
51
- background:
52
- radial-gradient(ellipse 80% 60% at 20% 10%, rgba(193,18,31,0.12) 0%, transparent 60%),
53
- radial-gradient(ellipse 60% 80% at 80% 90%, rgba(0,87,168,0.10) 0%, transparent 60%);
54
- pointer-events: none;
55
- z-index: 0;
56
- animation: bgShift 12s ease-in-out infinite alternate;
57
- }
58
-
59
- @keyframes bgShift { 0%{opacity:1;transform:scale(1)} 100%{opacity:0.7;transform:scale(1.05)} }
60
- @keyframes msgIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
61
- @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.5;transform:scale(1.3)} }
62
-
63
- .gradio-container {
64
- background: transparent !important;
65
- max-width: 1600px !important;
66
- margin: 0 auto !important;
67
- }
68
-
69
- .tab-nav {
70
- background: rgba(255,255,255,0.03) !important;
71
- backdrop-filter: blur(20px) !important;
72
- border: 1px solid rgba(255,255,255,0.08) !important;
73
- border-radius: 16px !important;
74
- padding: 6px !important;
75
- margin: 10px 0 !important;
76
- display: flex !important;
77
- flex-wrap: wrap !important;
78
- gap: 4px !important;
79
- }
80
-
81
- .tab-nav button {
82
- background: transparent !important;
83
- color: rgba(255,255,255,0.5) !important;
84
- border: none !important;
85
- border-radius: 10px !important;
86
- padding: 8px 14px !important;
87
- font-weight: 500 !important;
88
- font-size: 0.78em !important;
89
- white-space: nowrap !important;
90
- transition: all 0.25s ease !important;
91
- }
92
-
93
- .tab-nav button:hover {
94
- background: rgba(255,255,255,0.08) !important;
95
- color: rgba(255,255,255,0.9) !important;
96
- transform: translateY(-1px) !important;
97
- }
98
-
99
- .tab-nav button.selected {
100
- background: linear-gradient(135deg, #c1121f, #e63946) !important;
101
- color: white !important;
102
- font-weight: 700 !important;
103
- box-shadow: 0 4px 20px rgba(193,18,31,0.4) !important;
104
- transform: translateY(-1px) !important;
105
- }
106
-
107
- .message.user {
108
- background: linear-gradient(135deg, rgba(193,18,31,0.2), rgba(230,57,70,0.15)) !important;
109
- border: 1px solid rgba(193,18,31,0.3) !important;
110
- color: rgba(255,255,255,0.95) !important;
111
- border-radius: 18px 18px 4px 18px !important;
112
- padding: 14px 18px !important;
113
- backdrop-filter: blur(10px) !important;
114
- animation: msgIn 0.3s ease !important;
115
- }
116
-
117
- .message.bot {
118
- background: rgba(255,255,255,0.05) !important;
119
- border: 1px solid rgba(255,255,255,0.1) !important;
120
- color: rgba(255,255,255,0.9) !important;
121
- border-radius: 18px 18px 18px 4px !important;
122
- padding: 14px 18px !important;
123
- backdrop-filter: blur(10px) !important;
124
- border-left: 3px solid #c1121f !important;
125
- animation: msgIn 0.3s ease !important;
126
- }
127
-
128
- .chatbot {
129
- background: rgba(255,255,255,0.02) !important;
130
- border: 1px solid rgba(255,255,255,0.08) !important;
131
- border-radius: 20px !important;
132
- backdrop-filter: blur(20px) !important;
133
- }
134
-
135
- textarea, input[type=text], input[type=number] {
136
- background: rgba(255,255,255,0.06) !important;
137
- color: rgba(255,255,255,0.9) !important;
138
- border: 1px solid rgba(255,255,255,0.12) !important;
139
- border-radius: 14px !important;
140
- transition: all 0.25s ease !important;
141
- backdrop-filter: blur(10px) !important;
142
- }
143
-
144
- textarea:focus, input:focus {
145
- border-color: rgba(193,18,31,0.6) !important;
146
- box-shadow: 0 0 0 3px rgba(193,18,31,0.15) !important;
147
- outline: none !important;
148
- background: rgba(255,255,255,0.08) !important;
149
- }
150
-
151
- textarea::placeholder { color: rgba(255,255,255,0.3) !important; }
152
-
153
- button.primary {
154
- background: linear-gradient(135deg, #c1121f 0%, #e63946 100%) !important;
155
- color: white !important;
156
- border: none !important;
157
- border-radius: 12px !important;
158
- font-weight: 700 !important;
159
- box-shadow: 0 4px 20px rgba(193,18,31,0.35) !important;
160
- transition: all 0.2s ease !important;
161
- }
162
-
163
- button.primary:hover {
164
- transform: translateY(-2px) !important;
165
- box-shadow: 0 8px 30px rgba(193,18,31,0.5) !important;
166
- }
167
-
168
- button.secondary {
169
- background: rgba(255,255,255,0.07) !important;
170
- color: rgba(255,255,255,0.7) !important;
171
- border: 1px solid rgba(255,255,255,0.15) !important;
172
- border-radius: 12px !important;
173
- transition: all 0.2s ease !important;
174
- backdrop-filter: blur(10px) !important;
175
- }
176
-
177
- button.secondary:hover {
178
- background: rgba(255,255,255,0.12) !important;
179
- color: white !important;
180
- }
181
-
182
- label span {
183
- color: rgba(255,255,255,0.55) !important;
184
- font-weight: 500 !important;
185
- font-size: 0.78em !important;
186
- letter-spacing: 0.06em !important;
187
- text-transform: uppercase !important;
188
- }
189
-
190
- .block, .panel {
191
- background: rgba(255,255,255,0.03) !important;
192
- border: 1px solid rgba(255,255,255,0.07) !important;
193
- border-radius: 20px !important;
194
- backdrop-filter: blur(20px) !important;
195
- }
196
-
197
- .prose, .md { color: rgba(255,255,255,0.8) !important; }
198
-
199
- ::-webkit-scrollbar { width: 5px; }
200
- /* FIX DROPDOWNS - z-index critical */
201
- .multiselect, .dropdown-arrow, .wrap-inner, select {
202
- z-index: 9999 !important;
203
- position: relative !important;
204
- }
205
-
206
- ul.options {
207
- z-index: 99999 !important;
208
- position: absolute !important;
209
- background: #1a1a2e !important;
210
- border: 1px solid rgba(193,18,31,0.4) !important;
211
- border-radius: 10px !important;
212
- color: white !important;
213
- }
214
-
215
- ul.options li {
216
- background: #1a1a2e !important;
217
- color: rgba(255,255,255,0.85) !important;
218
- padding: 8px 12px !important;
219
- }
220
-
221
- ul.options li:hover {
222
- background: rgba(193,18,31,0.3) !important;
223
- }
224
-
225
- /* Fix body::before blocking clicks */
226
- body::before {
227
- pointer-events: none !important;
228
- z-index: -1 !important;
229
- }
230
-
231
- /* Fix dropdown container */
232
- .gradio-dropdown {
233
- z-index: 1000 !important;
234
- position: relative !important;
235
- }
236
-
237
- /* Fix session dropdown */
238
- .gradio-dropdown ul {
239
- z-index: 99999 !important;
240
- background: #0d1117 !important;
241
- border: 1px solid rgba(255,255,255,0.15) !important;
242
- border-radius: 10px !important;
243
- }
244
-
245
- /* Fix all interactive elements z-index */
246
- button, input, select, textarea, .gr-dropdown {
247
- position: relative !important;
248
- z-index: 100 !important;
249
- }
250
-
251
- ::-webkit-scrollbar-track { background: transparent; }
252
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
253
- ::-webkit-scrollbar-thumb:hover { background: rgba(193,18,31,0.5); }
254
-
255
- img { border-radius: 14px !important; border: 1px solid rgba(255,255,255,0.08) !important; }
256
- /* GRADIO 6.x OVERRIDES */
257
- .svelte-1gfkn6j, .svelte-1ipelgc, .svelte-1occ011 {
258
- background: rgba(255,255,255,0.06) !important;
259
- color: rgba(255,255,255,0.9) !important;
260
- border-color: rgba(255,255,255,0.12) !important;
261
- }
262
-
263
- /* Fix all containers */
264
- .container, .wrap, .form {
265
- background: transparent !important;
266
- border-color: rgba(255,255,255,0.08) !important;
267
- }
268
-
269
- /* Fix dropdowns */
270
- .multiselect, .dropdown, ul.options {
271
- background: #0d1117 !important;
272
- color: rgba(255,255,255,0.9) !important;
273
- border-color: rgba(255,255,255,0.15) !important;
274
- }
275
-
276
- ul.options li {
277
- background: #0d1117 !important;
278
- color: rgba(255,255,255,0.85) !important;
279
- }
280
-
281
- ul.options li:hover {
282
- background: rgba(193,18,31,0.2) !important;
283
- }
284
-
285
- /* Fix radio buttons */
286
- .radio-group label, .radio-group span {
287
- color: rgba(255,255,255,0.8) !important;
288
- }
289
-
290
- /* Fix all textboxes */
291
- .scroll-hide, .overflow-y-auto {
292
- background: rgba(255,255,255,0.05) !important;
293
- color: rgba(255,255,255,0.9) !important;
294
- }
295
-
296
- /* Fix number inputs */
297
- input[type=number] {
298
- background: rgba(255,255,255,0.06) !important;
299
- color: rgba(255,255,255,0.9) !important;
300
- border: 1px solid rgba(255,255,255,0.12) !important;
301
- }
302
-
303
- /* Fix file upload */
304
- .file-preview, .upload-button {
305
- background: rgba(255,255,255,0.04) !important;
306
- color: rgba(255,255,255,0.7) !important;
307
- border-color: rgba(255,255,255,0.15) !important;
308
- }
309
-
310
- /* Fix all white backgrounds */
311
- .bg-white, .bg-gray-50, .bg-gray-100 {
312
- background: rgba(255,255,255,0.04) !important;
313
- }
314
-
315
- /* Gradio panel backgrounds */
316
- div.gradio-group, div.gradio-row, div.gradio-column {
317
- background: transparent !important;
318
- }
319
-
320
- /* Fix image containers */
321
- .image-container, .preview-container {
322
- background: rgba(255,255,255,0.04) !important;
323
- border: 1px solid rgba(255,255,255,0.08) !important;
324
- border-radius: 14px !important;
325
- }
326
-
327
- /* Fix audio component */
328
- .audio-container, .waveform-container {
329
- background: rgba(255,255,255,0.04) !important;
330
- border-radius: 14px !important;
331
- }
332
-
333
- /* Fix markdown */
334
- .prose p, .prose h1, .prose h2, .prose h3 {
335
- color: rgba(255,255,255,0.85) !important;
336
- }
337
-
338
- /* Fix all borders */
339
- .border, .border-gray-200, .border-gray-300 {
340
- border-color: rgba(255,255,255,0.08) !important;
341
- }
342
-
343
- /* GRADIO 6.x OVERRIDES */
344
- .svelte-1gfkn6j, .svelte-1ipelgc, .svelte-1occ011 {
345
- background: rgba(255,255,255,0.06) !important;
346
- color: rgba(255,255,255,0.9) !important;
347
- border-color: rgba(255,255,255,0.12) !important;
348
- }
349
-
350
- /* Fix all containers */
351
- .container, .wrap, .form {
352
- background: transparent !important;
353
- border-color: rgba(255,255,255,0.08) !important;
354
- }
355
-
356
- /* Fix dropdowns */
357
- .multiselect, .dropdown, ul.options {
358
- background: #0d1117 !important;
359
- color: rgba(255,255,255,0.9) !important;
360
- border-color: rgba(255,255,255,0.15) !important;
361
- }
362
-
363
- ul.options li {
364
- background: #0d1117 !important;
365
- color: rgba(255,255,255,0.85) !important;
366
- }
367
-
368
- ul.options li:hover {
369
- background: rgba(193,18,31,0.2) !important;
370
- }
371
-
372
- /* Fix radio buttons */
373
- .radio-group label, .radio-group span {
374
- color: rgba(255,255,255,0.8) !important;
375
- }
376
-
377
- /* Fix all textboxes */
378
- .scroll-hide, .overflow-y-auto {
379
- background: rgba(255,255,255,0.05) !important;
380
- color: rgba(255,255,255,0.9) !important;
381
- }
382
-
383
- /* Fix number inputs */
384
- input[type=number] {
385
- background: rgba(255,255,255,0.06) !important;
386
- color: rgba(255,255,255,0.9) !important;
387
- border: 1px solid rgba(255,255,255,0.12) !important;
388
- }
389
-
390
- /* Fix file upload */
391
- .file-preview, .upload-button {
392
- background: rgba(255,255,255,0.04) !important;
393
- color: rgba(255,255,255,0.7) !important;
394
- border-color: rgba(255,255,255,0.15) !important;
395
- }
396
-
397
- /* Fix all white backgrounds */
398
- .bg-white, .bg-gray-50, .bg-gray-100 {
399
- background: rgba(255,255,255,0.04) !important;
400
- }
401
-
402
- /* Gradio panel backgrounds */
403
- div.gradio-group, div.gradio-row, div.gradio-column {
404
- background: transparent !important;
405
- }
406
-
407
- /* Fix image containers */
408
- .image-container, .preview-container {
409
- background: rgba(255,255,255,0.04) !important;
410
- border: 1px solid rgba(255,255,255,0.08) !important;
411
- border-radius: 14px !important;
412
- }
413
-
414
- /* Fix audio component */
415
- .audio-container, .waveform-container {
416
- background: rgba(255,255,255,0.04) !important;
417
- border-radius: 14px !important;
418
- }
419
-
420
- /* Fix markdown */
421
- .prose p, .prose h1, .prose h2, .prose h3 {
422
- color: rgba(255,255,255,0.85) !important;
423
- }
424
-
425
- /* Fix all borders */
426
- .border, .border-gray-200, .border-gray-300 {
427
- border-color: rgba(255,255,255,0.08) !important;
428
- }
429
-
430
  """
431
 
432
- HEADER = """
433
- <style>
434
- @keyframes hb{0%,100%{transform:scale(1)}15%{transform:scale(1.15)}30%{transform:scale(1)}45%{transform:scale(1.08)}60%{transform:scale(1)}}
435
- @keyframes ecg{from{stroke-dashoffset:500}to{stroke-dashoffset:0}}
436
- </style>
437
- <div style="background:linear-gradient(135deg,#0f172a 0%,#1e0a0a 100%);padding:14px 28px;display:flex;align-items:center;justify-content:space-between;border-bottom:2px solid #c1121f;">
438
-
439
- <div style="display:flex;align-items:center;gap:10px;">
440
- <div style="background:rgba(0,87,168,0.2);border:1px solid rgba(0,87,168,0.3);border-radius:10px;padding:6px 10px;">
441
- <div style="color:rgba(232,160,32,0.9);font-size:0.6em;font-weight:600;letter-spacing:2px;text-transform:uppercase;">SJSU</div>
442
- <div style="color:white;font-size:0.65em;font-weight:600;white-space:nowrap;">Biomedical Eng.</div>
443
- </div>
444
- </div>
445
-
446
- <div style="display:flex;align-items:center;gap:16px;">
447
- <svg width="80" height="24" viewBox="0 0 100 24" style="opacity:0.7;">
448
- <polyline points="0,12 15,12 20,4 24,20 28,2 32,18 36,12 100,12"
449
- fill="none" stroke="#c1121f" stroke-width="2" stroke-linecap="round"
450
- stroke-dasharray="500" style="animation:ecg 1.5s ease forwards;"/>
451
- </svg>
452
-
453
- <div style="text-align:center;">
454
- <div style="font-size:1.8em;font-weight:900;letter-spacing:1px;line-height:1.1;">
455
- <span style="color:white;">Cardio</span><span style="color:#c1121f;">Lab</span><span style="color:white;"> AI</span>
456
- </div>
457
- <div style="color:rgba(255,255,255,0.4);font-size:0.6em;letter-spacing:2px;text-transform:uppercase;margin-top:2px;">SJSU Biomedical Engineering</div>
458
- </div>
459
-
460
- <div style="animation:hb 1.4s ease infinite;">
461
- <svg width="36" height="34" viewBox="0 0 100 90">
462
- <defs>
463
- <radialGradient id="hg4" cx="50%" cy="35%">
464
- <stop offset="0%" stop-color="#e63946"/>
465
- <stop offset="100%" stop-color="#7d0a11"/>
466
- </radialGradient>
467
- </defs>
468
- <path d="M50 85 C50 85 5 55 5 30 C5 15 18 5 30 5 C38 5 45 9 50 15 C55 9 62 5 70 5 C82 5 95 15 95 30 C95 55 50 85 50 85Z" fill="url(#hg4)"/>
469
- <polyline points="22,45 30,45 34,34 38,56 42,28 46,50 52,45 78,45" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" opacity="0.9"/>
470
- </svg>
471
- </div>
472
-
473
- <svg width="80" height="24" viewBox="0 0 100 24" style="opacity:0.7;transform:scaleX(-1);">
474
- <polyline points="0,12 15,12 20,4 24,20 28,2 32,18 36,12 100,12"
475
- fill="none" stroke="#c1121f" stroke-width="2" stroke-linecap="round"
476
- stroke-dasharray="500" style="animation:ecg 1.8s ease forwards;"/>
477
- </svg>
478
- </div>
479
-
480
- <div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;">
481
- <div style="display:flex;gap:6px;">
482
- <span style="background:rgba(46,204,113,0.15);border:1px solid rgba(46,204,113,0.3);color:rgba(255,255,255,0.7);padding:2px 8px;border-radius:20px;font-size:0.6em;font-weight:600;">RAG ON</span>
483
- <span style="background:rgba(193,18,31,0.15);border:1px solid rgba(193,18,31,0.3);color:rgba(255,255,255,0.7);padding:2px 8px;border-radius:20px;font-size:0.6em;font-weight:600;">5 MODELS</span>
484
- </div>
485
- <div style="color:rgba(255,255,255,0.25);font-size:0.58em;">MHV · CKD · FSI</div>
486
- </div>
487
-
488
- </div>
489
- <div style="height:1px;background:linear-gradient(90deg,transparent,#0057a8,#c1121f,#e8a020,#c1121f,#0057a8,transparent);"></div>
490
- """
491
 
492
- SIDEBAR_HTML = """
493
- <div style="background:linear-gradient(135deg,#fff5f5,#fef2f2);border:1px solid #fecaca;border-radius:12px;padding:12px;margin-bottom:8px;">
494
- <div style="display:flex;align-items:center;gap:8px;">
495
- <div style="width:6px;height:6px;background:#c1121f;border-radius:50%;"></div>
496
- <span style="color:#c1121f;font-weight:700;font-size:0.8em;letter-spacing:1px;">SJSU CARDIOLAB</span>
497
- </div>
498
- <div style="color:#94a3b8;font-size:0.7em;margin-top:2px;">Conversations</div>
499
- </div>
500
- """
501
-
502
- STATUS_BANNER = """
503
- <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;margin:6px 0;padding:8px 16px;display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap;">
504
- <div style="display:flex;align-items:center;gap:6px;">
505
- <div style="width:7px;height:7px;background:#22c55e;border-radius:50%;"></div>
506
- <span style="color:#15803d;font-size:0.78em;font-weight:600;">RAG Active — 417 chunks from 16 SJSU papers</span>
507
- </div>
508
- <div style="color:#d1fae5;">|</div>
509
- <div style="display:flex;align-items:center;gap:6px;">
510
- <div style="width:7px;height:7px;background:#f59e0b;border-radius:50%;"></div>
511
- <span style="color:#92400e;font-size:0.78em;font-weight:600;">Fine-tuned Model Loaded</span>
512
- </div>
513
- <div style="color:#d1fae5;">|</div>
514
- <div style="display:flex;align-items:center;gap:6px;">
515
- <div style="width:7px;height:7px;background:#3b82f6;border-radius:50%;"></div>
516
- <span style="color:#1e40af;font-size:0.78em;font-weight:600;">5 AI Models Ready</span>
517
- </div>
518
- </div>
519
- """
520
-
521
- FOOTER_HTML = """
522
- <div style="text-align:center;padding:12px;border-top:1px solid #e2e8f0;background:#f8fafc;margin-top:8px;">
523
- <span style="color:#94a3b8;font-size:0.72em;">
524
- CardioLab AI v39 | SJSU Biomedical Engineering |
525
- Inspired by <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;text-decoration:none;">Biomni Stanford</a> |
526
- <a href="https://github.com/pranatechsol/Cardio-Lab-Ai" style="color:#0057a8;text-decoration:none;">GitHub</a> |
527
- Apache 2.0 | $0 Cost
528
- </span>
529
- </div>
530
- """
531
-
532
- # ── LOAD PAPERS + MODEL ────────────────────────────────────
533
- CHUNKS = []
534
- METADATA = []
535
- EMBEDDINGS = None
536
- PAPERS_LOADED = False
537
- EMBEDDER = None
538
- CARDIOLAB_TOKENIZER = None
539
- CARDIOLAB_LLM = None
540
- CARDIOLAB_MODEL_LOADED = False
541
-
542
- def load_papers():
543
- global CHUNKS, METADATA, EMBEDDINGS, PAPERS_LOADED, EMBEDDER
544
- try:
545
- from sentence_transformers import SentenceTransformer
546
- chunks_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="chunks.json", repo_type="dataset", token=HF_TOKEN)
547
- meta_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="metadata.json", repo_type="dataset", token=HF_TOKEN)
548
- emb_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="embeddings.npy", repo_type="dataset", token=HF_TOKEN)
549
- with open(chunks_path) as f: CHUNKS = json.load(f)
550
- with open(meta_path) as f: METADATA = json.load(f)
551
- EMBEDDINGS = np.load(emb_path)
552
- EMBEDDER = SentenceTransformer("all-MiniLM-L6-v2")
553
- PAPERS_LOADED = True
554
- print("Papers loaded: " + str(len(CHUNKS)) + " chunks")
555
- except Exception as e:
556
- print("Paper load error: " + str(e))
557
-
558
- def load_cardiolab_model():
559
- global CARDIOLAB_TOKENIZER, CARDIOLAB_LLM, CARDIOLAB_MODEL_LOADED
560
- try:
561
- import torch
562
- from transformers import AutoModelForCausalLM, AutoTokenizer
563
- print("Loading CardioLab fine-tuned model...")
564
- CARDIOLAB_TOKENIZER = AutoTokenizer.from_pretrained(CARDIOLAB_MODEL, token=HF_TOKEN)
565
- CARDIOLAB_TOKENIZER.pad_token = CARDIOLAB_TOKENIZER.eos_token
566
- device = "cuda" if torch.cuda.is_available() else "cpu"
567
- CARDIOLAB_LLM = AutoModelForCausalLM.from_pretrained(
568
- CARDIOLAB_MODEL, token=HF_TOKEN,
569
- torch_dtype=torch.float16 if device == "cuda" else torch.float32,
570
- device_map="auto" if device == "cuda" else None,
571
- low_cpu_mem_usage=True
572
- )
573
- CARDIOLAB_MODEL_LOADED = True
574
- print("CardioLab model loaded!")
575
- except Exception as e:
576
- print("CardioLab model error: " + str(e))
577
-
578
- load_papers()
579
- load_cardiolab_model()
580
-
581
- def search_papers(query, n=4):
582
- if not PAPERS_LOADED or EMBEDDINGS is None or EMBEDDER is None:
583
- return "", []
584
- try:
585
- q_emb = EMBEDDER.encode([query])
586
- norms = np.linalg.norm(EMBEDDINGS, axis=1, keepdims=True)
587
- emb_norm = EMBEDDINGS / (norms + 1e-10)
588
- q_norm = q_emb / (np.linalg.norm(q_emb) + 1e-10)
589
- scores = (emb_norm @ q_norm.T).flatten()
590
- top_idx = np.argsort(scores)[::-1][:n]
591
- context = ""
592
- results = []
593
- seen = set()
594
- for idx in top_idx:
595
- chunk = CHUNKS[idx]
596
- meta = METADATA[idx]
597
- score = float(scores[idx])
598
- if score > 0.25:
599
- results.append({"chunk": chunk, "paper": meta["paper"], "score": score})
600
- if meta["paper"] not in seen:
601
- context += chr(10) + "=== FROM: " + meta["paper"] + " ===" + chr(10)
602
- seen.add(meta["paper"])
603
- context += chunk[:500] + chr(10)
604
- return context, results
605
- except Exception as e:
606
- return "", []
607
-
608
- # ── SESSION MANAGEMENT ─────────────────────────────────────
609
  def load_all_sessions():
610
  if not HF_TOKEN: return {}
611
  try:
612
- path = hf_hub_download(repo_id=HISTORY_REPO, filename="chat_history.json", repo_type="dataset", token=HF_TOKEN)
613
- with open(path) as f: return json.load(f)
614
- except: return {}
 
 
 
 
 
 
 
 
615
 
616
  def save_all_sessions(sessions):
617
  if not HF_TOKEN: return False
618
  try:
619
- api2 = HfApi(token=HF_TOKEN)
620
- api2.upload_file(
621
- path_or_fileobj=json.dumps(sessions, indent=2).encode(),
 
622
  path_in_repo="chat_history.json",
623
- repo_id=HISTORY_REPO, repo_type="dataset",
624
- token=HF_TOKEN, commit_message="Update"
 
 
625
  )
626
  return True
627
- except: return False
 
 
628
 
629
  def get_session_list():
630
- s = load_all_sessions()
631
- return list(reversed(list(s.keys()))) if s else ["No saved sessions"]
632
-
633
- def save_session(history, name):
634
- if not history: return "Nothing to save", gr.update()
635
- if not name or not name.strip():
636
- name = "Chat " + datetime.now().strftime("%b %d %H:%M")
637
  sessions = load_all_sessions()
638
- sessions[name] = {"messages": history, "saved_at": datetime.now().isoformat()}
639
- ok = save_all_sessions(sessions)
640
- choices = get_session_list()
641
- return ("Saved: " + name if ok else "Save failed"), gr.update(choices=choices, value=name)
642
 
643
- def load_session(name):
644
- if not name or "No saved" in name: return [], "Select a session"
 
645
  sessions = load_all_sessions()
646
- return (sessions[name]["messages"], "Loaded: " + name) if name in sessions else ([], "Not found")
647
-
648
- def delete_session(name):
649
- if not name or "No saved" in name: return "Select a session", gr.update()
 
 
 
 
 
 
650
  sessions = load_all_sessions()
651
- if name in sessions:
652
- del sessions[name]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
653
  save_all_sessions(sessions)
654
- choices = get_session_list()
655
- return "Deleted: " + name, gr.update(choices=choices, value=choices[0] if choices else None)
656
- return "Not found", gr.update()
657
-
658
- def new_chat(): return [], "", "New chat started"
659
 
660
- # ── SEARCH ────────────────────────────────────────────────
661
- def get_pubmed_chat(query, n=3):
662
  try:
663
- r = requests.get(
664
- "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
665
- params={"db":"pubmed","term":query+" AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR CKD)","retmax":n,"retmode":"json","sort":"date","field":"tiab"},
666
- timeout=10
667
- )
668
  ids = r.json()["esearchresult"]["idlist"]
669
- return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids]) if ids else ""
 
670
  except: return ""
671
 
672
- def expand_query_ai(query):
673
- if not GROQ_KEY: return query
674
- try:
675
- client = Groq(api_key=GROQ_KEY)
676
- resp = client.chat.completions.create(
677
- model="llama-3.1-8b-instant",
678
- messages=[
679
- {"role":"system","content":"Biomedical PubMed expert. Convert to MeSH terms for heart valves hemodynamics PIV thrombogenicity FSI microfluidics CKD. Return ONLY terms."},
680
- {"role":"user","content":"Optimize: " + query}
681
- ],
682
- max_tokens=80
683
- )
684
- return resp.choices[0].message.content.strip() or query
685
- except: return query
686
-
687
- def quick_search(query, search_model="Llama 3.3 70B (Best)"):
688
  if not query.strip(): return "Please enter a topic."
689
- expanded = expand_query_ai(query)
690
- results = []
691
- try:
692
- forced = expanded + " AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CKD)"
693
- r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
694
- params={"db":"pubmed","term":forced,"retmax":8,"retmode":"json","sort":"date","field":"tiab"},timeout=12)
695
- ids = r.json()["esearchresult"]["idlist"]
696
- if ids:
697
- r2 = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
698
- params={"db":"pubmed","id":",".join(ids),"retmode":"xml","rettype":"abstract"},timeout=12)
699
- import xml.etree.ElementTree as ET
700
- root = ET.fromstring(r2.content)
701
- for article in root.findall(".//PubmedArticle"):
702
- try:
703
- title = article.find(".//ArticleTitle").text or "No title"
704
- pmid = article.find(".//PMID").text or ""
705
- year_el = article.find(".//PubDate/Year")
706
- year = year_el.text if year_el is not None else ""
707
- results.append({"source":"PubMed","title":str(title),"year":year,"url":"https://pubmed.ncbi.nlm.nih.gov/"+pmid})
708
- except: continue
709
- except: pass
710
  try:
711
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
712
- params={"query":expanded,"limit":6,"fields":"title,year,url,citationCount"},timeout=12)
713
- for p in r.json().get("data",[]):
714
- year = p.get("year",0) or 0
715
- if int(year) >= 2015:
716
- results.append({"source":"Scholar","title":p.get("title",""),"year":str(year),"url":p.get("url",""),"citations":str(p.get("citationCount",0))})
717
- except: pass
718
- out = "QUERY: " + query + chr(10) + "AI EXPANDED: " + expanded + chr(10) + "="*45 + chr(10) + chr(10)
719
- groups = {"PubMed":[],"Scholar":[]}
720
- seen = set()
721
- for r in results:
722
- key = r["title"][:50].lower()
723
- if key not in seen and r.get("url"):
724
- seen.add(key); groups.get(r["source"],[]).append(r)
725
- for source, papers in groups.items():
726
- if not papers: continue
727
- out += "--- " + source + " ---" + chr(10)
728
- for p in papers[:8]:
729
- out += p["title"][:85] + " (" + p["year"] + ")" + chr(10)
730
- out += " " + p.get("url","") + chr(10) + chr(10)
731
- out += "--- SJSU ScholarWorks ---" + chr(10)
732
- out += "https://scholarworks.sjsu.edu/do/search/?q=" + requests.utils.quote(query) + "&context=6781027"
733
- return out
734
-
735
- # ── CHAT ───────────────────────────────────────────────────
736
- def answer_with_cardiolab_model(question, paper_context=""):
737
- if not CARDIOLAB_MODEL_LOADED: return None
738
- try:
739
- import torch
740
- system = "You are CardioLab AI for SJSU Biomedical Engineering."
741
- if paper_context:
742
- system += " Use these SJSU research papers: " + paper_context[:400]
743
- prompt = "<|system|>" + system + "</s><|user|>" + question + "</s><|assistant|>"
744
- inputs = CARDIOLAB_TOKENIZER(prompt, return_tensors="pt", truncation=True, max_length=512)
745
- device = next(CARDIOLAB_LLM.parameters()).device
746
- inputs = {k: v.to(device) for k, v in inputs.items()}
747
- with torch.no_grad():
748
- outputs = CARDIOLAB_LLM.generate(
749
- **inputs, max_new_tokens=200, do_sample=True,
750
- temperature=0.3, pad_token_id=CARDIOLAB_TOKENIZER.eos_token_id
751
- )
752
- response = CARDIOLAB_TOKENIZER.decode(outputs[0], skip_special_tokens=True)
753
- answer = response.split("<|assistant|>")[-1].strip() if "<|assistant|>" in response else response[-300:].strip()
754
- return answer if len(answer) > 20 else None
755
- except Exception as e:
756
- print("CardioLab model error: " + str(e))
757
- return None
758
 
759
- def research_chat(message, history, chat_model="Llama 3.3 70B (Best)"):
760
- if not message.strip(): return "", history
761
- paper_context, paper_results = search_papers(message, n=4)
762
- if chat_model == "CardioLab Fine-tuned (SJSU)" and CARDIOLAB_MODEL_LOADED:
763
- answer = answer_with_cardiolab_model(message, paper_context)
764
- if answer:
765
- if paper_results:
766
- unique_papers = list(dict.fromkeys([r["paper"] for r in paper_results]))
767
- answer += chr(10) + chr(10) + "Sources from SJSU CardioLab papers:"
768
- for p in unique_papers[:3]:
769
- answer += chr(10) + " - " + p.replace(".pdf","").replace("_"," ")
770
- pubmed = get_pubmed_chat(message, n=2)
771
- if pubmed: answer += chr(10) + "PubMed: " + pubmed
772
- history.append({"role":"user","content":message})
773
- history.append({"role":"assistant","content":"[CardioLab Fine-tuned] " + answer})
774
- return "", history
775
  if not GROQ_KEY:
776
  history.append({"role":"user","content":message})
777
- history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
778
  return "", history
779
  try:
780
- model_id = CHAT_MODELS.get(chat_model, "llama-3.3-70b-versatile")
781
  client = Groq(api_key=GROQ_KEY)
782
- if paper_context:
783
- system_prompt = (
784
- "You are CardioLab AI for SJSU Biomedical Engineering. "
785
- "Answer using SJSU CardioLab research papers below. "
786
- "Always cite the paper name when using specific data." +
787
- chr(10) + chr(10) + "SJSU CARDIOLAB PAPERS:" + chr(10) + paper_context +
788
- chr(10) + chr(10) + "ADDITIONAL KNOWLEDGE: " + KNOWHOW
789
- )
790
- else:
791
- system_prompt = "You are CardioLab AI for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. " + KNOWHOW
792
- msgs = [{"role":"system","content":system_prompt}]
793
  for item in history:
794
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
795
  msgs.append({"role":"user","content":message})
796
- resp = client.chat.completions.create(model=model_id, messages=msgs, max_tokens=800)
797
  answer = resp.choices[0].message.content
798
- if paper_results:
799
- unique_papers = list(dict.fromkeys([r["paper"] for r in paper_results]))
800
- answer += chr(10) + chr(10) + "Sources from SJSU CardioLab papers:"
801
- for p in unique_papers[:3]:
802
- answer += chr(10) + " - " + p.replace(".pdf","").replace("_"," ")
803
- pubmed = get_pubmed_chat(message, n=2)
804
- if pubmed: answer += chr(10) + "PubMed: " + pubmed
805
  history.append({"role":"user","content":message})
806
  history.append({"role":"assistant","content":answer})
807
  return "", history
808
  except Exception as e:
809
  history.append({"role":"user","content":message})
810
- history.append({"role":"assistant","content":"Error: " + str(e)})
811
  return "", history
812
 
813
  def voice_chat(audio, history):
@@ -818,384 +167,263 @@ def voice_chat(audio, history):
818
  client = Groq(api_key=GROQ_KEY)
819
  with open(audio, "rb") as f:
820
  tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
821
- paper_context, _ = search_papers(tx.text, n=3)
822
- system = "You are CardioLab AI. " + KNOWHOW
823
- if paper_context:
824
- system = "You are CardioLab AI. Use these SJSU papers:" + chr(10) + paper_context + chr(10) + KNOWHOW
825
- msgs = [{"role":"system","content":system}]
826
  for item in history:
827
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
828
  msgs.append({"role":"user","content":tx.text})
829
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=msgs, max_tokens=500)
830
- history.append({"role":"user","content":"Voice: " + tx.text})
831
  history.append({"role":"assistant","content":resp.choices[0].message.content})
832
  return history
833
  except Exception as e:
834
- history.append({"role":"assistant","content":"Voice error: " + str(e)})
835
  return history
836
 
837
- # ── PHASE D ────────────────────────────────────────────────
838
- def generate_protocol(experiment_type, specific_params):
839
- if not GROQ_KEY: return "Error: Add GROQ_API_KEY to Space Settings."
840
- if not experiment_type: return "Please select an experiment type."
841
- try:
842
- client = Groq(api_key=GROQ_KEY)
843
- paper_context, _ = search_papers(experiment_type, n=4)
844
- lab_ctx = {
845
- "MCL": "Sylgard 184 PDMS 10:1 ratio 48hr cure. Tygon tubing. 70bpm 5L/min 80-120mmHg.",
846
- "PIV": "Green laser 532nm time-resolved. Normal velocity 0.5-2.0 m/s. Shear below 5 Pa.",
847
- "Thrombogenicity": "Arduino Uno stepper motor 48V. 150mL fresh blood. Sample at 0 20 40 60 min. Heska HT5. Measures TAT PF1.2 free hemoglobin platelets. TAT normal below 8 ng/mL. PF1.2 normal below 2.0 nmol/L.",
848
- "uPAD": "Whatman filter paper. Wax printer 120C. Picric acid alkaline solution. Jaffe reaction.",
849
- "FSI": "COMSOL Multiphysics ALE mesh. Blood 1060 kg/m3 0.0035 Pa.s. SJM bileaflet geometry.",
850
- }
851
- extra = next((v for k, v in lab_ctx.items() if k.lower() in experiment_type.lower()), "")
852
- system_msg = (
853
- "You are CardioLab AI protocol generator for SJSU Biomedical Engineering. "
854
- "Generate a COMPLETE detailed lab protocol with sections: "
855
- "1. OBJECTIVE 2. MATERIALS AND EQUIPMENT with exact quantities "
856
- "3. SAFETY CONSIDERATIONS 4. STEP-BY-STEP PROCEDURE numbered "
857
- "5. DATA COLLECTION 6. ANALYSIS METHOD "
858
- "7. EXPECTED RESULTS with CardioLab normal ranges 8. TROUBLESHOOTING "
859
- "Use exact SJSU CardioLab values."
860
- )
861
- user_msg = "Generate complete protocol for: " + experiment_type
862
- if specific_params and specific_params.strip():
863
- user_msg += chr(10) + "Parameters: " + specific_params
864
- if extra:
865
- user_msg += chr(10) + "CardioLab context: " + extra
866
- if paper_context:
867
- user_msg += chr(10) + "From SJSU papers: " + paper_context[:600]
868
- resp = client.chat.completions.create(
869
- model="llama-3.3-70b-versatile",
870
- messages=[{"role":"system","content":system_msg},{"role":"user","content":user_msg}],
871
- max_tokens=1200
872
- )
873
- return resp.choices[0].message.content
874
- except Exception as e:
875
- return "Error: " + str(e)
876
-
877
- def generate_report(data_description, experiment_type, results):
878
- if not GROQ_KEY: return "Error: Add GROQ_API_KEY to Space Settings."
879
- if not experiment_type: return "Please select a study type."
880
- try:
881
- client = Groq(api_key=GROQ_KEY)
882
- paper_context, _ = search_papers(experiment_type, n=3)
883
- system_msg = (
884
- "You are CardioLab AI report writer for SJSU Biomedical Engineering. "
885
- "Generate a professional research report with sections: "
886
- "1. ABSTRACT 150 words 2. INTRODUCTION background and objectives "
887
- "3. MATERIALS AND METHODS 4. RESULTS AND DISCUSSION "
888
- "5. CONCLUSION 6. RECOMMENDATIONS 7. REFERENCES cite SJSU CardioLab papers. "
889
- "Use specific values. Write in professional academic style."
890
- )
891
- user_msg = "Write research report for: " + experiment_type
892
- if data_description and data_description.strip():
893
- user_msg += chr(10) + "Description: " + data_description
894
- if results and results.strip():
895
- user_msg += chr(10) + "Results: " + results
896
- if paper_context:
897
- user_msg += chr(10) + "SJSU papers: " + paper_context[:600]
898
- resp = client.chat.completions.create(
899
- model="llama-3.3-70b-versatile",
900
- messages=[{"role":"system","content":system_msg},{"role":"user","content":user_msg}],
901
- max_tokens=1500
902
- )
903
- return resp.choices[0].message.content
904
- except Exception as e:
905
- return "Error: " + str(e)
906
-
907
- def generate_hypothesis(research_area, current_findings):
908
- if not GROQ_KEY: return "Error: Add GROQ_API_KEY to Space Settings."
909
- if not research_area: return "Please select a research area."
910
- try:
911
- client = Groq(api_key=GROQ_KEY)
912
- paper_context, _ = search_papers(research_area, n=3)
913
- system_msg = (
914
- "You are CardioLab AI research assistant for SJSU Biomedical Engineering. "
915
- "Generate 3 specific testable research hypotheses. For each provide: "
916
- "H0 null hypothesis, H1 alternative hypothesis, Scientific rationale, "
917
- "Suggested experiment, Expected outcome and measurable metrics. "
918
- "Base on SJSU CardioLab research."
919
- )
920
- user_msg = "Generate hypotheses for: " + research_area
921
- if current_findings and current_findings.strip():
922
- user_msg += chr(10) + "Current findings: " + current_findings
923
- if paper_context:
924
- user_msg += chr(10) + "SJSU papers: " + paper_context[:500]
925
- resp = client.chat.completions.create(
926
- model="llama-3.3-70b-versatile",
927
- messages=[{"role":"system","content":system_msg},{"role":"user","content":user_msg}],
928
- max_tokens=1000
929
- )
930
- return resp.choices[0].message.content
931
- except Exception as e:
932
- return "Error: " + str(e)
933
-
934
- # ── ANALYSIS TOOLS ─────────────────────────────────────────
935
  def analyze_upad_photo(image):
936
  if image is None: return None, "Upload a uPAD photo first."
937
  try:
938
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
939
- arr = np.array(img); h, w = arr.shape[:2]
940
- y1, y2, x1, x2 = int(h*0.35), int(h*0.65), int(w*0.35), int(w*0.65)
941
- zone = arr[y1:y2, x1:x2]
942
- R = float(np.mean(zone[:,:,0]))
943
- G = float(np.mean(zone[:,:,1]))
944
- B = float(np.mean(zone[:,:,2]))
945
  c = max(0, round(0.018*(R-B)-0.3, 2))
946
- if c < 1.2: s, a = "Normal", "Monitor annually."
947
- elif c < 1.5: s, a = "Borderline", "Repeat in 3 months."
948
- elif c < 3.0: s, a = "Stage 2 CKD", "Consult nephrologist."
949
- elif c < 6.0: s, a = "Stage 3-4 CKD", "Immediate consultation."
950
- else: s, a = "Stage 5 CKD", "Emergency care."
951
- ri = img.copy()
952
  import PIL.ImageDraw as D
953
- D.Draw(ri).rectangle([x1, y1, x2, y2], outline=(0,255,0), width=3)
954
- result = ("uPAD ANALYSIS" + chr(10) +
955
- "R:" + str(round(R,1)) + " G:" + str(round(G,1)) + " B:" + str(round(B,1)) + chr(10) +
956
- "Creatinine: " + str(c) + " mg/dL" + chr(10) +
957
- "Stage: " + s + chr(10) + "Action: " + a)
958
- return ri, result
959
- except Exception as e:
960
- return None, "Error: " + str(e)
961
-
962
- def mk_chart(fn, title, bg, fg, gc, ac, pb):
963
- fig2, ax = plt.subplots(figsize=(8,5))
964
- fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
965
- fn(ax)
966
- ax.set_title(title, color=fg, fontweight="bold", fontsize=13, pad=8)
967
- ax.tick_params(colors=ac, labelsize=10)
968
- ax.grid(True, alpha=0.3, color=gc, linestyle="--")
969
- for sp in ["top","right"]: ax.spines[sp].set_visible(False)
970
- for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
971
- plt.tight_layout()
972
- buf = io.BytesIO()
973
- plt.savefig(buf, format="png", facecolor=bg, bbox_inches="tight", dpi=130)
974
- buf.seek(0)
975
- res = Image.open(buf).copy()
976
- plt.close()
977
- return res
978
 
979
  def analyze_piv_csv(file, theme="White"):
980
- if file is None: return None, None, None, None, "Upload PIV CSV first."
981
  try:
982
  df = pd.read_csv(file.name)
983
- cols = [c.lower().strip() for c in df.columns]; df.columns = cols
 
984
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
985
- if not num_cols: return None, None, None, None, "No numeric columns."
986
- bg = "#fff" if theme=="White" else "#0a1628"
987
  fg = "#1a202c" if theme=="White" else "white"
988
  gc = "#e2e8f0" if theme=="White" else "#2d4a8a"
989
  ac = "#4a5568" if theme=="White" else "#a8b2d8"
990
  pb = "#f7fafc" if theme=="White" else "#132340"
991
  x = np.arange(len(df))
992
  vc = next((c for c in cols if any(k in c for k in ["vel","speed","v_mag"])), num_cols[0] if num_cols else None)
993
- sc2 = next((c for c in cols if any(k in c for k in ["shear","stress","tau","wss"])), num_cols[1] if len(num_cols)>1 else None)
994
  tc = next((c for c in cols if "time" in c or "frame" in c), None)
995
  xv = df[tc] if tc else x
 
 
 
 
 
 
 
 
 
 
 
 
996
  def pv(ax):
997
  if vc:
998
- ax.plot(xv, df[vc], color="#c1121f", linewidth=2.5, marker="o", markersize=5)
999
- ax.fill_between(xv, df[vc], alpha=0.15, color="#c1121f")
1000
- ax.axhline(y=2.0, color="#f59e0b", linestyle="--", linewidth=2, label="Risk 2.0 m/s")
1001
- ax.set_ylabel("Velocity (m/s)", color=ac)
1002
- ax.legend(fontsize=9, labelcolor=fg, facecolor=pb)
 
1003
  def ps(ax):
1004
- if sc2:
1005
  xp = xv.values if tc else x
1006
- ax.plot(xp, df[sc2], color="#0057a8", linewidth=2.5, marker="s", markersize=5)
1007
- ax.fill_between(xp, df[sc2], alpha=0.15, color="#0057a8")
1008
- ax.axhline(y=5, color="#f59e0b", linestyle="--", linewidth=2, label="Caution 5 Pa")
1009
- ax.axhline(y=10, color="#c1121f", linestyle="--", linewidth=2, label="Risk 10 Pa")
1010
- ax.set_ylabel("Shear (Pa)", color=ac)
1011
- ax.legend(fontsize=9, labelcolor=fg, facecolor=pb)
 
1012
  def psc(ax):
1013
- if vc and sc2:
1014
- s3 = ax.scatter(df[vc], df[sc2], c=x, cmap="RdYlGn_r", s=90, edgecolors=fg, linewidth=0.5, zorder=5)
1015
- cb = plt.colorbar(s3, ax=ax, label="Time")
1016
- cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
1017
- ax.axvline(x=2.0, color="#f59e0b", linestyle="--", linewidth=2)
1018
- ax.axhline(y=10, color="#c1121f", linestyle="--", linewidth=2)
1019
- ax.set_xlabel("Velocity (m/s)", color=ac); ax.set_ylabel("Shear (Pa)", color=ac)
 
1020
  def psum(ax):
1021
- ax.axis("off"); risk = []
1022
- st = "CLINICAL SUMMARY" + chr(10) + "="*20 + chr(10) + chr(10)
1023
  for col in num_cols[:3]:
1024
- mn = round(df[col].mean(), 3); mx = round(df[col].max(), 3)
1025
- st += col[:14] + ":" + chr(10) + " Mean: " + str(mn) + chr(10) + " Max: " + str(mx) + chr(10) + chr(10)
1026
- if "vel" in col and mx > 2.0: risk.append("HIGH VELOCITY")
1027
- if "shear" in col and mx > 10: risk.append("HIGH SHEAR")
1028
- bc = "#c1121f" if risk else "#2ecc71"
1029
- st += "="*20 + chr(10) + ("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
1030
- ax.text(0.05, 0.97, st, transform=ax.transAxes, color=fg, fontsize=10, va="top",
1031
- fontfamily="monospace", bbox=dict(boxstyle="round,pad=0.8", facecolor=pb, edgecolor=bc, linewidth=2.5))
1032
- i1 = mk_chart(pv, "Velocity Profile", bg, fg, gc, ac, pb)
1033
- i2 = mk_chart(ps, "Wall Shear Stress", bg, fg, gc, ac, pb)
1034
- i3 = mk_chart(psc, "Velocity vs Shear", bg, fg, gc, ac, pb)
1035
- i4 = mk_chart(psum, "Clinical Summary", bg, fg, gc, ac, pb)
1036
- ai = ""
 
 
1037
  if GROQ_KEY:
1038
  try:
1039
- client = Groq(api_key=GROQ_KEY)
1040
- resp = client.chat.completions.create(
1041
- model="llama-3.3-70b-versatile",
1042
- messages=[{"role":"system","content":"PIV expert SJSU CardioLab."},
1043
- {"role":"user","content":"PIV from 27mm SJM Regent:" + chr(10) + df.describe().to_string()[:500]}],
1044
- max_tokens=250
1045
- )
1046
- ai = chr(10) + "AI: " + resp.choices[0].message.content
1047
  except: pass
1048
- return i1, i2, i3, i4, "PIV: " + str(len(df)) + " rows" + ai
1049
- except Exception as e:
1050
- return None, None, None, None, "Error: " + str(e)
1051
 
1052
  def analyze_tgt_csv(file, theme="White"):
1053
- if file is None: return None, None, None, None, "Upload TGT CSV first."
1054
  try:
1055
  df = pd.read_csv(file.name)
1056
- cols = [c.lower().strip() for c in df.columns]; df.columns = cols
 
1057
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
1058
- bg = "#fff" if theme=="White" else "#0a1628"
1059
- fg = "#1a202c" if theme=="White" else "white"
1060
- gc = "#e2e8f0" if theme=="White" else "#2d4a8a"
1061
- ac = "#4a5568" if theme=="White" else "#a8b2d8"
1062
- pb = "#f7fafc" if theme=="White" else "#132340"
1063
- tc = next((c for c in cols if "time" in c or "min" in c), None)
1064
- tatc = next((c for c in cols if "tat" in c), num_cols[0] if num_cols else None)
1065
- pfc = next((c for c in cols if "pf" in c), num_cols[1] if len(num_cols)>1 else None)
1066
- hc = next((c for c in cols if "hemo" in c), num_cols[2] if len(num_cols)>2 else None)
1067
- plc = next((c for c in cols if "platelet" in c or "plt" in c), num_cols[3] if len(num_cols)>3 else None)
1068
- def mk2(dc, color, yl, lim, ll, title, bar=False):
1069
- def fn(ax):
1070
- if dc and dc in df.columns:
1071
- xp = df[tc].values if tc else range(len(df))
1072
- yp = df[dc].values
1073
- if bar:
1074
- bs = ax.bar(range(len(yp)), yp, color=color, alpha=0.85, edgecolor=bg, width=0.6)
1075
- for b, v in zip(bs, yp):
1076
- ax.text(b.get_x()+b.get_width()/2, b.get_height()+0.5, str(round(v,1)),
1077
- ha="center", va="bottom", color=fg, fontsize=10, fontweight="bold")
1078
- else:
1079
- ax.plot(xp, yp, color=color, linewidth=3, marker="o", markersize=8)
1080
- ax.fill_between(xp, yp, alpha=0.15, color=color)
1081
- for xi, yi in zip(xp, yp):
1082
- ax.annotate(str(round(yi,1)), (xi, yi), textcoords="offset points",
1083
- xytext=(0,10), ha="center", color=fg, fontsize=10, fontweight="bold")
1084
- ax.axhline(y=lim, color="#f59e0b", linestyle="--", linewidth=2.5, label=ll)
1085
- ax.legend(fontsize=10, labelcolor=fg, facecolor=pb)
1086
- ax.set_ylabel(yl, color=ac)
1087
- mv = round(float(np.max(yp)), 2)
1088
- ax.set_title(title + chr(10) + "Max: " + str(mv) + " - " + ("HIGH" if mv>lim else "NORMAL"),
1089
- color=fg, fontweight="bold", fontsize=12)
1090
- return mk_chart(fn, title, bg, fg, gc, ac, pb)
1091
- i1 = mk2(tatc, "#c1121f", "TAT (ng/mL)", 8, "Normal: 8", "TAT")
1092
- i2 = mk2(pfc, "#0057a8", "PF1.2", 2.0, "Normal: 2.0", "PF1.2")
1093
- i3 = mk2(hc, "#2ecc71", "Free Hgb (mg/L)", 20, "Normal: 20", "Free Hemoglobin", bar=True)
1094
- i4 = mk2(plc, "#e8a020", "Platelets", 150, "Normal>150", "Platelets")
1095
- ai = ""
 
1096
  if GROQ_KEY:
1097
  try:
1098
- client = Groq(api_key=GROQ_KEY)
1099
- resp = client.chat.completions.create(
1100
- model="llama-3.3-70b-versatile",
1101
- messages=[{"role":"system","content":"Hematology expert. Thrombogenicity risk."},
1102
- {"role":"user","content":"TGT:" + chr(10) + df.describe().to_string()[:500]}],
1103
- max_tokens=250
1104
- )
1105
- ai = chr(10) + "AI: " + resp.choices[0].message.content
1106
  except: pass
1107
- return i1, i2, i3, i4, "TGT: " + str(len(df)) + " rows" + ai
1108
- except Exception as e:
1109
- return None, None, None, None, "Error: " + str(e)
1110
 
1111
  def generate_image(prompt):
1112
- if not prompt.strip(): return None, "Enter description.", ""
1113
- if not HF_TOKEN: return None, "Add HF_TOKEN to Space secrets.", ""
1114
  try:
1115
- enhanced, desc = prompt, ""
1116
  if GROQ_KEY:
1117
  try:
1118
- client = Groq(api_key=GROQ_KEY)
1119
- resp = client.chat.completions.create(
1120
- model="llama-3.3-70b-versatile",
1121
  messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
1122
- {"role":"user","content":"Biomedical image: " + prompt}],
1123
- max_tokens=200
1124
- )
1125
- full = resp.choices[0].message.content
1126
  if "DESCRIPTION:" in full and "PROMPT:" in full:
1127
- desc = full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
1128
- enhanced = full.split("PROMPT:")[1].strip()
1129
  except: pass
1130
- headers = {"Authorization": "Bearer " + HF_TOKEN, "Content-Type": "application/json"}
1131
- for url in [
1132
- "https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell",
1133
- "https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"
1134
- ]:
1135
  try:
1136
- r = requests.post(url, headers=headers,
1137
- json={"inputs":enhanced,"parameters":{"num_inference_steps":8}}, timeout=60)
1138
- if r.status_code == 200:
1139
- return Image.open(io.BytesIO(r.content)), "Generated!", desc
1140
  except: continue
1141
- return None, "Models busy.", desc
1142
- except Exception as e:
1143
- return None, "Error: " + str(e), ""
1144
 
1145
- def piv_manual(v, s, h):
1146
- vr = "HIGH-stenosis" if float(v) > 2.0 else "NORMAL"
1147
- sr = "HIGH-thrombosis" if float(s) > 10 else "ELEVATED" if float(s) > 5 else "NORMAL"
1148
- return "Velocity: " + str(v) + " m/s - " + vr + chr(10) + "Shear: " + str(s) + " Pa - " + sr + chr(10) + "HR: " + str(h) + " bpm"
1149
 
1150
- def tgt_manual(t, p, h, pl, tm):
1151
- risk = sum([float(t)>15, float(p)>2.0, float(h)>50, float(pl)<150])
1152
- return ("TAT:" + str(t) + " PF1.2:" + str(p) + chr(10) +
1153
- "Hemo:" + str(h) + " Plt:" + str(pl) + chr(10) +
1154
- "RESULT: " + ("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK"))
1155
 
1156
- # ── BUILD UI ───────────────────────────────────────────────
1157
- with gr.Blocks(title="CardioLab AI v39 - SJSU", css=CSS) as demo:
1158
- gr.HTML(HEADER)
1159
- gr.HTML(STATUS_BANNER)
1160
 
1161
  with gr.Tabs():
1162
 
1163
  with gr.Tab("Chat"):
 
1164
  with gr.Row():
1165
- with gr.Column(scale=1, min_width=200):
1166
- gr.HTML(SIDEBAR_HTML)
1167
- new_chat_btn = gr.Button("New Chat", variant="secondary")
1168
- session_dropdown = gr.Dropdown(choices=get_session_list(), label="Saved Sessions", interactive=True, allow_custom_value=True)
1169
- load_btn = gr.Button("Load Session", variant="primary")
1170
- session_name_box = gr.Textbox(placeholder="Session name...", label="", lines=1, container=False)
1171
- with gr.Row():
1172
- save_btn = gr.Button("Save", variant="primary", scale=2)
1173
- delete_btn = gr.Button("Del", variant="secondary", scale=1)
1174
- session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
1175
- with gr.Column(scale=4):
1176
- chatbot = gr.Chatbot(label="", height=460, show_label=False, container=False)
1177
  with gr.Row():
1178
- msg_box = gr.Textbox(
1179
- placeholder="Ask anything — AI searches 16 SJSU papers + PubMed...",
1180
- label="", lines=2, scale=4, container=False
1181
- )
1182
- with gr.Column(scale=1, min_width=160):
1183
- chat_model_dd = gr.Radio(
1184
- choices=list(CHAT_MODELS.keys()),
1185
- value="Llama 3.3 70B (Best)", label="AI Model"
1186
- )
1187
  send_btn = gr.Button("Send", variant="primary")
1188
  clear_btn = gr.Button("Clear", variant="secondary")
1189
- send_btn.click(research_chat, inputs=[msg_box, chatbot, chat_model_dd], outputs=[msg_box, chatbot])
1190
- msg_box.submit(research_chat, inputs=[msg_box, chatbot, chat_model_dd], outputs=[msg_box, chatbot])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1191
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
1192
- new_chat_btn.click(new_chat, outputs=[chatbot, msg_box, session_status])
1193
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
1194
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
1195
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
1196
 
1197
  with gr.Tab("Voice"):
1198
- voice_chatbot = gr.Chatbot(label="", height=360, show_label=False)
1199
  audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
1200
  with gr.Row():
1201
  voice_btn = gr.Button("Ask by Voice", variant="primary")
@@ -1204,177 +432,93 @@ with gr.Blocks(title="CardioLab AI v39 - SJSU", css=CSS) as demo:
1204
  voice_clear.click(lambda: [], outputs=voice_chatbot)
1205
 
1206
  with gr.Tab("Papers"):
1207
- gr.Markdown("### Search PubMed + Semantic Scholar + SJSU ScholarWorks")
1208
  with gr.Row():
1209
- search_input = gr.Textbox(
1210
- placeholder="e.g. bileaflet mechanical heart valve thrombogenicity hemodynamics",
1211
- label="Research Topic", scale=3
1212
- )
1213
- search_model_dd = gr.Dropdown(
1214
- choices=list(CHAT_MODELS.keys()),
1215
- value="Llama 3.3 70B (Best)", label="AI Model", scale=1
1216
- )
1217
  search_btn = gr.Button("Search", variant="primary", scale=1)
1218
- search_output = gr.Textbox(label="Results", lines=22)
1219
- search_btn.click(quick_search, inputs=[search_input, search_model_dd], outputs=search_output)
1220
- search_input.submit(quick_search, inputs=[search_input, search_model_dd], outputs=search_output)
1221
 
1222
  with gr.Tab("PIV CSV"):
 
1223
  with gr.Row():
1224
- piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
1225
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
1226
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
1227
- piv_result = gr.Textbox(label="AI Analysis", lines=4)
1228
  with gr.Row():
1229
- piv_c1 = gr.Image(label="Velocity", type="pil")
1230
  piv_c2 = gr.Image(label="Shear Stress", type="pil")
1231
  with gr.Row():
1232
- piv_c3 = gr.Image(label="Vel vs Shear", type="pil")
1233
  piv_c4 = gr.Image(label="Clinical Summary", type="pil")
1234
- piv_btn.click(analyze_piv_csv, inputs=[piv_file, piv_theme], outputs=[piv_c1, piv_c2, piv_c3, piv_c4, piv_result])
1235
 
1236
  with gr.Tab("TGT CSV"):
 
1237
  with gr.Row():
1238
- tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
1239
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
1240
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
1241
- tgt_result = gr.Textbox(label="AI Assessment", lines=4)
1242
  with gr.Row():
1243
- tgt_c1 = gr.Image(label="TAT", type="pil")
1244
- tgt_c2 = gr.Image(label="PF1.2", type="pil")
1245
  with gr.Row():
1246
- tgt_c3 = gr.Image(label="Hemoglobin", type="pil")
1247
- tgt_c4 = gr.Image(label="Platelets", type="pil")
1248
- tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file, tgt_theme], outputs=[tgt_c1, tgt_c2, tgt_c3, tgt_c4, tgt_result])
1249
 
1250
- with gr.Tab("uPAD"):
 
1251
  with gr.Row():
1252
  with gr.Column():
1253
- photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=260)
1254
  analyze_btn = gr.Button("Analyze uPAD", variant="primary")
1255
  with gr.Column():
1256
- photo_img = gr.Image(label="Detection Zone", type="pil", height=260)
1257
- photo_text = gr.Textbox(label="CKD Result", lines=8)
1258
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
1259
- with gr.Row():
1260
- r = gr.Number(label="R", value=210)
1261
- g = gr.Number(label="G", value=140)
1262
- b = gr.Number(label="B", value=80)
1263
- out3 = gr.Textbox(label="Manual Result", lines=3)
1264
- gr.Button("Analyze RGB", variant="secondary").click(
1265
- lambda r, g, b: (
1266
- "Creatinine: " + str(max(0, round(0.02*(r-b)-0.5, 2))) + " mg/dL" + chr(10) +
1267
- ("Normal" if max(0, round(0.02*(r-b)-0.5, 2)) < 1.2
1268
- else "Borderline" if max(0, round(0.02*(r-b)-0.5, 2)) < 1.5
1269
- else "CKD")
1270
- ),
1271
- inputs=[r, g, b], outputs=out3
1272
- )
1273
 
1274
  with gr.Tab("AI Image"):
1275
  with gr.Row():
1276
- img_prompt = gr.Textbox(
1277
- placeholder="e.g. 27mm bileaflet mechanical heart valve cross section",
1278
- label="Describe image", lines=2, scale=4
1279
- )
1280
  with gr.Column(scale=1):
1281
  img_btn = gr.Button("Generate", variant="primary")
1282
  img_status = gr.Textbox(label="Status", lines=1)
1283
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
1284
- img_output = gr.Image(label="Generated Image", type="pil", height=400)
1285
- img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output, img_status, img_desc])
1286
 
1287
  with gr.Tab("PIV Manual"):
1288
  with gr.Row():
1289
  with gr.Column():
1290
- v = gr.Number(label="Max Velocity m/s", value=1.8)
1291
- s = gr.Number(label="Wall Shear Pa", value=6.5)
1292
- h = gr.Number(label="Heart Rate bpm", value=72)
1293
- piv_out = gr.Textbox(label="Result", lines=4)
1294
- gr.Button("Analyze PIV", variant="primary").click(piv_manual, inputs=[v, s, h], outputs=piv_out)
1295
 
1296
  with gr.Tab("TGT Manual"):
1297
  with gr.Row():
1298
  with gr.Column():
1299
- t1 = gr.Number(label="TAT ng/mL", value=18)
1300
- t2 = gr.Number(label="PF1.2", value=2.5)
1301
- t3 = gr.Number(label="Hemoglobin mg/L", value=60)
1302
- t4 = gr.Number(label="Platelets", value=140)
1303
- t5 = gr.Number(label="Time min", value=40)
1304
- out2 = gr.Textbox(label="Result", lines=6)
1305
- gr.Button("Analyze TGT", variant="primary").click(tgt_manual, inputs=[t1, t2, t3, t4, t5], outputs=out2)
1306
-
1307
- with gr.Tab("Protocol Generator"):
1308
- gr.Markdown("### Generate complete lab protocols from SJSU CardioLab knowledge")
1309
- with gr.Row():
1310
- with gr.Column(scale=1):
1311
- proto_type = gr.Dropdown(
1312
- choices=[
1313
- "MCL Setup", "PIV Experiment",
1314
- "Thrombogenicity Tester Blood Clotting Test",
1315
- "uPAD Fabrication", "uPAD Creatinine Test",
1316
- "FSI COMSOL Simulation", "Valve Testing"
1317
- ],
1318
- value="Thrombogenicity Tester Blood Clotting Test",
1319
- label="Experiment Type"
1320
- )
1321
- proto_params = gr.Textbox(
1322
- placeholder="e.g. 27mm SJM valve 70bpm porcine blood",
1323
- label="Specific Parameters", lines=2
1324
- )
1325
- proto_btn = gr.Button("Generate Protocol", variant="primary")
1326
- with gr.Column(scale=2):
1327
- proto_output = gr.Textbox(label="Generated Protocol", lines=28)
1328
- proto_btn.click(generate_protocol, inputs=[proto_type, proto_params], outputs=proto_output)
1329
-
1330
- with gr.Tab("Report Writer"):
1331
- gr.Markdown("### Generate professional research reports from your data")
1332
  with gr.Row():
1333
- with gr.Column(scale=1):
1334
- report_exp = gr.Dropdown(
1335
- choices=[
1336
- "MCL PIV Flow Analysis", "TGT Thrombogenicity Study",
1337
- "uPAD CKD Detection", "FSI Simulation Study",
1338
- "Heart Valve Comparison"
1339
- ],
1340
- value="TGT Thrombogenicity Study", label="Study Type"
1341
- )
1342
- report_desc = gr.Textbox(
1343
- placeholder="e.g. TGT with 27mm SJM bileaflet at 70bpm 150mL porcine blood",
1344
- label="Experiment Description", lines=3
1345
- )
1346
- report_results = gr.Textbox(
1347
- placeholder="e.g. TAT=12.3 ng/mL PF1.2=2.8 Hemo=45 Plt=142",
1348
- label="Your Results", lines=2
1349
- )
1350
- report_btn = gr.Button("Generate Report", variant="primary")
1351
- with gr.Column(scale=2):
1352
- report_output = gr.Textbox(label="Generated Report", lines=28)
1353
- report_btn.click(generate_report, inputs=[report_desc, report_exp, report_results], outputs=report_output)
1354
-
1355
- with gr.Tab("Hypothesis Generator"):
1356
- gr.Markdown("### Generate testable research hypotheses for CardioLab projects")
1357
- with gr.Row():
1358
- with gr.Column(scale=1):
1359
- hyp_area = gr.Dropdown(
1360
- choices=[
1361
- "Bileaflet MHV Thrombogenicity",
1362
- "uPAD CKD Detection Accuracy",
1363
- "PIV Flow Characterization",
1364
- "FSI Simulation Validation",
1365
- "Valve Design Comparison"
1366
- ],
1367
- value="Bileaflet MHV Thrombogenicity", label="Research Area"
1368
- )
1369
- hyp_findings = gr.Textbox(
1370
- placeholder="Current observations from your experiments",
1371
- label="Current Findings", lines=3
1372
- )
1373
- hyp_btn = gr.Button("Generate Hypotheses", variant="primary")
1374
- with gr.Column(scale=2):
1375
- hyp_output = gr.Textbox(label="Research Hypotheses", lines=25)
1376
- hyp_btn.click(generate_hypothesis, inputs=[hyp_area, hyp_findings], outputs=hyp_output)
1377
-
1378
- gr.HTML(FOOTER_HTML)
1379
 
1380
  demo.launch()
 
9
  from PIL import Image
10
  from datetime import datetime
11
  from huggingface_hub import HfApi, hf_hub_download
12
+ from huggingface_hub.utils import EntryNotFoundError
13
 
14
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
15
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
16
  HISTORY_REPO = "Saicharan21/cardiolab-chat-history"
 
 
17
 
18
+ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
19
+ "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
20
+ "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
 
 
 
 
 
 
 
 
 
 
 
 
21
  "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
22
+ "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
 
 
23
 
24
  CSS = """
25
+ body, .gradio-container { background: #f0f4f8 !important; }
26
+ .tab-nav { background: #ffffff !important; border-bottom: 2px solid #e2e8f0 !important; padding: 4px 5px 0 5px !important; display: flex !important; flex-wrap: wrap !important; gap: 3px !important; }
27
+ .tab-nav button { background: #f7fafc !important; color: #2d3748 !important; border: 1px solid #e2e8f0 !important; border-radius: 6px 6px 0 0 !important; padding: 8px 10px !important; font-weight: 600 !important; font-size: 0.8em !important; white-space: nowrap !important; }
28
+ .tab-nav button:hover { background: #ebf4ff !important; color: #1a237e !important; }
29
+ .tab-nav button.selected { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: #ffffff !important; font-weight: 700 !important; }
30
+ button.primary { background: linear-gradient(135deg, #e63946 0%, #c1121f 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; }
31
+ button.secondary { background: #edf2f7 !important; color: #4a5568 !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
32
+ textarea, input[type=number], input[type=text] { background: #f7fafc !important; color: #1a202c !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
33
+ .message.user { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: white !important; }
34
+ .message.bot { background: #ebf4ff !important; color: #1a202c !important; border: 1px solid #bee3f8 !important; }
35
+ label span { color: #2b6cb0 !important; font-weight: 600 !important; font-size: 0.85em !important; text-transform: uppercase !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  """
37
 
38
+ # ── PERSISTENT HISTORY FUNCTIONS ──────────────────────────────────
39
+ def get_history_api():
40
+ if not HF_TOKEN: return None
41
+ return HfApi(token=HF_TOKEN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def load_all_sessions():
44
  if not HF_TOKEN: return {}
45
  try:
46
+ api = get_history_api()
47
+ path = hf_hub_download(
48
+ repo_id=HISTORY_REPO,
49
+ filename="chat_history.json",
50
+ repo_type="dataset",
51
+ token=HF_TOKEN
52
+ )
53
+ with open(path, "r") as f:
54
+ return json.load(f)
55
+ except Exception:
56
+ return {}
57
 
58
  def save_all_sessions(sessions):
59
  if not HF_TOKEN: return False
60
  try:
61
+ api = get_history_api()
62
+ content = json.dumps(sessions, indent=2)
63
+ api.upload_file(
64
+ path_or_fileobj=content.encode(),
65
  path_in_repo="chat_history.json",
66
+ repo_id=HISTORY_REPO,
67
+ repo_type="dataset",
68
+ token=HF_TOKEN,
69
+ commit_message="Update chat history"
70
  )
71
  return True
72
+ except Exception as e:
73
+ print("Save error:", e)
74
+ return False
75
 
76
  def get_session_list():
 
 
 
 
 
 
 
77
  sessions = load_all_sessions()
78
+ if not sessions:
79
+ return ["No saved sessions yet"]
80
+ return list(sessions.keys())
 
81
 
82
+ def load_session(session_name):
83
+ if not session_name or session_name == "No saved sessions yet":
84
+ return [], "No session loaded"
85
  sessions = load_all_sessions()
86
+ if session_name in sessions:
87
+ history = sessions[session_name]["messages"]
88
+ return history, "Loaded: " + session_name + " (" + str(len(history)) + " messages)"
89
+ return [], "Session not found"
90
+
91
+ def save_session(history, session_name):
92
+ if not history:
93
+ return "Nothing to save — chat is empty", gr.update()
94
+ if not session_name.strip():
95
+ session_name = "Session " + datetime.now().strftime("%Y-%m-%d %H:%M")
96
  sessions = load_all_sessions()
97
+ sessions[session_name] = {
98
+ "messages": history,
99
+ "saved_at": datetime.now().isoformat(),
100
+ "message_count": len(history)
101
+ }
102
+ success = save_all_sessions(sessions)
103
+ if success:
104
+ return "Saved: " + session_name, gr.update(choices=get_session_list(), value=session_name)
105
+ return "Save failed — check HF_TOKEN in Space secrets", gr.update()
106
+
107
+ def delete_session(session_name):
108
+ if not session_name or session_name == "No saved sessions yet":
109
+ return "No session selected", gr.update()
110
+ sessions = load_all_sessions()
111
+ if session_name in sessions:
112
+ del sessions[session_name]
113
  save_all_sessions(sessions)
114
+ new_list = get_session_list()
115
+ return "Deleted: " + session_name, gr.update(choices=new_list, value=new_list[0] if new_list else None)
116
+ return "Session not found", gr.update()
 
 
117
 
118
+ # ── CHAT FUNCTIONS ────────────────────────────────────────────────
119
+ def get_pubmed(query, n=5):
120
  try:
121
+ r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
122
+ params={"db":"pubmed","term":query+" AND (mechanical heart valve OR microfluidic OR CKD OR thrombogenicity)","retmax":n,"retmode":"json","sort":"date"},timeout=10)
 
 
 
123
  ids = r.json()["esearchresult"]["idlist"]
124
+ if not ids: return ""
125
+ return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
126
  except: return ""
127
 
128
+ def quick_search(query):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  if not query.strip(): return "Please enter a topic."
130
+ pubmed = get_pubmed(query, n=8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  try:
132
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
133
+ params={"query":query+" biomedical","limit":5,"fields":"title,year,url"},timeout=10)
134
+ papers = r.json().get("data",[])
135
+ scholar = chr(10).join([p.get("title","")[:80]+" ("+str(p.get("year",""))+")"+chr(10)+" "+p.get("url","") for p in papers if p.get("url","")])
136
+ except: scholar = ""
137
+ return "PUBMED:"+chr(10)+pubmed+chr(10)+chr(10)+"SCHOLAR:"+chr(10)+scholar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ def research_chat(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  if not GROQ_KEY:
141
  history.append({"role":"user","content":message})
142
+ history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings Secrets."})
143
  return "", history
144
  try:
 
145
  client = Groq(api_key=GROQ_KEY)
146
+ msgs = [{"role":"system","content":"You are CardioLab AI. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember full conversation. Never invent URLs. "+KNOWHOW}]
 
 
 
 
 
 
 
 
 
 
147
  for item in history:
148
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
149
  msgs.append({"role":"user","content":message})
150
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=700)
151
  answer = resp.choices[0].message.content
152
+ pubmed = get_pubmed(message, n=3)
153
+ if pubmed: answer += chr(10)+chr(10)+"PUBMED:"+chr(10)+pubmed
 
 
 
 
 
154
  history.append({"role":"user","content":message})
155
  history.append({"role":"assistant","content":answer})
156
  return "", history
157
  except Exception as e:
158
  history.append({"role":"user","content":message})
159
+ history.append({"role":"assistant","content":"Error: "+str(e)})
160
  return "", history
161
 
162
  def voice_chat(audio, history):
 
167
  client = Groq(api_key=GROQ_KEY)
168
  with open(audio, "rb") as f:
169
  tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
170
+ msgs = [{"role":"system","content":"You are CardioLab AI. "+KNOWHOW}]
 
 
 
 
171
  for item in history:
172
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
173
  msgs.append({"role":"user","content":tx.text})
174
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
175
+ history.append({"role":"user","content":"[Voice] "+tx.text})
176
  history.append({"role":"assistant","content":resp.choices[0].message.content})
177
  return history
178
  except Exception as e:
179
+ history.append({"role":"assistant","content":"Voice error: "+str(e)})
180
  return history
181
 
182
+ # ── ANALYSIS TOOLS ────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def analyze_upad_photo(image):
184
  if image is None: return None, "Upload a uPAD photo first."
185
  try:
186
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
187
+ arr = np.array(img)
188
+ h,w = arr.shape[:2]
189
+ y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
190
+ zone = arr[y1:y2,x1:x2]
191
+ R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
 
192
  c = max(0, round(0.018*(R-B)-0.3, 2))
193
+ if c<1.2: s,a="Normal","Monitor annually."
194
+ elif c<1.5: s,a="Borderline","Repeat in 3 months."
195
+ elif c<3.0: s,a="Stage 2 CKD","Consult nephrologist."
196
+ elif c<6.0: s,a="Stage 3-4 CKD","Immediate consultation."
197
+ else: s,a="Stage 5 CKD","Emergency care needed."
198
+ result_img = img.copy()
199
  import PIL.ImageDraw as D
200
+ draw = D.Draw(result_img)
201
+ draw.rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
202
+ return result_img, ("uPAD ANALYSIS"+chr(10)+""*22+chr(10)+
203
+ "R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+
204
+ "Orange Score: "+str(round(R-B,1))+chr(10)+""*22+chr(10)+
205
+ "CREATININE: "+str(c)+" mg/dL"+chr(10)+"CKD STAGE: "+s+chr(10)+
206
+ "ACTION: "+a+chr(10)+"Confirm: Heska Element HT5")
207
+ except Exception as e: return None, "Error: "+str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  def analyze_piv_csv(file, theme="White"):
210
+ if file is None: return None,None,None,None,"Upload a PIV CSV file first."
211
  try:
212
  df = pd.read_csv(file.name)
213
+ cols = [c.lower().strip() for c in df.columns]
214
+ df.columns = cols
215
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
216
+ if not num_cols: return None,None,None,None,"No numeric columns found."
217
+ bg = "#ffffff" if theme=="White" else "#0a1628"
218
  fg = "#1a202c" if theme=="White" else "white"
219
  gc = "#e2e8f0" if theme=="White" else "#2d4a8a"
220
  ac = "#4a5568" if theme=="White" else "#a8b2d8"
221
  pb = "#f7fafc" if theme=="White" else "#132340"
222
  x = np.arange(len(df))
223
  vc = next((c for c in cols if any(k in c for k in ["vel","speed","v_mag"])), num_cols[0] if num_cols else None)
224
+ sc = next((c for c in cols if any(k in c for k in ["shear","stress","tau","wss"])), num_cols[1] if len(num_cols)>1 else None)
225
  tc = next((c for c in cols if "time" in c or "frame" in c), None)
226
  xv = df[tc] if tc else x
227
+ def mk(fn, title):
228
+ fig2,ax = plt.subplots(figsize=(8,5))
229
+ fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
230
+ fn(ax)
231
+ ax.set_title(title, color=fg, fontweight="bold", fontsize=13, pad=8)
232
+ ax.tick_params(colors=ac, labelsize=10)
233
+ ax.grid(True, alpha=0.3, color=gc, linestyle="--")
234
+ for sp in ["top","right"]: ax.spines[sp].set_visible(False)
235
+ for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
236
+ plt.tight_layout()
237
+ buf2=io.BytesIO(); plt.savefig(buf2,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf2.seek(0)
238
+ res=Image.open(buf2).copy(); plt.close(); return res
239
  def pv(ax):
240
  if vc:
241
+ ax.plot(xv,df[vc],color="#e63946",linewidth=2.5,marker="o",markersize=5)
242
+ ax.fill_between(xv,df[vc],alpha=0.2,color="#e63946")
243
+ ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
244
+ ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11)
245
+ ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
246
+ ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
247
  def ps(ax):
248
+ if sc:
249
  xp = xv.values if tc else x
250
+ ax.plot(xp,df[sc],color="#4361ee",linewidth=2.5,marker="s",markersize=5)
251
+ ax.fill_between(xp,df[sc],alpha=0.2,color="#4361ee")
252
+ ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
253
+ ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="High risk: 10 Pa")
254
+ ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11)
255
+ ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
256
+ ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
257
  def psc(ax):
258
+ if vc and sc:
259
+ s2 = ax.scatter(df[vc],df[sc],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
260
+ cb=plt.colorbar(s2,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
261
+ ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Vel risk")
262
+ ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="Shear risk")
263
+ ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11)
264
+ ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11)
265
+ ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
266
  def psum(ax):
267
+ ax.axis("off"); risk=[]
268
+ st="CLINICAL SUMMARY"+chr(10)+""*20+chr(10)+chr(10)
269
  for col in num_cols[:3]:
270
+ mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
271
+ st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
272
+ if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY (>2.0 m/s)")
273
+ if "shear" in col and mx>10: risk.append("HIGH SHEAR (>10 Pa)")
274
+ st+=""*20+chr(10)
275
+ if risk:
276
+ st+="RISK FLAGS:"+chr(10)+"".join([" ⚠ "+r+chr(10) for r in risk])
277
+ st+="OVERALL: HIGH RISK"; bc="#e63946"
278
+ else:
279
+ st+="OVERALL: LOW RISK"; bc="#2ecc71"
280
+ ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
281
+ bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
282
+ i1=mk(pv,"Velocity Profile"); i2=mk(ps,"Wall Shear Stress")
283
+ i3=mk(psc,"Velocity vs Shear"); i4=mk(psum,"Clinical Summary")
284
+ ai=""
285
  if GROQ_KEY:
286
  try:
287
+ client=Groq(api_key=GROQ_KEY)
288
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
289
+ messages=[{"role":"system","content":"PIV expert SJSU CardioLab. Analyze PIV stats give clinical interpretation."},
290
+ {"role":"user","content":"PIV data from 27mm SJM Regent MHV 70bpm 5L/min:"+chr(10)+df.describe().to_string()[:600]}],max_tokens=300)
291
+ ai=chr(10)+""*20+chr(10)+"AI:"+chr(10)+resp.choices[0].message.content
 
 
 
292
  except: pass
293
+ return i1,i2,i3,i4,"PIV LOADED: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
294
+ except Exception as e: return None,None,None,None,"Error: "+str(e)
 
295
 
296
  def analyze_tgt_csv(file, theme="White"):
297
+ if file is None: return None,None,None,None,"Upload a TGT CSV file first."
298
  try:
299
  df = pd.read_csv(file.name)
300
+ cols = [c.lower().strip() for c in df.columns]
301
+ df.columns = cols
302
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
303
+ bg="#ffffff" if theme=="White" else "#0a1628"
304
+ fg="#1a202c" if theme=="White" else "white"
305
+ gc="#e2e8f0" if theme=="White" else "#2d4a8a"
306
+ ac="#4a5568" if theme=="White" else "#a8b2d8"
307
+ pb="#f7fafc" if theme=="White" else "#132340"
308
+ tc=next((c for c in cols if "time" in c or "min" in c),None)
309
+ tatc=next((c for c in cols if "tat" in c),num_cols[0] if num_cols else None)
310
+ pfc=next((c for c in cols if "pf" in c),num_cols[1] if len(num_cols)>1 else None)
311
+ hc=next((c for c in cols if "hemo" in c or "hgb" in c),num_cols[2] if len(num_cols)>2 else None)
312
+ plc=next((c for c in cols if "platelet" in c or "plt" in c),num_cols[3] if len(num_cols)>3 else None)
313
+ xv=df[tc] if tc else range(len(df))
314
+ def mk(dc,color,yl,lim,ll,title,bar=False):
315
+ fig2,ax=plt.subplots(figsize=(8,5))
316
+ fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
317
+ if dc and dc in df.columns:
318
+ xp=df[tc].values if tc else range(len(df)); yp=df[dc].values
319
+ if bar:
320
+ bs=ax.bar(range(len(yp)),yp,color=color,alpha=0.85,edgecolor=bg,width=0.6)
321
+ for b,v in zip(bs,yp): ax.text(b.get_x()+b.get_width()/2,b.get_height()+0.5,str(round(v,1)),ha="center",va="bottom",color=fg,fontsize=10,fontweight="bold")
322
+ else:
323
+ ax.plot(xp,yp,color=color,linewidth=3,marker="o",markersize=8)
324
+ ax.fill_between(xp,yp,alpha=0.2,color=color)
325
+ for xi,yi in zip(xp,yp): ax.annotate(str(round(yi,1)),(xi,yi),textcoords="offset points",xytext=(0,10),ha="center",color=fg,fontsize=10,fontweight="bold")
326
+ ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
327
+ ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
328
+ ax.set_ylabel(yl,color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
329
+ mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
330
+ ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
331
+ ax.tick_params(colors=ac,labelsize=10); ax.grid(True,alpha=0.3,color=gc,linestyle="--")
332
+ for sp in ["top","right"]: ax.spines[sp].set_visible(False)
333
+ for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
334
+ plt.tight_layout()
335
+ buf2=io.BytesIO(); plt.savefig(buf2,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf2.seek(0)
336
+ res=Image.open(buf2).copy(); plt.close(); return res
337
+ i1=mk(tatc,"#e63946","TAT (ng/mL)",8,"Normal: 8 ng/mL","Thrombin-Antithrombin TAT")
338
+ i2=mk(pfc,"#4361ee","PF1.2 (nmol/L)",2.0,"Normal: 2.0","Prothrombin Fragment PF1.2")
339
+ i3=mk(hc,"#2ecc71","Free Hemoglobin (mg/L)",20,"Normal: 20 mg/L","Free Hemoglobin Hemolysis",bar=True)
340
+ i4=mk(plc,"#e67e22","Platelet Count (10³/μL)",150,"Normal min: 150","Platelet Count")
341
+ ai=""
342
  if GROQ_KEY:
343
  try:
344
+ client=Groq(api_key=GROQ_KEY)
345
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
346
+ messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Analyze TGT data give thrombogenicity risk LOW MODERATE or HIGH. Normal: TAT<8, PF1.2<2.0, Hemo<20, Plt>150."},
347
+ {"role":"user","content":"TGT from 27mm SJM Regent MHV:"+chr(10)+df.describe().to_string()[:600]}],max_tokens=300)
348
+ ai=chr(10)+""*20+chr(10)+"AI:"+chr(10)+resp.choices[0].message.content
 
 
 
349
  except: pass
350
+ return i1,i2,i3,i4,"TGT LOADED: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
351
+ except Exception as e: return None,None,None,None,"Error: "+str(e)
 
352
 
353
  def generate_image(prompt):
354
+ if not prompt.strip(): return None,"Enter description.","";
355
+ if not HF_TOKEN: return None,"Add HF_TOKEN to Space secrets.","";
356
  try:
357
+ enhanced,desc=prompt,""
358
  if GROQ_KEY:
359
  try:
360
+ client=Groq(api_key=GROQ_KEY)
361
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
 
362
  messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
363
+ {"role":"user","content":"Biomedical image for CardioLab: "+prompt}],max_tokens=200)
364
+ full=resp.choices[0].message.content
 
 
365
  if "DESCRIPTION:" in full and "PROMPT:" in full:
366
+ desc=full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
367
+ enhanced=full.split("PROMPT:")[1].strip()
368
  except: pass
369
+ headers={"Authorization":"Bearer "+HF_TOKEN,"Content-Type":"application/json"}
370
+ for url in ["https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell",
371
+ "https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"]:
 
 
372
  try:
373
+ r=requests.post(url,headers=headers,json={"inputs":enhanced,"parameters":{"num_inference_steps":8}},timeout=60)
374
+ if r.status_code==200: return Image.open(io.BytesIO(r.content)),"Generated!",desc
 
 
375
  except: continue
376
+ return None,"Models busy. Try again.",desc
377
+ except Exception as e: return None,"Error: "+str(e),""
 
378
 
379
+ def piv_manual(v,s,h):
380
+ vr="HIGH-stenosis" if float(v)>2.0 else "NORMAL"
381
+ sr="HIGH-thrombosis" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
382
+ return "Velocity: "+str(v)+" - "+vr+chr(10)+"Shear: "+str(s)+" - "+sr+chr(10)+"HR: "+str(h)+" bpm"
383
 
384
+ def tgt_manual(t,p,h,pl,tm):
385
+ risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
386
+ return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"Time:"+str(tm)+"min"+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
 
 
387
 
388
+ # ── UI ─────────────────────────────────────────────────────────────
389
+ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
390
+ gr.HTML('''<div style="background:linear-gradient(135deg,#1a237e,#b71c1c);padding:20px;text-align:center;border-radius:12px 12px 0 0"><div style="font-size:2.5em;font-weight:900;color:#fff;letter-spacing:3px">CardioLab AI</div></div>''')
 
391
 
392
  with gr.Tabs():
393
 
394
  with gr.Tab("Chat"):
395
+ gr.Markdown("### Chat with memory — saves conversations like ChatGPT")
396
  with gr.Row():
397
+ with gr.Column(scale=3):
398
+ chatbot = gr.Chatbot(label="", height=420)
 
 
 
 
 
 
 
 
 
 
399
  with gr.Row():
400
+ msg_box = gr.Textbox(placeholder="Ask about CardioLab research...", label="", lines=2, scale=4)
401
+ with gr.Column(scale=1, min_width=80):
 
 
 
 
 
 
 
402
  send_btn = gr.Button("Send", variant="primary")
403
  clear_btn = gr.Button("Clear", variant="secondary")
404
+ with gr.Column(scale=1, min_width=200):
405
+ gr.Markdown("### Saved Sessions")
406
+ session_dropdown = gr.Dropdown(
407
+ choices=get_session_list(),
408
+ label="Load a saved session",
409
+ interactive=True
410
+ )
411
+ load_btn = gr.Button("Load Session", variant="primary")
412
+ session_status = gr.Textbox(label="Status", lines=1, interactive=False)
413
+ gr.Markdown("### Save Current Chat")
414
+ session_name_box = gr.Textbox(label="Session name", placeholder="e.g. TGT Research May 2026")
415
+ save_btn = gr.Button("Save Chat", variant="primary")
416
+ delete_btn = gr.Button("Delete Session", variant="secondary")
417
+
418
+ send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
419
+ msg_box.submit(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
420
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
 
421
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
422
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
423
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
424
 
425
  with gr.Tab("Voice"):
426
+ voice_chatbot = gr.Chatbot(label="", height=320)
427
  audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
428
  with gr.Row():
429
  voice_btn = gr.Button("Ask by Voice", variant="primary")
 
432
  voice_clear.click(lambda: [], outputs=voice_chatbot)
433
 
434
  with gr.Tab("Papers"):
 
435
  with gr.Row():
436
+ search_input = gr.Textbox(placeholder="e.g. mechanical heart valve thrombogenicity", label="Research Topic", scale=4)
 
 
 
 
 
 
 
437
  search_btn = gr.Button("Search", variant="primary", scale=1)
438
+ search_output = gr.Textbox(label="Verified Results", lines=18)
439
+ search_btn.click(quick_search, inputs=search_input, outputs=search_output)
440
+ search_input.submit(quick_search, inputs=search_input, outputs=search_output)
441
 
442
  with gr.Tab("PIV CSV"):
443
+ gr.Markdown("### Upload PIV CSV — 4 separate charts + AI analysis")
444
  with gr.Row():
445
+ piv_file = gr.File(label="UPLOAD PIV CSV", file_types=[".csv"], scale=3)
446
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
447
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
448
+ piv_result = gr.Textbox(label="AI Analysis", lines=5)
449
  with gr.Row():
450
+ piv_c1 = gr.Image(label="Velocity Profile", type="pil")
451
  piv_c2 = gr.Image(label="Shear Stress", type="pil")
452
  with gr.Row():
453
+ piv_c3 = gr.Image(label="Velocity vs Shear", type="pil")
454
  piv_c4 = gr.Image(label="Clinical Summary", type="pil")
455
+ piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
456
 
457
  with gr.Tab("TGT CSV"):
458
+ gr.Markdown("### Upload TGT CSV — blood biomarker charts + thrombogenicity assessment")
459
  with gr.Row():
460
+ tgt_file = gr.File(label="UPLOAD TGT CSV", file_types=[".csv"], scale=3)
461
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
462
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
463
+ tgt_result = gr.Textbox(label="AI Assessment", lines=5)
464
  with gr.Row():
465
+ tgt_c1 = gr.Image(label="TAT Over Time", type="pil")
466
+ tgt_c2 = gr.Image(label="PF1.2 Over Time", type="pil")
467
  with gr.Row():
468
+ tgt_c3 = gr.Image(label="Free Hemoglobin", type="pil")
469
+ tgt_c4 = gr.Image(label="Platelet Count", type="pil")
470
+ tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
471
 
472
+ with gr.Tab("uPAD Photo"):
473
+ gr.Markdown("### Upload uPAD Photo — Instant CKD diagnosis")
474
  with gr.Row():
475
  with gr.Column():
476
+ photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=280)
477
  analyze_btn = gr.Button("Analyze uPAD", variant="primary")
478
  with gr.Column():
479
+ photo_img = gr.Image(label="Detection Zone", type="pil", height=280)
480
+ photo_text = gr.Textbox(label="CKD Result", lines=10)
481
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
  with gr.Tab("AI Image"):
484
  with gr.Row():
485
+ img_prompt = gr.Textbox(placeholder="e.g. bileaflet heart valve | uPAD device | Arduino TGT", label="Describe image", lines=2, scale=4)
 
 
 
486
  with gr.Column(scale=1):
487
  img_btn = gr.Button("Generate", variant="primary")
488
  img_status = gr.Textbox(label="Status", lines=1)
489
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
490
+ img_output = gr.Image(label="Generated Image", type="pil", height=380)
491
+ img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
492
 
493
  with gr.Tab("PIV Manual"):
494
  with gr.Row():
495
  with gr.Column():
496
+ v=gr.Number(label="Max Velocity m/s",value=1.8)
497
+ s=gr.Number(label="Wall Shear Stress Pa",value=6.5)
498
+ h=gr.Number(label="Heart Rate bpm",value=72)
499
+ piv_out=gr.Textbox(label="Result",lines=4)
500
+ gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
501
 
502
  with gr.Tab("TGT Manual"):
503
  with gr.Row():
504
  with gr.Column():
505
+ t1=gr.Number(label="TAT ng/mL",value=18)
506
+ t2=gr.Number(label="PF1.2",value=2.5)
507
+ t3=gr.Number(label="Hemoglobin mg/L",value=60)
508
+ t4=gr.Number(label="Platelets",value=140)
509
+ t5=gr.Number(label="Time min",value=40)
510
+ out2=gr.Textbox(label="Result",lines=6)
511
+ gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
512
+
513
+ with gr.Tab("uPAD Manual"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  with gr.Row():
515
+ with gr.Column():
516
+ r=gr.Number(label="R value",value=210)
517
+ g=gr.Number(label="G value",value=140)
518
+ b=gr.Number(label="B value",value=80)
519
+ out3=gr.Textbox(label="Result",lines=4)
520
+ gr.Button("Analyze",variant="primary").click(
521
+ lambda r,g,b:"Creatinine: "+str(max(0,round(0.02*(r-b)-0.5,2)))+" mg/dL"+chr(10)+("Normal" if max(0,round(0.02*(r-b)-0.5,2))<1.2 else "Borderline" if max(0,round(0.02*(r-b)-0.5,2))<1.5 else "CKD Stage 2+"),
522
+ inputs=[r,g,b],outputs=out3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
  demo.launch()