Saicharan21 commited on
Commit
8b1b9dd
Β·
verified Β·
1 Parent(s): c846c68

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +219 -308
app.py CHANGED
@@ -21,206 +21,149 @@ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/
21
  "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
22
 
23
  CSS = """
24
- /* Reset and base */
25
  body, .gradio-container {
26
  background: #f7f7f8 !important;
27
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important;
28
- margin: 0 !important;
29
- padding: 0 !important;
30
  }
31
-
32
- /* Hide default gradio header */
33
- .gradio-container > .main > .wrap > .panel {
34
- border: none !important;
35
- box-shadow: none !important;
36
- }
37
-
38
- /* CHATGPT STYLE SIDEBAR */
39
- .sidebar {
40
- background: #202123 !important;
41
- min-height: 100vh !important;
42
- padding: 10px !important;
43
- border-right: 1px solid #3a3a3a !important;
44
- }
45
-
46
- .sidebar-title {
47
- color: white !important;
48
- font-size: 1.1em !important;
49
- font-weight: 700 !important;
50
- padding: 10px 5px !important;
51
- border-bottom: 1px solid #3a3a3a !important;
52
- margin-bottom: 10px !important;
53
- }
54
-
55
- .session-item {
56
- background: #2d2d30 !important;
57
- color: #ececf1 !important;
58
- border-radius: 6px !important;
59
- padding: 8px 12px !important;
60
- margin-bottom: 4px !important;
61
- cursor: pointer !important;
62
- font-size: 0.85em !important;
63
- }
64
-
65
- .session-item:hover {
66
- background: #3a3a3c !important;
67
- }
68
-
69
- /* TABS - TOP NAV STYLE */
70
  .tab-nav {
71
  background: #ffffff !important;
72
  border-bottom: 1px solid #e5e7eb !important;
73
  padding: 0 16px !important;
74
  display: flex !important;
75
- flex-wrap: nowrap !important;
76
- overflow-x: auto !important;
77
  gap: 0 !important;
78
  }
79
-
80
  .tab-nav button {
81
  background: transparent !important;
82
  color: #6b7280 !important;
83
  border: none !important;
84
  border-bottom: 2px solid transparent !important;
85
- padding: 12px 16px !important;
86
  font-weight: 500 !important;
87
- font-size: 0.85em !important;
88
  white-space: nowrap !important;
89
  border-radius: 0 !important;
90
- margin: 0 !important;
91
- transition: all 0.15s !important;
92
- }
93
-
94
- .tab-nav button:hover {
95
- color: #111827 !important;
96
- background: #f9fafb !important;
97
- }
98
-
99
- .tab-nav button.selected {
100
- color: #e63946 !important;
101
- border-bottom: 2px solid #e63946 !important;
102
- font-weight: 600 !important;
103
- background: transparent !important;
104
- }
105
-
106
- /* MAIN CHAT AREA */
107
- .chat-container {
108
- background: #ffffff !important;
109
- min-height: 80vh !important;
110
- }
111
-
112
- /* CHATBOT MESSAGES */
113
- .chatbot {
114
- background: #ffffff !important;
115
- border: none !important;
116
- border-radius: 0 !important;
117
- }
118
-
119
- .message.user {
120
- background: #f7f7f8 !important;
121
- color: #1a202c !important;
122
- border-radius: 12px !important;
123
- padding: 12px 16px !important;
124
- margin: 4px 0 !important;
125
- }
126
-
127
- .message.bot {
128
- background: #ffffff !important;
129
- color: #1a202c !important;
130
- border-radius: 12px !important;
131
- padding: 12px 16px !important;
132
- margin: 4px 0 !important;
133
- border-left: 3px solid #e63946 !important;
134
- }
135
-
136
- /* INPUT AREA */
137
- textarea {
138
- background: #ffffff !important;
139
- color: #1a202c !important;
140
- border: 1px solid #d1d5db !important;
141
- border-radius: 12px !important;
142
- font-size: 0.95em !important;
143
- padding: 12px !important;
144
- box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important;
145
- }
146
-
147
- textarea:focus {
148
- border-color: #e63946 !important;
149
- box-shadow: 0 0 0 2px rgba(230,57,70,0.1) !important;
150
- outline: none !important;
151
- }
152
-
153
- /* BUTTONS */
154
- button.primary {
155
- background: #e63946 !important;
156
- color: white !important;
157
- border: none !important;
158
- border-radius: 8px !important;
159
- font-weight: 600 !important;
160
- padding: 10px 20px !important;
161
- transition: background 0.15s !important;
162
- }
163
-
164
- button.primary:hover {
165
- background: #c1121f !important;
166
- }
167
-
168
- button.secondary {
169
- background: #f3f4f6 !important;
170
- color: #374151 !important;
171
- border: 1px solid #d1d5db !important;
172
- border-radius: 8px !important;
173
- font-weight: 500 !important;
174
- }
175
-
176
- /* NEW CHAT BUTTON */
177
- .new-chat-btn {
178
- background: transparent !important;
179
- color: #ececf1 !important;
180
- border: 1px solid #3a3a3c !important;
181
- border-radius: 6px !important;
182
- padding: 8px 12px !important;
183
- width: 100% !important;
184
- text-align: left !important;
185
- margin-bottom: 8px !important;
186
- font-size: 0.85em !important;
187
- }
188
-
189
- /* DROPDOWN */
190
- select, .gr-dropdown {
191
- background: #2d2d30 !important;
192
- color: #ececf1 !important;
193
- border: 1px solid #3a3a3c !important;
194
- border-radius: 6px !important;
195
- }
196
-
197
- /* INPUT NUMBERS */
198
- input[type=number] {
199
- background: #f9fafb !important;
200
- color: #1a202c !important;
201
- border: 1px solid #d1d5db !important;
202
- border-radius: 8px !important;
203
- }
204
-
205
- /* LABELS */
206
- label span {
207
- color: #374151 !important;
208
- font-weight: 500 !important;
209
- font-size: 0.85em !important;
210
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
- /* FILE UPLOAD */
213
- .file-preview {
214
- background: #f9fafb !important;
215
- border: 2px dashed #d1d5db !important;
216
- border-radius: 12px !important;
217
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- /* SCROLLBAR */
220
- ::-webkit-scrollbar { width: 6px; height: 6px; }
221
- ::-webkit-scrollbar-track { background: transparent; }
222
- ::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
223
- ::-webkit-scrollbar-thumb:hover { background: #9ca3af; }
 
 
224
  """
225
 
226
  def load_all_sessions():
@@ -235,42 +178,37 @@ def save_all_sessions(sessions):
235
  try:
236
  api = HfApi(token=HF_TOKEN)
237
  api.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(), path_in_repo="chat_history.json",
238
- repo_id=HISTORY_REPO, repo_type="dataset", token=HF_TOKEN, commit_message="Update chat history")
239
  return True
240
  except: return False
241
 
242
  def get_session_list():
243
- sessions = load_all_sessions()
244
- if not sessions: return ["No saved sessions"]
245
- return list(reversed(list(sessions.keys())))
246
 
247
- def save_session(history, session_name):
248
  if not history: return "Nothing to save", gr.update()
249
- if not session_name or not session_name.strip():
250
- session_name = "Chat " + datetime.now().strftime("%b %d %H:%M")
251
  sessions = load_all_sessions()
252
- sessions[session_name] = {"messages": history, "saved_at": datetime.now().isoformat()}
253
  ok = save_all_sessions(sessions)
254
  choices = get_session_list()
255
- if ok: return "Saved: "+session_name, gr.update(choices=choices, value=session_name)
256
- return "Save failed β€” check HF_TOKEN", gr.update()
257
 
258
- def load_session(session_name):
259
- if not session_name or "No saved" in session_name: return [], "Select a session first"
260
  sessions = load_all_sessions()
261
- if session_name in sessions:
262
- msgs = sessions[session_name]["messages"]
263
- return msgs, "Loaded: "+session_name
264
- return [], "Session not found"
265
 
266
- def delete_session(session_name):
267
- if not session_name or "No saved" in session_name: return "Select a session first", gr.update()
268
  sessions = load_all_sessions()
269
- if session_name in sessions:
270
- del sessions[session_name]
271
- save_all_sessions(sessions)
272
  choices = get_session_list()
273
- return "Deleted: "+session_name, gr.update(choices=choices, value=choices[0] if choices else None)
274
  return "Not found", gr.update()
275
 
276
  def new_chat(): return [], "", "New chat started"
@@ -302,7 +240,7 @@ def research_chat(message, history):
302
  return "", history
303
  try:
304
  client = Groq(api_key=GROQ_KEY)
305
- msgs = [{"role":"system","content":"You are CardioLab AI assistant for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember full conversation. Never invent URLs. "+KNOWHOW}]
306
  for item in history:
307
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
308
  msgs.append({"role":"user","content":message})
@@ -342,72 +280,65 @@ def analyze_upad_photo(image):
342
  if image is None: return None, "Upload a uPAD photo first."
343
  try:
344
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
345
- arr = np.array(img)
346
- h,w = arr.shape[:2]
347
  y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
348
  zone = arr[y1:y2,x1:x2]
349
  R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
350
- c = max(0, round(0.018*(R-B)-0.3, 2))
351
  if c<1.2: s,a="Normal","Monitor annually."
352
  elif c<1.5: s,a="Borderline","Repeat in 3 months."
353
  elif c<3.0: s,a="Stage 2 CKD","Consult nephrologist."
354
  elif c<6.0: s,a="Stage 3-4 CKD","Immediate consultation."
355
- else: s,a="Stage 5 CKD","Emergency care needed."
356
- ri = img.copy()
357
- import PIL.ImageDraw as D
358
- D.Draw(ri).rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
359
- return ri, ("uPAD ANALYSIS"+chr(10)+"━"*22+chr(10)+"R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+"Creatinine: "+str(c)+" mg/dL"+chr(10)+"Stage: "+s+chr(10)+"Action: "+a)
360
- except Exception as e: return None, "Error: "+str(e)
361
-
362
- def mk_chart(fn, title, bg, fg, gc, ac, pb):
363
- fig2,ax = plt.subplots(figsize=(8,5))
364
- fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
365
- fn(ax)
366
- ax.set_title(title, color=fg, fontweight="bold", fontsize=13, pad=8)
367
- ax.tick_params(colors=ac, labelsize=10)
368
- ax.grid(True, alpha=0.3, color=gc, linestyle="--")
369
  for sp in ["top","right"]: ax.spines[sp].set_visible(False)
370
  for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
371
- plt.tight_layout()
372
- buf=io.BytesIO(); plt.savefig(buf,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf.seek(0)
373
  res=Image.open(buf).copy(); plt.close(); return res
374
 
375
- def analyze_piv_csv(file, theme="White"):
376
- if file is None: return None,None,None,None,"Upload a PIV CSV file first."
377
  try:
378
- df = pd.read_csv(file.name)
379
- cols = [c.lower().strip() for c in df.columns]; df.columns = cols
380
- num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
381
- if not num_cols: return None,None,None,None,"No numeric columns found."
382
  bg="#fff" if theme=="White" else "#0a1628"; fg="#1a202c" if theme=="White" else "white"
383
  gc="#e2e8f0" if theme=="White" else "#2d4a8a"; ac="#4a5568" if theme=="White" else "#a8b2d8"
384
  pb="#f7fafc" if theme=="White" else "#132340"
385
  x=np.arange(len(df))
386
  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)
387
  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)
388
- tc=next((c for c in cols if "time" in c or "frame" in c),None)
389
- xv=df[tc] if tc else x
390
  def pv(ax):
391
  if vc:
392
- ax.plot(xv,df[vc],color="#e63946",linewidth=2.5,marker="o",markersize=5)
393
- ax.fill_between(xv,df[vc],alpha=0.15,color="#e63946")
394
  ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
395
  ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
396
  ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
397
  def ps(ax):
398
  if sc:
399
  xp=xv.values if tc else x
400
- ax.plot(xp,df[sc],color="#4361ee",linewidth=2.5,marker="s",markersize=5)
401
- ax.fill_between(xp,df[sc],alpha=0.15,color="#4361ee")
402
  ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
403
- ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="High risk: 10 Pa")
404
- ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
405
  ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
406
  def psc(ax):
407
  if vc and sc:
408
  s2=ax.scatter(df[vc],df[sc],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
409
  cb=plt.colorbar(s2,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
410
- ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Vel risk"); ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="Shear risk")
 
411
  ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11)
412
  ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
413
  def psum(ax):
@@ -418,7 +349,7 @@ def analyze_piv_csv(file, theme="White"):
418
  st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
419
  if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY")
420
  if "shear" in col and mx>10: risk.append("HIGH SHEAR")
421
- bc="#e63946" if risk else "#2ecc71"
422
  st+="━"*20+chr(10)+("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
423
  ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
424
  bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
@@ -438,12 +369,11 @@ def analyze_piv_csv(file, theme="White"):
438
  return i1,i2,i3,i4,"PIV: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
439
  except Exception as e: return None,None,None,None,"Error: "+str(e)
440
 
441
- def analyze_tgt_csv(file, theme="White"):
442
- if file is None: return None,None,None,None,"Upload a TGT CSV file first."
443
  try:
444
- df = pd.read_csv(file.name)
445
- cols = [c.lower().strip() for c in df.columns]; df.columns = cols
446
- num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
447
  bg="#fff" if theme=="White" else "#0a1628"; fg="#1a202c" if theme=="White" else "white"
448
  gc="#e2e8f0" if theme=="White" else "#2d4a8a"; ac="#4a5568" if theme=="White" else "#a8b2d8"
449
  pb="#f7fafc" if theme=="White" else "#132340"
@@ -469,16 +399,16 @@ def analyze_tgt_csv(file, theme="White"):
469
  mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
470
  ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
471
  return mk_chart(fn,title,bg,fg,gc,ac,pb)
472
- i1=mk2(tatc,"#e63946","TAT (ng/mL)",8,"Normal: 8","TAT Thrombin-Antithrombin")
473
- i2=mk2(pfc,"#4361ee","PF1.2 (nmol/L)",2.0,"Normal: 2.0","PF1.2 Prothrombin Fragment")
474
  i3=mk2(hc,"#2ecc71","Free Hemoglobin (mg/L)",20,"Normal: 20","Free Hemoglobin",bar=True)
475
- i4=mk2(plc,"#e67e22","Platelet Count",150,"Normal min: 150","Platelet Count")
476
  ai=""
477
  if GROQ_KEY:
478
  try:
479
  client=Groq(api_key=GROQ_KEY)
480
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
481
- messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Give thrombogenicity risk LOW MODERATE or HIGH. Normal: TAT<8, PF1.2<2.0, Hemo<20, Plt>150."},
482
  {"role":"user","content":"TGT from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
483
  ai=chr(10)+"━"*20+chr(10)+"AI: "+resp.choices[0].message.content
484
  except: pass
@@ -520,59 +450,33 @@ def tgt_manual(t,p,h,pl,tm):
520
  risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
521
  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")
522
 
523
- with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
524
 
525
- gr.HTML("""
526
- <div style="background:linear-gradient(135deg,#1a237e 0%,#b71c1c 100%);padding:16px 24px;display:flex;align-items:center;gap:16px;">
527
- <div style="font-size:1.8em;font-weight:900;color:#fff;letter-spacing:2px;">❀️ CardioLab AI</div>
528
- <div style="color:rgba(255,255,255,0.7);font-size:0.85em;">SJSU Biomedical Engineering</div>
529
- </div>
530
- """)
531
 
532
  with gr.Tabs():
533
 
534
  with gr.Tab("πŸ’¬ Chat"):
535
  with gr.Row():
536
-
537
- # LEFT SIDEBAR - ChatGPT style
538
- with gr.Column(scale=1, min_width=220):
539
- gr.HTML('<div style="background:#202123;padding:12px;border-radius:8px;margin-bottom:8px;"><div style="color:white;font-weight:700;font-size:0.9em;margin-bottom:8px;">πŸ’¬ Conversations</div></div>')
540
- new_chat_btn = gr.Button("✏️ New Chat", variant="secondary")
541
- gr.HTML('<div style="color:#9ca3af;font-size:0.75em;padding:8px 0 4px 0;">SAVED SESSIONS</div>')
542
- session_dropdown = gr.Dropdown(
543
- choices=get_session_list(),
544
- label="",
545
- interactive=True,
546
- container=False
547
- )
548
- load_btn = gr.Button("πŸ“‚ Load", variant="primary")
549
- session_name_box = gr.Textbox(
550
- placeholder="Session name...",
551
- label="",
552
- lines=1,
553
- container=False
554
- )
555
  with gr.Row():
556
- save_btn = gr.Button("πŸ’Ύ Save", variant="primary", scale=1)
557
- delete_btn = gr.Button("πŸ—‘οΈ", variant="secondary", scale=0)
558
  session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
559
 
560
- # RIGHT - Main chat area
561
  with gr.Column(scale=4):
562
- chatbot = gr.Chatbot(
563
- label="",
564
- height=520,
565
- show_label=False,
566
- container=False
567
- )
568
  with gr.Row():
569
- msg_box = gr.Textbox(
570
- placeholder="Message CardioLab AI...",
571
- label="",
572
- lines=2,
573
- scale=5,
574
- container=False
575
- )
576
  with gr.Column(scale=1, min_width=80):
577
  send_btn = gr.Button("Send ↑", variant="primary")
578
  clear_btn = gr.Button("Clear", variant="secondary")
@@ -586,13 +490,11 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
586
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
587
 
588
  with gr.Tab("πŸŽ™οΈ Voice"):
 
 
589
  with gr.Row():
590
- with gr.Column():
591
- voice_chatbot = gr.Chatbot(label="", height=400, show_label=False)
592
- audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
593
- with gr.Row():
594
- voice_btn = gr.Button("Ask by Voice", variant="primary")
595
- voice_clear = gr.Button("Clear", variant="secondary")
596
  voice_btn.click(voice_chat, inputs=[audio_input, voice_chatbot], outputs=voice_chatbot)
597
  voice_clear.click(lambda: [], outputs=voice_chatbot)
598
 
@@ -600,7 +502,7 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
600
  with gr.Row():
601
  search_input = gr.Textbox(placeholder="e.g. mechanical heart valve thrombogenicity 2024", label="Research Topic", scale=4)
602
  search_btn = gr.Button("Search", variant="primary", scale=1)
603
- search_output = gr.Textbox(label="Verified Results", lines=18)
604
  search_btn.click(quick_search, inputs=search_input, outputs=search_output)
605
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
606
 
@@ -612,11 +514,11 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
612
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
613
  piv_result = gr.Textbox(label="AI Analysis", lines=4)
614
  with gr.Row():
615
- piv_c1 = gr.Image(label="Velocity Profile", type="pil")
616
- piv_c2 = gr.Image(label="Shear Stress", type="pil")
617
  with gr.Row():
618
- piv_c3 = gr.Image(label="Velocity vs Shear", type="pil")
619
- piv_c4 = gr.Image(label="Clinical Summary", type="pil")
620
  piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
621
 
622
  with gr.Tab("🩸 TGT CSV"):
@@ -627,23 +529,21 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
627
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
628
  tgt_result = gr.Textbox(label="AI Assessment", lines=4)
629
  with gr.Row():
630
- tgt_c1 = gr.Image(label="TAT", type="pil")
631
- tgt_c2 = gr.Image(label="PF1.2", type="pil")
632
  with gr.Row():
633
- tgt_c3 = gr.Image(label="Hemoglobin", type="pil")
634
- tgt_c4 = gr.Image(label="Platelets", type="pil")
635
  tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
636
 
637
  with gr.Tab("πŸ§ͺ uPAD"):
638
  with gr.Row():
639
  with gr.Column():
640
- photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=280)
641
  analyze_btn = gr.Button("Analyze uPAD Photo", variant="primary")
642
  with gr.Column():
643
- photo_img = gr.Image(label="Detection Zone (green box)", type="pil", height=280)
644
- photo_text = gr.Textbox(label="CKD Result", lines=10)
645
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
646
- gr.Markdown("**Manual RGB entry:**")
647
  with gr.Row():
648
  r=gr.Number(label="R",value=210); g=gr.Number(label="G",value=140); b=gr.Number(label="B",value=80)
649
  out3=gr.Textbox(label="Result",lines=3)
@@ -658,14 +558,14 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
658
  img_btn = gr.Button("Generate Image", variant="primary")
659
  img_status = gr.Textbox(label="Status", lines=1)
660
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
661
- img_output = gr.Image(label="Generated Image", type="pil", height=420)
662
  img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
663
 
664
  with gr.Tab("πŸ“ PIV Manual"):
665
  with gr.Row():
666
  with gr.Column():
667
  v=gr.Number(label="Max Velocity m/s",value=1.8,info="Normal: 0.5-2.0")
668
- s=gr.Number(label="Wall Shear Stress Pa",value=6.5,info="Normal: <5 Pa")
669
  h=gr.Number(label="Heart Rate bpm",value=72,info="Normal: 60-100")
670
  piv_out=gr.Textbox(label="Result",lines=4)
671
  gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
@@ -681,4 +581,15 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
681
  out2=gr.Textbox(label="Result",lines=6)
682
  gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
683
 
 
 
 
 
 
 
 
 
 
 
 
684
  demo.launch()
 
21
  "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
22
 
23
  CSS = """
 
24
  body, .gradio-container {
25
  background: #f7f7f8 !important;
26
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important;
27
+ margin: 0 !important; padding: 0 !important;
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  .tab-nav {
30
  background: #ffffff !important;
31
  border-bottom: 1px solid #e5e7eb !important;
32
  padding: 0 16px !important;
33
  display: flex !important;
34
+ flex-wrap: wrap !important;
 
35
  gap: 0 !important;
36
  }
 
37
  .tab-nav button {
38
  background: transparent !important;
39
  color: #6b7280 !important;
40
  border: none !important;
41
  border-bottom: 2px solid transparent !important;
42
+ padding: 12px 14px !important;
43
  font-weight: 500 !important;
44
+ font-size: 0.82em !important;
45
  white-space: nowrap !important;
46
  border-radius: 0 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
+ .tab-nav button:hover { color: #111827 !important; background: #f9fafb !important; }
49
+ .tab-nav button.selected { color: #c1121f !important; border-bottom: 2px solid #c1121f !important; font-weight: 700 !important; background: transparent !important; }
50
+ .message.user { background: #f3f4f6 !important; color: #1a202c !important; border-radius: 12px !important; }
51
+ .message.bot { background: #ffffff !important; color: #1a202c !important; border-left: 3px solid #c1121f !important; border-radius: 0 12px 12px 12px !important; }
52
+ textarea { background: #ffffff !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 12px !important; }
53
+ textarea:focus { border-color: #c1121f !important; outline: none !important; }
54
+ button.primary { background: #c1121f !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; }
55
+ button.primary:hover { background: #a00e18 !important; }
56
+ button.secondary { background: #f3f4f6 !important; color: #374151 !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
57
+ input[type=number] { background: #f9fafb !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
58
+ label span { color: #374151 !important; font-weight: 500 !important; font-size: 0.85em !important; }
59
+ ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
60
+ """
61
 
62
+ HEADER_HTML = """
63
+ <div style="
64
+ background: linear-gradient(135deg, #0a0f2e 0%, #1a0a0a 40%, #0a0f2e 100%);
65
+ padding: 0;
66
+ margin: 0;
67
+ border-bottom: 3px solid #c1121f;
68
+ position: relative;
69
+ overflow: hidden;
70
+ ">
71
+ <!-- ECG Background Line -->
72
+ <svg style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0.08;" viewBox="0 0 1200 120" preserveAspectRatio="none">
73
+ <polyline points="0,60 100,60 130,20 150,100 170,10 200,90 220,60 400,60 430,20 450,100 470,10 500,90 520,60 700,60 730,20 750,100 770,10 800,90 820,60 1000,60 1030,20 1050,100 1070,10 1100,90 1120,60 1200,60"
74
+ fill="none" stroke="#c1121f" stroke-width="3"/>
75
+ </svg>
76
+
77
+ <div style="
78
+ max-width: 1200px;
79
+ margin: 0 auto;
80
+ padding: 18px 24px;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: space-between;
84
+ position: relative;
85
+ z-index: 1;
86
+ ">
87
+ <!-- LEFT: SJSU Spartan Logo SVG -->
88
+ <div style="display:flex;align-items:center;gap:16px;">
89
+ <svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
90
+ <!-- Spartan helmet simplified -->
91
+ <circle cx="50" cy="35" r="28" fill="#0057a8" opacity="0.9"/>
92
+ <!-- Helmet crest -->
93
+ <ellipse cx="50" cy="14" rx="22" ry="10" fill="#0057a8"/>
94
+ <!-- Crest spikes -->
95
+ <polygon points="30,14 33,4 36,14" fill="#e8a020"/>
96
+ <polygon points="36,12 39,2 42,12" fill="#e8a020"/>
97
+ <polygon points="42,11 45,1 48,11" fill="#e8a020"/>
98
+ <polygon points="48,11 51,1 54,11" fill="#e8a020"/>
99
+ <polygon points="54,12 57,2 60,12" fill="#e8a020"/>
100
+ <polygon points="60,14 63,4 66,14" fill="#e8a020"/>
101
+ <!-- Helmet face -->
102
+ <rect x="36" y="30" width="28" height="22" rx="4" fill="#0057a8"/>
103
+ <rect x="40" y="35" width="8" height="12" rx="2" fill="#e8a020"/>
104
+ <!-- Chin guard -->
105
+ <rect x="34" y="50" width="32" height="8" rx="4" fill="#0057a8"/>
106
+ <!-- Helmet shine -->
107
+ <ellipse cx="42" cy="28" rx="5" ry="3" fill="white" opacity="0.25"/>
108
+ </svg>
109
+
110
+ <div>
111
+ <div style="color:#9ca3af;font-size:0.7em;font-weight:500;letter-spacing:2px;text-transform:uppercase;">San Jose State University</div>
112
+ <div style="color:#e8a020;font-size:0.85em;font-weight:700;letter-spacing:1px;">Biomedical Engineering</div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- CENTER: CardioLab AI Branding -->
117
+ <div style="text-align:center;flex:1;padding:0 20px;">
118
+ <!-- ECG + Heart icon inline -->
119
+ <div style="display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:4px;">
120
+ <svg width="120" height="32" viewBox="0 0 120 32">
121
+ <polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16"
122
+ fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
123
+ <!-- Heart dot on ECG -->
124
+ <circle cx="34" cy="2" r="3" fill="#c1121f"/>
125
+ </svg>
126
+ <div style="font-size:2.2em;font-weight:900;letter-spacing:2px;">
127
+ <span style="color:#ffffff;">Cardio</span><span style="color:#c1121f;">Lab</span><span style="color:#ffffff;"> AI</span>
128
+ </div>
129
+ <svg width="120" height="32" viewBox="0 0 120 32" style="transform:scaleX(-1);">
130
+ <polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16"
131
+ fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
132
+ <circle cx="34" cy="2" r="3" fill="#c1121f"/>
133
+ </svg>
134
+ </div>
135
+ <div style="color:#9ca3af;font-size:0.72em;letter-spacing:3px;text-transform:uppercase;">
136
+ AI Research Agent &nbsp;|&nbsp; Built on Biomni Stanford &nbsp;|&nbsp; Llama 3.3 70B
137
+ </div>
138
+ </div>
139
+
140
+ <!-- RIGHT: Heart + Stats -->
141
+ <div style="display:flex;align-items:center;gap:16px;">
142
+ <div style="text-align:right;">
143
+ <div style="color:#9ca3af;font-size:0.7em;letter-spacing:1px;text-transform:uppercase;">Research Pillars</div>
144
+ <div style="color:#ffffff;font-size:0.75em;margin-top:4px;">πŸ«€ MHV &nbsp; πŸ”¬ CKD &nbsp; πŸ’» FSI</div>
145
+ <div style="color:#9ca3af;font-size:0.65em;margin-top:2px;">MCL Β· PIV Β· TGT Β· uPAD Β· COMSOL</div>
146
+ </div>
147
+ <!-- Heart SVG -->
148
+ <svg width="50" height="50" viewBox="0 0 100 90" xmlns="http://www.w3.org/2000/svg">
149
+ <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"
150
+ fill="#c1121f" opacity="0.9"/>
151
+ <path d="M50 75 C50 75 12 50 12 30 C12 18 22 12 30 12 C38 12 45 16 50 22"
152
+ fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="3"/>
153
+ <!-- ECG inside heart -->
154
+ <polyline points="25,45 32,45 35,35 38,55 41,30 44,50 50,45 75,45"
155
+ fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
156
+ </svg>
157
+ </div>
158
+ </div>
159
 
160
+ <!-- Bottom accent bar -->
161
+ <div style="
162
+ height: 3px;
163
+ background: linear-gradient(90deg, #0057a8, #c1121f, #e8a020, #c1121f, #0057a8);
164
+ margin: 0;
165
+ "></div>
166
+ </div>
167
  """
168
 
169
  def load_all_sessions():
 
178
  try:
179
  api = HfApi(token=HF_TOKEN)
180
  api.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(), path_in_repo="chat_history.json",
181
+ repo_id=HISTORY_REPO, repo_type="dataset", token=HF_TOKEN, commit_message="Update")
182
  return True
183
  except: return False
184
 
185
  def get_session_list():
186
+ s = load_all_sessions()
187
+ if not s: return ["No saved sessions"]
188
+ return list(reversed(list(s.keys())))
189
 
190
+ def save_session(history, name):
191
  if not history: return "Nothing to save", gr.update()
192
+ if not name or not name.strip(): name = "Chat "+datetime.now().strftime("%b %d %H:%M")
 
193
  sessions = load_all_sessions()
194
+ sessions[name] = {"messages":history,"saved_at":datetime.now().isoformat()}
195
  ok = save_all_sessions(sessions)
196
  choices = get_session_list()
197
+ return ("Saved: "+name if ok else "Save failed"), gr.update(choices=choices, value=name)
 
198
 
199
+ def load_session(name):
200
+ if not name or "No saved" in name: return [], "Select a session"
201
  sessions = load_all_sessions()
202
+ if name in sessions: return sessions[name]["messages"], "Loaded: "+name
203
+ return [], "Not found"
 
 
204
 
205
+ def delete_session(name):
206
+ if not name or "No saved" in name: return "Select a session", gr.update()
207
  sessions = load_all_sessions()
208
+ if name in sessions:
209
+ del sessions[name]; save_all_sessions(sessions)
 
210
  choices = get_session_list()
211
+ return "Deleted: "+name, gr.update(choices=choices, value=choices[0] if choices else None)
212
  return "Not found", gr.update()
213
 
214
  def new_chat(): return [], "", "New chat started"
 
240
  return "", history
241
  try:
242
  client = Groq(api_key=GROQ_KEY)
243
+ msgs = [{"role":"system","content":"You are CardioLab AI for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember conversation. Never invent URLs. "+KNOWHOW}]
244
  for item in history:
245
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
246
  msgs.append({"role":"user","content":message})
 
280
  if image is None: return None, "Upload a uPAD photo first."
281
  try:
282
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
283
+ arr = np.array(img); h,w = arr.shape[:2]
 
284
  y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
285
  zone = arr[y1:y2,x1:x2]
286
  R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
287
+ c = max(0,round(0.018*(R-B)-0.3,2))
288
  if c<1.2: s,a="Normal","Monitor annually."
289
  elif c<1.5: s,a="Borderline","Repeat in 3 months."
290
  elif c<3.0: s,a="Stage 2 CKD","Consult nephrologist."
291
  elif c<6.0: s,a="Stage 3-4 CKD","Immediate consultation."
292
+ else: s,a="Stage 5 CKD","Emergency care."
293
+ ri=img.copy()
294
+ import PIL.ImageDraw as D; D.Draw(ri).rectangle([x1,y1,x2,y2],outline=(0,255,0),width=3)
295
+ return ri,("uPAD ANALYSIS"+chr(10)+"━"*22+chr(10)+"R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+"Creatinine: "+str(c)+" mg/dL"+chr(10)+"Stage: "+s+chr(10)+"Action: "+a)
296
+ except Exception as e: return None,"Error: "+str(e)
297
+
298
+ def mk_chart(fn,title,bg,fg,gc,ac,pb):
299
+ fig2,ax=plt.subplots(figsize=(8,5)); fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
300
+ fn(ax); ax.set_title(title,color=fg,fontweight="bold",fontsize=13,pad=8)
301
+ ax.tick_params(colors=ac,labelsize=10); ax.grid(True,alpha=0.3,color=gc,linestyle="--")
 
 
 
 
302
  for sp in ["top","right"]: ax.spines[sp].set_visible(False)
303
  for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
304
+ plt.tight_layout(); buf=io.BytesIO(); plt.savefig(buf,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf.seek(0)
 
305
  res=Image.open(buf).copy(); plt.close(); return res
306
 
307
+ def analyze_piv_csv(file,theme="White"):
308
+ if file is None: return None,None,None,None,"Upload PIV CSV first."
309
  try:
310
+ df=pd.read_csv(file.name); cols=[c.lower().strip() for c in df.columns]; df.columns=cols
311
+ num_cols=df.select_dtypes(include=[np.number]).columns.tolist()
312
+ if not num_cols: return None,None,None,None,"No numeric columns."
 
313
  bg="#fff" if theme=="White" else "#0a1628"; fg="#1a202c" if theme=="White" else "white"
314
  gc="#e2e8f0" if theme=="White" else "#2d4a8a"; ac="#4a5568" if theme=="White" else "#a8b2d8"
315
  pb="#f7fafc" if theme=="White" else "#132340"
316
  x=np.arange(len(df))
317
  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)
318
  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)
319
+ tc=next((c for c in cols if "time" in c or "frame" in c),None); xv=df[tc] if tc else x
 
320
  def pv(ax):
321
  if vc:
322
+ ax.plot(xv,df[vc],color="#c1121f",linewidth=2.5,marker="o",markersize=5)
323
+ ax.fill_between(xv,df[vc],alpha=0.15,color="#c1121f")
324
  ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
325
  ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
326
  ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
327
  def ps(ax):
328
  if sc:
329
  xp=xv.values if tc else x
330
+ ax.plot(xp,df[sc],color="#0057a8",linewidth=2.5,marker="s",markersize=5)
331
+ ax.fill_between(xp,df[sc],alpha=0.15,color="#0057a8")
332
  ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
333
+ ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="High risk: 10 Pa")
334
+ ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
335
  ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
336
  def psc(ax):
337
  if vc and sc:
338
  s2=ax.scatter(df[vc],df[sc],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
339
  cb=plt.colorbar(s2,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
340
+ ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Vel risk")
341
+ ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="Shear risk")
342
  ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11)
343
  ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
344
  def psum(ax):
 
349
  st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
350
  if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY")
351
  if "shear" in col and mx>10: risk.append("HIGH SHEAR")
352
+ bc="#c1121f" if risk else "#2ecc71"
353
  st+="━"*20+chr(10)+("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
354
  ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
355
  bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
 
369
  return i1,i2,i3,i4,"PIV: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
370
  except Exception as e: return None,None,None,None,"Error: "+str(e)
371
 
372
+ def analyze_tgt_csv(file,theme="White"):
373
+ if file is None: return None,None,None,None,"Upload TGT CSV first."
374
  try:
375
+ df=pd.read_csv(file.name); cols=[c.lower().strip() for c in df.columns]; df.columns=cols
376
+ num_cols=df.select_dtypes(include=[np.number]).columns.tolist()
 
377
  bg="#fff" if theme=="White" else "#0a1628"; fg="#1a202c" if theme=="White" else "white"
378
  gc="#e2e8f0" if theme=="White" else "#2d4a8a"; ac="#4a5568" if theme=="White" else "#a8b2d8"
379
  pb="#f7fafc" if theme=="White" else "#132340"
 
399
  mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
400
  ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
401
  return mk_chart(fn,title,bg,fg,gc,ac,pb)
402
+ i1=mk2(tatc,"#c1121f","TAT (ng/mL)",8,"Normal: 8","TAT Thrombin-Antithrombin")
403
+ i2=mk2(pfc,"#0057a8","PF1.2 (nmol/L)",2.0,"Normal: 2.0","PF1.2 Prothrombin Fragment")
404
  i3=mk2(hc,"#2ecc71","Free Hemoglobin (mg/L)",20,"Normal: 20","Free Hemoglobin",bar=True)
405
+ i4=mk2(plc,"#e8a020","Platelet Count",150,"Normal min: 150","Platelet Count")
406
  ai=""
407
  if GROQ_KEY:
408
  try:
409
  client=Groq(api_key=GROQ_KEY)
410
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
411
+ messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Give thrombogenicity risk LOW MODERATE or HIGH."},
412
  {"role":"user","content":"TGT from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
413
  ai=chr(10)+"━"*20+chr(10)+"AI: "+resp.choices[0].message.content
414
  except: pass
 
450
  risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
451
  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")
452
 
453
+ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
454
 
455
+ gr.HTML(HEADER_HTML)
 
 
 
 
 
456
 
457
  with gr.Tabs():
458
 
459
  with gr.Tab("πŸ’¬ Chat"):
460
  with gr.Row():
461
+ with gr.Column(scale=1, min_width=210):
462
+ gr.HTML('''<div style="background:#202123;padding:10px;border-radius:8px;margin-bottom:6px;">
463
+ <div style="color:#e8a020;font-weight:700;font-size:0.85em;letter-spacing:1px;">βš”οΈ SJSU CARDIOLAB</div>
464
+ <div style="color:#9ca3af;font-size:0.7em;margin-top:2px;">Conversations</div>
465
+ </div>''')
466
+ new_chat_btn = gr.Button("✏️ New Chat", variant="secondary")
467
+ gr.HTML('''<div style="color:#9ca3af;font-size:0.72em;padding:8px 2px 4px 2px;letter-spacing:1px;">SAVED SESSIONS</div>''')
468
+ session_dropdown = gr.Dropdown(choices=get_session_list(), label="", interactive=True, container=False)
469
+ load_btn = gr.Button("πŸ“‚ Load Session", variant="primary")
470
+ session_name_box = gr.Textbox(placeholder="Name this session...", label="", lines=1, container=False)
 
 
 
 
 
 
 
 
 
471
  with gr.Row():
472
+ save_btn = gr.Button("πŸ’Ύ Save", variant="primary", scale=2)
473
+ delete_btn = gr.Button("πŸ—‘οΈ", variant="secondary", scale=1)
474
  session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
475
 
 
476
  with gr.Column(scale=4):
477
+ chatbot = gr.Chatbot(label="", height=500, show_label=False, container=False)
 
 
 
 
 
478
  with gr.Row():
479
+ msg_box = gr.Textbox(placeholder="Message CardioLab AI...", label="", lines=2, scale=5, container=False)
 
 
 
 
 
 
480
  with gr.Column(scale=1, min_width=80):
481
  send_btn = gr.Button("Send ↑", variant="primary")
482
  clear_btn = gr.Button("Clear", variant="secondary")
 
490
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
491
 
492
  with gr.Tab("πŸŽ™οΈ Voice"):
493
+ voice_chatbot = gr.Chatbot(label="", height=380, show_label=False)
494
+ audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record your question")
495
  with gr.Row():
496
+ voice_btn = gr.Button("Ask by Voice", variant="primary")
497
+ voice_clear = gr.Button("Clear", variant="secondary")
 
 
 
 
498
  voice_btn.click(voice_chat, inputs=[audio_input, voice_chatbot], outputs=voice_chatbot)
499
  voice_clear.click(lambda: [], outputs=voice_chatbot)
500
 
 
502
  with gr.Row():
503
  search_input = gr.Textbox(placeholder="e.g. mechanical heart valve thrombogenicity 2024", label="Research Topic", scale=4)
504
  search_btn = gr.Button("Search", variant="primary", scale=1)
505
+ search_output = gr.Textbox(label="Verified Results β€” PubMed + Semantic Scholar", lines=18)
506
  search_btn.click(quick_search, inputs=search_input, outputs=search_output)
507
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
508
 
 
514
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
515
  piv_result = gr.Textbox(label="AI Analysis", lines=4)
516
  with gr.Row():
517
+ piv_c1=gr.Image(label="Velocity Profile",type="pil")
518
+ piv_c2=gr.Image(label="Shear Stress",type="pil")
519
  with gr.Row():
520
+ piv_c3=gr.Image(label="Velocity vs Shear",type="pil")
521
+ piv_c4=gr.Image(label="Clinical Summary",type="pil")
522
  piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
523
 
524
  with gr.Tab("🩸 TGT CSV"):
 
529
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
530
  tgt_result = gr.Textbox(label="AI Assessment", lines=4)
531
  with gr.Row():
532
+ tgt_c1=gr.Image(label="TAT",type="pil"); tgt_c2=gr.Image(label="PF1.2",type="pil")
 
533
  with gr.Row():
534
+ tgt_c3=gr.Image(label="Hemoglobin",type="pil"); tgt_c4=gr.Image(label="Platelets",type="pil")
 
535
  tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
536
 
537
  with gr.Tab("πŸ§ͺ uPAD"):
538
  with gr.Row():
539
  with gr.Column():
540
+ photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=260)
541
  analyze_btn = gr.Button("Analyze uPAD Photo", variant="primary")
542
  with gr.Column():
543
+ photo_img = gr.Image(label="Detection Zone", type="pil", height=260)
544
+ photo_text = gr.Textbox(label="CKD Result", lines=8)
545
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
546
+ gr.Markdown("**Manual RGB:**")
547
  with gr.Row():
548
  r=gr.Number(label="R",value=210); g=gr.Number(label="G",value=140); b=gr.Number(label="B",value=80)
549
  out3=gr.Textbox(label="Result",lines=3)
 
558
  img_btn = gr.Button("Generate Image", variant="primary")
559
  img_status = gr.Textbox(label="Status", lines=1)
560
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
561
+ img_output = gr.Image(label="Generated Image", type="pil", height=400)
562
  img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
563
 
564
  with gr.Tab("πŸ“ PIV Manual"):
565
  with gr.Row():
566
  with gr.Column():
567
  v=gr.Number(label="Max Velocity m/s",value=1.8,info="Normal: 0.5-2.0")
568
+ s=gr.Number(label="Wall Shear Stress Pa",value=6.5,info="Normal: <5")
569
  h=gr.Number(label="Heart Rate bpm",value=72,info="Normal: 60-100")
570
  piv_out=gr.Textbox(label="Result",lines=4)
571
  gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
 
581
  out2=gr.Textbox(label="Result",lines=6)
582
  gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
583
 
584
+ gr.HTML("""
585
+ <div style="text-align:center;padding:12px;border-top:1px solid #e5e7eb;margin-top:8px;background:#f9fafb;">
586
+ <span style="color:#9ca3af;font-size:0.75em;">
587
+ ❀️ CardioLab AI &nbsp;|&nbsp; SJSU Biomedical Engineering &nbsp;|&nbsp;
588
+ Built on <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;">Biomni Stanford</a> &nbsp;|&nbsp;
589
+ <a href="https://github.com/pranatechsol/Cardio-Lab-Ai" style="color:#0057a8;">GitHub</a> &nbsp;|&nbsp;
590
+ Apache 2.0 &nbsp;|&nbsp; $0 Cost
591
+ </span>
592
+ </div>
593
+ """)
594
+
595
  demo.launch()