Saicharan21 commited on
Commit
a4bd4ea
Β·
verified Β·
1 Parent(s): 77efdf2

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +160 -157
app.py CHANGED
@@ -13,12 +13,7 @@ from huggingface_hub import HfApi, hf_hub_download
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
-
17
- KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
18
- "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
19
- "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
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",
@@ -27,6 +22,71 @@ CHAT_MODELS = {
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; }
@@ -39,32 +99,32 @@ textarea { background: #ffffff !important; color: #1a202c !important; border: 1p
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>
@@ -75,7 +135,7 @@ def load_all_sessions():
75
  if not HF_TOKEN: return {}
76
  try:
77
  path = hf_hub_download(repo_id=HISTORY_REPO, filename="chat_history.json", repo_type="dataset", token=HF_TOKEN)
78
- with open(path, "r") as f: return json.load(f)
79
  except: return {}
80
 
81
  def save_all_sessions(sessions):
@@ -83,8 +143,8 @@ def save_all_sessions(sessions):
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
 
@@ -125,16 +185,16 @@ def expand_query_ai(query, model_id="llama-3.3-70b-versatile"):
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",
@@ -154,7 +214,7 @@ def fetch_pubmed(query, n=8):
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)
@@ -163,23 +223,22 @@ def fetch_scholar(query, n=8):
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,
@@ -187,106 +246,38 @@ def fetch_europe_pmc(query, n=6):
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)
@@ -295,7 +286,7 @@ def get_pubmed(query, n=3):
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})
@@ -304,14 +295,28 @@ def research_chat(message, history, chat_model="Llama 3.3 70B (Best)"):
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
@@ -328,7 +333,10 @@ def voice_chat(audio, history):
328
  client = Groq(api_key=GROQ_KEY)
329
  with open(audio, "rb") as f:
330
  tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
331
- msgs = [{"role":"system","content":"You are CardioLab AI. "+KNOWHOW}]
 
 
 
332
  for item in history:
333
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
334
  msgs.append({"role":"user","content":tx.text})
@@ -340,7 +348,6 @@ def voice_chat(audio, history):
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:
@@ -393,8 +400,8 @@ def analyze_piv_csv(file,theme="White"):
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:
@@ -414,20 +421,18 @@ def analyze_piv_csv(file,theme="White"):
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)
418
- i2=mk_chart(ps,"Wall Shear Stress",bg,fg,gc,ac,pb)
419
- i3=mk_chart(psc,"Velocity vs Shear",bg,fg,gc,ac,pb)
420
- i4=mk_chart(psum,"Clinical Summary",bg,fg,gc,ac,pb)
421
  ai=""
422
  if GROQ_KEY:
423
  try:
424
  client=Groq(api_key=GROQ_KEY)
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)
432
 
433
  def analyze_tgt_csv(file,theme="White"):
@@ -457,8 +462,8 @@ def analyze_tgt_csv(file,theme="White"):
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)
463
  i1=mk2(tatc,"#c1121f","TAT (ng/mL)",8,"Normal: 8","TAT Thrombin-Antithrombin")
464
  i2=mk2(pfc,"#0057a8","PF1.2 (nmol/L)",2.0,"Normal: 2.0","PF1.2 Prothrombin Fragment")
@@ -469,11 +474,11 @@ def analyze_tgt_csv(file,theme="White"):
469
  try:
470
  client=Groq(api_key=GROQ_KEY)
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)
478
 
479
  def generate_image(prompt):
@@ -514,12 +519,20 @@ def tgt_manual(t,p,h,pl,tm):
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")
@@ -531,7 +544,7 @@ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
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")
@@ -554,33 +567,28 @@ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
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)
572
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
573
  piv_result = gr.Textbox(label="AI Analysis", lines=4)
574
  with gr.Row():
575
- piv_c1=gr.Image(label="Velocity Profile",type="pil")
576
- piv_c2=gr.Image(label="Shear Stress",type="pil")
577
  with gr.Row():
578
- piv_c3=gr.Image(label="Velocity vs Shear",type="pil")
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)
@@ -610,7 +618,7 @@ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
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):
615
  img_btn = gr.Button("Generate Image", variant="primary")
616
  img_status = gr.Textbox(label="Status", lines=1)
@@ -621,24 +629,19 @@ with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
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")
625
- s=gr.Number(label="Wall Shear Stress Pa",value=6.5,info="Normal: <5")
626
- h=gr.Number(label="Heart Rate bpm",value=72,info="Normal: 60-100")
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")
634
- t2=gr.Number(label="PF1.2 nmol/L",value=2.5,info="Normal: <2.0")
635
- t3=gr.Number(label="Free Hemoglobin mg/L",value=60,info="Normal: <20")
636
- t4=gr.Number(label="Platelet Count",value=140,info="Normal: >150")
637
- t5=gr.Number(label="Time minutes",value=40)
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()
 
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
 
18
  CHAT_MODELS = {
19
  "Llama 3.3 70B (Best)": "llama-3.3-70b-versatile",
 
22
  "Gemma 2 9B": "gemma2-9b-it",
23
  }
24
 
25
+ KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
26
+ "TGT: Arduino Uno Stepper Motor 150mL blood 0 20 40 60min TAT PF1.2 hemolysis platelets. "
27
+ "uPAD: Jaffe reaction creatinine picric acid orange-red 0.6-1.2 mg/dL CKD above 1.5. "
28
+ "MHV: 27mm SJM Regent bileaflet trileaflet monoleaflet pediatric. "
29
+ "Equipment: Heska HT5 analyzer PIV green laser Tygon tubing Arduino Uno.")
30
+
31
+ # ── LOAD PAPERS ON STARTUP ─────────────────────────────────────────
32
+ CHUNKS = []
33
+ METADATA = []
34
+ EMBEDDINGS = None
35
+ PAPERS_LOADED = False
36
+ EMBEDDER = None
37
+
38
+ def load_papers():
39
+ global CHUNKS, METADATA, EMBEDDINGS, PAPERS_LOADED, EMBEDDER
40
+ try:
41
+ from sentence_transformers import SentenceTransformer
42
+ print("Loading paper database from HuggingFace...")
43
+ chunks_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="chunks.json", repo_type="dataset", token=HF_TOKEN)
44
+ meta_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="metadata.json", repo_type="dataset", token=HF_TOKEN)
45
+ emb_path = hf_hub_download(repo_id=PAPERS_DB_REPO, filename="embeddings.npy", repo_type="dataset", token=HF_TOKEN)
46
+ with open(chunks_path) as f: CHUNKS = json.load(f)
47
+ with open(meta_path) as f: METADATA = json.load(f)
48
+ EMBEDDINGS = np.load(emb_path)
49
+ EMBEDDER = SentenceTransformer("all-MiniLM-L6-v2")
50
+ PAPERS_LOADED = True
51
+ papers_count = len(set(m["paper"] for m in METADATA))
52
+ print(f"Loaded {len(CHUNKS)} chunks from {papers_count} SJSU papers!")
53
+ return True
54
+ except Exception as e:
55
+ print(f"Paper load error: {e}")
56
+ return False
57
+
58
+ load_papers()
59
+
60
+ # ── SEMANTIC SEARCH ────────────────────────────────────────────────
61
+ def search_papers(query, n=4):
62
+ global CHUNKS, METADATA, EMBEDDINGS, EMBEDDER, PAPERS_LOADED
63
+ if not PAPERS_LOADED or EMBEDDINGS is None or EMBEDDER is None:
64
+ return "", []
65
+ try:
66
+ q_emb = EMBEDDER.encode([query])
67
+ norms = np.linalg.norm(EMBEDDINGS, axis=1, keepdims=True)
68
+ emb_norm = EMBEDDINGS / (norms + 1e-10)
69
+ q_norm = q_emb / (np.linalg.norm(q_emb) + 1e-10)
70
+ scores = (emb_norm @ q_norm.T).flatten()
71
+ top_idx = np.argsort(scores)[::-1][:n]
72
+ context = ""
73
+ results = []
74
+ seen = set()
75
+ for idx in top_idx:
76
+ chunk = CHUNKS[idx]
77
+ meta = METADATA[idx]
78
+ score = float(scores[idx])
79
+ if score > 0.25:
80
+ results.append({"chunk": chunk, "paper": meta["paper"], "pillar": meta.get("pillar",""), "score": score})
81
+ if meta["paper"] not in seen:
82
+ context += chr(10)+"=== FROM: "+meta["paper"]+" ["+meta.get("pillar","")+"] ==="+chr(10)
83
+ seen.add(meta["paper"])
84
+ context += chunk[:500]+chr(10)
85
+ return context, results
86
+ except Exception as e:
87
+ print(f"Search error: {e}")
88
+ return "", []
89
+
90
  CSS = """
91
  body, .gradio-container { background: #f7f7f8 !important; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif !important; }
92
  .tab-nav { background: #ffffff !important; border-bottom: 1px solid #e5e7eb !important; padding: 0 16px !important; display: flex !important; flex-wrap: wrap !important; }
 
99
  button.primary { background: #c1121f !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 600 !important; }
100
  button.secondary { background: #f3f4f6 !important; color: #374151 !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
101
  input[type=number] { background: #f9fafb !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius: 8px !important; }
 
102
  """
103
 
104
+ HEADER = """<div style="background:linear-gradient(135deg,#0a0f2e 0%,#1a0a0a 100%);padding:0;border-bottom:3px solid #c1121f;overflow:hidden;">
105
+ <svg style="position:absolute;opacity:0.07;width:100%;height:100%;" viewBox="0 0 1200 120" preserveAspectRatio="none">
106
  <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"/>
107
  </svg>
108
  <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;">
109
  <div style="display:flex;align-items:center;gap:14px;">
110
  <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"/>
111
+ <polygon points="30,14 33,4 36,14" fill="#e8a020"/><polygon points="36,12 39,2 42,12" fill="#e8a020"/>
112
+ <polygon points="42,11 45,1 48,11" fill="#e8a020"/><polygon points="48,11 51,1 54,11" fill="#e8a020"/>
113
+ <polygon points="54,12 57,2 60,12" fill="#e8a020"/><polygon points="60,14 63,4 66,14" fill="#e8a020"/>
114
  <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"/>
115
  <rect x="34" y="50" width="32" height="8" rx="4" fill="#0057a8"/></svg>
116
+ <div><div style="color:#9ca3af;font-size:0.7em;letter-spacing:2px;text-transform:uppercase;">San Jose State University</div>
117
  <div style="color:#e8a020;font-size:0.82em;font-weight:700;">Biomedical Engineering</div></div></div>
118
  <div style="text-align:center;flex:1;padding:0 20px;">
119
  <div style="display:flex;align-items:center;justify-content:center;gap:10px;margin-bottom:3px;">
120
+ <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"/></svg>
121
  <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>
122
+ <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"/></svg></div>
123
+ <div style="color:#9ca3af;font-size:0.68em;letter-spacing:2px;text-transform:uppercase;">RAG Agent | 16 SJSU Papers | Llama 3.3 70B | 5 Search Sources</div></div>
124
  <div style="display:flex;align-items:center;gap:14px;">
125
+ <div style="text-align:right;"><div style="color:#9ca3af;font-size:0.68em;text-transform:uppercase;">Research Pillars</div>
126
  <div style="color:#ffffff;font-size:0.72em;margin-top:3px;">MHV CKD FSI</div>
127
+ <div style="color:#9ca3af;font-size:0.62em;margin-top:2px;">MCL PIV TGT uPAD COMSOL</div></div>
128
  <svg width="48" height="48" viewBox="0 0 100 90">
129
  <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"/>
130
  <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>
 
135
  if not HF_TOKEN: return {}
136
  try:
137
  path = hf_hub_download(repo_id=HISTORY_REPO, filename="chat_history.json", repo_type="dataset", token=HF_TOKEN)
138
+ with open(path) as f: return json.load(f)
139
  except: return {}
140
 
141
  def save_all_sessions(sessions):
 
143
  try:
144
  api2 = HfApi(token=HF_TOKEN)
145
  api2.upload_file(path_or_fileobj=json.dumps(sessions, indent=2).encode(),
146
+ path_in_repo="chat_history.json", repo_id=HISTORY_REPO,
147
+ repo_type="dataset", token=HF_TOKEN, commit_message="Update")
148
  return True
149
  except: return False
150
 
 
185
  try:
186
  client = Groq(api_key=GROQ_KEY)
187
  resp = client.chat.completions.create(model=model_id,
188
+ messages=[{"role":"system","content":"Biomedical PubMed expert. Convert to optimized MeSH terms for heart valves hemodynamics PIV thrombogenicity FSI microfluidics CKD creatinine. Return ONLY terms."},
189
+ {"role":"user","content":"Optimize: "+query}],max_tokens=80)
190
  return resp.choices[0].message.content.strip() or query
191
  except: return query
192
 
193
+ def fetch_pubmed(query, n=6):
194
  try:
195
+ forced = query+" AND (heart valve OR hemodynamics OR microfluidic OR thrombogen OR creatinine OR PIV OR CFD OR CKD OR fluid structure)"
196
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
197
+ params={"db":"pubmed","term":forced,"retmax":n,"retmode":"json","sort":"date","field":"tiab"},timeout=12)
198
  ids = r.json()["esearchresult"]["idlist"]
199
  if not ids: return []
200
  r2 = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi",
 
214
  return results
215
  except: return []
216
 
217
+ def fetch_scholar(query, n=6):
218
  try:
219
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
220
  params={"query":query,"limit":n,"fields":"title,year,url,citationCount"},timeout=12)
 
223
  for p in papers:
224
  year = p.get("year",0) or 0
225
  if int(year) < 2015: continue
226
+ results.append({"source":"Scholar","title":p.get("title",""),"year":str(year),
227
  "url":p.get("url",""),"citations":str(p.get("citationCount",0))})
228
  results.sort(key=lambda x:(x["year"],int(x["citations"]) if x["citations"].isdigit() else 0),reverse=True)
229
  return results
230
  except: return []
231
 
232
+ def fetch_europe_pmc(query, n=5):
233
  try:
234
  r = requests.get("https://www.ebi.ac.uk/europepmc/webservices/rest/search",
235
+ params={"query":query,"format":"json","pageSize":n,"sort":"P_PDATE_D desc"},timeout=12)
236
  articles = r.json().get("resultList",{}).get("result",[])
237
  results = []
238
  for a in articles:
239
  year = str(a.get("pubYear",""))
240
  if year and int(year) < 2015: continue
241
+ pmid = a.get("pmid",""); doi = a.get("doi","")
 
242
  url = ("https://pubmed.ncbi.nlm.nih.gov/"+pmid if pmid else "https://doi.org/"+doi if doi else "")
243
  if not url: continue
244
  results.append({"source":"Europe PMC","title":a.get("title",""),"year":year,
 
246
  return results
247
  except: return []
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  def quick_search(query, search_model="Llama 3.3 70B (Best)"):
250
  if not query.strip(): return "Please enter a research topic."
251
  model_id = CHAT_MODELS.get(search_model, "llama-3.3-70b-versatile")
252
  expanded = expand_query_ai(query, model_id)
253
+ r1 = fetch_pubmed(expanded, n=6)
254
+ r2 = fetch_scholar(expanded, n=6)
255
+ r3 = fetch_europe_pmc(expanded, n=5)
256
+ sjsu_url = "https://scholarworks.sjsu.edu/do/search/?q="+requests.utils.quote(query)+"&context=6781027"
257
+ all_results = r1+r2+r3
 
258
  seen = set()
259
  unique = []
260
  for r in all_results:
261
  key = r["title"][:50].lower().strip()
262
  if key not in seen and r["url"]:
263
  seen.add(key); unique.append(r)
264
+ out = "QUERY: "+query+chr(10)+"AI EXPANDED: "+expanded+chr(10)
 
 
 
 
 
265
  out += "="*45+chr(10)+chr(10)
266
+ groups = {"PubMed":[],"Scholar":[],"Europe PMC":[]}
267
+ for r in unique[:20]:
268
  if r["source"] in groups: groups[r["source"]].append(r)
269
  for source, papers in groups.items():
270
  if not papers: continue
271
+ out += "--- "+source+" ---"+chr(10)
272
  for p in papers:
273
  out += p["title"][:85]+" ("+p["year"]+")"
274
+ if p["citations"] not in ("N/A","",): out += " | "+p["citations"]+" citations"
275
  out += chr(10)+" "+p["url"]+chr(10)+chr(10)
276
+ out += "--- SJSU ScholarWorks ---"+chr(10)
277
+ out += "Search SJSU papers: "+sjsu_url+chr(10)
278
  return out
279
 
280
+ def get_pubmed_chat(query, n=3):
281
  try:
282
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
283
  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)
 
286
  return chr(10).join(["https://pubmed.ncbi.nlm.nih.gov/"+i for i in ids])
287
  except: return ""
288
 
289
+ # ── CHAT WITH RAG ──────────────────────────────────────────────────
290
  def research_chat(message, history, chat_model="Llama 3.3 70B (Best)"):
291
  if not GROQ_KEY:
292
  history.append({"role":"user","content":message})
 
295
  try:
296
  model_id = CHAT_MODELS.get(chat_model, "llama-3.3-70b-versatile")
297
  client = Groq(api_key=GROQ_KEY)
298
+ paper_context, paper_results = search_papers(message, n=4)
299
+ if paper_context:
300
+ system_prompt = ("You are CardioLab AI for SJSU Biomedical Engineering. "
301
+ "Answer using SJSU CardioLab research papers below. "
302
+ "Always cite the paper name when using specific data. Be precise with numbers and protocols."+chr(10)+chr(10)+
303
+ "SJSU CARDIOLAB PAPERS:"+chr(10)+paper_context+chr(10)+chr(10)+
304
+ "ADDITIONAL KNOWLEDGE: "+KNOWHOW)
305
+ else:
306
+ system_prompt = "You are CardioLab AI for SJSU Biomedical Engineering. Expert in MHV MCL PIV TGT uPAD CKD FSI. "+KNOWHOW
307
+ msgs = [{"role":"system","content":system_prompt}]
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=800)
312
  answer = resp.choices[0].message.content
313
+ if paper_results:
314
+ unique_papers = list(dict.fromkeys([r["paper"] for r in paper_results]))
315
+ answer += chr(10)+chr(10)+"Sources from SJSU CardioLab papers:"
316
+ for p in unique_papers[:3]:
317
+ answer += chr(10)+" - "+p.replace('.pdf','').replace('_',' ')
318
+ pubmed = get_pubmed_chat(message, n=2)
319
+ if pubmed: answer += chr(10)+"PubMed: "+pubmed
320
  history.append({"role":"user","content":message})
321
  history.append({"role":"assistant","content":answer})
322
  return "", history
 
333
  client = Groq(api_key=GROQ_KEY)
334
  with open(audio, "rb") as f:
335
  tx = client.audio.transcriptions.create(file=("audio.wav", f, "audio/wav"), model="whisper-large-v3")
336
+ paper_context, _ = search_papers(tx.text, n=3)
337
+ system = "You are CardioLab AI. "+KNOWHOW
338
+ if paper_context: system = "You are CardioLab AI. Use these SJSU papers:"+chr(10)+paper_context+chr(10)+KNOWHOW
339
+ msgs = [{"role":"system","content":system}]
340
  for item in history:
341
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
342
  msgs.append({"role":"user","content":tx.text})
 
348
  history.append({"role":"assistant","content":"Voice error: "+str(e)})
349
  return history
350
 
 
351
  def analyze_upad_photo(image):
352
  if image is None: return None, "Upload a uPAD photo first."
353
  try:
 
400
  xp=xv.values if tc else x
401
  ax.plot(xp,df[sc2],color="#0057a8",linewidth=2.5,marker="s",markersize=5)
402
  ax.fill_between(xp,df[sc2],alpha=0.15,color="#0057a8")
403
+ ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution 5 Pa")
404
+ ax.axhline(y=10,color="#c1121f",linestyle="--",linewidth=2,label="High risk 10 Pa")
405
  ax.set_ylabel("Shear (Pa)",color=ac,fontsize=11); ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
406
  def psc(ax):
407
  if vc and sc2:
 
421
  st+="="*20+chr(10)+("OVERALL: HIGH RISK" if risk else "OVERALL: LOW RISK")
422
  ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
423
  bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
424
+ i1=mk_chart(pv,"Velocity Profile",bg,fg,gc,ac,pb); i2=mk_chart(ps,"Wall Shear Stress",bg,fg,gc,ac,pb)
425
+ i3=mk_chart(psc,"Velocity vs Shear",bg,fg,gc,ac,pb); i4=mk_chart(psum,"Clinical Summary",bg,fg,gc,ac,pb)
 
 
426
  ai=""
427
  if GROQ_KEY:
428
  try:
429
  client=Groq(api_key=GROQ_KEY)
430
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
431
+ messages=[{"role":"system","content":"PIV expert SJSU CardioLab."},
432
+ {"role":"user","content":"PIV from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
433
  ai=chr(10)+"AI: "+resp.choices[0].message.content
434
  except: pass
435
+ return i1,i2,i3,i4,"PIV: "+str(len(df))+" rows"+ai
436
  except Exception as e: return None,None,None,None,"Error: "+str(e)
437
 
438
  def analyze_tgt_csv(file,theme="White"):
 
462
  ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
463
  ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
464
  ax.set_ylabel(yl,color=ac,fontsize=11)
465
+ mv=round(float(np.max(yp)),2)
466
+ ax.set_title(title+chr(10)+"Max: "+str(mv)+" - "+("HIGH" if mv>lim else "NORMAL"),color=fg,fontweight="bold",fontsize=12)
467
  return mk_chart(fn,title,bg,fg,gc,ac,pb)
468
  i1=mk2(tatc,"#c1121f","TAT (ng/mL)",8,"Normal: 8","TAT Thrombin-Antithrombin")
469
  i2=mk2(pfc,"#0057a8","PF1.2 (nmol/L)",2.0,"Normal: 2.0","PF1.2 Prothrombin Fragment")
 
474
  try:
475
  client=Groq(api_key=GROQ_KEY)
476
  resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
477
+ messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Give thrombogenicity risk."},
478
  {"role":"user","content":"TGT from 27mm SJM Regent:"+chr(10)+df.describe().to_string()[:500]}],max_tokens=250)
479
  ai=chr(10)+"AI: "+resp.choices[0].message.content
480
  except: pass
481
+ return i1,i2,i3,i4,"TGT: "+str(len(df))+" rows"+ai
482
  except Exception as e: return None,None,None,None,"Error: "+str(e)
483
 
484
  def generate_image(prompt):
 
519
  # ── UI ─────────────────────────────────────────────────────────────
520
  with gr.Blocks(title="CardioLab AI - SJSU", css=CSS) as demo:
521
  gr.HTML(HEADER)
 
522
 
523
+ papers_count = len(set(m["paper"] for m in METADATA)) if PAPERS_LOADED else 0
524
+ chunks_count = len(CHUNKS) if PAPERS_LOADED else 0
525
+ status_color = "#27ae60" if PAPERS_LOADED else "#e67e22"
526
+ status_msg = f"RAG Active: {chunks_count} chunks from {papers_count} SJSU papers | AI reads actual lab papers before every answer" if PAPERS_LOADED else "Loading paper database..."
527
+ gr.HTML(f'''<div style="background:{status_color};color:white;text-align:center;padding:6px;font-size:0.8em;font-weight:700;">{status_msg}</div>''')
528
+
529
+ with gr.Tabs():
530
  with gr.Tab("Chat"):
531
  with gr.Row():
532
  with gr.Column(scale=1, min_width=200):
533
+ gr.HTML('''<div style="background:#202123;padding:10px;border-radius:8px;margin-bottom:6px;">
534
+ <div style="color:#e8a020;font-weight:700;font-size:0.85em;">SJSU CARDIOLAB</div>
535
+ <div style="color:#9ca3af;font-size:0.7em;">Conversations</div></div>''')
536
  new_chat_btn = gr.Button("New Chat", variant="secondary")
537
  session_dropdown = gr.Dropdown(choices=get_session_list(), label="Saved Sessions", interactive=True)
538
  load_btn = gr.Button("Load Session", variant="primary")
 
544
  with gr.Column(scale=4):
545
  chatbot = gr.Chatbot(label="", height=480, show_label=False, container=False)
546
  with gr.Row():
547
+ msg_box = gr.Textbox(placeholder="Ask anything about CardioLab β€” AI searches 16 SJSU papers + PubMed live...", label="", lines=2, scale=4, container=False)
548
  with gr.Column(scale=1, min_width=120):
549
  chat_model_dd = gr.Dropdown(choices=list(CHAT_MODELS.keys()), value="Llama 3.3 70B (Best)", label="Model")
550
  send_btn = gr.Button("Send", variant="primary")
 
567
  voice_clear.click(lambda: [], outputs=voice_chatbot)
568
 
569
  with gr.Tab("Papers"):
570
+ gr.Markdown("### Search PubMed + Semantic Scholar + Europe PMC + SJSU ScholarWorks")
571
  with gr.Row():
572
+ search_input = gr.Textbox(placeholder="e.g. bileaflet mechanical heart valve hemodynamics thrombogenicity", label="Research Topic", scale=3)
573
  search_model_dd = gr.Dropdown(choices=list(CHAT_MODELS.keys()), value="Llama 3.3 70B (Best)", label="AI Model", scale=1)
574
+ search_btn = gr.Button("Search All Sources", variant="primary", scale=1)
575
  search_output = gr.Textbox(label="AI Ranked Results", lines=25)
576
  search_btn.click(quick_search, inputs=[search_input, search_model_dd], outputs=search_output)
577
  search_input.submit(quick_search, inputs=[search_input, search_model_dd], outputs=search_output)
 
578
 
579
  with gr.Tab("PIV CSV"):
 
580
  with gr.Row():
581
  piv_file = gr.File(label="Upload PIV CSV", file_types=[".csv"], scale=3)
582
  piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
583
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
584
  piv_result = gr.Textbox(label="AI Analysis", lines=4)
585
  with gr.Row():
586
+ piv_c1=gr.Image(label="Velocity Profile",type="pil"); piv_c2=gr.Image(label="Shear Stress",type="pil")
 
587
  with gr.Row():
588
+ piv_c3=gr.Image(label="Velocity vs Shear",type="pil"); piv_c4=gr.Image(label="Clinical Summary",type="pil")
 
589
  piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
590
 
591
  with gr.Tab("TGT CSV"):
 
592
  with gr.Row():
593
  tgt_file = gr.File(label="Upload TGT CSV", file_types=[".csv"], scale=3)
594
  tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
 
618
 
619
  with gr.Tab("AI Image"):
620
  with gr.Row():
621
+ img_prompt = gr.Textbox(placeholder="e.g. 27mm bileaflet mechanical heart valve cross section", label="Describe image", lines=2, scale=4)
622
  with gr.Column(scale=1):
623
  img_btn = gr.Button("Generate Image", variant="primary")
624
  img_status = gr.Textbox(label="Status", lines=1)
 
629
  with gr.Tab("PIV Manual"):
630
  with gr.Row():
631
  with gr.Column():
632
+ v=gr.Number(label="Max Velocity m/s",value=1.8); s=gr.Number(label="Wall Shear Pa",value=6.5)
633
+ h=gr.Number(label="Heart Rate bpm",value=72); piv_out=gr.Textbox(label="Result",lines=4)
 
 
634
  gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
635
 
636
  with gr.Tab("TGT Manual"):
637
  with gr.Row():
638
  with gr.Column():
639
+ t1=gr.Number(label="TAT ng/mL",value=18); t2=gr.Number(label="PF1.2",value=2.5)
640
+ t3=gr.Number(label="Hemoglobin mg/L",value=60); t4=gr.Number(label="Platelets",value=140)
641
+ t5=gr.Number(label="Time minutes",value=40); out2=gr.Textbox(label="Result",lines=6)
 
 
 
642
  gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
643
 
644
  gr.HTML("""<div style="text-align:center;padding:10px;border-top:1px solid #e5e7eb;background:#f9fafb;">
645
+ <span style="color:#9ca3af;font-size:0.75em;">CardioLab AI v35 | SJSU Biomedical Engineering | RAG + 16 Papers Embedded | Inspired by <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>""")
646
 
647
  demo.launch()