Spaces:
Running
Running
Upload app.py with huggingface_hub
Browse files
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 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 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;
|
| 52 |
-
textarea { background: #ffffff !important; color: #1a202c !important; border: 1px solid #d1d5db !important; border-radius:
|
| 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 | Built on Biomni Stanford | 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 π¬ CKD π» 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 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
| 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 |
-
|
| 217 |
-
|
| 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 |
-
|
| 231 |
-
"
|
| 232 |
-
"
|
| 233 |
-
|
| 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
|
| 247 |
try:
|
| 248 |
-
|
| 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":
|
| 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=
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 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
|
| 280 |
try:
|
| 281 |
-
expanded = expand_query(query)
|
| 282 |
r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
|
| 283 |
-
params={"query":
|
| 284 |
-
timeout=15)
|
| 285 |
papers = r.json().get("data",[])
|
| 286 |
-
|
| 287 |
-
return "No Semantic Scholar results found."
|
| 288 |
-
out = []
|
| 289 |
for p in papers:
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
if not query.strip(): return "Please enter a research topic."
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 326 |
answer = resp.choices[0].message.content
|
| 327 |
pubmed = get_pubmed(message, n=3)
|
| 328 |
-
if pubmed: answer += chr(10)+chr(10)+"
|
| 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":"
|
| 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)+"
|
| 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 |
-
|
| 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.
|
| 404 |
-
ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
|
| 405 |
def ps(ax):
|
| 406 |
-
if
|
| 407 |
xp=xv.values if tc else x
|
| 408 |
-
ax.plot(xp,df[
|
| 409 |
-
ax.fill_between(xp,df[
|
| 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.
|
| 413 |
-
ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
|
| 414 |
def psc(ax):
|
| 415 |
-
if vc and
|
| 416 |
-
|
| 417 |
-
cb=plt.colorbar(
|
| 418 |
-
ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,
|
| 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)+"
|
| 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+="
|
| 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)+"
|
| 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)
|
| 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)+"
|
| 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
|
| 524 |
-
sr="HIGH
|
| 525 |
-
return "Velocity: "+str(v)+" m/s
|
| 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)+"
|
| 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("
|
| 538 |
with gr.Row():
|
| 539 |
-
with gr.Column(scale=1, min_width=
|
| 540 |
-
gr.HTML('''<div style="background:#202123;padding:10px;border-radius:8px;margin-bottom:6px;">
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 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("
|
| 551 |
-
delete_btn = gr.Button("
|
| 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=
|
| 556 |
with gr.Row():
|
| 557 |
-
msg_box = gr.Textbox(placeholder="Message CardioLab AI...", label="", lines=2, scale=
|
| 558 |
-
with gr.Column(scale=1, min_width=
|
| 559 |
-
|
|
|
|
| 560 |
clear_btn = gr.Button("Clear", variant="secondary")
|
| 561 |
-
|
| 562 |
-
|
| 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("
|
| 571 |
-
voice_chatbot = gr.Chatbot(label="", height=
|
| 572 |
-
audio_input = gr.Audio(sources=["microphone"], type="filepath", label="Record
|
| 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("
|
|
|
|
| 580 |
with gr.Row():
|
| 581 |
-
search_input = gr.Textbox(placeholder="e.g. mechanical heart valve
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
|
|
|
|
|
|
| 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("
|
| 603 |
-
gr.Markdown("Upload TGT CSV
|
| 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("
|
| 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("
|
| 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("
|
| 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("
|
| 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 |
-
<
|
| 664 |
-
<span style="color:#9ca3af;font-size:0.75em;">
|
| 665 |
-
β€οΈ CardioLab AI | SJSU Biomedical Engineering |
|
| 666 |
-
Built on <a href="https://github.com/snap-stanford/Biomni" style="color:#c1121f;">Biomni Stanford</a> |
|
| 667 |
-
<a href="https://github.com/pranatechsol/Cardio-Lab-Ai" style="color:#0057a8;">GitHub</a> |
|
| 668 |
-
Apache 2.0 | $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()
|