Saicharan21 commited on
Commit
e01a849
Β·
verified Β·
1 Parent(s): 27c8369

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +276 -305
app.py CHANGED
@@ -20,152 +20,57 @@ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/
20
  "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
21
  "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
22
 
23
- CSS = """
24
- body, .gradio-container {
25
- background: #f7f7f8 !important;
26
- font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important;
27
- margin: 0 !important; padding: 0 !important;
28
- }
29
- .tab-nav {
30
- background: #ffffff !important;
31
- border-bottom: 1px solid #e5e7eb !important;
32
- padding: 0 16px !important;
33
- display: flex !important;
34
- flex-wrap: wrap !important;
35
- gap: 0 !important;
36
- }
37
- .tab-nav button {
38
- background: transparent !important;
39
- color: #6b7280 !important;
40
- border: none !important;
41
- border-bottom: 2px solid transparent !important;
42
- padding: 12px 14px !important;
43
- font-weight: 500 !important;
44
- font-size: 0.82em !important;
45
- white-space: nowrap !important;
46
- border-radius: 0 !important;
47
  }
 
 
 
 
 
48
  .tab-nav button:hover { color: #111827 !important; background: #f9fafb !important; }
49
  .tab-nav button.selected { color: #c1121f !important; border-bottom: 2px solid #c1121f !important; font-weight: 700 !important; background: transparent !important; }
50
  .message.user { background: #f3f4f6 !important; color: #1a202c !important; border-radius: 12px !important; }
51
- .message.bot { background: #ffffff !important; color: #1a202c !important; border-left: 3px solid #c1121f !important; border-radius: 0 12px 12px 12px !important; }
52
- textarea { background: #ffffff !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 12px !important; }
53
- textarea:focus { border-color: #c1121f !important; outline: none !important; }
54
  button.primary { background: #c1121f !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; }
55
- button.primary:hover { background: #a00e18 !important; }
56
  button.secondary { background: #f3f4f6 !important; color: #374151 !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
57
  input[type=number] { background: #f9fafb !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
58
  label span { color: #374151 !important; font-weight: 500 !important; font-size: 0.85em !important; }
59
- ::-webkit-scrollbar { width: 5px; } ::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; }
60
- """
61
-
62
- HEADER_HTML = """
63
- <div style="
64
- background: linear-gradient(135deg, #0a0f2e 0%, #1a0a0a 40%, #0a0f2e 100%);
65
- padding: 0;
66
- margin: 0;
67
- border-bottom: 3px solid #c1121f;
68
- position: relative;
69
- overflow: hidden;
70
- ">
71
- <!-- ECG Background Line -->
72
- <svg style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0.08;" viewBox="0 0 1200 120" preserveAspectRatio="none">
73
- <polyline points="0,60 100,60 130,20 150,100 170,10 200,90 220,60 400,60 430,20 450,100 470,10 500,90 520,60 700,60 730,20 750,100 770,10 800,90 820,60 1000,60 1030,20 1050,100 1070,10 1100,90 1120,60 1200,60"
74
- fill="none" stroke="#c1121f" stroke-width="3"/>
75
- </svg>
76
-
77
- <div style="
78
- max-width: 1200px;
79
- margin: 0 auto;
80
- padding: 18px 24px;
81
- display: flex;
82
- align-items: center;
83
- justify-content: space-between;
84
- position: relative;
85
- z-index: 1;
86
- ">
87
- <!-- LEFT: SJSU Spartan Logo SVG -->
88
- <div style="display:flex;align-items:center;gap:16px;">
89
- <svg width="60" height="60" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
90
- <!-- Spartan helmet simplified -->
91
- <circle cx="50" cy="35" r="28" fill="#0057a8" opacity="0.9"/>
92
- <!-- Helmet crest -->
93
- <ellipse cx="50" cy="14" rx="22" ry="10" fill="#0057a8"/>
94
- <!-- Crest spikes -->
95
- <polygon points="30,14 33,4 36,14" fill="#e8a020"/>
96
- <polygon points="36,12 39,2 42,12" fill="#e8a020"/>
97
- <polygon points="42,11 45,1 48,11" fill="#e8a020"/>
98
- <polygon points="48,11 51,1 54,11" fill="#e8a020"/>
99
- <polygon points="54,12 57,2 60,12" fill="#e8a020"/>
100
- <polygon points="60,14 63,4 66,14" fill="#e8a020"/>
101
- <!-- Helmet face -->
102
- <rect x="36" y="30" width="28" height="22" rx="4" fill="#0057a8"/>
103
- <rect x="40" y="35" width="8" height="12" rx="2" fill="#e8a020"/>
104
- <!-- Chin guard -->
105
- <rect x="34" y="50" width="32" height="8" rx="4" fill="#0057a8"/>
106
- <!-- Helmet shine -->
107
- <ellipse cx="42" cy="28" rx="5" ry="3" fill="white" opacity="0.25"/>
108
- </svg>
109
-
110
- <div>
111
- <div style="color:#9ca3af;font-size:0.7em;font-weight:500;letter-spacing:2px;text-transform:uppercase;">San Jose State University</div>
112
- <div style="color:#e8a020;font-size:0.85em;font-weight:700;letter-spacing:1px;">Biomedical Engineering</div>
113
- </div>
114
- </div>
115
-
116
- <!-- CENTER: CardioLab AI Branding -->
117
- <div style="text-align:center;flex:1;padding:0 20px;">
118
- <!-- ECG + Heart icon inline -->
119
- <div style="display:flex;align-items:center;justify-content:center;gap:12px;margin-bottom:4px;">
120
- <svg width="120" height="32" viewBox="0 0 120 32">
121
- <polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16"
122
- fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
123
- <!-- Heart dot on ECG -->
124
- <circle cx="34" cy="2" r="3" fill="#c1121f"/>
125
- </svg>
126
- <div style="font-size:2.2em;font-weight:900;letter-spacing:2px;">
127
- <span style="color:#ffffff;">Cardio</span><span style="color:#c1121f;">Lab</span><span style="color:#ffffff;"> AI</span>
128
- </div>
129
- <svg width="120" height="32" viewBox="0 0 120 32" style="transform:scaleX(-1);">
130
- <polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16"
131
- fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
132
- <circle cx="34" cy="2" r="3" fill="#c1121f"/>
133
- </svg>
134
- </div>
135
- <div style="color:#9ca3af;font-size:0.72em;letter-spacing:3px;text-transform:uppercase;">
136
- AI Research Agent &nbsp;|&nbsp; Built on Biomni Stanford &nbsp;|&nbsp; Llama 3.3 70B
137
- </div>
138
- </div>
139
-
140
- <!-- RIGHT: Heart + Stats -->
141
- <div style="display:flex;align-items:center;gap:16px;">
142
- <div style="text-align:right;">
143
- <div style="color:#9ca3af;font-size:0.7em;letter-spacing:1px;text-transform:uppercase;">Research Pillars</div>
144
- <div style="color:#ffffff;font-size:0.75em;margin-top:4px;">πŸ«€ MHV &nbsp; πŸ”¬ CKD &nbsp; πŸ’» FSI</div>
145
- <div style="color:#9ca3af;font-size:0.65em;margin-top:2px;">MCL Β· PIV Β· TGT Β· uPAD Β· COMSOL</div>
146
- </div>
147
- <!-- Heart SVG -->
148
- <svg width="50" height="50" viewBox="0 0 100 90" xmlns="http://www.w3.org/2000/svg">
149
- <path d="M50 85 C50 85 5 55 5 30 C5 15 18 5 30 5 C38 5 45 9 50 15 C55 9 62 5 70 5 C82 5 95 15 95 30 C95 55 50 85 50 85Z"
150
- fill="#c1121f" opacity="0.9"/>
151
- <path d="M50 75 C50 75 12 50 12 30 C12 18 22 12 30 12 C38 12 45 16 50 22"
152
- fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="3"/>
153
- <!-- ECG inside heart -->
154
- <polyline points="25,45 32,45 35,35 38,55 41,30 44,50 50,45 75,45"
155
- fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/>
156
- </svg>
157
- </div>
158
- </div>
159
-
160
- <!-- Bottom accent bar -->
161
- <div style="
162
- height: 3px;
163
- background: linear-gradient(90deg, #0057a8, #c1121f, #e8a020, #c1121f, #0057a8);
164
- margin: 0;
165
- "></div>
166
- </div>
167
  """
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def load_all_sessions():
170
  if not HF_TOKEN: return {}
171
  try:
@@ -176,9 +81,10 @@ def load_all_sessions():
176
  def save_all_sessions(sessions):
177
  if not HF_TOKEN: return False
178
  try:
179
- api = HfApi(token=HF_TOKEN)
180
- api.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(), path_in_repo="chat_history.json",
181
- repo_id=HISTORY_REPO, repo_type="dataset", token=HF_TOKEN, commit_message="Update")
 
182
  return True
183
  except: return False
184
 
@@ -213,119 +119,199 @@ def delete_session(name):
213
 
214
  def new_chat(): return [], "", "New chat started"
215
 
216
- def get_pubmed(query, n=5):
217
- try:
218
- forced = query + " AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CKD)"
219
- r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
220
- params={"db":"pubmed","term":forced,"retmax":n,"retmode":"json","sort":"date","field":"tiab"},timeout=10)
221
- ids = r.json()["esearchresult"]["idlist"]
222
- if not ids: return ""
223
- return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
224
- except: return ""
225
-
226
- def expand_query(query):
227
  if not GROQ_KEY: return query
228
  try:
229
  client = Groq(api_key=GROQ_KEY)
230
- system_msg = ("You are a biomedical engineering PubMed search expert for SJSU CardioLab. "
231
- "Convert the user query into optimized PubMed keywords. "
232
- "Focus on: mechanical heart valves, hemodynamics, blood flow, PIV, thrombogenicity, FSI, CFD, microfluidics, CKD, creatinine. "
233
- "Return ONLY the search terms, no explanation, no dashes, no punctuation.")
234
- resp = client.chat.completions.create(
235
- model="llama-3.3-70b-versatile",
236
- messages=[
237
- {"role":"system","content":system_msg},
238
- {"role":"user","content":"Optimize for PubMed: "+query}
239
- ],
240
- max_tokens=60
241
- )
242
- expanded = resp.choices[0].message.content.strip()
243
- return expanded if expanded else query
244
  except: return query
245
 
246
- def search_pubmed_smart(query, n=8):
247
  try:
248
- expanded = expand_query(query)
249
- biomedical_filter = " AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CFD OR fluid structure OR CKD)"
250
- forced = expanded + biomedical_filter
251
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
252
- params={"db":"pubmed","term":forced,"retmax":n,"retmode":"json","sort":"date","field":"tiab"},timeout=10)
253
  ids = r.json()["esearchresult"]["idlist"]
254
- if not ids:
255
- return "No PubMed results found for this topic.", expanded
256
  r2 = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
257
- params={"db":"pubmed","id":",".join(ids),"retmode":"xml","rettype":"abstract"},timeout=10)
258
- lines = []
259
- try:
260
- import xml.etree.ElementTree as ET
261
- root = ET.fromstring(r2.content)
262
- for article in root.findall(".//PubmedArticle"):
263
- try:
264
- title_el = article.find(".//ArticleTitle")
265
- title = title_el.text if title_el is not None else "No title"
266
- pmid_el = article.find(".//PMID")
267
- pmid = pmid_el.text if pmid_el is not None else ""
268
- year_el = article.find(".//PubDate/Year")
269
- year = year_el.text if year_el is not None else ""
270
- url = "https://pubmed.ncbi.nlm.nih.gov/"+pmid
271
- lines.append(str(title)[:90]+" ("+year+")"+chr(10)+" "+url)
272
- except: continue
273
- except:
274
- lines = ["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids]
275
- return chr(10)+chr(10).join(lines), expanded
276
- except Exception as e:
277
- return "PubMed error: "+str(e), query
278
 
279
- def search_scholar_smart(query, n=8):
280
  try:
281
- expanded = expand_query(query)
282
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
283
- params={"query":expanded,"limit":n,"fields":"title,year,url,citationCount"},
284
- timeout=15)
285
  papers = r.json().get("data",[])
286
- if not papers:
287
- return "No Semantic Scholar results found."
288
- out = []
289
  for p in papers:
290
- title = p.get("title","")
291
- year = str(p.get("year",""))
292
- url = p.get("url","")
293
- citations = str(p.get("citationCount",0))
294
- if url:
295
- out.append(str(title)[:90]+" ("+year+") | "+citations+" citations"+chr(10)+" "+url)
296
- return chr(10)+chr(10).join(out) if out else "No results."
297
- except Exception as e:
298
- return "Scholar error: "+str(e)
299
-
300
- def quick_search(query):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  if not query.strip(): return "Please enter a research topic."
302
- pubmed_results, expanded = search_pubmed_smart(query, n=8)
303
- scholar_results = search_scholar_smart(query, n=6)
304
- result = "YOUR QUERY: " + query + chr(10)
305
- result += "AI EXPANDED: " + expanded + chr(10)
306
- result += "="*45 + chr(10) + chr(10)
307
- result += "PUBMED RESULTS (titles + verified links):" + chr(10)
308
- result += pubmed_results + chr(10) + chr(10)
309
- result += "="*45 + chr(10)
310
- result += "SEMANTIC SCHOLAR RESULTS:" + chr(10)
311
- result += scholar_results
312
- return result
313
-
314
- def research_chat(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  if not GROQ_KEY:
316
  history.append({"role":"user","content":message})
317
  history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
318
  return "", history
319
  try:
 
320
  client = Groq(api_key=GROQ_KEY)
321
  msgs = [{"role":"system","content":"You are CardioLab AI for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember conversation. Never invent URLs. "+KNOWHOW}]
322
  for item in history:
323
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
324
  msgs.append({"role":"user","content":message})
325
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=700)
326
  answer = resp.choices[0].message.content
327
  pubmed = get_pubmed(message, n=3)
328
- if pubmed: answer += chr(10)+chr(10)+"πŸ“š PubMed:"+chr(10)+pubmed
329
  history.append({"role":"user","content":message})
330
  history.append({"role":"assistant","content":answer})
331
  return "", history
@@ -347,13 +333,14 @@ def voice_chat(audio, history):
347
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
348
  msgs.append({"role":"user","content":tx.text})
349
  resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
350
- history.append({"role":"user","content":"πŸŽ™οΈ "+tx.text})
351
  history.append({"role":"assistant","content":resp.choices[0].message.content})
352
  return history
353
  except Exception as e:
354
  history.append({"role":"assistant","content":"Voice error: "+str(e)})
355
  return history
356
 
 
357
  def analyze_upad_photo(image):
358
  if image is None: return None, "Upload a uPAD photo first."
359
  try:
@@ -370,7 +357,7 @@ def analyze_upad_photo(image):
370
  else: s,a="Stage 5 CKD","Emergency care."
371
  ri=img.copy()
372
  import PIL.ImageDraw as D; D.Draw(ri).rectangle([x1,y1,x2,y2],outline=(0,255,0),width=3)
373
- 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)
374
  except Exception as e: return None,"Error: "+str(e)
375
 
376
  def mk_chart(fn,title,bg,fg,gc,ac,pb):
@@ -393,42 +380,38 @@ def analyze_piv_csv(file,theme="White"):
393
  pb="#f7fafc" if theme=="White" else "#132340"
394
  x=np.arange(len(df))
395
  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)
396
- 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)
397
  tc=next((c for c in cols if "time" in c or "frame" in c),None); xv=df[tc] if tc else x
398
  def pv(ax):
399
  if vc:
400
  ax.plot(xv,df[vc],color="#c1121f",linewidth=2.5,marker="o",markersize=5)
401
  ax.fill_between(xv,df[vc],alpha=0.15,color="#c1121f")
402
  ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
403
- ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
404
- ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
405
  def ps(ax):
406
- if sc:
407
  xp=xv.values if tc else x
408
- ax.plot(xp,df[sc],color="#0057a8",linewidth=2.5,marker="s",markersize=5)
409
- ax.fill_between(xp,df[sc],alpha=0.15,color="#0057a8")
410
  ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
411
  ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="High risk: 10 Pa")
412
- ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
413
- ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
414
  def psc(ax):
415
- if vc and sc:
416
- s2=ax.scatter(df[vc],df[sc],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
417
- cb=plt.colorbar(s2,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
418
- ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Vel risk")
419
- ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="Shear risk")
420
  ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11)
421
- ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
422
  def psum(ax):
423
  ax.axis("off"); risk=[]
424
- st="CLINICAL SUMMARY"+chr(10)+"━"*20+chr(10)+chr(10)
425
  for col in num_cols[:3]:
426
  mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
427
  st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
428
  if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY")
429
  if "shear" in col and mx>10: risk.append("HIGH SHEAR")
430
  bc="#c1121f" if risk else "#2ecc71"
431
- st+="━"*20+chr(10)+("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
432
  ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
433
  bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
434
  i1=mk_chart(pv,"Velocity Profile",bg,fg,gc,ac,pb)
@@ -442,7 +425,7 @@ def analyze_piv_csv(file,theme="White"):
442
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
443
  messages=[{"role":"system","content":"PIV expert SJSU CardioLab. Analyze stats give clinical interpretation."},
444
  {"role":"user","content":"PIV from 27mm SJM Regent MHV 70bpm:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
445
- ai=chr(10)+"━"*20+chr(10)+"AI: "+resp.choices[0].message.content
446
  except: pass
447
  return i1,i2,i3,i4,"PIV: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
448
  except Exception as e: return None,None,None,None,"Error: "+str(e)
@@ -473,7 +456,7 @@ def analyze_tgt_csv(file,theme="White"):
473
  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")
474
  ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
475
  ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
476
- ax.set_ylabel(yl,color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
477
  mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
478
  ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
479
  return mk_chart(fn,title,bg,fg,gc,ac,pb)
@@ -488,7 +471,7 @@ def analyze_tgt_csv(file,theme="White"):
488
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
489
  messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Give thrombogenicity risk LOW MODERATE or HIGH."},
490
  {"role":"user","content":"TGT from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
491
- ai=chr(10)+"━"*20+chr(10)+"AI: "+resp.choices[0].message.content
492
  except: pass
493
  return i1,i2,i3,i4,"TGT: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
494
  except Exception as e: return None,None,None,None,"Error: "+str(e)
@@ -520,72 +503,69 @@ def generate_image(prompt):
520
  except Exception as e: return None,"Error: "+str(e),""
521
 
522
  def piv_manual(v,s,h):
523
- vr="HIGH β€” stenosis risk" if float(v)>2.0 else "NORMAL"
524
- sr="HIGH β€” thrombosis risk" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
525
- return "Velocity: "+str(v)+" m/s β€” "+vr+chr(10)+"Shear: "+str(s)+" Pa β€” "+sr+chr(10)+"HR: "+str(h)+" bpm"
526
 
527
  def tgt_manual(t,p,h,pl,tm):
528
  risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
529
- 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")
530
-
531
- with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
532
-
533
- gr.HTML(HEADER_HTML)
534
 
 
 
 
535
  with gr.Tabs():
536
 
537
- with gr.Tab("πŸ’¬ Chat"):
538
  with gr.Row():
539
- with gr.Column(scale=1, min_width=210):
540
- gr.HTML('''<div style="background:#202123;padding:10px;border-radius:8px;margin-bottom:6px;">
541
- <div style="color:#e8a020;font-weight:700;font-size:0.85em;letter-spacing:1px;">βš”οΈ SJSU CARDIOLAB</div>
542
- <div style="color:#9ca3af;font-size:0.7em;margin-top:2px;">Conversations</div>
543
- </div>''')
544
- new_chat_btn = gr.Button("✏️ New Chat", variant="secondary")
545
- gr.HTML('''<div style="color:#9ca3af;font-size:0.72em;padding:8px 2px 4px 2px;letter-spacing:1px;">SAVED SESSIONS</div>''')
546
- session_dropdown = gr.Dropdown(choices=get_session_list(), label="", interactive=True, container=False)
547
- load_btn = gr.Button("πŸ“‚ Load Session", variant="primary")
548
- session_name_box = gr.Textbox(placeholder="Name this session...", label="", lines=1, container=False)
549
  with gr.Row():
550
- save_btn = gr.Button("πŸ’Ύ Save", variant="primary", scale=2)
551
- delete_btn = gr.Button("πŸ—‘οΈ", variant="secondary", scale=1)
552
  session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
553
-
554
  with gr.Column(scale=4):
555
- chatbot = gr.Chatbot(label="", height=500, show_label=False, container=False)
556
  with gr.Row():
557
- msg_box = gr.Textbox(placeholder="Message CardioLab AI...", label="", lines=2, scale=5, container=False)
558
- with gr.Column(scale=1, min_width=80):
559
- send_btn = gr.Button("Send ↑", variant="primary")
 
560
  clear_btn = gr.Button("Clear", variant="secondary")
561
-
562
- send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
563
- msg_box.submit(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
564
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
565
  new_chat_btn.click(new_chat, outputs=[chatbot, msg_box, session_status])
566
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
567
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
568
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
569
 
570
- with gr.Tab("πŸŽ™οΈ Voice"):
571
- voice_chatbot = gr.Chatbot(label="", height=380, show_label=False)
572
- audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record your question")
573
  with gr.Row():
574
  voice_btn = gr.Button("Ask by Voice", variant="primary")
575
  voice_clear = gr.Button("Clear", variant="secondary")
576
  voice_btn.click(voice_chat, inputs=[audio_input, voice_chatbot], outputs=voice_chatbot)
577
  voice_clear.click(lambda: [], outputs=voice_chatbot)
578
 
579
- with gr.Tab("πŸ” Papers"):
 
580
  with gr.Row():
581
- search_input = gr.Textbox(placeholder="e.g. mechanical heart valve thrombogenicity 2024", label="Research Topic", scale=4)
582
- search_btn = gr.Button("Search", variant="primary", scale=1)
583
- search_output = gr.Textbox(label="Verified Results β€” PubMed + Semantic Scholar", lines=18)
584
- search_btn.click(quick_search, inputs=search_input, outputs=search_output)
585
- search_input.submit(quick_search, inputs=search_input, outputs=search_output)
586
-
587
- with gr.Tab("πŸ“Š PIV CSV"):
588
- gr.Markdown("Upload PIV CSV β†’ 4 separate charts + AI clinical analysis")
 
 
589
  with gr.Row():
590
  piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
591
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
@@ -599,8 +579,8 @@ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
599
  piv_c4=gr.Image(label="Clinical Summary",type="pil")
600
  piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
601
 
602
- with gr.Tab("🩸 TGT CSV"):
603
- gr.Markdown("Upload TGT CSV β†’ blood biomarker charts + thrombogenicity assessment")
604
  with gr.Row():
605
  tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
606
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
@@ -612,7 +592,7 @@ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
612
  tgt_c3=gr.Image(label="Hemoglobin",type="pil"); tgt_c4=gr.Image(label="Platelets",type="pil")
613
  tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
614
 
615
- with gr.Tab("πŸ§ͺ uPAD"):
616
  with gr.Row():
617
  with gr.Column():
618
  photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=260)
@@ -621,15 +601,14 @@ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
621
  photo_img = gr.Image(label="Detection Zone", type="pil", height=260)
622
  photo_text = gr.Textbox(label="CKD Result", lines=8)
623
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
624
- gr.Markdown("**Manual RGB:**")
625
  with gr.Row():
626
  r=gr.Number(label="R",value=210); g=gr.Number(label="G",value=140); b=gr.Number(label="B",value=80)
627
- out3=gr.Textbox(label="Result",lines=3)
628
  gr.Button("Analyze RGB",variant="secondary").click(
629
  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"),
630
  inputs=[r,g,b],outputs=out3)
631
 
632
- with gr.Tab("🎨 AI Image"):
633
  with gr.Row():
634
  img_prompt = gr.Textbox(placeholder="e.g. 27mm bileaflet mechanical heart valve cross section", label="Describe the image", lines=2, scale=4)
635
  with gr.Column(scale=1):
@@ -639,7 +618,7 @@ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
639
  img_output = gr.Image(label="Generated Image", type="pil", height=400)
640
  img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
641
 
642
- with gr.Tab("πŸ“ PIV Manual"):
643
  with gr.Row():
644
  with gr.Column():
645
  v=gr.Number(label="Max Velocity m/s",value=1.8,info="Normal: 0.5-2.0")
@@ -648,7 +627,7 @@ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
648
  piv_out=gr.Textbox(label="Result",lines=4)
649
  gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
650
 
651
- with gr.Tab("πŸ”¬ TGT Manual"):
652
  with gr.Row():
653
  with gr.Column():
654
  t1=gr.Number(label="TAT ng/mL",value=18,info="Normal: <8")
@@ -659,15 +638,7 @@ with gr.Blocks(title="CardioLab AI β€” SJSU", css=CSS) as demo:
659
  out2=gr.Textbox(label="Result",lines=6)
660
  gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
661
 
662
- gr.HTML("""
663
- <div style="text-align:center;padding:12px;border-top:1px solid #e5e7eb;margin-top:8px;background:#f9fafb;">
664
- <span style="color:#9ca3af;font-size:0.75em;">
665
- ❀️ CardioLab AI &nbsp;|&nbsp; SJSU Biomedical Engineering &nbsp;|&nbsp;
666
- Built on <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;">Biomni Stanford</a> &nbsp;|&nbsp;
667
- <a href="https://github.com/pranatechsol/Cardio-Lab-Ai" style="color:#0057a8;">GitHub</a> &nbsp;|&nbsp;
668
- Apache 2.0 &nbsp;|&nbsp; $0 Cost
669
- </span>
670
- </div>
671
- """)
672
 
673
  demo.launch()
 
20
  "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
21
  "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
22
 
23
+ CHAT_MODELS = {
24
+ "Llama 3.3 70B (Best)": "llama-3.3-70b-versatile",
25
+ "Llama 3.1 8B (Fast)": "llama-3.1-8b-instant",
26
+ "Mixtral 8x7B": "mixtral-8x7b-32768",
27
+ "Gemma 2 9B": "gemma2-9b-it",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
+
30
+ CSS = """
31
+ body, .gradio-container { background: #f7f7f8 !important; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important; }
32
+ .tab-nav { background: #ffffff !important; border-bottom: 1px solid #e5e7eb !important; padding: 0 16px !important; display: flex !important; flex-wrap: wrap !important; }
33
+ .tab-nav button { background: transparent !important; color: #6b7280 !important; border: none !important; border-bottom: 2px solid transparent !important; padding: 10px 12px !important; font-weight: 500 !important; font-size: 0.8em !important; white-space: nowrap !important; border-radius: 0 !important; }
34
  .tab-nav button:hover { color: #111827 !important; background: #f9fafb !important; }
35
  .tab-nav button.selected { color: #c1121f !important; border-bottom: 2px solid #c1121f !important; font-weight: 700 !important; background: transparent !important; }
36
  .message.user { background: #f3f4f6 !important; color: #1a202c !important; border-radius: 12px !important; }
37
+ .message.bot { background: #ffffff !important; color: #1a202c !important; border-left: 3px solid #c1121f !important; }
38
+ textarea { background: #ffffff !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 10px !important; }
 
39
  button.primary { background: #c1121f !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; }
 
40
  button.secondary { background: #f3f4f6 !important; color: #374151 !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
41
  input[type=number] { background: #f9fafb !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
42
  label span { color: #374151 !important; font-weight: 500 !important; font-size: 0.85em !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  """
44
 
45
+ HEADER = """<div style="background:linear-gradient(135deg,#0a0f2e 0%,#1a0a0a 100%);padding:0;border-bottom:3px solid #c1121f;position:relative;overflow:hidden;">
46
+ <svg style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0.07;" viewBox="0 0 1200 120" preserveAspectRatio="none">
47
+ <polyline points="0,60 100,60 130,20 150,100 170,10 200,90 220,60 400,60 430,20 450,100 470,10 500,90 520,60 700,60 730,20 750,100 770,10 800,90 820,60 1000,60 1030,20 1050,100 1070,10 1100,90 1120,60 1200,60" fill="none" stroke="#c1121f" stroke-width="3"/>
48
+ </svg>
49
+ <div style="max-width:1200px;margin:0 auto;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1;">
50
+ <div style="display:flex;align-items:center;gap:14px;">
51
+ <svg width="55" height="55" viewBox="0 0 100 100"><circle cx="50" cy="35" r="28" fill="#0057a8" opacity="0.9"/><ellipse cx="50" cy="14" rx="22" ry="10" fill="#0057a8"/>
52
+ <polygon points="30,14 33,4 36,14" fill="#e8a020"/><polygon points="36,12 39,2 42,12" fill="#e8a020"/><polygon points="42,11 45,1 48,11" fill="#e8a020"/>
53
+ <polygon points="48,11 51,1 54,11" fill="#e8a020"/><polygon points="54,12 57,2 60,12" fill="#e8a020"/><polygon points="60,14 63,4 66,14" fill="#e8a020"/>
54
+ <rect x="36" y="30" width="28" height="22" rx="4" fill="#0057a8"/><rect x="40" y="35" width="8" height="12" rx="2" fill="#e8a020"/>
55
+ <rect x="34" y="50" width="32" height="8" rx="4" fill="#0057a8"/></svg>
56
+ <div><div style="color:#9ca3af;font-size:0.7em;font-weight:500;letter-spacing:2px;text-transform:uppercase;">San Jose State University</div>
57
+ <div style="color:#e8a020;font-size:0.82em;font-weight:700;">Biomedical Engineering</div></div></div>
58
+ <div style="text-align:center;flex:1;padding:0 20px;">
59
+ <div style="display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:3px;">
60
+ <svg width="100" height="28" viewBox="0 0 120 32"><polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16" fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
61
+ <div style="font-size:2em;font-weight:900;letter-spacing:2px;"><span style="color:#ffffff;">Cardio</span><span style="color:#c1121f;">Lab</span><span style="color:#ffffff;"> AI</span></div>
62
+ <svg width="100" height="28" viewBox="0 0 120 32" style="transform:scaleX(-1);"><polyline points="0,16 20,16 26,4 30,28 34,2 38,26 44,16 120,16" fill="none" stroke="#c1121f" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
63
+ <div style="color:#9ca3af;font-size:0.7em;letter-spacing:2px;text-transform:uppercase;">AI Research Agent | Biomni Stanford | Llama 3.3 70B</div></div>
64
+ <div style="display:flex;align-items:center;gap:14px;">
65
+ <div style="text-align:right;"><div style="color:#9ca3af;font-size:0.68em;letter-spacing:1px;text-transform:uppercase;">Research Pillars</div>
66
+ <div style="color:#ffffff;font-size:0.72em;margin-top:3px;">MHV CKD FSI</div>
67
+ <div style="color:#9ca3af;font-size:0.62em;margin-top:2px;">MCL Β· PIV Β· TGT Β· uPAD Β· COMSOL</div></div>
68
+ <svg width="48" height="48" viewBox="0 0 100 90">
69
+ <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" opacity="0.9"/>
70
+ <polyline points="25,45 32,45 35,35 38,55 41,30 44,50 50,45 75,45" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" opacity="0.9"/></svg></div></div>
71
+ <div style="height:3px;background:linear-gradient(90deg,#0057a8,#c1121f,#e8a020,#c1121f,#0057a8);"></div></div>"""
72
+
73
+ # ── SESSION MANAGEMENT ─────────────────────────────────────────────
74
  def load_all_sessions():
75
  if not HF_TOKEN: return {}
76
  try:
 
81
  def save_all_sessions(sessions):
82
  if not HF_TOKEN: return False
83
  try:
84
+ api2 = HfApi(token=HF_TOKEN)
85
+ api2.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(),
86
+ path_in_repo="chat_history.json", repo_id=HISTORY_REPO, repo_type="dataset",
87
+ token=HF_TOKEN, commit_message="Update")
88
  return True
89
  except: return False
90
 
 
119
 
120
  def new_chat(): return [], "", "New chat started"
121
 
122
+ # ── SEARCH FUNCTIONS ───────────────────────────────────────────────
123
+ def expand_query_ai(query, model_id="llama-3.3-70b-versatile"):
 
 
 
 
 
 
 
 
 
124
  if not GROQ_KEY: return query
125
  try:
126
  client = Groq(api_key=GROQ_KEY)
127
+ resp = client.chat.completions.create(model=model_id,
128
+ messages=[{"role":"system","content":"Biomedical PubMed search expert for SJSU CardioLab. Convert query to optimized MeSH terms and technical keywords. Focus on: mechanical heart valves, hemodynamics, blood flow, PIV, thrombogenicity, FSI, CFD, microfluidics, CKD, creatinine. Return ONLY search terms no explanation."},
129
+ {"role":"user","content":"Optimize for PubMed: "+query}],max_tokens=80)
130
+ return resp.choices[0].message.content.strip() or query
 
 
 
 
 
 
 
 
 
 
131
  except: return query
132
 
133
+ def fetch_pubmed(query, n=8):
134
  try:
135
+ biofilter = " AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CFD OR CKD OR fluid structure)"
 
 
136
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
137
+ params={"db":"pubmed","term":query+biofilter,"retmax":n,"retmode":"json","sort":"date","field":"tiab"},timeout=12)
138
  ids = r.json()["esearchresult"]["idlist"]
139
+ if not ids: return []
 
140
  r2 = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
141
+ params={"db":"pubmed","id":",".join(ids),"retmode":"xml","rettype":"abstract"},timeout=12)
142
+ import xml.etree.ElementTree as ET
143
+ root = ET.fromstring(r2.content)
144
+ results = []
145
+ for article in root.findall(".//PubmedArticle"):
146
+ try:
147
+ title = article.find(".//ArticleTitle").text or "No title"
148
+ pmid = article.find(".//PMID").text or ""
149
+ year_el = article.find(".//PubDate/Year")
150
+ year = year_el.text if year_el is not None else ""
151
+ results.append({"source":"PubMed","title":str(title),"year":year,
152
+ "url":"https://pubmed.ncbi.nlm.nih.gov/"+pmid,"citations":"N/A"})
153
+ except: continue
154
+ return results
155
+ except: return []
 
 
 
 
 
 
156
 
157
+ def fetch_scholar(query, n=8):
158
  try:
 
159
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
160
+ params={"query":query,"limit":n,"fields":"title,year,url,citationCount"},timeout=12)
 
161
  papers = r.json().get("data",[])
162
+ results = []
 
 
163
  for p in papers:
164
+ year = p.get("year",0) or 0
165
+ if int(year) < 2015: continue
166
+ results.append({"source":"Semantic Scholar","title":p.get("title",""),"year":str(year),
167
+ "url":p.get("url",""),"citations":str(p.get("citationCount",0))})
168
+ results.sort(key=lambda x:(x["year"],int(x["citations"]) if x["citations"].isdigit() else 0),reverse=True)
169
+ return results
170
+ except: return []
171
+
172
+ def fetch_europe_pmc(query, n=6):
173
+ try:
174
+ r = requests.get("https://www.ebi.ac.uk/europepmc/webservices/rest/search",
175
+ params={"query":query,"format":"json","pageSize":n,"sort":"P_PDATE_D desc","resulttype":"core"},timeout=12)
176
+ articles = r.json().get("resultList",{}).get("result",[])
177
+ results = []
178
+ for a in articles:
179
+ year = str(a.get("pubYear",""))
180
+ if year and int(year) < 2015: continue
181
+ pmid = a.get("pmid","")
182
+ doi = a.get("doi","")
183
+ url = ("https://pubmed.ncbi.nlm.nih.gov/"+pmid if pmid else "https://doi.org/"+doi if doi else "")
184
+ if not url: continue
185
+ results.append({"source":"Europe PMC","title":a.get("title",""),"year":year,
186
+ "url":url,"citations":str(a.get("citedByCount",0))})
187
+ return results
188
+ except: return []
189
+
190
+ def fetch_crossref(query, n=5):
191
+ try:
192
+ r = requests.get("https://api.crossref.org/works",
193
+ params={"query":query,"rows":n,"sort":"relevance","select":"title,DOI,published"},timeout=12)
194
+ items = r.json().get("message",{}).get("items",[])
195
+ results = []
196
+ for item in items:
197
+ title = item.get("title",[""])[0] if item.get("title") else ""
198
+ doi = item.get("DOI","")
199
+ pub = item.get("published",{}).get("date-parts",[[""]])[0]
200
+ year = str(pub[0]) if pub else ""
201
+ if year and int(year) < 2015: continue
202
+ if not doi: continue
203
+ results.append({"source":"CrossRef","title":title,"year":year,
204
+ "url":"https://doi.org/"+doi,"citations":"N/A"})
205
+ return results
206
+ except: return []
207
+
208
+ def fetch_sjsu_scholarworks(query, n=6):
209
+ try:
210
+ # SJSU ScholarWorks Digital Commons search
211
+ r = requests.get("https://scholarworks.sjsu.edu/do/search/",
212
+ params={"q":query,"start":"0","context":"6781027","format":"json"},
213
+ timeout=12, headers={"User-Agent":"CardioLab-AI/1.0"})
214
+ results = []
215
+ if r.status_code == 200:
216
+ try:
217
+ data = r.json()
218
+ docs = data.get("response",{}).get("docs",[])
219
+ for doc in docs[:n]:
220
+ title = doc.get("title","")
221
+ year = str(doc.get("publication_date",""))[:4]
222
+ url = doc.get("url","") or "https://scholarworks.sjsu.edu/"
223
+ if title:
224
+ results.append({"source":"SJSU ScholarWorks","title":str(title),"year":year,
225
+ "url":url,"citations":"SJSU"})
226
+ except: pass
227
+ if not results:
228
+ # Fallback: provide direct SJSU search link
229
+ search_url = "https://scholarworks.sjsu.edu/do/search/?q="+requests.utils.quote(query)+"&context=6781027"
230
+ results.append({"source":"SJSU ScholarWorks",
231
+ "title":"Click to search SJSU ScholarWorks for: "+query,
232
+ "year":"","url":search_url,"citations":"SJSU"})
233
+ return results
234
+ except:
235
+ search_url = "https://scholarworks.sjsu.edu/do/search/?q="+requests.utils.quote(query)
236
+ return [{"source":"SJSU ScholarWorks","title":"Search SJSU ScholarWorks: "+query,
237
+ "year":"","url":search_url,"citations":"SJSU"}]
238
+
239
+ def rank_with_ai(query, results, model_id="llama-3.3-70b-versatile"):
240
+ if not GROQ_KEY or not results: return results
241
+ try:
242
+ client = Groq(api_key=GROQ_KEY)
243
+ papers_text = chr(10).join([str(i+1)+". "+r["title"]+" ("+r["year"]+")" for i,r in enumerate(results[:15])])
244
+ resp = client.chat.completions.create(model=model_id,
245
+ messages=[{"role":"system","content":"Biomedical research expert. Rank papers by relevance to query. Return ONLY numbers comma separated. Example: 3,1,5,2,4"},
246
+ {"role":"user","content":"Query: "+query+chr(10)+"Papers:"+chr(10)+papers_text}],max_tokens=60)
247
+ order_text = resp.choices[0].message.content.strip()
248
+ order = [int(x.strip())-1 for x in order_text.split(",") if x.strip().isdigit()]
249
+ ranked = [results[i] for i in order if i < len(results)]
250
+ rest = [r for i,r in enumerate(results) if i not in order]
251
+ return ranked + rest
252
+ except: return results
253
+
254
+ def quick_search(query, search_model="Llama 3.3 70B (Best)"):
255
  if not query.strip(): return "Please enter a research topic."
256
+ model_id = CHAT_MODELS.get(search_model, "llama-3.3-70b-versatile")
257
+ expanded = expand_query_ai(query, model_id)
258
+ r1 = fetch_pubmed(expanded, n=8)
259
+ r2 = fetch_scholar(expanded, n=8)
260
+ r3 = fetch_europe_pmc(expanded, n=6)
261
+ r4 = fetch_crossref(expanded, n=5)
262
+ r5 = fetch_sjsu_scholarworks(query, n=6)
263
+ all_results = r1 + r2 + r3 + r4 + r5
264
+ seen = set()
265
+ unique = []
266
+ for r in all_results:
267
+ key = r["title"][:50].lower().strip()
268
+ if key not in seen and r["url"]:
269
+ seen.add(key); unique.append(r)
270
+ ranked = rank_with_ai(query, unique, model_id)
271
+ out = "QUERY: "+query+chr(10)
272
+ out += "AI MODEL: "+search_model+chr(10)
273
+ out += "AI EXPANDED: "+expanded+chr(10)
274
+ out += "SOURCES: PubMed + Semantic Scholar + Europe PMC + CrossRef + SJSU ScholarWorks"+chr(10)
275
+ out += "TOTAL UNIQUE PAPERS: "+str(len(ranked))+chr(10)
276
+ out += "="*45+chr(10)+chr(10)
277
+ groups = {"PubMed":[],"Semantic Scholar":[],"Europe PMC":[],"CrossRef":[],"SJSU ScholarWorks":[]}
278
+ for r in ranked[:25]:
279
+ if r["source"] in groups: groups[r["source"]].append(r)
280
+ for source, papers in groups.items():
281
+ if not papers: continue
282
+ out += "--- "+source+" ("+str(len(papers))+" papers) ---"+chr(10)
283
+ for p in papers:
284
+ out += p["title"][:85]+" ("+p["year"]+")"
285
+ if p["citations"] not in ("N/A","SJSU",""): out += " | "+p["citations"]+" citations"
286
+ out += chr(10)+" "+p["url"]+chr(10)+chr(10)
287
+ return out
288
+
289
+ def get_pubmed(query, n=3):
290
+ try:
291
+ r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
292
+ 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)
293
+ ids = r.json()["esearchresult"]["idlist"]
294
+ if not ids: return ""
295
+ return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
296
+ except: return ""
297
+
298
+ # ── CHAT ───────────────────────────────────────────────────────────
299
+ def research_chat(message, history, chat_model="Llama 3.3 70B (Best)"):
300
  if not GROQ_KEY:
301
  history.append({"role":"user","content":message})
302
  history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
303
  return "", history
304
  try:
305
+ model_id = CHAT_MODELS.get(chat_model, "llama-3.3-70b-versatile")
306
  client = Groq(api_key=GROQ_KEY)
307
  msgs = [{"role":"system","content":"You are CardioLab AI for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember conversation. Never invent URLs. "+KNOWHOW}]
308
  for item in history:
309
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
310
  msgs.append({"role":"user","content":message})
311
+ resp = client.chat.completions.create(model=model_id,messages=msgs,max_tokens=700)
312
  answer = resp.choices[0].message.content
313
  pubmed = get_pubmed(message, n=3)
314
+ if pubmed: answer += chr(10)+chr(10)+"PubMed:"+chr(10)+pubmed
315
  history.append({"role":"user","content":message})
316
  history.append({"role":"assistant","content":answer})
317
  return "", history
 
333
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
334
  msgs.append({"role":"user","content":tx.text})
335
  resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=500)
336
+ history.append({"role":"user","content":"Voice: "+tx.text})
337
  history.append({"role":"assistant","content":resp.choices[0].message.content})
338
  return history
339
  except Exception as e:
340
  history.append({"role":"assistant","content":"Voice error: "+str(e)})
341
  return history
342
 
343
+ # ── ANALYSIS TOOLS ─────────────────────────────────────────────────
344
  def analyze_upad_photo(image):
345
  if image is None: return None, "Upload a uPAD photo first."
346
  try:
 
357
  else: s,a="Stage 5 CKD","Emergency care."
358
  ri=img.copy()
359
  import PIL.ImageDraw as D; D.Draw(ri).rectangle([x1,y1,x2,y2],outline=(0,255,0),width=3)
360
+ return ri,("uPAD ANALYSIS"+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)
361
  except Exception as e: return None,"Error: "+str(e)
362
 
363
  def mk_chart(fn,title,bg,fg,gc,ac,pb):
 
380
  pb="#f7fafc" if theme=="White" else "#132340"
381
  x=np.arange(len(df))
382
  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)
383
+ 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)
384
  tc=next((c for c in cols if "time" in c or "frame" in c),None); xv=df[tc] if tc else x
385
  def pv(ax):
386
  if vc:
387
  ax.plot(xv,df[vc],color="#c1121f",linewidth=2.5,marker="o",markersize=5)
388
  ax.fill_between(xv,df[vc],alpha=0.15,color="#c1121f")
389
  ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
390
+ ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11); ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
 
391
  def ps(ax):
392
+ if sc2:
393
  xp=xv.values if tc else x
394
+ ax.plot(xp,df[sc2],color="#0057a8",linewidth=2.5,marker="s",markersize=5)
395
+ ax.fill_between(xp,df[sc2],alpha=0.15,color="#0057a8")
396
  ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
397
  ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="High risk: 10 Pa")
398
+ ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11); ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
 
399
  def psc(ax):
400
+ if vc and sc2:
401
+ s3=ax.scatter(df[vc],df[sc2],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
402
+ cb=plt.colorbar(s3,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
403
+ ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2); ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2)
 
404
  ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11); ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11)
 
405
  def psum(ax):
406
  ax.axis("off"); risk=[]
407
+ st="CLINICAL SUMMARY"+chr(10)+"="*20+chr(10)+chr(10)
408
  for col in num_cols[:3]:
409
  mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
410
  st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
411
  if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY")
412
  if "shear" in col and mx>10: risk.append("HIGH SHEAR")
413
  bc="#c1121f" if risk else "#2ecc71"
414
+ st+="="*20+chr(10)+("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
415
  ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
416
  bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
417
  i1=mk_chart(pv,"Velocity Profile",bg,fg,gc,ac,pb)
 
425
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
426
  messages=[{"role":"system","content":"PIV expert SJSU CardioLab. Analyze stats give clinical interpretation."},
427
  {"role":"user","content":"PIV from 27mm SJM Regent MHV 70bpm:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
428
+ ai=chr(10)+"AI: "+resp.choices[0].message.content
429
  except: pass
430
  return i1,i2,i3,i4,"PIV: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
431
  except Exception as e: return None,None,None,None,"Error: "+str(e)
 
456
  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")
457
  ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
458
  ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
459
+ ax.set_ylabel(yl,color=ac,fontsize=11)
460
  mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
461
  ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
462
  return mk_chart(fn,title,bg,fg,gc,ac,pb)
 
471
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
472
  messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Give thrombogenicity risk LOW MODERATE or HIGH."},
473
  {"role":"user","content":"TGT from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
474
+ ai=chr(10)+"AI: "+resp.choices[0].message.content
475
  except: pass
476
  return i1,i2,i3,i4,"TGT: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
477
  except Exception as e: return None,None,None,None,"Error: "+str(e)
 
503
  except Exception as e: return None,"Error: "+str(e),""
504
 
505
  def piv_manual(v,s,h):
506
+ vr="HIGH - stenosis" if float(v)>2.0 else "NORMAL"
507
+ sr="HIGH - thrombosis" if float(s)>10 else "ELEVATED" if float(s)>5 else "NORMAL"
508
+ return "Velocity: "+str(v)+" m/s - "+vr+chr(10)+"Shear: "+str(s)+" Pa - "+sr+chr(10)+"HR: "+str(h)+" bpm"
509
 
510
  def tgt_manual(t,p,h,pl,tm):
511
  risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
512
+ return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
 
 
 
 
513
 
514
+ # ── UI ─────────────────────────────────────────────────────────────
515
+ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
516
+ gr.HTML(HEADER)
517
  with gr.Tabs():
518
 
519
+ with gr.Tab("Chat"):
520
  with gr.Row():
521
+ with gr.Column(scale=1, min_width=200):
522
+ gr.HTML('''<div style="background:#202123;padding:10px;border-radius:8px;margin-bottom:6px;"><div style="color:#e8a020;font-weight:700;font-size:0.85em;">SJSU CARDIOLAB</div><div style="color:#9ca3af;font-size:0.7em;">Conversations</div></div>''')
523
+ new_chat_btn = gr.Button("New Chat", variant="secondary")
524
+ session_dropdown = gr.Dropdown(choices=get_session_list(), label="Saved Sessions", interactive=True)
525
+ load_btn = gr.Button("Load Session", variant="primary")
526
+ session_name_box = gr.Textbox(placeholder="Session name...", label="", lines=1, container=False)
 
 
 
 
527
  with gr.Row():
528
+ save_btn = gr.Button("Save", variant="primary", scale=2)
529
+ delete_btn = gr.Button("Del", variant="secondary", scale=1)
530
  session_status = gr.Textbox(label="", lines=1, interactive=False, container=False)
 
531
  with gr.Column(scale=4):
532
+ chatbot = gr.Chatbot(label="", height=480, show_label=False, container=False)
533
  with gr.Row():
534
+ msg_box = gr.Textbox(placeholder="Message CardioLab AI...", label="", lines=2, scale=4, container=False)
535
+ with gr.Column(scale=1, min_width=120):
536
+ chat_model_dd = gr.Dropdown(choices=list(CHAT_MODELS.keys()), value="Llama 3.3 70B (Best)", label="Model")
537
+ send_btn = gr.Button("Send", variant="primary")
538
  clear_btn = gr.Button("Clear", variant="secondary")
539
+ send_btn.click(research_chat, inputs=[msg_box, chatbot, chat_model_dd], outputs=[msg_box, chatbot])
540
+ msg_box.submit(research_chat, inputs=[msg_box, chatbot, chat_model_dd], outputs=[msg_box, chatbot])
 
541
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
542
  new_chat_btn.click(new_chat, outputs=[chatbot, msg_box, session_status])
543
  save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
544
  load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
545
  delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
546
 
547
+ with gr.Tab("Voice"):
548
+ voice_chatbot = gr.Chatbot(label="", height=360, show_label=False)
549
+ audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record Question")
550
  with gr.Row():
551
  voice_btn = gr.Button("Ask by Voice", variant="primary")
552
  voice_clear = gr.Button("Clear", variant="secondary")
553
  voice_btn.click(voice_chat, inputs=[audio_input, voice_chatbot], outputs=voice_chatbot)
554
  voice_clear.click(lambda: [], outputs=voice_chatbot)
555
 
556
+ with gr.Tab("Papers"):
557
+ gr.Markdown("### Search 5 sources: PubMed + Semantic Scholar + Europe PMC + CrossRef + SJSU ScholarWorks")
558
  with gr.Row():
559
+ search_input = gr.Textbox(placeholder="e.g. unsteady flow bileaflet mechanical heart valve hemodynamics", label="Research Topic", scale=3)
560
+ search_model_dd = gr.Dropdown(choices=list(CHAT_MODELS.keys()), value="Llama 3.3 70B (Best)", label="AI Model", scale=1)
561
+ search_btn = gr.Button("Search All 5 Sources", variant="primary", scale=1)
562
+ search_output = gr.Textbox(label="AI Ranked Results", lines=25)
563
+ search_btn.click(quick_search, inputs=[search_input, search_model_dd], outputs=search_output)
564
+ search_input.submit(quick_search, inputs=[search_input, search_model_dd], outputs=search_output)
565
+ gr.Markdown("**Try:** `bileaflet heart valve thrombogenicity` | `uPAD microfluidic creatinine CKD` | `PIV hemodynamics prosthetic valve` | `SJSU CardioLab biomedical`")
566
+
567
+ with gr.Tab("PIV CSV"):
568
+ gr.Markdown("Upload PIV CSV - 4 separate charts + AI clinical analysis")
569
  with gr.Row():
570
  piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
571
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
 
579
  piv_c4=gr.Image(label="Clinical Summary",type="pil")
580
  piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
581
 
582
+ with gr.Tab("TGT CSV"):
583
+ gr.Markdown("Upload TGT CSV - blood biomarker charts + thrombogenicity assessment")
584
  with gr.Row():
585
  tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
586
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
 
592
  tgt_c3=gr.Image(label="Hemoglobin",type="pil"); tgt_c4=gr.Image(label="Platelets",type="pil")
593
  tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
594
 
595
+ with gr.Tab("uPAD"):
596
  with gr.Row():
597
  with gr.Column():
598
  photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=260)
 
601
  photo_img = gr.Image(label="Detection Zone", type="pil", height=260)
602
  photo_text = gr.Textbox(label="CKD Result", lines=8)
603
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
 
604
  with gr.Row():
605
  r=gr.Number(label="R",value=210); g=gr.Number(label="G",value=140); b=gr.Number(label="B",value=80)
606
+ out3=gr.Textbox(label="Manual Result",lines=3)
607
  gr.Button("Analyze RGB",variant="secondary").click(
608
  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"),
609
  inputs=[r,g,b],outputs=out3)
610
 
611
+ with gr.Tab("AI Image"):
612
  with gr.Row():
613
  img_prompt = gr.Textbox(placeholder="e.g. 27mm bileaflet mechanical heart valve cross section", label="Describe the image", lines=2, scale=4)
614
  with gr.Column(scale=1):
 
618
  img_output = gr.Image(label="Generated Image", type="pil", height=400)
619
  img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
620
 
621
+ with gr.Tab("PIV Manual"):
622
  with gr.Row():
623
  with gr.Column():
624
  v=gr.Number(label="Max Velocity m/s",value=1.8,info="Normal: 0.5-2.0")
 
627
  piv_out=gr.Textbox(label="Result",lines=4)
628
  gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
629
 
630
+ with gr.Tab("TGT Manual"):
631
  with gr.Row():
632
  with gr.Column():
633
  t1=gr.Number(label="TAT ng/mL",value=18,info="Normal: <8")
 
638
  out2=gr.Textbox(label="Result",lines=6)
639
  gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
640
 
641
+ gr.HTML("""<div style="text-align:center;padding:10px;border-top:1px solid #e5e7eb;background:#f9fafb;">
642
+ <span style="color:#9ca3af;font-size:0.75em;">CardioLab AI | SJSU Biomedical Engineering | Built on <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;">Biomni Stanford</a> | <a href="https://github.com/pranatechsol/Cardio-Lab-Ai" style="color:#0057a8;">GitHub</a> | Apache 2.0 | $0 Cost</span></div>""")
 
 
 
 
 
 
 
 
643
 
644
  demo.launch()