Saicharan21 commited on
Commit
55c23a2
Β·
verified Β·
1 Parent(s): c1a2885

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +406 -246
app.py CHANGED
@@ -9,7 +9,6 @@ from groq import Groq
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", "")
@@ -22,100 +21,260 @@ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/
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",
@@ -139,18 +298,18 @@ def quick_search(query):
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
@@ -172,14 +331,13 @@ def voice_chat(audio, 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:
@@ -195,73 +353,62 @@ def analyze_upad_photo(image):
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=[]
@@ -269,85 +416,73 @@ def analyze_piv_csv(file, theme="White"):
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):
@@ -360,7 +495,7 @@ def generate_image(prompt):
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()
@@ -377,75 +512,105 @@ def generate_image(prompt):
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")
430
- voice_clear = gr.Button("Clear", variant="secondary")
 
 
 
 
431
  voice_btn.click(voice_chat, inputs=[audio_input, voice_chatbot], outputs=voice_chatbot)
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")
@@ -454,71 +619,66 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
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()
 
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", "")
 
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():
227
  if not HF_TOKEN: return {}
228
  try:
229
+ path = hf_hub_download(repo_id=HISTORY_REPO, filename="chat_history.json", repo_type="dataset", token=HF_TOKEN)
230
+ with open(path, "r") as f: return json.load(f)
231
+ except: return {}
 
 
 
 
 
 
 
 
232
 
233
  def save_all_sessions(sessions):
234
  if not HF_TOKEN: return False
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"
277
 
 
278
  def get_pubmed(query, n=5):
279
  try:
280
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
 
298
  def research_chat(message, history):
299
  if not GROQ_KEY:
300
  history.append({"role":"user","content":message})
301
+ history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
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})
309
  resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=700)
310
  answer = resp.choices[0].message.content
311
  pubmed = get_pubmed(message, n=3)
312
+ if pubmed: answer += chr(10)+chr(10)+"πŸ“š PubMed:"+chr(10)+pubmed
313
  history.append({"role":"user","content":message})
314
  history.append({"role":"assistant","content":answer})
315
  return "", history
 
331
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
332
  msgs.append({"role":"user","content":tx.text})
333
  resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
334
+ history.append({"role":"user","content":"πŸŽ™οΈ "+tx.text})
335
  history.append({"role":"assistant","content":resp.choices[0].message.content})
336
  return history
337
  except Exception as e:
338
  history.append({"role":"assistant","content":"Voice error: "+str(e)})
339
  return history
340
 
 
341
  def analyze_upad_photo(image):
342
  if image is None: return None, "Upload a uPAD photo first."
343
  try:
 
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):
414
  ax.axis("off"); risk=[]
 
416
  for col in num_cols[:3]:
417
  mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
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))
425
+ i1=mk_chart(pv,"Velocity Profile",bg,fg,gc,ac,pb)
426
+ i2=mk_chart(ps,"Wall Shear Stress",bg,fg,gc,ac,pb)
427
+ i3=mk_chart(psc,"Velocity vs Shear",bg,fg,gc,ac,pb)
428
+ i4=mk_chart(psum,"Clinical Summary",bg,fg,gc,ac,pb)
429
  ai=""
430
  if GROQ_KEY:
431
  try:
432
  client=Groq(api_key=GROQ_KEY)
433
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
434
+ messages=[{"role":"system","content":"PIV expert SJSU CardioLab. Analyze stats give clinical interpretation."},
435
+ {"role":"user","content":"PIV from 27mm SJM Regent MHV 70bpm:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
436
+ ai=chr(10)+"━"*20+chr(10)+"AI: "+resp.choices[0].message.content
437
  except: pass
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"
450
  tc=next((c for c in cols if "time" in c or "min" in c),None)
451
  tatc=next((c for c in cols if "tat" in c),num_cols[0] if num_cols else None)
452
  pfc=next((c for c in cols if "pf" in c),num_cols[1] if len(num_cols)>1 else None)
453
+ hc=next((c for c in cols if "hemo" in c),num_cols[2] if len(num_cols)>2 else None)
454
  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)
455
+ def mk2(dc,color,yl,lim,ll,title,bar=False):
456
+ def fn(ax):
457
+ if dc and dc in df.columns:
458
+ xp=df[tc].values if tc else range(len(df)); yp=df[dc].values
459
+ if bar:
460
+ bs=ax.bar(range(len(yp)),yp,color=color,alpha=0.85,edgecolor=bg,width=0.6)
461
+ 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")
462
+ else:
463
+ ax.plot(xp,yp,color=color,linewidth=3,marker="o",markersize=8)
464
+ ax.fill_between(xp,yp,alpha=0.15,color=color)
465
+ 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")
466
+ ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
467
+ ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
468
+ ax.set_ylabel(yl,color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
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
485
+ return i1,i2,i3,i4,"TGT: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
486
  except Exception as e: return None,None,None,None,"Error: "+str(e)
487
 
488
  def generate_image(prompt):
 
495
  client=Groq(api_key=GROQ_KEY)
496
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
497
  messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
498
+ {"role":"user","content":"Biomedical image: "+prompt}],max_tokens=200)
499
  full=resp.choices[0].message.content
500
  if "DESCRIPTION:" in full and "PROMPT:" in full:
501
  desc=full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
 
512
  except Exception as e: return None,"Error: "+str(e),""
513
 
514
  def piv_manual(v,s,h):
515
+ vr="HIGH β€” stenosis risk" if float(v)>2.0 else "NORMAL"
516
+ sr="HIGH β€” thrombosis risk" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
517
+ return "Velocity: "+str(v)+" m/s β€” "+vr+chr(10)+"Shear: "+str(s)+" Pa β€” "+sr+chr(10)+"HR: "+str(h)+" bpm"
518
 
519
  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")
579
 
580
  send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
581
  msg_box.submit(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
582
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
583
+ new_chat_btn.click(new_chat, outputs=[chatbot, msg_box, session_status])
584
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
585
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
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
 
599
+ with gr.Tab("πŸ” Papers"):
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
 
607
+ with gr.Tab("πŸ“Š PIV CSV"):
608
+ gr.Markdown("Upload PIV CSV β†’ 4 separate charts + AI clinical analysis")
609
  with gr.Row():
610
+ piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
611
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
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")
 
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"):
623
+ gr.Markdown("Upload TGT CSV β†’ blood biomarker charts + thrombogenicity assessment")
624
  with gr.Row():
625
+ tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
626
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
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)
650
+ gr.Button("Analyze RGB",variant="secondary").click(
651
+ 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"),
652
+ inputs=[r,g,b],outputs=out3)
653
 
654
+ with gr.Tab("🎨 AI Image"):
655
  with gr.Row():
656
+ img_prompt = gr.Textbox(placeholder="e.g. 27mm bileaflet mechanical heart valve cross section", label="Describe the image", lines=2, scale=4)
657
  with gr.Column(scale=1):
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)
672
 
673
+ with gr.Tab("πŸ”¬ TGT Manual"):
674
  with gr.Row():
675
  with gr.Column():
676
+ t1=gr.Number(label="TAT ng/mL",value=18,info="Normal: <8")
677
+ t2=gr.Number(label="PF1.2 nmol/L",value=2.5,info="Normal: <2.0")
678
+ t3=gr.Number(label="Free Hemoglobin mg/L",value=60,info="Normal: <20")
679
+ t4=gr.Number(label="Platelet Count",value=140,info="Normal: >150")
680
+ t5=gr.Number(label="Time minutes",value=40)
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()