Saicharan21 commited on
Commit
1750d6e
·
verified ·
1 Parent(s): 9f268b2

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +550 -337
app.py CHANGED
@@ -9,421 +9,596 @@ 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", "")
16
  HISTORY_REPO = "Saicharan21/cardiolab-chat-history"
 
 
 
 
 
 
 
 
 
17
 
18
  KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
19
- "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
20
- "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
21
- "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
22
- "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
 
 
23
 
24
  CSS = """
25
- body, .gradio-container { background: #f0f4f8 !important; }
26
- .tab-nav { background: #ffffff !important; border-bottom: 2px solid #e2e8f0 !important; padding: 4px 5px 0 5px !important; display: flex !important; flex-wrap: wrap !important; gap: 3px !important; }
27
- .tab-nav button { background: #f7fafc !important; color: #2d3748 !important; border: 1px solid #e2e8f0 !important; border-radius: 6px 6px 0 0 !important; padding: 8px 10px !important; font-weight: 600 !important; font-size: 0.8em !important; white-space: nowrap !important; }
28
- .tab-nav button:hover { background: #ebf4ff !important; color: #1a237e !important; }
29
- .tab-nav button.selected { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: #ffffff !important; font-weight: 700 !important; }
30
- button.primary { background: linear-gradient(135deg, #e63946 0%, #c1121f 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; }
31
- button.secondary { background: #edf2f7 !important; color: #4a5568 !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
32
- textarea, input[type=number], input[type=text] { background: #f7fafc !important; color: #1a202c !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
33
- .message.user { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: white !important; }
34
- .message.bot { background: #ebf4ff !important; color: #1a202c !important; border: 1px solid #bee3f8 !important; }
35
- label span { color: #2b6cb0 !important; font-weight: 600 !important; font-size: 0.85em !important; text-transform: uppercase !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  """
37
 
38
- # ── PERSISTENT HISTORY FUNCTIONS ──────────────────────────────────
39
- def get_history_api():
40
- if not HF_TOKEN: return None
41
- return HfApi(token=HF_TOKEN)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def load_all_sessions():
44
  if not HF_TOKEN: return {}
45
  try:
46
- api = get_history_api()
47
- path = hf_hub_download(
48
- repo_id=HISTORY_REPO,
49
- filename="chat_history.json",
50
- repo_type="dataset",
51
- token=HF_TOKEN
52
- )
53
- with open(path, "r") as f:
54
- return json.load(f)
55
- except Exception:
56
- return {}
57
 
58
  def save_all_sessions(sessions):
59
  if not HF_TOKEN: return False
60
  try:
61
- api = get_history_api()
62
- content = json.dumps(sessions, indent=2)
63
- api.upload_file(
64
- path_or_fileobj=content.encode(),
65
- path_in_repo="chat_history.json",
66
- repo_id=HISTORY_REPO,
67
- repo_type="dataset",
68
- token=HF_TOKEN,
69
- commit_message="Update chat history"
70
- )
71
  return True
72
- except Exception as e:
73
- print("Save error:", e)
74
- return False
75
 
76
  def get_session_list():
77
- sessions = load_all_sessions()
78
- if not sessions:
79
- return ["No saved sessions yet"]
80
- return list(sessions.keys())
81
 
82
- def load_session(session_name):
83
- if not session_name or session_name == "No saved sessions yet":
84
- return [], "No session loaded"
85
  sessions = load_all_sessions()
86
- if session_name in sessions:
87
- history = sessions[session_name]["messages"]
88
- return history, "Loaded: " + session_name + " (" + str(len(history)) + " messages)"
89
- return [], "Session not found"
90
-
91
- def save_session(history, session_name):
92
- if not history:
93
- return "Nothing to save — chat is empty", gr.update()
94
- if not session_name.strip():
95
- session_name = "Session " + datetime.now().strftime("%Y-%m-%d %H:%M")
96
  sessions = load_all_sessions()
97
- sessions[session_name] = {
98
- "messages": history,
99
- "saved_at": datetime.now().isoformat(),
100
- "message_count": len(history)
101
- }
102
- success = save_all_sessions(sessions)
103
- if success:
104
- return "Saved: " + session_name, gr.update(choices=get_session_list(), value=session_name)
105
- return "Save failed — check HF_TOKEN in Space secrets", gr.update()
106
-
107
- def delete_session(session_name):
108
- if not session_name or session_name == "No saved sessions yet":
109
- return "No session selected", gr.update()
110
  sessions = load_all_sessions()
111
- if session_name in sessions:
112
- del sessions[session_name]
113
- save_all_sessions(sessions)
114
- new_list = get_session_list()
115
- return "Deleted: " + session_name, gr.update(choices=new_list, value=new_list[0] if new_list else None)
116
- return "Session not found", gr.update()
117
-
118
- # ── CHAT FUNCTIONS ────────────────────────────────────────────────
119
- def get_pubmed(query, n=5):
 
120
  try:
121
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
122
- params={"db":"pubmed","term":query+" AND (mechanical heart valve OR microfluidic OR CKD OR thrombogenicity)","retmax":n,"retmode":"json","sort":"date"},timeout=10)
123
  ids = r.json()["esearchresult"]["idlist"]
124
- if not ids: return ""
125
- return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
126
  except: return ""
127
 
128
  def quick_search(query):
129
  if not query.strip(): return "Please enter a topic."
130
- pubmed = get_pubmed(query, n=8)
131
  try:
132
- r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
133
- params={"query":query+" biomedical","limit":5,"fields":"title,year,url"},timeout=10)
134
- papers = r.json().get("data",[])
135
- scholar = chr(10).join([p.get("title","")[:80]+" ("+str(p.get("year",""))+")"+chr(10)+" "+p.get("url","") for p in papers if p.get("url","")])
136
- except: scholar = ""
137
- return "PUBMED:"+chr(10)+pubmed+chr(10)+chr(10)+"SCHOLAR:"+chr(10)+scholar
138
-
139
- def research_chat(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  if not GROQ_KEY:
141
  history.append({"role":"user","content":message})
142
- history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings Secrets."})
143
  return "", history
144
  try:
 
145
  client = Groq(api_key=GROQ_KEY)
146
- msgs = [{"role":"system","content":"You are CardioLab AI. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember full conversation. Never invent URLs. "+KNOWHOW}]
 
 
 
 
 
 
 
147
  for item in history:
148
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
149
  msgs.append({"role":"user","content":message})
150
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=700)
151
  answer = resp.choices[0].message.content
152
- pubmed = get_pubmed(message, n=3)
153
- if pubmed: answer += chr(10)+chr(10)+"PUBMED:"+chr(10)+pubmed
 
 
 
 
 
154
  history.append({"role":"user","content":message})
155
  history.append({"role":"assistant","content":answer})
156
  return "", history
157
  except Exception as e:
158
  history.append({"role":"user","content":message})
159
- history.append({"role":"assistant","content":"Error: "+str(e)})
160
  return "", history
161
 
162
  def voice_chat(audio, history):
163
  if audio is None:
164
- history.append({"role":"assistant","content":"Please record your question first."})
165
  return history
166
  try:
167
  client = Groq(api_key=GROQ_KEY)
168
  with open(audio, "rb") as f:
169
  tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
170
- msgs = [{"role":"system","content":"You are CardioLab AI. "+KNOWHOW}]
 
 
 
171
  for item in history:
172
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
173
  msgs.append({"role":"user","content":tx.text})
174
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
175
- history.append({"role":"user","content":"[Voice] "+tx.text})
176
  history.append({"role":"assistant","content":resp.choices[0].message.content})
177
  return history
178
  except Exception as e:
179
- history.append({"role":"assistant","content":"Voice error: "+str(e)})
180
  return history
181
 
182
- # ── ANALYSIS TOOLS ────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  def analyze_upad_photo(image):
184
  if image is None: return None, "Upload a uPAD photo first."
185
  try:
186
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
187
- arr = np.array(img)
188
- h,w = arr.shape[:2]
189
- y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
190
- zone = arr[y1:y2,x1:x2]
191
- R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
192
  c = max(0, round(0.018*(R-B)-0.3, 2))
193
- if c<1.2: s,a="Normal","Monitor annually."
194
- elif c<1.5: s,a="Borderline","Repeat in 3 months."
195
- elif c<3.0: s,a="Stage 2 CKD","Consult nephrologist."
196
- elif c<6.0: s,a="Stage 3-4 CKD","Immediate consultation."
197
- else: s,a="Stage 5 CKD","Emergency care needed."
198
- result_img = img.copy()
199
- import PIL.ImageDraw as D
200
- draw = D.Draw(result_img)
201
- draw.rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
202
- return result_img, ("uPAD ANALYSIS"+chr(10)+"━"*22+chr(10)+
203
- "R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+
204
- "Orange Score: "+str(round(R-B,1))+chr(10)+"━"*22+chr(10)+
205
- "CREATININE: "+str(c)+" mg/dL"+chr(10)+"CKD STAGE: "+s+chr(10)+
206
- "ACTION: "+a+chr(10)+"Confirm: Heska Element HT5")
207
- except Exception as e: return None, "Error: "+str(e)
 
 
 
 
 
208
 
209
  def analyze_piv_csv(file, theme="White"):
210
- if file is None: return None,None,None,None,"Upload a PIV CSV file first."
211
  try:
212
- df = pd.read_csv(file.name)
213
- cols = [c.lower().strip() for c in df.columns]
214
- df.columns = cols
215
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
216
- if not num_cols: return None,None,None,None,"No numeric columns found."
217
- bg = "#ffffff" if theme=="White" else "#0a1628"
218
- fg = "#1a202c" if theme=="White" else "white"
219
- gc = "#e2e8f0" if theme=="White" else "#2d4a8a"
220
- ac = "#4a5568" if theme=="White" else "#a8b2d8"
221
  pb = "#f7fafc" if theme=="White" else "#132340"
222
  x = np.arange(len(df))
223
  vc = next((c for c in cols if any(k in c for k in ["vel","speed","v_mag"])), num_cols[0] if num_cols else None)
224
- sc = next((c for c in cols if any(k in c for k in ["shear","stress","tau","wss"])), num_cols[1] if len(num_cols)>1 else None)
225
- tc = next((c for c in cols if "time" in c or "frame" in c), None)
226
- xv = df[tc] if tc else x
227
- def mk(fn, title):
228
- fig2,ax = plt.subplots(figsize=(8,5))
229
- fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
230
- fn(ax)
231
- ax.set_title(title, color=fg, fontweight="bold", fontsize=13, pad=8)
232
- ax.tick_params(colors=ac, labelsize=10)
233
- ax.grid(True, alpha=0.3, color=gc, linestyle="--")
234
- for sp in ["top","right"]: ax.spines[sp].set_visible(False)
235
- for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
236
- plt.tight_layout()
237
- buf2=io.BytesIO(); plt.savefig(buf2,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf2.seek(0)
238
- res=Image.open(buf2).copy(); plt.close(); return res
239
  def pv(ax):
240
  if vc:
241
- ax.plot(xv,df[vc],color="#e63946",linewidth=2.5,marker="o",markersize=5)
242
- ax.fill_between(xv,df[vc],alpha=0.2,color="#e63946")
243
- ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
244
- ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11)
245
- ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
246
- ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
247
  def ps(ax):
248
- if sc:
249
  xp = xv.values if tc else x
250
- ax.plot(xp,df[sc],color="#4361ee",linewidth=2.5,marker="s",markersize=5)
251
- ax.fill_between(xp,df[sc],alpha=0.2,color="#4361ee")
252
- ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
253
- ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="High risk: 10 Pa")
254
- ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11)
255
- ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
256
- ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
257
  def psc(ax):
258
- if vc and sc:
259
- s2 = ax.scatter(df[vc],df[sc],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
260
- cb=plt.colorbar(s2,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
261
- ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Vel risk")
262
- ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="Shear risk")
263
- ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11)
264
- ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11)
265
- ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
266
  def psum(ax):
267
- ax.axis("off"); risk=[]
268
- st="CLINICAL SUMMARY"+chr(10)+""*20+chr(10)+chr(10)
269
  for col in num_cols[:3]:
270
- mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
271
- st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
272
- if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY (>2.0 m/s)")
273
- if "shear" in col and mx>10: risk.append("HIGH SHEAR (>10 Pa)")
274
- st+=""*20+chr(10)
275
- if risk:
276
- st+="RISK FLAGS:"+chr(10)+"".join([" ⚠ "+r+chr(10) for r in risk])
277
- st+="OVERALL: HIGH RISK"; bc="#e63946"
278
- else:
279
- st+="OVERALL: LOW RISK"; bc="#2ecc71"
280
- ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
281
- bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
282
- i1=mk(pv,"Velocity Profile"); i2=mk(ps,"Wall Shear Stress")
283
- i3=mk(psc,"Velocity vs Shear"); i4=mk(psum,"Clinical Summary")
284
- ai=""
285
  if GROQ_KEY:
286
  try:
287
- client=Groq(api_key=GROQ_KEY)
288
- resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
289
- messages=[{"role":"system","content":"PIV expert SJSU CardioLab. Analyze PIV stats give clinical interpretation."},
290
- {"role":"user","content":"PIV data from 27mm SJM Regent MHV 70bpm 5L/min:"+chr(10)+df.describe().to_string()[:600]}],max_tokens=300)
291
- ai=chr(10)+"━"*20+chr(10)+"AI:"+chr(10)+resp.choices[0].message.content
292
  except: pass
293
- return i1,i2,i3,i4,"PIV LOADED: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
294
- except Exception as e: return None,None,None,None,"Error: "+str(e)
295
 
296
  def analyze_tgt_csv(file, theme="White"):
297
- if file is None: return None,None,None,None,"Upload a TGT CSV file first."
298
  try:
299
- df = pd.read_csv(file.name)
300
- cols = [c.lower().strip() for c in df.columns]
301
- df.columns = cols
302
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
303
- bg="#ffffff" if theme=="White" else "#0a1628"
304
- fg="#1a202c" if theme=="White" else "white"
305
- gc="#e2e8f0" if theme=="White" else "#2d4a8a"
306
- ac="#4a5568" if theme=="White" else "#a8b2d8"
307
- pb="#f7fafc" if theme=="White" else "#132340"
308
- tc=next((c for c in cols if "time" in c or "min" in c),None)
309
- tatc=next((c for c in cols if "tat" in c),num_cols[0] if num_cols else None)
310
- pfc=next((c for c in cols if "pf" in c),num_cols[1] if len(num_cols)>1 else None)
311
- hc=next((c for c in cols if "hemo" in c or "hgb" in c),num_cols[2] if len(num_cols)>2 else None)
312
- plc=next((c for c in cols if "platelet" in c or "plt" in c),num_cols[3] if len(num_cols)>3 else None)
313
- xv=df[tc] if tc else range(len(df))
314
- def mk(dc,color,yl,lim,ll,title,bar=False):
315
- fig2,ax=plt.subplots(figsize=(8,5))
316
- fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
317
- if dc and dc in df.columns:
318
- xp=df[tc].values if tc else range(len(df)); yp=df[dc].values
319
- if bar:
320
- bs=ax.bar(range(len(yp)),yp,color=color,alpha=0.85,edgecolor=bg,width=0.6)
321
- for b,v in zip(bs,yp): ax.text(b.get_x()+b.get_width()/2,b.get_height()+0.5,str(round(v,1)),ha="center",va="bottom",color=fg,fontsize=10,fontweight="bold")
322
- else:
323
- ax.plot(xp,yp,color=color,linewidth=3,marker="o",markersize=8)
324
- ax.fill_between(xp,yp,alpha=0.2,color=color)
325
- for xi,yi in zip(xp,yp): ax.annotate(str(round(yi,1)),(xi,yi),textcoords="offset points",xytext=(0,10),ha="center",color=fg,fontsize=10,fontweight="bold")
326
- ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
327
- ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
328
- ax.set_ylabel(yl,color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
329
- mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
330
- ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
331
- ax.tick_params(colors=ac,labelsize=10); ax.grid(True,alpha=0.3,color=gc,linestyle="--")
332
- for sp in ["top","right"]: ax.spines[sp].set_visible(False)
333
- for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
334
- plt.tight_layout()
335
- buf2=io.BytesIO(); plt.savefig(buf2,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf2.seek(0)
336
- res=Image.open(buf2).copy(); plt.close(); return res
337
- i1=mk(tatc,"#e63946","TAT (ng/mL)",8,"Normal: 8 ng/mL","Thrombin-Antithrombin TAT")
338
- i2=mk(pfc,"#4361ee","PF1.2 (nmol/L)",2.0,"Normal: 2.0","Prothrombin Fragment PF1.2")
339
- i3=mk(hc,"#2ecc71","Free Hemoglobin (mg/L)",20,"Normal: 20 mg/L","Free Hemoglobin Hemolysis",bar=True)
340
- i4=mk(plc,"#e67e22","Platelet Count (10³/μL)",150,"Normal min: 150","Platelet Count")
341
- ai=""
342
  if GROQ_KEY:
343
  try:
344
- client=Groq(api_key=GROQ_KEY)
345
- resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
346
- messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Analyze TGT data give thrombogenicity risk LOW MODERATE or HIGH. Normal: TAT<8, PF1.2<2.0, Hemo<20, Plt>150."},
347
- {"role":"user","content":"TGT from 27mm SJM Regent MHV:"+chr(10)+df.describe().to_string()[:600]}],max_tokens=300)
348
- ai=chr(10)+"━"*20+chr(10)+"AI:"+chr(10)+resp.choices[0].message.content
349
  except: pass
350
- return i1,i2,i3,i4,"TGT LOADED: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
351
- except Exception as e: return None,None,None,None,"Error: "+str(e)
352
 
353
  def generate_image(prompt):
354
- if not prompt.strip(): return None,"Enter description.","";
355
- if not HF_TOKEN: return None,"Add HF_TOKEN to Space secrets.","";
356
  try:
357
- enhanced,desc=prompt,""
358
  if GROQ_KEY:
359
  try:
360
- client=Groq(api_key=GROQ_KEY)
361
- resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
362
  messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
363
- {"role":"user","content":"Biomedical image for CardioLab: "+prompt}],max_tokens=200)
364
- full=resp.choices[0].message.content
365
  if "DESCRIPTION:" in full and "PROMPT:" in full:
366
- desc=full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
367
- enhanced=full.split("PROMPT:")[1].strip()
368
  except: pass
369
- headers={"Authorization":"Bearer "+HF_TOKEN,"Content-Type":"application/json"}
370
  for url in ["https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell",
371
  "https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"]:
372
  try:
373
- r=requests.post(url,headers=headers,json={"inputs":enhanced,"parameters":{"num_inference_steps":8}},timeout=60)
374
- if r.status_code==200: return Image.open(io.BytesIO(r.content)),"Generated!",desc
375
  except: continue
376
- return None,"Models busy. Try again.",desc
377
- except Exception as e: return None,"Error: "+str(e),""
378
 
379
- def piv_manual(v,s,h):
380
- vr="HIGH-stenosis" if float(v)>2.0 else "NORMAL"
381
- sr="HIGH-thrombosis" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
382
- return "Velocity: "+str(v)+" - "+vr+chr(10)+"Shear: "+str(s)+" - "+sr+chr(10)+"HR: "+str(h)+" bpm"
383
 
384
- def tgt_manual(t,p,h,pl,tm):
385
- risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
386
- return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"Time:"+str(tm)+"min"+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
387
 
388
- # ── UI ─────────────────────────────────────────────────────────────
389
- with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
390
- gr.HTML('''<div style="background:linear-gradient(135deg,#1a237e,#b71c1c);padding:20px;text-align:center;border-radius:12px 12px 0 0"><div style="font-size:2.5em;font-weight:900;color:#fff;letter-spacing:3px">CardioLab AI</div></div>''')
 
 
391
 
392
  with gr.Tabs():
393
 
394
  with gr.Tab("Chat"):
395
- gr.Markdown("### Chat with memory — saves conversations like ChatGPT")
396
  with gr.Row():
397
- with gr.Column(scale=3):
398
- chatbot = gr.Chatbot(label="", height=420)
 
 
 
 
 
 
 
 
399
  with gr.Row():
400
- msg_box = gr.Textbox(placeholder="Ask about CardioLab research...", label="", lines=2, scale=4)
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  with gr.Column(scale=1, min_width=80):
402
  send_btn = gr.Button("Send", variant="primary")
403
  clear_btn = gr.Button("Clear", variant="secondary")
404
- with gr.Column(scale=1, min_width=200):
405
- gr.Markdown("### Saved Sessions")
406
- session_dropdown = gr.Dropdown(
407
- choices=get_session_list(),
408
- label="Load a saved session",
409
- interactive=True
410
- )
411
- load_btn = gr.Button("Load Session", variant="primary")
412
- session_status = gr.Textbox(label="Status", lines=1, interactive=False)
413
- gr.Markdown("### Save Current Chat")
414
- session_name_box = gr.Textbox(label="Session name", placeholder="e.g. TGT Research May 2026")
415
- save_btn = gr.Button("Save Chat", variant="primary")
416
- delete_btn = gr.Button("Delete Session", variant="secondary")
417
-
418
- send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
419
- msg_box.submit(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
420
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
 
421
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
422
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
423
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
424
 
425
  with gr.Tab("Voice"):
426
- voice_chatbot = gr.Chatbot(label="", height=320)
427
  audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
428
  with gr.Row():
429
  voice_btn = gr.Button("Ask by Voice", variant="primary")
@@ -432,93 +607,131 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
432
  voice_clear.click(lambda: [], outputs=voice_chatbot)
433
 
434
  with gr.Tab("Papers"):
 
435
  with gr.Row():
436
- search_input = gr.Textbox(placeholder="e.g. mechanical heart valve thrombogenicity", label="Research Topic", scale=4)
437
  search_btn = gr.Button("Search", variant="primary", scale=1)
438
- search_output = gr.Textbox(label="Verified Results", lines=18)
439
  search_btn.click(quick_search, inputs=search_input, outputs=search_output)
440
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
441
 
442
  with gr.Tab("PIV CSV"):
443
- gr.Markdown("### Upload PIV CSV — 4 separate charts + AI analysis")
444
  with gr.Row():
445
- piv_file = gr.File(label="UPLOAD PIV CSV", file_types=[".csv"], scale=3)
446
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
447
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
448
- piv_result = gr.Textbox(label="AI Analysis", lines=5)
449
  with gr.Row():
450
  piv_c1 = gr.Image(label="Velocity Profile", type="pil")
451
  piv_c2 = gr.Image(label="Shear Stress", type="pil")
452
  with gr.Row():
453
  piv_c3 = gr.Image(label="Velocity vs Shear", type="pil")
454
  piv_c4 = gr.Image(label="Clinical Summary", type="pil")
455
- piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
456
 
457
  with gr.Tab("TGT CSV"):
458
- gr.Markdown("### Upload TGT CSV — blood biomarker charts + thrombogenicity assessment")
459
  with gr.Row():
460
- tgt_file = gr.File(label="UPLOAD TGT CSV", file_types=[".csv"], scale=3)
461
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
462
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
463
- tgt_result = gr.Textbox(label="AI Assessment", lines=5)
464
  with gr.Row():
465
- tgt_c1 = gr.Image(label="TAT Over Time", type="pil")
466
- tgt_c2 = gr.Image(label="PF1.2 Over Time", type="pil")
467
  with gr.Row():
468
- tgt_c3 = gr.Image(label="Free Hemoglobin", type="pil")
469
- tgt_c4 = gr.Image(label="Platelet Count", type="pil")
470
- tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
471
 
472
- with gr.Tab("uPAD Photo"):
473
- gr.Markdown("### Upload uPAD Photo — Instant CKD diagnosis")
474
  with gr.Row():
475
  with gr.Column():
476
- photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=280)
477
- analyze_btn = gr.Button("Analyze uPAD", variant="primary")
478
  with gr.Column():
479
- photo_img = gr.Image(label="Detection Zone", type="pil", height=280)
480
- photo_text = gr.Textbox(label="CKD Result", lines=10)
481
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
 
 
 
 
 
 
 
 
482
 
483
  with gr.Tab("AI Image"):
484
  with gr.Row():
485
- img_prompt = gr.Textbox(placeholder="e.g. bileaflet heart valve | uPAD device | Arduino TGT", label="Describe image", lines=2, scale=4)
486
  with gr.Column(scale=1):
487
  img_btn = gr.Button("Generate", variant="primary")
488
  img_status = gr.Textbox(label="Status", lines=1)
489
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
490
- img_output = gr.Image(label="Generated Image", type="pil", height=380)
491
- img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
492
 
493
  with gr.Tab("PIV Manual"):
494
  with gr.Row():
495
  with gr.Column():
496
- v=gr.Number(label="Max Velocity m/s",value=1.8)
497
- s=gr.Number(label="Wall Shear Stress Pa",value=6.5)
498
- h=gr.Number(label="Heart Rate bpm",value=72)
499
- piv_out=gr.Textbox(label="Result",lines=4)
500
- gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
501
 
502
  with gr.Tab("TGT Manual"):
503
  with gr.Row():
504
  with gr.Column():
505
- t1=gr.Number(label="TAT ng/mL",value=18)
506
- t2=gr.Number(label="PF1.2",value=2.5)
507
- t3=gr.Number(label="Hemoglobin mg/L",value=60)
508
- t4=gr.Number(label="Platelets",value=140)
509
- t5=gr.Number(label="Time min",value=40)
510
- out2=gr.Textbox(label="Result",lines=6)
511
- gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
512
-
513
- with gr.Tab("uPAD Manual"):
514
  with gr.Row():
515
- with gr.Column():
516
- r=gr.Number(label="R value",value=210)
517
- g=gr.Number(label="G value",value=140)
518
- b=gr.Number(label="B value",value=80)
519
- out3=gr.Textbox(label="Result",lines=4)
520
- gr.Button("Analyze",variant="primary").click(
521
- lambda r,g,b:"Creatinine: "+str(max(0,round(0.02*(r-b)-0.5,2)))+" mg/dL"+chr(10)+("Normal" if max(0,round(0.02*(r-b)-0.5,2))<1.2 else "Borderline" if max(0,round(0.02*(r-b)-0.5,2))<1.5 else "CKD Stage 2+"),
522
- inputs=[r,g,b],outputs=out3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
 
524
  demo.launch()
 
9
  from PIL import Image
10
  from datetime import datetime
11
  from huggingface_hub import HfApi, hf_hub_download
 
12
 
13
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
14
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
15
  HISTORY_REPO = "Saicharan21/cardiolab-chat-history"
16
+ PAPERS_DB_REPO = "Saicharan21/cardiolab-papers-db"
17
+ CARDIOLAB_MODEL_REPO = "Saicharan21/CardioLab-AI-Model"
18
+
19
+ CHAT_MODELS = {
20
+ "Llama 3.3 70B (Best)": "llama-3.3-70b-versatile",
21
+ "Llama 3.1 8B (Fast)": "llama-3.1-8b-instant",
22
+ "Llama 4 Scout": "meta-llama/llama-4-scout-17b-16e-instruct",
23
+ "Llama 4 Maverick": "meta-llama/llama-4-maverick-17b-128e-instruct",
24
+ }
25
 
26
  KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
27
+ "TGT: Arduino Uno Stepper Motor 150mL blood 0 20 40 60min TAT PF1.2 hemolysis platelets. "
28
+ "NORMAL: TAT below 8. PF1.2 below 2.0. Hemo below 20. Plt above 150. "
29
+ "uPAD: Jaffe reaction creatinine picric acid orange-red. Normal 0.6-1.2 mg/dL. CKD above 1.5. "
30
+ "MHV: 27mm SJM Regent bileaflet trileaflet monoleaflet pediatric. "
31
+ "PIV: green laser 532nm. Normal velocity 0.5-2.0 m/s. Shear below 5 Pa. Risk above 10 Pa. "
32
+ "Equipment: Heska HT5 analyzer PIV Tygon tubing Arduino Uno.")
33
 
34
  CSS = """
35
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
36
+ body, .gradio-container { background: #f8fafc !important; font-family: Inter, sans-serif !important; }
37
+ .tab-nav { background: #fff !important; border-bottom: 1px solid #e2e8f0 !important; padding: 0 8px !important; display: flex !important; flex-wrap: wrap !important; }
38
+ .tab-nav button { background: transparent !important; color: #64748b !important; border: none !important; border-bottom: 2px solid transparent !important; border-radius: 0 !important; padding: 10px 12px !important; font-weight: 500 !important; font-size: 0.8em !important; white-space: nowrap !important; margin-bottom: -1px !important; }
39
+ .tab-nav button:hover { color: #c1121f !important; background: #fff5f5 !important; }
40
+ .tab-nav button.selected { color: #c1121f !important; border-bottom: 2px solid #c1121f !important; font-weight: 700 !important; background: transparent !important; }
41
+ .message.user { background: linear-gradient(135deg, #c1121f, #e63946) !important; color: white !important; border-radius: 14px 14px 4px 14px !important; padding: 12px 16px !important; }
42
+ .message.bot { background: #ffffff !important; color: #1a202c !important; border: 1px solid #e2e8f0 !important; border-left: 3px solid #c1121f !important; border-radius: 4px 14px 14px 14px !important; padding: 12px 16px !important; }
43
+ textarea { background: #fff !important; color: #1a202c !important; border: 1px solid #e2e8f0 !important; border-radius: 10px !important; }
44
+ textarea:focus { border-color: #c1121f !important; outline: none !important; box-shadow: 0 0 0 2px rgba(193,18,31,0.1) !important; }
45
+ button.primary { background: #c1121f !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; }
46
+ button.primary:hover { background: #a00e18 !important; transform: translateY(-1px) !important; }
47
+ button.secondary { background: #f1f5f9 !important; color: #475569 !important; border: 1px solid #e2e8f0 !important; border-radius: 8px !important; }
48
+ input[type=number] { background: #fff !important; color: #1a202c !important; border: 1px solid #e2e8f0 !important; border-radius: 8px !important; }
49
+ label span { color: #475569 !important; font-weight: 500 !important; font-size: 0.82em !important; }
50
+ ::-webkit-scrollbar { width: 5px; }
51
+ ::-webkit-scrollbar-thumb { background: #c1121f; border-radius: 4px; }
52
+ """
53
+
54
+ HEADER = """
55
+ <style>
56
+ @keyframes hb{0%,100%{transform:scale(1)}15%{transform:scale(1.14)}30%{transform:scale(1)}45%{transform:scale(1.08)}60%{transform:scale(1)}}
57
+ @keyframes ecg{from{stroke-dashoffset:400}to{stroke-dashoffset:0}}
58
+ @keyframes fadeD{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
59
+ </style>
60
+ <div style="background:#fff;border-bottom:2px solid #c1121f;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 1px 8px rgba(0,0,0,0.06);animation:fadeD 0.4s ease;">
61
+ <div style="display:flex;align-items:center;gap:10px;">
62
+ <div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:7px 12px;display:flex;align-items:center;gap:8px;">
63
+ <svg width="20" height="20" viewBox="0 0 100 100">
64
+ <circle cx="50" cy="35" r="28" fill="#0057a8"/><ellipse cx="50" cy="14" rx="18" ry="8" fill="#0057a8"/>
65
+ <polygon points="35,12 37,5 40,12" fill="#e8a020"/><polygon points="40,11 43,4 46,11" fill="#e8a020"/>
66
+ <polygon points="46,11 49,4 52,11" fill="#e8a020"/><polygon points="52,11 55,4 58,11" fill="#e8a020"/>
67
+ <polygon points="58,12 61,5 64,12" fill="#e8a020"/>
68
+ <rect x="38" y="30" width="24" height="18" rx="3" fill="#0057a8"/>
69
+ <rect x="42" y="34" width="6" height="10" rx="2" fill="#e8a020"/>
70
+ <rect x="36" y="46" width="28" height="6" rx="3" fill="#0057a8"/>
71
+ </svg>
72
+ <div>
73
+ <div style="color:#1d4ed8;font-size:0.68em;font-weight:700;line-height:1.2;">SJSU</div>
74
+ <div style="color:#374151;font-size:0.6em;line-height:1.2;">Biomedical Eng.</div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <div style="display:flex;align-items:center;gap:14px;">
79
+ <svg width="80" height="22" viewBox="0 0 100 22">
80
+ <polyline points="0,11 18,11 23,3 27,19 31,1 35,17 39,11 100,11" fill="none" stroke="#c1121f" stroke-width="2" stroke-linecap="round" stroke-dasharray="400" style="animation:ecg 1.5s ease forwards;"/>
81
+ </svg>
82
+ <div style="display:flex;align-items:center;gap:12px;">
83
+ <div style="animation:hb 1.4s ease infinite;">
84
+ <svg width="34" height="30" viewBox="0 0 100 90">
85
+ <defs><radialGradient id="hg" cx="50%" cy="35%"><stop offset="0%" stop-color="#e63946"/><stop offset="100%" stop-color="#9b0a14"/></radialGradient></defs>
86
+ <path d="M50 85 C50 85 5 55 5 30 C5 15 18 5 30 5 C38 5 45 9 50 15 C55 9 62 5 70 5 C82 5 95 15 95 30 C95 55 50 85 50 85Z" fill="url(#hg)"/>
87
+ <polyline points="22,46 30,46 34,35 38,57 42,28 46,51 52,46 78,46" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round" opacity="0.95"/>
88
+ </svg>
89
+ </div>
90
+ <div>
91
+ <div style="font-size:1.6em;font-weight:700;color:#111;letter-spacing:-0.5px;line-height:1.1;">Cardio<span style="color:#c1121f;">Lab</span> AI</div>
92
+ <div style="font-size:0.6em;color:#9ca3af;margin-top:1px;">SJSU Biomedical Engineering</div>
93
+ </div>
94
+ </div>
95
+ <svg width="80" height="22" viewBox="0 0 100 22" style="transform:scaleX(-1);">
96
+ <polyline points="0,11 18,11 23,3 27,19 31,1 35,17 39,11 100,11" fill="none" stroke="#c1121f" stroke-width="2" stroke-linecap="round" stroke-dasharray="400" style="animation:ecg 1.8s ease forwards;"/>
97
+ </svg>
98
+ </div>
99
+ <div style="display:flex;gap:6px;align-items:center;">
100
+ <span style="background:#fef2f2;border:1px solid #fecaca;color:#c1121f;padding:3px 10px;border-radius:20px;font-size:0.65em;font-weight:600;">RAG Active</span>
101
+ <span style="background:#eff6ff;border:1px solid #bfdbfe;color:#1d4ed8;padding:3px 10px;border-radius:20px;font-size:0.65em;font-weight:600;">4 Models</span>
102
+ <span style="background:#f0fdf4;border:1px solid #bbf7d0;color:#15803d;padding:3px 10px;border-radius:20px;font-size:0.65em;font-weight:600;">16 Papers</span>
103
+ </div>
104
+ </div>
105
  """
106
 
107
+ # ── PAPER DATABASE ─────────────────────────────────────────
108
+ CHUNKS = []
109
+ METADATA = []
110
+ EMBEDDINGS = None
111
+ PAPERS_LOADED = False
112
+ EMBEDDER = None
113
+
114
+ def load_papers():
115
+ global CHUNKS, METADATA, EMBEDDINGS, PAPERS_LOADED, EMBEDDER
116
+ try:
117
+ from sentence_transformers import SentenceTransformer
118
+ chunks_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="chunks.json", repo_type="dataset", token=HF_TOKEN)
119
+ meta_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="metadata.json", repo_type="dataset", token=HF_TOKEN)
120
+ emb_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="embeddings.npy", repo_type="dataset", token=HF_TOKEN)
121
+ with open(chunks_path) as f: CHUNKS = json.load(f)
122
+ with open(meta_path) as f: METADATA = json.load(f)
123
+ EMBEDDINGS = np.load(emb_path)
124
+ EMBEDDER = SentenceTransformer("all-MiniLM-L6-v2")
125
+ PAPERS_LOADED = True
126
+ print("Papers loaded: " + str(len(CHUNKS)) + " chunks")
127
+ except Exception as e:
128
+ print("Paper load error: " + str(e))
129
 
130
+ load_papers()
131
+
132
+ def search_papers(query, n=4):
133
+ if not PAPERS_LOADED or EMBEDDINGS is None or EMBEDDER is None: return "", []
134
+ try:
135
+ q_emb = EMBEDDER.encode([query])
136
+ norms = np.linalg.norm(EMBEDDINGS, axis=1, keepdims=True)
137
+ emb_norm = EMBEDDINGS / (norms + 1e-10)
138
+ q_norm = q_emb / (np.linalg.norm(q_emb) + 1e-10)
139
+ scores = (emb_norm @ q_norm.T).flatten()
140
+ top_idx = np.argsort(scores)[::-1][:n]
141
+ context = ""; results = []; seen = set()
142
+ for idx in top_idx:
143
+ chunk = CHUNKS[idx]; meta = METADATA[idx]; score = float(scores[idx])
144
+ if score > 0.25:
145
+ results.append({"chunk": chunk, "paper": meta["paper"], "score": score})
146
+ if meta["paper"] not in seen:
147
+ context += chr(10) + "=== FROM: " + meta["paper"] + " ===" + chr(10)
148
+ seen.add(meta["paper"])
149
+ context += chunk[:500] + chr(10)
150
+ return context, results
151
+ except: return "", []
152
+
153
+ # ── SESSION MANAGEMENT ─────────────────────────────────────
154
  def load_all_sessions():
155
  if not HF_TOKEN: return {}
156
  try:
157
+ path = hf_hub_download(repo_id=HISTORY_REPO, filename="chat_history.json", repo_type="dataset", token=HF_TOKEN)
158
+ with open(path) as f: return json.load(f)
159
+ except: return {}
 
 
 
 
 
 
 
 
160
 
161
  def save_all_sessions(sessions):
162
  if not HF_TOKEN: return False
163
  try:
164
+ api2 = HfApi(token=HF_TOKEN)
165
+ api2.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(),
166
+ path_in_repo="chat_history.json", repo_id=HISTORY_REPO,
167
+ repo_type="dataset", token=HF_TOKEN, commit_message="Update")
 
 
 
 
 
 
168
  return True
169
+ except: return False
 
 
170
 
171
  def get_session_list():
172
+ s = load_all_sessions()
173
+ return list(reversed(list(s.keys()))) if s else ["No saved sessions"]
 
 
174
 
175
+ def save_session(history, name):
176
+ if not history: return "Nothing to save", gr.update()
177
+ if not name or not name.strip(): name = "Chat " + datetime.now().strftime("%b %d %H:%M")
178
  sessions = load_all_sessions()
179
+ sessions[name] = {"messages": history, "saved_at": datetime.now().isoformat()}
180
+ ok = save_all_sessions(sessions)
181
+ choices = get_session_list()
182
+ return ("Saved: " + name if ok else "Save failed"), gr.update(choices=choices, value=name)
183
+
184
+ def load_session(name):
185
+ if not name or "No saved" in name: return [], "Select a session"
 
 
 
186
  sessions = load_all_sessions()
187
+ return (sessions[name]["messages"], "Loaded: " + name) if name in sessions else ([], "Not found")
188
+
189
+ def delete_session(name):
190
+ if not name or "No saved" in name: return "Select a session", gr.update()
 
 
 
 
 
 
 
 
 
191
  sessions = load_all_sessions()
192
+ if name in sessions:
193
+ del sessions[name]; save_all_sessions(sessions)
194
+ choices = get_session_list()
195
+ return "Deleted: " + name, gr.update(choices=choices, value=choices[0] if choices else None)
196
+ return "Not found", gr.update()
197
+
198
+ def new_chat(): return [], "", "New chat"
199
+
200
+ # ── SEARCH ─────────────────────────────────────────────────
201
+ def get_pubmed(query, n=3):
202
  try:
203
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
204
+ params={"db":"pubmed","term":query+" AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR CKD)","retmax":n,"retmode":"json","sort":"date","field":"tiab"},timeout=10)
205
  ids = r.json()["esearchresult"]["idlist"]
206
+ return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids]) if ids else ""
 
207
  except: return ""
208
 
209
  def quick_search(query):
210
  if not query.strip(): return "Please enter a topic."
 
211
  try:
212
+ expanded = query
213
+ if GROQ_KEY:
214
+ try:
215
+ client = Groq(api_key=GROQ_KEY)
216
+ resp = client.chat.completions.create(model="llama-3.1-8b-instant",
217
+ messages=[{"role":"system","content":"Biomedical PubMed expert. Convert to MeSH terms. Return ONLY terms."},
218
+ {"role":"user","content":"Optimize: " + query}], max_tokens=60)
219
+ expanded = resp.choices[0].message.content.strip() or query
220
+ except: pass
221
+ forced = expanded + " AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CKD)"
222
+ r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
223
+ params={"db":"pubmed","term":forced,"retmax":8,"retmode":"json","sort":"date","field":"tiab"},timeout=12)
224
+ ids = r.json()["esearchresult"]["idlist"]
225
+ out = "QUERY: " + query + chr(10) + "="*40 + chr(10) + chr(10)
226
+ if ids:
227
+ r2 = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
228
+ params={"db":"pubmed","id":",".join(ids),"retmode":"xml","rettype":"abstract"},timeout=12)
229
+ import xml.etree.ElementTree as ET
230
+ root = ET.fromstring(r2.content)
231
+ out += "PUBMED:" + chr(10)
232
+ for article in root.findall(".//PubmedArticle"):
233
+ try:
234
+ title = article.find(".//ArticleTitle").text or "No title"
235
+ pmid = article.find(".//PMID").text or ""
236
+ year_el = article.find(".//PubDate/Year")
237
+ year = year_el.text if year_el is not None else ""
238
+ out += str(title)[:85] + " (" + year + ")" + chr(10)
239
+ out += " https://pubmed.ncbi.nlm.nih.gov/" + pmid + chr(10) + chr(10)
240
+ except: continue
241
+ try:
242
+ r3 = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
243
+ params={"query":expanded,"limit":5,"fields":"title,year,url,citationCount"},timeout=12)
244
+ papers = r3.json().get("data",[])
245
+ out += "SEMANTIC SCHOLAR:" + chr(10)
246
+ for p in papers:
247
+ year = p.get("year",0) or 0
248
+ if int(year) >= 2015:
249
+ out += p.get("title","")[:85] + " (" + str(year) + ")"
250
+ cites = p.get("citationCount",0)
251
+ if cites: out += " | " + str(cites) + " citations"
252
+ out += chr(10) + " " + p.get("url","") + chr(10) + chr(10)
253
+ except: pass
254
+ out += "SJSU SCHOLARWORKS:" + chr(10)
255
+ out += " https://scholarworks.sjsu.edu/do/search/?q=" + requests.utils.quote(query) + "&context=6781027"
256
+ return out
257
+ except Exception as e:
258
+ return "Search error: " + str(e)
259
+
260
+ # ── CHAT ───────────────────────────────────────────────────
261
+ def research_chat(message, history, chat_model):
262
+ if not message.strip(): return "", history
263
  if not GROQ_KEY:
264
  history.append({"role":"user","content":message})
265
+ history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
266
  return "", history
267
  try:
268
+ model_id = CHAT_MODELS.get(chat_model, "llama-3.3-70b-versatile")
269
  client = Groq(api_key=GROQ_KEY)
270
+ paper_context, paper_results = search_papers(message, n=4)
271
+ if paper_context:
272
+ system_prompt = ("You are CardioLab AI for SJSU Biomedical Engineering. "
273
+ "Answer using SJSU CardioLab research papers below. Cite paper names." +
274
+ chr(10) + "SJSU PAPERS:" + chr(10) + paper_context + chr(10) + "KNOWLEDGE: " + KNOWHOW)
275
+ else:
276
+ system_prompt = "You are CardioLab AI for SJSU Biomedical Engineering. " + KNOWHOW
277
+ msgs = [{"role":"system","content":system_prompt}]
278
  for item in history:
279
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
280
  msgs.append({"role":"user","content":message})
281
+ resp = client.chat.completions.create(model=model_id, messages=msgs, max_tokens=800)
282
  answer = resp.choices[0].message.content
283
+ if paper_results:
284
+ unique_papers = list(dict.fromkeys([r["paper"] for r in paper_results]))
285
+ answer += chr(10) + chr(10) + "Sources:"
286
+ for p in unique_papers[:3]:
287
+ answer += chr(10) + " - " + p.replace(".pdf","").replace("_"," ")
288
+ pubmed = get_pubmed(message, n=2)
289
+ if pubmed: answer += chr(10) + "PubMed: " + pubmed
290
  history.append({"role":"user","content":message})
291
  history.append({"role":"assistant","content":answer})
292
  return "", history
293
  except Exception as e:
294
  history.append({"role":"user","content":message})
295
+ history.append({"role":"assistant","content":"Error: " + str(e)})
296
  return "", history
297
 
298
  def voice_chat(audio, history):
299
  if audio is None:
300
+ history.append({"role":"assistant","content":"Please record first."})
301
  return history
302
  try:
303
  client = Groq(api_key=GROQ_KEY)
304
  with open(audio, "rb") as f:
305
  tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
306
+ paper_context, _ = search_papers(tx.text, n=3)
307
+ system = "You are CardioLab AI. " + KNOWHOW
308
+ if paper_context: system = "You are CardioLab AI. Use these SJSU papers:" + chr(10) + paper_context + chr(10) + KNOWHOW
309
+ msgs = [{"role":"system","content":system}]
310
  for item in history:
311
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
312
  msgs.append({"role":"user","content":tx.text})
313
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile", messages=msgs, max_tokens=500)
314
+ history.append({"role":"user","content":"Voice: " + tx.text})
315
  history.append({"role":"assistant","content":resp.choices[0].message.content})
316
  return history
317
  except Exception as e:
318
+ history.append({"role":"assistant","content":"Voice error: " + str(e)})
319
  return history
320
 
321
+ # ── PHASE D ────────────────────────────────────────────────
322
+ def generate_protocol(experiment_type, specific_params):
323
+ if not GROQ_KEY: return "Error: Add GROQ_API_KEY"
324
+ if not experiment_type: return "Select experiment type"
325
+ try:
326
+ client = Groq(api_key=GROQ_KEY)
327
+ paper_context, _ = search_papers(experiment_type, n=4)
328
+ lab_ctx = {
329
+ "MCL": "Sylgard 184 PDMS 10:1 ratio 48hr cure. Tygon tubing. 70bpm 5L/min.",
330
+ "PIV": "Green laser 532nm. Normal velocity 0.5-2.0 m/s. Shear below 5 Pa.",
331
+ "Thrombogenicity": "Arduino Uno stepper motor 48V. 150mL fresh blood. Sample 0 20 40 60 min. Heska HT5. TAT below 8 ng/mL. PF1.2 below 2.0 nmol/L.",
332
+ "uPAD": "Whatman filter paper. Wax printer 120C. Jaffe reaction picric acid.",
333
+ "FSI": "COMSOL ALE mesh. Blood 1060 kg/m3 0.0035 Pa.s.",
334
+ }
335
+ extra = next((v for k, v in lab_ctx.items() if k.lower() in experiment_type.lower()), "")
336
+ system_msg = ("You are CardioLab AI protocol generator for SJSU. Generate COMPLETE protocol with: "
337
+ "1.OBJECTIVE 2.MATERIALS AND EQUIPMENT 3.SAFETY 4.PROCEDURE 5.DATA COLLECTION "
338
+ "6.ANALYSIS 7.EXPECTED RESULTS with normal ranges 8.TROUBLESHOOTING. "
339
+ "Use exact SJSU CardioLab values.")
340
+ user_msg = "Generate protocol for: " + experiment_type
341
+ if specific_params and specific_params.strip(): user_msg += chr(10) + "Parameters: " + specific_params
342
+ if extra: user_msg += chr(10) + "Context: " + extra
343
+ if paper_context: user_msg += chr(10) + "SJSU papers: " + paper_context[:600]
344
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",
345
+ messages=[{"role":"system","content":system_msg},{"role":"user","content":user_msg}], max_tokens=1200)
346
+ return resp.choices[0].message.content
347
+ except Exception as e: return "Error: " + str(e)
348
+
349
+ def generate_report(data_description, experiment_type, results):
350
+ if not GROQ_KEY: return "Error: Add GROQ_API_KEY"
351
+ try:
352
+ client = Groq(api_key=GROQ_KEY)
353
+ paper_context, _ = search_papers(experiment_type, n=3)
354
+ system_msg = ("You are CardioLab AI report writer for SJSU. Generate professional research report with: "
355
+ "1.ABSTRACT 2.INTRODUCTION 3.MATERIALS AND METHODS 4.RESULTS AND DISCUSSION "
356
+ "5.CONCLUSION 6.RECOMMENDATIONS 7.REFERENCES. Academic style.")
357
+ user_msg = "Write report for: " + experiment_type
358
+ if data_description and data_description.strip(): user_msg += chr(10) + "Description: " + data_description
359
+ if results and results.strip(): user_msg += chr(10) + "Results: " + results
360
+ if paper_context: user_msg += chr(10) + "SJSU papers: " + paper_context[:600]
361
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",
362
+ messages=[{"role":"system","content":system_msg},{"role":"user","content":user_msg}], max_tokens=1500)
363
+ return resp.choices[0].message.content
364
+ except Exception as e: return "Error: " + str(e)
365
+
366
+ def generate_hypothesis(research_area, current_findings):
367
+ if not GROQ_KEY: return "Error: Add GROQ_API_KEY"
368
+ try:
369
+ client = Groq(api_key=GROQ_KEY)
370
+ paper_context, _ = search_papers(research_area, n=3)
371
+ system_msg = ("You are CardioLab AI research assistant for SJSU. Generate 3 testable hypotheses. "
372
+ "For each: H0 null, H1 alternative, rationale, suggested experiment, expected outcome.")
373
+ user_msg = "Hypotheses for: " + research_area
374
+ if current_findings and current_findings.strip(): user_msg += chr(10) + "Findings: " + current_findings
375
+ if paper_context: user_msg += chr(10) + "SJSU papers: " + paper_context[:500]
376
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",
377
+ messages=[{"role":"system","content":system_msg},{"role":"user","content":user_msg}], max_tokens=1000)
378
+ return resp.choices[0].message.content
379
+ except Exception as e: return "Error: " + str(e)
380
+
381
+ # ── ANALYSIS TOOLS ─────────────────────────────────────────
382
  def analyze_upad_photo(image):
383
  if image is None: return None, "Upload a uPAD photo first."
384
  try:
385
  img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
386
+ arr = np.array(img); h, w = arr.shape[:2]
387
+ y1, y2, x1, x2 = int(h*0.35), int(h*0.65), int(w*0.35), int(w*0.65)
388
+ zone = arr[y1:y2, x1:x2]
389
+ R = float(np.mean(zone[:,:,0])); G = float(np.mean(zone[:,:,1])); B = float(np.mean(zone[:,:,2]))
 
390
  c = max(0, round(0.018*(R-B)-0.3, 2))
391
+ if c < 1.2: s, a = "Normal", "Monitor annually."
392
+ elif c < 1.5: s, a = "Borderline", "Repeat in 3 months."
393
+ elif c < 3.0: s, a = "Stage 2 CKD", "Consult nephrologist."
394
+ elif c < 6.0: s, a = "Stage 3-4 CKD", "Immediate consultation."
395
+ else: s, a = "Stage 5 CKD", "Emergency care."
396
+ ri = img.copy()
397
+ import PIL.ImageDraw as D; D.Draw(ri).rectangle([x1, y1, x2, y2], outline=(0,255,0), width=3)
398
+ return ri, ("R:" + str(round(R,1)) + " G:" + str(round(G,1)) + " B:" + str(round(B,1)) + chr(10) +
399
+ "Creatinine: " + str(c) + " mg/dL" + chr(10) + "Stage: " + s + chr(10) + "Action: " + a)
400
+ except Exception as e: return None, "Error: " + str(e)
401
+
402
+ def mk_chart(fn, title, bg, fg, gc, ac, pb):
403
+ fig2, ax = plt.subplots(figsize=(8,5)); fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
404
+ fn(ax); ax.set_title(title, color=fg, fontweight="bold", fontsize=13, pad=8)
405
+ ax.tick_params(colors=ac, labelsize=10); ax.grid(True, alpha=0.3, color=gc, linestyle="--")
406
+ for sp in ["top","right"]: ax.spines[sp].set_visible(False)
407
+ for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
408
+ plt.tight_layout(); buf = io.BytesIO()
409
+ plt.savefig(buf, format="png", facecolor=bg, bbox_inches="tight", dpi=130); buf.seek(0)
410
+ res = Image.open(buf).copy(); plt.close(); return res
411
 
412
  def analyze_piv_csv(file, theme="White"):
413
+ if file is None: return None, None, None, None, "Upload PIV CSV first."
414
  try:
415
+ df = pd.read_csv(file.name); cols = [c.lower().strip() for c in df.columns]; df.columns = cols
 
 
416
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
417
+ if not num_cols: return None, None, None, None, "No numeric columns."
418
+ bg = "#fff" if theme=="White" else "#0a1628"; fg = "#1a202c" if theme=="White" else "white"
419
+ gc = "#e2e8f0" if theme=="White" else "#2d4a8a"; ac = "#4a5568" if theme=="White" else "#a8b2d8"
 
 
420
  pb = "#f7fafc" if theme=="White" else "#132340"
421
  x = np.arange(len(df))
422
  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)
423
+ sc2 = next((c for c in cols if any(k in c for k in ["shear","stress","tau","wss"])), num_cols[1] if len(num_cols)>1 else None)
424
+ tc = next((c for c in cols if "time" in c or "frame" in c), None); xv = df[tc] if tc else x
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  def pv(ax):
426
  if vc:
427
+ ax.plot(xv, df[vc], color="#c1121f", linewidth=2.5, marker="o", markersize=5)
428
+ ax.fill_between(xv, df[vc], alpha=0.15, color="#c1121f")
429
+ ax.axhline(y=2.0, color="#f59e0b", linestyle="--", linewidth=2, label="Risk 2.0 m/s")
430
+ ax.set_ylabel("Velocity (m/s)", color=ac); ax.legend(fontsize=9, labelcolor=fg, facecolor=pb)
 
 
431
  def ps(ax):
432
+ if sc2:
433
  xp = xv.values if tc else x
434
+ ax.plot(xp, df[sc2], color="#0057a8", linewidth=2.5, marker="s", markersize=5)
435
+ ax.fill_between(xp, df[sc2], alpha=0.15, color="#0057a8")
436
+ ax.axhline(y=5, color="#f59e0b", linestyle="--", linewidth=2, label="Caution 5 Pa")
437
+ ax.axhline(y=10, color="#c1121f", linestyle="--", linewidth=2, label="Risk 10 Pa")
438
+ ax.set_ylabel("Shear (Pa)", color=ac); ax.legend(fontsize=9, labelcolor=fg, facecolor=pb)
 
 
439
  def psc(ax):
440
+ if vc and sc2:
441
+ s3 = ax.scatter(df[vc], df[sc2], c=x, cmap="RdYlGn_r", s=90, edgecolors=fg, linewidth=0.5, zorder=5)
442
+ cb = plt.colorbar(s3, ax=ax, label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
443
+ ax.axvline(x=2.0, color="#f59e0b", linestyle="--", linewidth=2); ax.axhline(y=10, color="#c1121f", linestyle="--", linewidth=2)
444
+ ax.set_xlabel("Velocity (m/s)", color=ac); ax.set_ylabel("Shear (Pa)", color=ac)
 
 
 
445
  def psum(ax):
446
+ ax.axis("off"); risk = []
447
+ st = "CLINICAL SUMMARY" + chr(10) + "="*20 + chr(10) + chr(10)
448
  for col in num_cols[:3]:
449
+ mn = round(df[col].mean(), 3); mx = round(df[col].max(), 3)
450
+ st += col[:14] + ":" + chr(10) + " Mean: " + str(mn) + chr(10) + " Max: " + str(mx) + chr(10) + chr(10)
451
+ if "vel" in col and mx > 2.0: risk.append("HIGH VELOCITY")
452
+ if "shear" in col and mx > 10: risk.append("HIGH SHEAR")
453
+ bc = "#c1121f" if risk else "#2ecc71"
454
+ st += "="*20 + chr(10) + ("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
455
+ ax.text(0.05, 0.97, st, transform=ax.transAxes, color=fg, fontsize=10, va="top",
456
+ fontfamily="monospace", bbox=dict(boxstyle="round,pad=0.8", facecolor=pb, edgecolor=bc, linewidth=2.5))
457
+ i1 = mk_chart(pv, "Velocity Profile", bg, fg, gc, ac, pb)
458
+ i2 = mk_chart(ps, "Wall Shear Stress", bg, fg, gc, ac, pb)
459
+ i3 = mk_chart(psc, "Velocity vs Shear", bg, fg, gc, ac, pb)
460
+ i4 = mk_chart(psum, "Clinical Summary", bg, fg, gc, ac, pb)
461
+ ai = ""
 
 
462
  if GROQ_KEY:
463
  try:
464
+ client = Groq(api_key=GROQ_KEY)
465
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",
466
+ messages=[{"role":"system","content":"PIV expert SJSU CardioLab."},
467
+ {"role":"user","content":"PIV from 27mm SJM Regent:" + chr(10) + df.describe().to_string()[:500]}], max_tokens=250)
468
+ ai = chr(10) + "AI: " + resp.choices[0].message.content
469
  except: pass
470
+ return i1, i2, i3, i4, "PIV: " + str(len(df)) + " rows" + ai
471
+ except Exception as e: return None, None, None, None, "Error: " + str(e)
472
 
473
  def analyze_tgt_csv(file, theme="White"):
474
+ if file is None: return None, None, None, None, "Upload TGT CSV first."
475
  try:
476
+ df = pd.read_csv(file.name); cols = [c.lower().strip() for c in df.columns]; df.columns = cols
 
 
477
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
478
+ bg = "#fff" if theme=="White" else "#0a1628"; fg = "#1a202c" if theme=="White" else "white"
479
+ gc = "#e2e8f0" if theme=="White" else "#2d4a8a"; ac = "#4a5568" if theme=="White" else "#a8b2d8"
480
+ pb = "#f7fafc" if theme=="White" else "#132340"
481
+ tc = next((c for c in cols if "time" in c or "min" in c), None)
482
+ tatc = next((c for c in cols if "tat" in c), num_cols[0] if num_cols else None)
483
+ pfc = next((c for c in cols if "pf" in c), num_cols[1] if len(num_cols)>1 else None)
484
+ hc = next((c for c in cols if "hemo" in c), num_cols[2] if len(num_cols)>2 else None)
485
+ 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)
486
+ def mk2(dc, color, yl, lim, ll, title, bar=False):
487
+ def fn(ax):
488
+ if dc and dc in df.columns:
489
+ xp = df[tc].values if tc else range(len(df)); yp = df[dc].values
490
+ if bar:
491
+ bs = ax.bar(range(len(yp)), yp, color=color, alpha=0.85, edgecolor=bg, width=0.6)
492
+ 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")
493
+ else:
494
+ ax.plot(xp, yp, color=color, linewidth=3, marker="o", markersize=8)
495
+ ax.fill_between(xp, yp, alpha=0.15, color=color)
496
+ 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")
497
+ ax.axhline(y=lim, color="#f59e0b", linestyle="--", linewidth=2.5, label=ll)
498
+ ax.legend(fontsize=10, labelcolor=fg, facecolor=pb); ax.set_ylabel(yl, color=ac)
499
+ mv = round(float(np.max(yp)), 2)
500
+ ax.set_title(title + chr(10) + "Max: " + str(mv) + " - " + ("HIGH" if mv>lim else "NORMAL"), color=fg, fontweight="bold", fontsize=12)
501
+ return mk_chart(fn, title, bg, fg, gc, ac, pb)
502
+ i1 = mk2(tatc, "#c1121f", "TAT (ng/mL)", 8, "Normal: 8", "TAT")
503
+ i2 = mk2(pfc, "#0057a8", "PF1.2", 2.0, "Normal: 2.0", "PF1.2")
504
+ i3 = mk2(hc, "#2ecc71", "Free Hgb (mg/L)", 20, "Normal: 20", "Free Hemoglobin", bar=True)
505
+ i4 = mk2(plc, "#e8a020", "Platelets", 150, "Normal>150", "Platelets")
506
+ ai = ""
 
 
 
 
 
 
 
 
 
 
507
  if GROQ_KEY:
508
  try:
509
+ client = Groq(api_key=GROQ_KEY)
510
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",
511
+ messages=[{"role":"system","content":"Hematology expert. Thrombogenicity risk."},
512
+ {"role":"user","content":"TGT:" + chr(10) + df.describe().to_string()[:500]}], max_tokens=250)
513
+ ai = chr(10) + "AI: " + resp.choices[0].message.content
514
  except: pass
515
+ return i1, i2, i3, i4, "TGT: " + str(len(df)) + " rows" + ai
516
+ except Exception as e: return None, None, None, None, "Error: " + str(e)
517
 
518
  def generate_image(prompt):
519
+ if not prompt.strip(): return None, "Enter description.", ""
520
+ if not HF_TOKEN: return None, "Add HF_TOKEN.", ""
521
  try:
522
+ enhanced, desc = prompt, ""
523
  if GROQ_KEY:
524
  try:
525
+ client = Groq(api_key=GROQ_KEY)
526
+ resp = client.chat.completions.create(model="llama-3.3-70b-versatile",
527
  messages=[{"role":"system","content":"Format: DESCRIPTION: [2 sentences] PROMPT: [detailed image prompt]"},
528
+ {"role":"user","content":"Biomedical image: " + prompt}], max_tokens=200)
529
+ full = resp.choices[0].message.content
530
  if "DESCRIPTION:" in full and "PROMPT:" in full:
531
+ desc = full.split("DESCRIPTION:")[1].split("PROMPT:")[0].strip()
532
+ enhanced = full.split("PROMPT:")[1].strip()
533
  except: pass
534
+ headers = {"Authorization": "Bearer " + HF_TOKEN, "Content-Type": "application/json"}
535
  for url in ["https://router.huggingface.co/hf-inference/models/black-forest-labs/FLUX.1-schnell",
536
  "https://router.huggingface.co/hf-inference/models/stabilityai/stable-diffusion-xl-base-1.0"]:
537
  try:
538
+ r = requests.post(url, headers=headers, json={"inputs":enhanced,"parameters":{"num_inference_steps":8}}, timeout=60)
539
+ if r.status_code == 200: return Image.open(io.BytesIO(r.content)), "Generated!", desc
540
  except: continue
541
+ return None, "Models busy.", desc
542
+ except Exception as e: return None, "Error: " + str(e), ""
543
 
544
+ def piv_manual(v, s, h):
545
+ vr = "HIGH-stenosis" if float(v)>2.0 else "NORMAL"
546
+ sr = "HIGH-thrombosis" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
547
+ return "Velocity: " + str(v) + " m/s - " + vr + chr(10) + "Shear: " + str(s) + " Pa - " + sr + chr(10) + "HR: " + str(h) + " bpm"
548
 
549
+ def tgt_manual(t, p, h, pl, tm):
550
+ risk = sum([float(t)>15, float(p)>2.0, float(h)>50, float(pl)<150])
551
+ return "TAT:" + str(t) + " PF1.2:" + str(p) + chr(10) + "Hemo:" + str(h) + " Plt:" + str(pl) + chr(10) + ("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
552
 
553
+ # ── UI ─────────────────────────────────────────────────────
554
+ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
555
+ gr.HTML(HEADER)
556
+ gr.HTML("""<div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:8px 16px;margin:6px 0;text-align:center;">
557
+ <span style="color:#166534;font-size:0.8em;font-weight:500;">RAG Active: 417 chunks from 16 SJSU papers &nbsp;·&nbsp; Fine-tuned Model &nbsp;·&nbsp; Select model using radio buttons in Chat tab</span></div>""")
558
 
559
  with gr.Tabs():
560
 
561
  with gr.Tab("Chat"):
 
562
  with gr.Row():
563
+ with gr.Column(scale=1, min_width=200):
564
+ gr.HTML("""<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:10px;padding:12px;margin-bottom:8px;">
565
+ <div style="display:flex;align-items:center;gap:6px;margin-bottom:3px;">
566
+ <svg width="12" height="11" viewBox="0 0 100 90"><path d="M50 85 C50 85 5 55 5 30 C5 15 18 5 30 5 C38 5 45 9 50 15 C55 9 62 5 70 5 C82 5 95 15 95 30 C95 55 50 85 50 85Z" fill="#c1121f"/></svg>
567
+ <span style="color:#c1121f;font-weight:700;font-size:0.82em;">CardioLab</span></div>
568
+ <div style="color:#9ca3af;font-size:0.7em;">Conversations</div></div>""")
569
+ new_chat_btn = gr.Button("+ New Chat", variant="secondary")
570
+ session_dropdown = gr.Dropdown(choices=get_session_list(), label="Saved Sessions", interactive=True)
571
+ load_btn = gr.Button("Load Session", variant="primary")
572
+ session_name_box = gr.Textbox(placeholder="Name this session...", label="Session Name", lines=1)
573
  with gr.Row():
574
+ save_btn = gr.Button("Save", variant="primary", scale=2)
575
+ delete_btn = gr.Button("Del", variant="secondary", scale=1)
576
+ session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
577
+
578
+ with gr.Column(scale=4):
579
+ chat_model_radio = gr.Radio(
580
+ choices=list(CHAT_MODELS.keys()),
581
+ value="Llama 3.3 70B (Best)",
582
+ label="Select AI Model",
583
+ container=True
584
+ )
585
+ chatbot = gr.Chatbot(label="", height=400, show_label=False, container=False)
586
+ with gr.Row():
587
+ msg_box = gr.Textbox(placeholder="Ask anything — AI searches 16 SJSU papers + PubMed...", label="", lines=2, scale=5, container=False)
588
  with gr.Column(scale=1, min_width=80):
589
  send_btn = gr.Button("Send", variant="primary")
590
  clear_btn = gr.Button("Clear", variant="secondary")
591
+
592
+ send_btn.click(research_chat, inputs=[msg_box, chatbot, chat_model_radio], outputs=[msg_box, chatbot])
593
+ msg_box.submit(research_chat, inputs=[msg_box, chatbot, chat_model_radio], outputs=[msg_box, chatbot])
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
595
+ new_chat_btn.click(new_chat, outputs=[chatbot, msg_box, session_status])
596
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
597
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
598
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
599
 
600
  with gr.Tab("Voice"):
601
+ voice_chatbot = gr.Chatbot(label="", height=360, show_label=False)
602
  audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
603
  with gr.Row():
604
  voice_btn = gr.Button("Ask by Voice", variant="primary")
 
607
  voice_clear.click(lambda: [], outputs=voice_chatbot)
608
 
609
  with gr.Tab("Papers"):
610
+ gr.Markdown("### Search PubMed + Semantic Scholar + SJSU ScholarWorks")
611
  with gr.Row():
612
+ search_input = gr.Textbox(placeholder="e.g. bileaflet mechanical heart valve thrombogenicity hemodynamics", label="Research Topic", scale=4)
613
  search_btn = gr.Button("Search", variant="primary", scale=1)
614
+ search_output = gr.Textbox(label="Results", lines=22)
615
  search_btn.click(quick_search, inputs=search_input, outputs=search_output)
616
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
617
 
618
  with gr.Tab("PIV CSV"):
 
619
  with gr.Row():
620
+ piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
621
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
622
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
623
+ piv_result = gr.Textbox(label="AI Analysis", lines=4)
624
  with gr.Row():
625
  piv_c1 = gr.Image(label="Velocity Profile", type="pil")
626
  piv_c2 = gr.Image(label="Shear Stress", type="pil")
627
  with gr.Row():
628
  piv_c3 = gr.Image(label="Velocity vs Shear", type="pil")
629
  piv_c4 = gr.Image(label="Clinical Summary", type="pil")
630
+ piv_btn.click(analyze_piv_csv, inputs=[piv_file, piv_theme], outputs=[piv_c1, piv_c2, piv_c3, piv_c4, piv_result])
631
 
632
  with gr.Tab("TGT CSV"):
 
633
  with gr.Row():
634
+ tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
635
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
636
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
637
+ tgt_result = gr.Textbox(label="AI Assessment", lines=4)
638
  with gr.Row():
639
+ tgt_c1 = gr.Image(label="TAT", type="pil"); tgt_c2 = gr.Image(label="PF1.2", type="pil")
 
640
  with gr.Row():
641
+ tgt_c3 = gr.Image(label="Hemoglobin", type="pil"); tgt_c4 = gr.Image(label="Platelets", type="pil")
642
+ tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file, tgt_theme], outputs=[tgt_c1, tgt_c2, tgt_c3, tgt_c4, tgt_result])
 
643
 
644
+ with gr.Tab("uPAD"):
 
645
  with gr.Row():
646
  with gr.Column():
647
+ photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=260)
648
+ analyze_btn = gr.Button("Analyze uPAD Photo", variant="primary")
649
  with gr.Column():
650
+ photo_img = gr.Image(label="Detection Zone", type="pil", height=260)
651
+ photo_text = gr.Textbox(label="CKD Result", lines=8)
652
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
653
+ gr.Markdown("**Manual RGB:**")
654
+ with gr.Row():
655
+ r = gr.Number(label="R", value=210); g = gr.Number(label="G", value=140); b = gr.Number(label="B", value=80)
656
+ out3 = gr.Textbox(label="Result", lines=3)
657
+ gr.Button("Analyze RGB", variant="secondary").click(
658
+ lambda r, g, b: "Creatinine: " + str(max(0,round(0.02*(r-b)-0.5,2))) + " mg/dL" + chr(10) +
659
+ ("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"),
660
+ inputs=[r, g, b], outputs=out3)
661
 
662
  with gr.Tab("AI Image"):
663
  with gr.Row():
664
+ img_prompt = gr.Textbox(placeholder="e.g. 27mm bileaflet mechanical heart valve cross section", label="Describe image", lines=2, scale=4)
665
  with gr.Column(scale=1):
666
  img_btn = gr.Button("Generate", variant="primary")
667
  img_status = gr.Textbox(label="Status", lines=1)
668
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
669
+ img_output = gr.Image(label="Generated Image", type="pil", height=400)
670
+ img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output, img_status, img_desc])
671
 
672
  with gr.Tab("PIV Manual"):
673
  with gr.Row():
674
  with gr.Column():
675
+ v = gr.Number(label="Max Velocity m/s", value=1.8)
676
+ s = gr.Number(label="Wall Shear Pa", value=6.5)
677
+ h = gr.Number(label="Heart Rate bpm", value=72)
678
+ piv_out = gr.Textbox(label="Result", lines=4)
679
+ gr.Button("Analyze PIV", variant="primary").click(piv_manual, inputs=[v, s, h], outputs=piv_out)
680
 
681
  with gr.Tab("TGT Manual"):
682
  with gr.Row():
683
  with gr.Column():
684
+ t1 = gr.Number(label="TAT ng/mL", value=18); t2 = gr.Number(label="PF1.2", value=2.5)
685
+ t3 = gr.Number(label="Hemoglobin mg/L", value=60); t4 = gr.Number(label="Platelets", value=140)
686
+ t5 = gr.Number(label="Time min", value=40); out2 = gr.Textbox(label="Result", lines=6)
687
+ gr.Button("Analyze TGT", variant="primary").click(tgt_manual, inputs=[t1, t2, t3, t4, t5], outputs=out2)
688
+
689
+ with gr.Tab("Protocol Generator"):
690
+ gr.Markdown("### Generate complete lab protocols from SJSU CardioLab knowledge")
 
 
691
  with gr.Row():
692
+ with gr.Column(scale=1):
693
+ proto_type = gr.Dropdown(
694
+ choices=["MCL Setup", "PIV Experiment", "Thrombogenicity Tester Blood Clotting Test",
695
+ "uPAD Fabrication", "uPAD Creatinine Test", "FSI COMSOL Simulation", "Valve Testing"],
696
+ value="Thrombogenicity Tester Blood Clotting Test", label="Experiment Type")
697
+ proto_params = gr.Textbox(placeholder="e.g. 27mm SJM valve 70bpm porcine blood", label="Specific Parameters", lines=2)
698
+ proto_btn = gr.Button("Generate Protocol", variant="primary")
699
+ with gr.Column(scale=2):
700
+ proto_output = gr.Textbox(label="Generated Protocol", lines=28)
701
+ proto_btn.click(generate_protocol, inputs=[proto_type, proto_params], outputs=proto_output)
702
+
703
+ with gr.Tab("Report Writer"):
704
+ gr.Markdown("### Generate professional research reports")
705
+ with gr.Row():
706
+ with gr.Column(scale=1):
707
+ report_exp = gr.Dropdown(
708
+ choices=["MCL PIV Flow Analysis", "TGT Thrombogenicity Study", "uPAD CKD Detection",
709
+ "FSI Simulation Study", "Heart Valve Comparison"],
710
+ value="TGT Thrombogenicity Study", label="Study Type")
711
+ report_desc = gr.Textbox(placeholder="e.g. TGT with 27mm SJM bileaflet at 70bpm 150mL porcine blood", label="Experiment Description", lines=3)
712
+ report_results = gr.Textbox(placeholder="e.g. TAT=12.3 PF1.2=2.8 Hemo=45 Plt=142", label="Your Results", lines=2)
713
+ report_btn = gr.Button("Generate Report", variant="primary")
714
+ with gr.Column(scale=2):
715
+ report_output = gr.Textbox(label="Generated Report", lines=28)
716
+ report_btn.click(generate_report, inputs=[report_desc, report_exp, report_results], outputs=report_output)
717
+
718
+ with gr.Tab("Hypothesis Generator"):
719
+ gr.Markdown("### Generate testable research hypotheses")
720
+ with gr.Row():
721
+ with gr.Column(scale=1):
722
+ hyp_area = gr.Dropdown(
723
+ choices=["Bileaflet MHV Thrombogenicity", "uPAD CKD Detection Accuracy",
724
+ "PIV Flow Characterization", "FSI Simulation Validation", "Valve Design Comparison"],
725
+ value="Bileaflet MHV Thrombogenicity", label="Research Area")
726
+ hyp_findings = gr.Textbox(placeholder="Current observations from your experiments", label="Current Findings", lines=3)
727
+ hyp_btn = gr.Button("Generate Hypotheses", variant="primary")
728
+ with gr.Column(scale=2):
729
+ hyp_output = gr.Textbox(label="Research Hypotheses", lines=25)
730
+ hyp_btn.click(generate_hypothesis, inputs=[hyp_area, hyp_findings], outputs=hyp_output)
731
+
732
+ gr.HTML("""<div style="text-align:center;padding:12px;border-top:1px solid #e2e8f0;background:#f8fafc;margin-top:8px;">
733
+ <span style="color:#9ca3af;font-size:0.72em;">CardioLab AI v40 &nbsp;·&nbsp; SJSU Biomedical Engineering &nbsp;·&nbsp;
734
+ Inspired by <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;text-decoration:none;">Biomni Stanford</a>
735
+ &nbsp;·&nbsp; Apache 2.0 &nbsp;·&nbsp; $0 Cost</span></div>""")
736
 
737
  demo.launch()