Saicharan21 commited on
Commit
4ae7b93
Β·
verified Β·
1 Parent(s): 335dbf5

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +311 -415
app.py CHANGED
@@ -1,37 +1,121 @@
1
- # CardioLab AI v25.1 - PIV CSV TGT CSV tabs
2
  import gradio as gr
3
- import os, requests, io
4
  import numpy as np
5
  import pandas as pd
6
  import matplotlib
7
  matplotlib.use("Agg")
8
  import matplotlib.pyplot as plt
9
- import matplotlib.patches as mpatches
10
  from groq import Groq
11
  from PIL import Image
 
 
 
12
 
13
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
14
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
 
15
 
16
  KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
17
  "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
18
  "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
19
- "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric.")
 
20
 
21
  CSS = """
22
  body, .gradio-container { background: #f0f4f8 !important; }
23
- .tab-nav { background: #ffffff !important; border-bottom: 2px solid #e2e8f0 !important; padding: 4px 5px 0 5px !important; display: flex !important; flex-wrap: wrap !important; gap: 3px !important; overflow: visible !important; }
24
- .tab-nav button { background: #f7fafc !important; color: #2d3748 !important; border: 1px solid #e2e8f0 !important; border-radius: 6px 6px 0 0 !important; padding: 8px 10px !important; font-weight: 600 !important; font-size: 0.8em !important; margin: 0 !important; white-space: nowrap !important; min-width: 0 !important; }
25
  .tab-nav button:hover { background: #ebf4ff !important; color: #1a237e !important; }
26
  .tab-nav button.selected { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: #ffffff !important; font-weight: 700 !important; }
27
  button.primary { background: linear-gradient(135deg, #e63946 0%, #c1121f 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; }
28
  button.secondary { background: #edf2f7 !important; color: #4a5568 !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
29
- textarea, input[type=number] { background: #f7fafc !important; color: #1a202c !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
30
  .message.user { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: white !important; }
31
  .message.bot { background: #ebf4ff !important; color: #1a202c !important; border: 1px solid #bee3f8 !important; }
32
  label span { color: #2b6cb0 !important; font-weight: 600 !important; font-size: 0.85em !important; text-transform: uppercase !important; }
33
  """
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def get_pubmed(query, n=5):
36
  try:
37
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
@@ -46,7 +130,7 @@ def quick_search(query):
46
  pubmed = get_pubmed(query, n=8)
47
  try:
48
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
49
- params={"query":query+" biomedical","limit":5,"fields":"title,year,url,citationCount"},timeout=10)
50
  papers = r.json().get("data",[])
51
  scholar = chr(10).join([p.get("title","")[:80]+" ("+str(p.get("year",""))+")"+chr(10)+" "+p.get("url","") for p in papers if p.get("url","")])
52
  except: scholar = ""
@@ -55,11 +139,11 @@ def quick_search(query):
55
  def research_chat(message, history):
56
  if not GROQ_KEY:
57
  history.append({"role":"user","content":message})
58
- history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings."})
59
  return "", history
60
  try:
61
  client = Groq(api_key=GROQ_KEY)
62
- msgs = [{"role":"system","content":"You are CardioLab AI. Expert in MHV MCL PIV TGT uPAD CKD FSI. Never invent URLs. "+KNOWHOW}]
63
  for item in history:
64
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
65
  msgs.append({"role":"user","content":message})
@@ -95,388 +179,182 @@ def voice_chat(audio, history):
95
  history.append({"role":"assistant","content":"Voice error: "+str(e)})
96
  return history
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  def analyze_piv_csv(file, theme="White"):
99
- if file is None:
100
- return None, None, None, None, "Please upload a PIV CSV file first."
101
  try:
102
  df = pd.read_csv(file.name)
103
  cols = [c.lower().strip() for c in df.columns]
104
  df.columns = cols
105
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
106
- if not num_cols:
107
- return None, "No numeric columns found. Check your CSV file."
108
-
109
- fig = plt.figure(figsize=(16, 11))
110
- fig.patch.set_facecolor("#0a1628")
111
- fig.suptitle("PIV Data Analysis β€” SJSU CardioLab MCL", color="white", fontsize=18, fontweight="bold", y=0.98)
112
-
113
- gs = fig.add_gridspec(2, 2, hspace=0.38, wspace=0.32, left=0.08, right=0.97, top=0.93, bottom=0.08)
114
- axes = [fig.add_subplot(gs[0,0]), fig.add_subplot(gs[0,1]), fig.add_subplot(gs[1,0]), fig.add_subplot(gs[1,1])]
115
-
116
- def style_ax(ax, title, xlabel, ylabel):
117
- ax.set_facecolor("#132340")
118
- ax.set_title(title, color="white", fontweight="bold", fontsize=13, pad=10)
119
- ax.set_xlabel(xlabel, color="#7eb8f7", fontsize=11)
120
- ax.set_ylabel(ylabel, color="#7eb8f7", fontsize=11)
121
- ax.tick_params(colors="#a8b2d8", labelsize=10)
122
- ax.grid(True, alpha=0.25, color="#2d4a8a", linestyle="--")
123
- for spine in ["top","right"]: ax.spines[spine].set_visible(False)
124
- for spine in ["bottom","left"]: ax.spines[spine].set_color("#2d4a8a")
125
-
126
- x = np.arange(len(df))
127
- time_col = next((c for c in cols if "time" in c or "frame" in c or "x" == c), None)
128
- vel_col = next((c for c in cols if any(k in c for k in ["vel","speed","v_mag","magnitude"])), num_cols[0] if num_cols else None)
129
- shear_col = 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)
130
- x_vals = df[time_col] if time_col else x
131
- x_label = time_col.title() if time_col else "Sample Index"
132
-
133
- # Plot 1 - Velocity profile - large filled area chart
134
- ax1 = axes[0]
135
- if vel_col:
136
- v_data = df[vel_col].values
137
- ax1.fill_between(x_vals, v_data, alpha=0.25, color="#e63946")
138
- ax1.plot(x_vals, v_data, color="#e63946", linewidth=3, marker="o", markersize=5, label="Velocity")
139
- ax1.axhline(y=2.0, color="#ffd700", linestyle="--", linewidth=2, label="Risk threshold: 2.0 m/s")
140
- max_v = v_data.max()
141
- max_i = v_data.argmax()
142
- ax1.annotate(f"Peak: {max_v:.2f} m/s", xy=(x_vals.iloc[max_i] if time_col else max_i, max_v),
143
- xytext=(10, 10), textcoords="offset points", color="#ffd700", fontsize=10, fontweight="bold",
144
- arrowprops=dict(arrowstyle="->", color="#ffd700", lw=1.5))
145
- ax1.legend(fontsize=10, labelcolor="white", facecolor="#132340", framealpha=0.8)
146
- ax1.set_ylim(bottom=0)
147
- style_ax(ax1, "Velocity Profile", x_label, "Velocity (m/s)")
148
-
149
- # Plot 2 - Shear stress with risk zones
150
- ax2 = axes[1]
151
- if shear_col:
152
- s_data = df[shear_col].values
153
- x_plot = x_vals.values if time_col else x
154
- ax2.fill_between(x_plot, s_data, alpha=0.25, color="#4361ee")
155
- ax2.fill_between(x_plot, s_data, 10, where=s_data>10, alpha=0.4, color="#e63946", label="High risk zone")
156
- ax2.fill_between(x_plot, s_data, 5, where=(s_data>5)&(s_data<=10), alpha=0.3, color="#ffd700", label="Caution zone")
157
- ax2.plot(x_plot, s_data, color="#4361ee", linewidth=3, marker="s", markersize=5)
158
- ax2.axhline(y=5, color="#ffd700", linestyle="--", linewidth=2, label="Caution: 5 Pa")
159
- ax2.axhline(y=10, color="#e63946", linestyle="--", linewidth=2, label="High risk: 10 Pa")
160
- ax2.legend(fontsize=9, labelcolor="white", facecolor="#132340", framealpha=0.8)
161
- ax2.set_ylim(bottom=0)
162
- style_ax(ax2, "Wall Shear Stress", x_label, "Shear Stress (Pa)")
163
-
164
- # Plot 3 - Velocity vs Shear scatter plot
165
- ax3 = axes[2]
166
- if vel_col and shear_col:
167
- sc = ax3.scatter(df[vel_col], df[shear_col], c=x, cmap="RdYlGn_r", s=80, edgecolors="white", linewidth=0.5, zorder=5)
168
- plt.colorbar(sc, ax=ax3, label="Time progression").ax.yaxis.label.set_color("white")
169
- ax3.axvline(x=2.0, color="#ffd700", linestyle="--", linewidth=2, label="Vel. risk: 2.0")
170
- ax3.axhline(y=10, color="#e63946", linestyle="--", linewidth=2, label="Shear risk: 10")
171
- ax3.legend(fontsize=9, labelcolor="white", facecolor="#132340", framealpha=0.8)
172
- style_ax(ax3, "Velocity vs Shear Stress", "Velocity (m/s)", "Shear Stress (Pa)")
173
-
174
- # Plot 4 - Clinical summary dashboard
175
- ax4 = axes[3]
176
- ax4.set_facecolor("#0d1b3e")
177
- ax4.axis("off")
178
- risk = []
179
- summary = "CLINICAL SUMMARY"+chr(10)+"━"*24+chr(10)+chr(10)
180
- for col in num_cols[:4]:
181
- mn = round(df[col].mean(),3)
182
- mx = round(df[col].max(),3)
183
- mn_v = round(df[col].min(),3)
184
- summary += f"{col[:14]:14s}"+chr(10)+f" Mean: {mn:8.3f}"+chr(10)+f" Max: {mx:8.3f}"+chr(10)+f" Min: {mn_v:8.3f}"+chr(10)+chr(10)
185
- if "vel" in col and mx > 2.0: risk.append("HIGH VELOCITY (>2.0 m/s)")
186
- if "shear" in col and mx > 10: risk.append("HIGH SHEAR (>10 Pa)")
187
- summary += "━"*24+chr(10)
188
- if risk:
189
- summary += "RISK FLAGS DETECTED:"+chr(10)
190
- for r in risk: summary += " ⚠ "+r+chr(10)
191
- overall = "HIGH RISK β€” Clinical review needed"
192
- else:
193
- summary += "STATUS: All values normal"+chr(10)
194
- overall = "LOW RISK β€” Continue monitoring"
195
- summary += chr(10)+"OVERALL: "+overall
196
- ax4.text(0.05, 0.97, summary, transform=ax4.transAxes, color="white", fontsize=10,
197
- va="top", fontfamily="monospace",
198
- bbox=dict(boxstyle="round,pad=0.8", facecolor="#132340", edgecolor="#e63946" if risk else "#2ecc71", linewidth=2))
199
-
200
- plt.tight_layout()
201
- buf = io.BytesIO()
202
- plt.savefig(buf, format="png", facecolor=fig.get_facecolor(), bbox_inches="tight", dpi=120)
203
- buf.seek(0)
204
- img = Image.open(buf)
205
- plt.close()
206
-
207
- ai_text = ""
208
- if GROQ_KEY:
209
- try:
210
- client = Groq(api_key=GROQ_KEY)
211
- msgs = [{"role":"system","content":"You are a PIV expert for SJSU CardioLab. Analyze PIV statistics and give clinical interpretation about velocity, shear stress, stenosis and thrombosis risk."}]
212
- msgs.append({"role":"user","content":"PIV data stats from 27mm SJM Regent MHV at 70bpm 5L/min:"+chr(10)+df.describe().to_string()[:800]})
213
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=350)
214
- ai_text = chr(10)+"━"*25+chr(10)+"AI ANALYSIS:"+chr(10)+resp.choices[0].message.content
215
- except: pass
216
-
217
- # Generate 4 separate charts
218
  bg = "#ffffff" if theme=="White" else "#0a1628"
219
  fg = "#1a202c" if theme=="White" else "white"
220
- grid_color = "#e2e8f0" if theme=="White" else "#2d4a8a"
221
- ax_color = "#4a5568" if theme=="White" else "#a8b2d8"
222
- plot_bg = "#f7fafc" if theme=="White" else "#132340"
223
-
224
- def make_single_chart(plot_fn, title):
225
- fig2, ax = plt.subplots(figsize=(8, 5))
226
- fig2.patch.set_facecolor(bg)
227
- ax.set_facecolor(plot_bg)
228
- plot_fn(ax)
229
- ax.set_title(title, color=fg, fontweight="bold", fontsize=14, pad=10)
230
- ax.tick_params(colors=ax_color, labelsize=11)
231
- ax.grid(True, alpha=0.3, color=grid_color, linestyle="--")
232
- for spine in ["top","right"]: ax.spines[spine].set_visible(False)
233
- for spine in ["bottom","left"]: ax.spines[spine].set_color(grid_color)
234
- plt.tight_layout()
235
- buf2 = io.BytesIO()
236
- plt.savefig(buf2, format="png", facecolor=bg, bbox_inches="tight", dpi=130)
237
- buf2.seek(0)
238
- result = Image.open(buf2).copy()
239
- plt.close()
240
- return result
241
-
242
  x = np.arange(len(df))
243
- vel_col2 = 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)
244
- shear_col2 = 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)
245
- time_col2 = next((c for c in cols if "time" in c or "frame" in c), None)
246
- x_vals2 = df[time_col2] if time_col2 else x
247
-
248
- def plot_velocity(ax):
249
- if vel_col2:
250
- ax.plot(x_vals2, df[vel_col2], color="#e63946", linewidth=2.5, marker="o", markersize=5)
251
- ax.fill_between(x_vals2, df[vel_col2], alpha=0.2, color="#e63946")
252
- ax.axhline(y=2.0, color="#f59e0b", linestyle="--", linewidth=2, label="Risk: 2.0 m/s")
253
- ax.set_ylabel("Velocity (m/s)", color=ax_color, fontsize=12)
254
- ax.set_xlabel(time_col2 or "Sample", color=ax_color, fontsize=12)
255
- ax.legend(fontsize=10, labelcolor=fg, facecolor=plot_bg)
256
-
257
- def plot_shear(ax):
258
- if shear_col2:
259
- xp = x_vals2.values if time_col2 else x
260
- ax.plot(xp, df[shear_col2], color="#4361ee", linewidth=2.5, marker="s", markersize=5)
261
- ax.fill_between(xp, df[shear_col2], alpha=0.2, color="#4361ee")
262
- ax.axhline(y=5, color="#f59e0b", linestyle="--", linewidth=2, label="Caution: 5 Pa")
263
- ax.axhline(y=10, color="#e63946", linestyle="--", linewidth=2, label="High risk: 10 Pa")
264
- ax.set_ylabel("Shear Stress (Pa)", color=ax_color, fontsize=12)
265
- ax.set_xlabel(time_col2 or "Sample", color=ax_color, fontsize=12)
266
- ax.legend(fontsize=10, labelcolor=fg, facecolor=plot_bg)
267
-
268
- def plot_scatter(ax):
269
- if vel_col2 and shear_col2:
270
- sc = ax.scatter(df[vel_col2], df[shear_col2], c=x, cmap="RdYlGn_r", s=100, edgecolors=fg, linewidth=0.5, zorder=5)
271
- cb = plt.colorbar(sc, ax=ax, label="Time")
272
- cb.ax.yaxis.label.set_color(fg)
273
- cb.ax.tick_params(colors=ax_color)
274
- ax.axvline(x=2.0, color="#f59e0b", linestyle="--", linewidth=2, label="Vel risk: 2.0")
275
- ax.axhline(y=10, color="#e63946", linestyle="--", linewidth=2, label="Shear risk: 10")
276
- ax.set_xlabel("Velocity (m/s)", color=ax_color, fontsize=12)
277
- ax.set_ylabel("Shear Stress (Pa)", color=ax_color, fontsize=12)
278
- ax.legend(fontsize=10, labelcolor=fg, facecolor=plot_bg)
279
-
280
- def plot_summary(ax):
281
- ax.axis("off")
282
- risk = []
283
- stats = "CLINICAL SUMMARY"+chr(10)+"━"*22+chr(10)+chr(10)
 
 
 
 
 
284
  for col in num_cols[:3]:
285
- mn = round(df[col].mean(),3)
286
- mx = round(df[col].max(),3)
287
- mn_v = round(df[col].min(),3)
288
- stats += col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+" Min: "+str(mn_v)+chr(10)+chr(10)
289
- if "vel" in col and mx > 2.0: risk.append("HIGH VELOCITY (>2.0 m/s)")
290
- if "shear" in col and mx > 10: risk.append("HIGH SHEAR (>10 Pa)")
291
- stats += "━"*22+chr(10)
292
  if risk:
293
- stats += "RISK FLAGS:"+chr(10)
294
- for r in risk: stats += " ⚠ "+r+chr(10)
295
- stats += chr(10)+"OVERALL: HIGH RISK"
296
- border_color = "#e63946"
297
  else:
298
- stats += "STATUS: All values normal"+chr(10)+"OVERALL: LOW RISK"
299
- border_color = "#2ecc71"
300
- ax.text(0.05, 0.97, stats, transform=ax.transAxes, color=fg, fontsize=11,
301
- va="top", fontfamily="monospace",
302
- bbox=dict(boxstyle="round,pad=0.8", facecolor=plot_bg, edgecolor=border_color, linewidth=2.5))
303
-
304
- img1 = make_single_chart(plot_velocity, "Velocity Profile")
305
- img2 = make_single_chart(plot_shear, "Wall Shear Stress")
306
- img3 = make_single_chart(plot_scatter, "Velocity vs Shear Stress")
307
- img4 = make_single_chart(plot_summary, "Clinical Summary")
308
-
309
- return img1, img2, img3, img4, "PIV CSV LOADED: "+str(len(df))+" rows | Columns: "+", ".join(df.columns.tolist())+ai_text
310
- except Exception as e:
311
- return None, None, None, None, "Error: "+str(e)
 
 
312
 
313
  def analyze_tgt_csv(file, theme="White"):
314
- if file is None:
315
- return None, "Please upload a TGT CSV file first."
316
  try:
317
  df = pd.read_csv(file.name)
318
  cols = [c.lower().strip() for c in df.columns]
319
  df.columns = cols
320
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
321
- if not num_cols:
322
- return None, "No numeric columns found."
323
-
324
- fig, axes = plt.subplots(2, 2, figsize=(14, 10))
325
- fig.patch.set_facecolor("#0d1b3e")
326
- fig.suptitle("TGT Blood Analysis β€” SJSU CardioLab", color="white", fontsize=16, fontweight="bold")
327
-
328
- time_col = next((c for c in cols if "time" in c or "min" in c), None)
329
- tat_col = next((c for c in cols if "tat" in c or "thrombin" in c), num_cols[0] if num_cols else None)
330
- pf_col = next((c for c in cols if "pf" in c or "prothrombin" in c), num_cols[1] if len(num_cols)>1 else None)
331
- hemo_col = next((c for c in cols if "hemo" in c or "hgb" in c), num_cols[2] if len(num_cols)>2 else None)
332
- plt_col = next((c for c in cols if "platelet" in c or "plt" in c), num_cols[3] if len(num_cols)>3 else None)
333
- x = df[time_col] if time_col else range(len(df))
334
- xl = time_col if time_col else "Sample"
335
-
336
- def style_ax(ax, title, ylabel):
337
- ax.set_facecolor("#1a2744")
338
- ax.set_title(title, color="white", fontweight="bold")
339
- ax.set_ylabel(ylabel, color="#a8b2d8")
340
- ax.set_xlabel(xl, color="#a8b2d8")
341
- ax.tick_params(colors="#a8b2d8")
342
- ax.grid(True, alpha=0.2, color="#2d4a8a")
343
- for spine in ["top","right"]: ax.spines[spine].set_visible(False)
344
- for spine in ["bottom","left"]: ax.spines[spine].set_color("#2d4a8a")
345
-
346
- ax1 = axes[0,0]
347
- if tat_col:
348
- ax1.plot(x, df[tat_col], color="#e63946", linewidth=2.5, marker="o", markersize=6)
349
- ax1.axhline(y=8, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal (8 ng/mL)")
350
- ax1.fill_between(x, df[tat_col], alpha=0.3, color="#e63946")
351
- ax1.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
352
- style_ax(ax1, "TAT (Thrombin-Antithrombin)", "ng/mL")
353
-
354
- ax2 = axes[0,1]
355
- if pf_col:
356
- ax2.plot(x, df[pf_col], color="#4361ee", linewidth=2.5, marker="s", markersize=6)
357
- ax2.axhline(y=2.0, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal (2.0)")
358
- ax2.fill_between(x, df[pf_col], alpha=0.3, color="#4361ee")
359
- ax2.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
360
- style_ax(ax2, "PF1.2 (Prothrombin Fragment)", "nmol/L")
361
-
362
- ax3 = axes[1,0]
363
- if hemo_col:
364
- ax3.bar(range(len(df)), df[hemo_col], color="#2ecc71", alpha=0.85, edgecolor="#0d1b3e")
365
- ax3.axhline(y=20, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal (20 mg/L)")
366
- ax3.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
367
- style_ax(ax3, "Free Hemoglobin (Hemolysis)", "mg/L")
368
-
369
- ax4 = axes[1,1]
370
- if plt_col:
371
- ax4.plot(x, df[plt_col], color="#e67e22", linewidth=2.5, marker="^", markersize=6)
372
- ax4.axhline(y=150, color="#ffd700", linestyle="--", linewidth=1.5, label="Normal min (150)")
373
- ax4.fill_between(x, df[plt_col], 150, where=df[plt_col]<150, alpha=0.3, color="#e63946", label="Below normal")
374
- ax4.legend(fontsize=8, labelcolor="white", facecolor="#1a2744")
375
- style_ax(ax4, "Platelet Count", "10Β³/ΞΌL")
376
-
377
- plt.tight_layout()
378
- buf = io.BytesIO()
379
- plt.savefig(buf, format="png", facecolor=fig.get_facecolor(), bbox_inches="tight", dpi=120)
380
- buf.seek(0)
381
- img = Image.open(buf)
382
- plt.close()
383
-
384
- ai_text = ""
385
  if GROQ_KEY:
386
  try:
387
- client = Groq(api_key=GROQ_KEY)
388
- msgs = [{"role":"system","content":"You are a hematology expert for SJSU CardioLab. Analyze TGT blood biomarker data. Give thrombogenicity risk: LOW MODERATE or HIGH. Normal: TAT<8, PF1.2<2.0, Hemo<20, Platelets>150."}]
389
- msgs.append({"role":"user","content":"TGT data from 27mm SJM Regent MHV:"+chr(10)+df.describe().to_string()[:800]})
390
- resp = client.chat.completions.create(model="llama-3.3-70b-versatile",messages=msgs,max_tokens=350)
391
- ai_text = chr(10)+"━"*25+chr(10)+"AI ASSESSMENT:"+chr(10)+resp.choices[0].message.content
392
  except: pass
393
-
394
- bg = "#ffffff" if theme=="White" else "#0a1628"
395
- fg = "#1a202c" if theme=="White" else "white"
396
- grid_color = "#e2e8f0" if theme=="White" else "#2d4a8a"
397
- ax_color = "#4a5568" if theme=="White" else "#a8b2d8"
398
- plot_bg = "#f7fafc" if theme=="White" else "#132340"
399
-
400
- def make_tgt_chart(data_col, color, ylabel, limit, limit_label, title, chart_type="line"):
401
- fig2, ax = plt.subplots(figsize=(8, 5))
402
- fig2.patch.set_facecolor(bg)
403
- ax.set_facecolor(plot_bg)
404
- if data_col and data_col in df.columns:
405
- xp = df[time_col].values if time_col else range(len(df))
406
- yp = df[data_col].values
407
- if chart_type == "bar":
408
- bars = ax.bar(range(len(yp)), yp, color=color, alpha=0.85, edgecolor=bg, width=0.6)
409
- for bar, val in zip(bars, yp):
410
- ax.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.5, str(round(val,1)),
411
- ha="center", va="bottom", color=fg, fontsize=10, fontweight="bold")
412
- else:
413
- ax.plot(xp, yp, color=color, linewidth=3, marker="o", markersize=8)
414
- ax.fill_between(xp, yp, alpha=0.2, color=color)
415
- for i,(xi,yi) in enumerate(zip(xp,yp)):
416
- ax.annotate(str(round(yi,1)), (xi,yi), textcoords="offset points",
417
- xytext=(0,10), ha="center", color=fg, fontsize=10, fontweight="bold")
418
- ax.axhline(y=limit, color="#f59e0b", linestyle="--", linewidth=2.5, label=limit_label)
419
- ax.legend(fontsize=11, labelcolor=fg, facecolor=plot_bg)
420
- ax.set_ylabel(ylabel, color=ax_color, fontsize=12)
421
- ax.set_xlabel(time_col or "Sample", color=ax_color, fontsize=12)
422
- mean_val = round(float(np.mean(yp)),2)
423
- max_val = round(float(np.max(yp)),2)
424
- status = "HIGH" if max_val > limit else "NORMAL"
425
- ax.set_title(title+chr(10)+"Mean: "+str(mean_val)+" Max: "+str(max_val)+" Status: "+status,
426
- color=fg, fontweight="bold", fontsize=12, pad=8)
427
- ax.tick_params(colors=ax_color, labelsize=11)
428
- ax.grid(True, alpha=0.3, color=grid_color, linestyle="--")
429
- for spine in ["top","right"]: ax.spines[spine].set_visible(False)
430
- for spine in ["bottom","left"]: ax.spines[spine].set_color(grid_color)
431
- plt.tight_layout()
432
- buf2 = io.BytesIO()
433
- plt.savefig(buf2, format="png", facecolor=bg, bbox_inches="tight", dpi=130)
434
- buf2.seek(0)
435
- result = Image.open(buf2).copy()
436
- plt.close()
437
- return result
438
-
439
- img1 = make_tgt_chart(tat_col, "#e63946", "TAT (ng/mL)", 8, "Normal limit: 8 ng/mL", "Thrombin-Antithrombin (TAT)")
440
- img2 = make_tgt_chart(pf_col, "#4361ee", "PF1.2 (nmol/L)", 2.0, "Normal limit: 2.0", "Prothrombin Fragment PF1.2")
441
- img3 = make_tgt_chart(hemo_col, "#2ecc71", "Free Hemoglobin (mg/L)", 20, "Normal limit: 20 mg/L", "Free Hemoglobin - Hemolysis", "bar")
442
- img4 = make_tgt_chart(plt_col, "#e67e22", "Platelet Count (10Β³/ΞΌL)", 150, "Normal minimum: 150", "Platelet Count")
443
-
444
- return img1, img2, img3, img4, "TGT CSV LOADED: "+str(len(df))+" rows | Columns: "+", ".join(df.columns.tolist())+ai_text
445
- except Exception as e:
446
- return None, None, None, None, "Error: "+str(e)
447
-
448
- def analyze_upad_photo(image):
449
- if image is None: return None, "Upload a uPAD photo first."
450
- try:
451
- img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
452
- arr = np.array(img)
453
- h,w = arr.shape[:2]
454
- y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
455
- zone = arr[y1:y2,x1:x2]
456
- R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
457
- creatinine = max(0, round(0.018*(R-B)-0.3, 2))
458
- if creatinine < 1.2: stage,action = "Normal","Monitor annually."
459
- elif creatinine < 1.5: stage,action = "Borderline","Repeat in 3 months."
460
- elif creatinine < 3.0: stage,action = "Stage 2 CKD","Consult nephrologist."
461
- elif creatinine < 6.0: stage,action = "Stage 3-4 CKD","Immediate consultation."
462
- else: stage,action = "Stage 5 CKD","Emergency care needed."
463
- result_img = img.copy()
464
- import PIL.ImageDraw as D
465
- draw = D.Draw(result_img)
466
- draw.rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
467
- return result_img, ("uPAD ANALYSIS"+chr(10)+"━"*22+chr(10)+
468
- "R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+
469
- "Orange Score: "+str(round(R-B,1))+chr(10)+"━"*22+chr(10)+
470
- "CREATININE: "+str(creatinine)+" mg/dL"+chr(10)+
471
- "CKD STAGE: "+stage+chr(10)+"ACTION: "+action+chr(10)+
472
- "Confirm: Heska Element HT5")
473
- except Exception as e: return None, "Error: "+str(e)
474
 
475
  def generate_image(prompt):
476
  if not prompt.strip(): return None,"Enter description.","";
477
  if not HF_TOKEN: return None,"Add HF_TOKEN to Space secrets.","";
478
  try:
479
- enhanced,desc = prompt,""
480
  if GROQ_KEY:
481
  try:
482
  client=Groq(api_key=GROQ_KEY)
@@ -507,20 +385,42 @@ def tgt_manual(t,p,h,pl,tm):
507
  risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
508
  return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"Time:"+str(tm)+"min"+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
509
 
 
510
  with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
511
  gr.HTML('''<div style="background:linear-gradient(135deg,#1a237e,#b71c1c);padding:20px;text-align:center;border-radius:12px 12px 0 0"><div style="font-size:2.5em;font-weight:900;color:#fff;letter-spacing:3px">CardioLab AI</div></div>''')
512
 
513
  with gr.Tabs():
 
514
  with gr.Tab("Chat"):
515
- chatbot = gr.Chatbot(label="", height=420)
516
  with gr.Row():
517
- msg_box = gr.Textbox(placeholder="Ask about CardioLab research...", label="", lines=2, scale=4)
518
- with gr.Column(scale=1, min_width=80):
519
- send_btn = gr.Button("Send", variant="primary")
520
- clear_btn = gr.Button("Clear", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
522
  msg_box.submit(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
523
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
 
 
 
524
 
525
  with gr.Tab("Voice"):
526
  voice_chatbot = gr.Chatbot(label="", height=320)
@@ -540,89 +440,85 @@ with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
540
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
541
 
542
  with gr.Tab("PIV CSV"):
543
- gr.Markdown("### Upload PIV CSV file β€” AI generates separate charts + clinical analysis")
544
- gr.Markdown("CSV columns: **time, velocity, shear_stress** (any column names work)")
545
  with gr.Row():
546
- piv_file = gr.File(label="CLICK HERE TO UPLOAD PIV CSV", file_types=[".csv"], scale=3)
547
- piv_theme = gr.Radio(["Dark", "White"], value="White", label="Chart Theme", scale=1)
548
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
549
- piv_result = gr.Textbox(label="AI Clinical Analysis", lines=6)
550
- gr.Markdown("### Charts")
551
  with gr.Row():
552
- piv_chart1 = gr.Image(label="Velocity Profile", type="pil")
553
- piv_chart2 = gr.Image(label="Shear Stress", type="pil")
554
  with gr.Row():
555
- piv_chart3 = gr.Image(label="Velocity vs Shear", type="pil")
556
- piv_chart4 = gr.Image(label="Clinical Summary", type="pil")
557
- piv_btn.click(analyze_piv_csv, inputs=[piv_file, piv_theme], outputs=[piv_chart1, piv_chart2, piv_chart3, piv_chart4, piv_result])
558
 
559
  with gr.Tab("TGT CSV"):
560
- gr.Markdown("### Upload TGT CSV file β€” AI generates blood biomarker charts + thrombogenicity assessment")
561
- gr.Markdown("CSV columns: **time, TAT, PF12, hemoglobin, platelets** (any column names work)")
562
  with gr.Row():
563
- tgt_file = gr.File(label="CLICK HERE TO UPLOAD TGT CSV", file_types=[".csv"], scale=3)
564
- tgt_theme = gr.Radio(["Dark", "White"], value="White", label="Chart Theme", scale=1)
565
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
566
- tgt_result = gr.Textbox(label="AI Thrombogenicity Assessment", lines=6)
567
- gr.Markdown("### Charts")
568
  with gr.Row():
569
- tgt_chart1 = gr.Image(label="TAT Over Time", type="pil")
570
- tgt_chart2 = gr.Image(label="PF1.2 Over Time", type="pil")
571
  with gr.Row():
572
- tgt_chart3 = gr.Image(label="Free Hemoglobin", type="pil")
573
- tgt_chart4 = gr.Image(label="Platelet Count", type="pil")
574
- tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file, tgt_theme], outputs=[tgt_chart1, tgt_chart2, tgt_chart3, tgt_chart4, tgt_result])
575
 
576
  with gr.Tab("uPAD Photo"):
577
- gr.Markdown("### Upload uPAD Photo β€” Instant CKD diagnosis from Jaffe reaction color")
578
  with gr.Row():
579
  with gr.Column():
580
  photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=280)
581
  analyze_btn = gr.Button("Analyze uPAD", variant="primary")
582
  with gr.Column():
583
- photo_img = gr.Image(label="Detection Zone (green box)", type="pil", height=280)
584
  photo_text = gr.Textbox(label="CKD Result", lines=10)
585
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
586
 
587
  with gr.Tab("AI Image"):
588
  with gr.Row():
589
- img_prompt = gr.Textbox(placeholder="e.g. bileaflet heart valve | uPAD device | Arduino TGT circuit", label="Describe image", lines=2, scale=4)
590
  with gr.Column(scale=1):
591
  img_btn = gr.Button("Generate", variant="primary")
592
  img_status = gr.Textbox(label="Status", lines=1)
593
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
594
  img_output = gr.Image(label="Generated Image", type="pil", height=380)
595
- img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output, img_status, img_desc])
596
 
597
  with gr.Tab("PIV Manual"):
598
  with gr.Row():
599
  with gr.Column():
600
- v=gr.Number(label="Max Velocity m/s", value=1.8)
601
- s=gr.Number(label="Wall Shear Stress Pa", value=6.5)
602
- h=gr.Number(label="Heart Rate bpm", value=72)
603
- piv_out=gr.Textbox(label="Result", lines=4)
604
- gr.Button("Analyze", variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
605
 
606
  with gr.Tab("TGT Manual"):
607
  with gr.Row():
608
  with gr.Column():
609
- t1=gr.Number(label="TAT ng/mL", value=18)
610
- t2=gr.Number(label="PF1.2", value=2.5)
611
- t3=gr.Number(label="Hemoglobin mg/L", value=60)
612
- t4=gr.Number(label="Platelets", value=140)
613
- t5=gr.Number(label="Time min", value=40)
614
- out2=gr.Textbox(label="Result", lines=6)
615
- gr.Button("Analyze", variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
616
 
617
  with gr.Tab("uPAD Manual"):
618
  with gr.Row():
619
  with gr.Column():
620
- r=gr.Number(label="R value", value=210)
621
- g=gr.Number(label="G value", value=140)
622
- b=gr.Number(label="B value", value=80)
623
- out3=gr.Textbox(label="Result", lines=4)
624
- gr.Button("Analyze", variant="primary").click(
625
- lambda r,g,b: "Creatinine: "+str(max(0,round(0.02*(r-b)-0.5,2)))+" mg/dL"+chr(10)+("Normal" if max(0,round(0.02*(r-b)-0.5,2))<1.2 else "Borderline" if max(0,round(0.02*(r-b)-0.5,2))<1.5 else "CKD Stage 2+" ),
626
- inputs=[r,g,b], outputs=out3)
627
 
628
  demo.launch()
 
 
1
  import gradio as gr
2
+ import os, requests, io, json
3
  import numpy as np
4
  import pandas as pd
5
  import matplotlib
6
  matplotlib.use("Agg")
7
  import matplotlib.pyplot as plt
 
8
  from groq import Groq
9
  from PIL import Image
10
+ from datetime import datetime
11
+ from huggingface_hub import HfApi, hf_hub_download
12
+ from huggingface_hub.utils import EntryNotFoundError
13
 
14
  GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
15
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
16
+ HISTORY_REPO = "Saicharan21/cardiolab-chat-history"
17
 
18
  KNOWHOW = ("MCL: Sylgard 184 PDMS 10:1 ratio 48hr cure green laser PIV 70bpm 5L/min. "
19
  "TGT: Arduino Uno Stepper Motor 150mL blood sampled at 0 20 40 60min measures TAT PF1.2 hemolysis platelets. "
20
  "uPAD: Jaffe reaction creatinine plus picric acid gives orange-red color normal 0.6-1.2 mg/dL CKD above 1.5. "
21
+ "MHV: 27mm SJM Regent bileaflet also trileaflet monoleaflet pediatric. "
22
+ "Equipment: Heska HT5 hematology analyzer time-resolved PIV Tygon tubing Arduino Uno.")
23
 
24
  CSS = """
25
  body, .gradio-container { background: #f0f4f8 !important; }
26
+ .tab-nav { background: #ffffff !important; border-bottom: 2px solid #e2e8f0 !important; padding: 4px 5px 0 5px !important; display: flex !important; flex-wrap: wrap !important; gap: 3px !important; }
27
+ .tab-nav button { background: #f7fafc !important; color: #2d3748 !important; border: 1px solid #e2e8f0 !important; border-radius: 6px 6px 0 0 !important; padding: 8px 10px !important; font-weight: 600 !important; font-size: 0.8em !important; white-space: nowrap !important; }
28
  .tab-nav button:hover { background: #ebf4ff !important; color: #1a237e !important; }
29
  .tab-nav button.selected { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: #ffffff !important; font-weight: 700 !important; }
30
  button.primary { background: linear-gradient(135deg, #e63946 0%, #c1121f 100%) !important; color: white !important; border: none !important; border-radius: 8px !important; font-weight: 700 !important; }
31
  button.secondary { background: #edf2f7 !important; color: #4a5568 !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
32
+ textarea, input[type=number], input[type=text] { background: #f7fafc !important; color: #1a202c !important; border: 1px solid #cbd5e0 !important; border-radius: 8px !important; }
33
  .message.user { background: linear-gradient(135deg, #e63946, #c1121f) !important; color: white !important; }
34
  .message.bot { background: #ebf4ff !important; color: #1a202c !important; border: 1px solid #bee3f8 !important; }
35
  label span { color: #2b6cb0 !important; font-weight: 600 !important; font-size: 0.85em !important; text-transform: uppercase !important; }
36
  """
37
 
38
+ # ── PERSISTENT HISTORY FUNCTIONS ──────────────────────────────────
39
+ def get_history_api():
40
+ if not HF_TOKEN: return None
41
+ return HfApi(token=HF_TOKEN)
42
+
43
+ def load_all_sessions():
44
+ if not HF_TOKEN: return {}
45
+ try:
46
+ api = get_history_api()
47
+ path = hf_hub_download(
48
+ repo_id=HISTORY_REPO,
49
+ filename="chat_history.json",
50
+ repo_type="dataset",
51
+ token=HF_TOKEN
52
+ )
53
+ with open(path, "r") as f:
54
+ return json.load(f)
55
+ except Exception:
56
+ return {}
57
+
58
+ def save_all_sessions(sessions):
59
+ if not HF_TOKEN: return False
60
+ try:
61
+ api = get_history_api()
62
+ content = json.dumps(sessions, indent=2)
63
+ api.upload_file(
64
+ path_or_fileobj=content.encode(),
65
+ path_in_repo="chat_history.json",
66
+ repo_id=HISTORY_REPO,
67
+ repo_type="dataset",
68
+ token=HF_TOKEN,
69
+ commit_message="Update chat history"
70
+ )
71
+ return True
72
+ except Exception as e:
73
+ print("Save error:", e)
74
+ return False
75
+
76
+ def get_session_list():
77
+ sessions = load_all_sessions()
78
+ if not sessions:
79
+ return ["No saved sessions yet"]
80
+ return list(sessions.keys())
81
+
82
+ def load_session(session_name):
83
+ if not session_name or session_name == "No saved sessions yet":
84
+ return [], "No session loaded"
85
+ sessions = load_all_sessions()
86
+ if session_name in sessions:
87
+ history = sessions[session_name]["messages"]
88
+ return history, "Loaded: " + session_name + " (" + str(len(history)) + " messages)"
89
+ return [], "Session not found"
90
+
91
+ def save_session(history, session_name):
92
+ if not history:
93
+ return "Nothing to save β€” chat is empty", gr.update()
94
+ if not session_name.strip():
95
+ session_name = "Session " + datetime.now().strftime("%Y-%m-%d %H:%M")
96
+ sessions = load_all_sessions()
97
+ sessions[session_name] = {
98
+ "messages": history,
99
+ "saved_at": datetime.now().isoformat(),
100
+ "message_count": len(history)
101
+ }
102
+ success = save_all_sessions(sessions)
103
+ if success:
104
+ return "Saved: " + session_name, gr.update(choices=get_session_list(), value=session_name)
105
+ return "Save failed β€” check HF_TOKEN in Space secrets", gr.update()
106
+
107
+ def delete_session(session_name):
108
+ if not session_name or session_name == "No saved sessions yet":
109
+ return "No session selected", gr.update()
110
+ sessions = load_all_sessions()
111
+ if session_name in sessions:
112
+ del sessions[session_name]
113
+ save_all_sessions(sessions)
114
+ new_list = get_session_list()
115
+ return "Deleted: " + session_name, gr.update(choices=new_list, value=new_list[0] if new_list else None)
116
+ return "Session not found", gr.update()
117
+
118
+ # ── CHAT FUNCTIONS ────────────────────────────────────────────────
119
  def get_pubmed(query, n=5):
120
  try:
121
  r = requests.get("https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi",
 
130
  pubmed = get_pubmed(query, n=8)
131
  try:
132
  r = requests.get("https://api.semanticscholar.org/graph/v1/paper/search",
133
+ params={"query":query+" biomedical","limit":5,"fields":"title,year,url"},timeout=10)
134
  papers = r.json().get("data",[])
135
  scholar = chr(10).join([p.get("title","")[:80]+" ("+str(p.get("year",""))+")"+chr(10)+" "+p.get("url","") for p in papers if p.get("url","")])
136
  except: scholar = ""
 
139
  def research_chat(message, history):
140
  if not GROQ_KEY:
141
  history.append({"role":"user","content":message})
142
+ history.append({"role":"assistant","content":"Error: Add GROQ_API_KEY to Space Settings Secrets."})
143
  return "", history
144
  try:
145
  client = Groq(api_key=GROQ_KEY)
146
+ msgs = [{"role":"system","content":"You are CardioLab AI. Expert in MHV MCL PIV TGT uPAD CKD FSI. Remember full conversation. Never invent URLs. "+KNOWHOW}]
147
  for item in history:
148
  if isinstance(item, dict): msgs.append({"role":item["role"],"content":item["content"]})
149
  msgs.append({"role":"user","content":message})
 
179
  history.append({"role":"assistant","content":"Voice error: "+str(e)})
180
  return history
181
 
182
+ # ── ANALYSIS TOOLS ────────────────────────────────────────────────
183
+ def analyze_upad_photo(image):
184
+ if image is None: return None, "Upload a uPAD photo first."
185
+ try:
186
+ img = Image.fromarray(image) if not isinstance(image, Image.Image) else image
187
+ arr = np.array(img)
188
+ h,w = arr.shape[:2]
189
+ y1,y2,x1,x2 = int(h*0.35),int(h*0.65),int(w*0.35),int(w*0.65)
190
+ zone = arr[y1:y2,x1:x2]
191
+ R,G,B = float(np.mean(zone[:,:,0])),float(np.mean(zone[:,:,1])),float(np.mean(zone[:,:,2]))
192
+ c = max(0, round(0.018*(R-B)-0.3, 2))
193
+ if c<1.2: s,a="Normal","Monitor annually."
194
+ elif c<1.5: s,a="Borderline","Repeat in 3 months."
195
+ elif c<3.0: s,a="Stage 2 CKD","Consult nephrologist."
196
+ elif c<6.0: s,a="Stage 3-4 CKD","Immediate consultation."
197
+ else: s,a="Stage 5 CKD","Emergency care needed."
198
+ result_img = img.copy()
199
+ import PIL.ImageDraw as D
200
+ draw = D.Draw(result_img)
201
+ draw.rectangle([x1,y1,x2,y2], outline=(0,255,0), width=3)
202
+ return result_img, ("uPAD ANALYSIS"+chr(10)+"━"*22+chr(10)+
203
+ "R:"+str(round(R,1))+" G:"+str(round(G,1))+" B:"+str(round(B,1))+chr(10)+
204
+ "Orange Score: "+str(round(R-B,1))+chr(10)+"━"*22+chr(10)+
205
+ "CREATININE: "+str(c)+" mg/dL"+chr(10)+"CKD STAGE: "+s+chr(10)+
206
+ "ACTION: "+a+chr(10)+"Confirm: Heska Element HT5")
207
+ except Exception as e: return None, "Error: "+str(e)
208
+
209
  def analyze_piv_csv(file, theme="White"):
210
+ if file is None: return None,None,None,None,"Upload a PIV CSV file first."
 
211
  try:
212
  df = pd.read_csv(file.name)
213
  cols = [c.lower().strip() for c in df.columns]
214
  df.columns = cols
215
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
216
+ if not num_cols: return None,None,None,None,"No numeric columns found."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  bg = "#ffffff" if theme=="White" else "#0a1628"
218
  fg = "#1a202c" if theme=="White" else "white"
219
+ gc = "#e2e8f0" if theme=="White" else "#2d4a8a"
220
+ ac = "#4a5568" if theme=="White" else "#a8b2d8"
221
+ pb = "#f7fafc" if theme=="White" else "#132340"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  x = np.arange(len(df))
223
+ vc = next((c for c in cols if any(k in c for k in ["vel","speed","v_mag"])), num_cols[0] if num_cols else None)
224
+ sc = next((c for c in cols if any(k in c for k in ["shear","stress","tau","wss"])), num_cols[1] if len(num_cols)>1 else None)
225
+ tc = next((c for c in cols if "time" in c or "frame" in c), None)
226
+ xv = df[tc] if tc else x
227
+ def mk(fn, title):
228
+ fig2,ax = plt.subplots(figsize=(8,5))
229
+ fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
230
+ fn(ax)
231
+ ax.set_title(title, color=fg, fontweight="bold", fontsize=13, pad=8)
232
+ ax.tick_params(colors=ac, labelsize=10)
233
+ ax.grid(True, alpha=0.3, color=gc, linestyle="--")
234
+ for sp in ["top","right"]: ax.spines[sp].set_visible(False)
235
+ for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
236
+ plt.tight_layout()
237
+ buf2=io.BytesIO(); plt.savefig(buf2,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf2.seek(0)
238
+ res=Image.open(buf2).copy(); plt.close(); return res
239
+ def pv(ax):
240
+ if vc:
241
+ ax.plot(xv,df[vc],color="#e63946",linewidth=2.5,marker="o",markersize=5)
242
+ ax.fill_between(xv,df[vc],alpha=0.2,color="#e63946")
243
+ ax.axhline(y=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Risk: 2.0 m/s")
244
+ ax.set_ylabel("Velocity (m/s)",color=ac,fontsize=11)
245
+ ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
246
+ ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
247
+ def ps(ax):
248
+ if sc:
249
+ xp = xv.values if tc else x
250
+ ax.plot(xp,df[sc],color="#4361ee",linewidth=2.5,marker="s",markersize=5)
251
+ ax.fill_between(xp,df[sc],alpha=0.2,color="#4361ee")
252
+ ax.axhline(y=5,color="#f59e0b",linestyle="--",linewidth=2,label="Caution: 5 Pa")
253
+ ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="High risk: 10 Pa")
254
+ ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11)
255
+ ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
256
+ ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
257
+ def psc(ax):
258
+ if vc and sc:
259
+ s2 = ax.scatter(df[vc],df[sc],c=x,cmap="RdYlGn_r",s=90,edgecolors=fg,linewidth=0.5,zorder=5)
260
+ cb=plt.colorbar(s2,ax=ax,label="Time"); cb.ax.yaxis.label.set_color(fg); cb.ax.tick_params(colors=ac)
261
+ ax.axvline(x=2.0,color="#f59e0b",linestyle="--",linewidth=2,label="Vel risk")
262
+ ax.axhline(y=10,color="#e63946",linestyle="--",linewidth=2,label="Shear risk")
263
+ ax.set_xlabel("Velocity (m/s)",color=ac,fontsize=11)
264
+ ax.set_ylabel("Shear Stress (Pa)",color=ac,fontsize=11)
265
+ ax.legend(fontsize=9,labelcolor=fg,facecolor=pb)
266
+ def psum(ax):
267
+ ax.axis("off"); risk=[]
268
+ st="CLINICAL SUMMARY"+chr(10)+"━"*20+chr(10)+chr(10)
269
  for col in num_cols[:3]:
270
+ mn=round(df[col].mean(),3); mx=round(df[col].max(),3)
271
+ st+=col[:14]+":"+chr(10)+" Mean: "+str(mn)+chr(10)+" Max: "+str(mx)+chr(10)+chr(10)
272
+ if "vel" in col and mx>2.0: risk.append("HIGH VELOCITY (>2.0 m/s)")
273
+ if "shear" in col and mx>10: risk.append("HIGH SHEAR (>10 Pa)")
274
+ st+="━"*20+chr(10)
 
 
275
  if risk:
276
+ st+="RISK FLAGS:"+chr(10)+"".join([" ⚠ "+r+chr(10) for r in risk])
277
+ st+="OVERALL: HIGH RISK"; bc="#e63946"
 
 
278
  else:
279
+ st+="OVERALL: LOW RISK"; bc="#2ecc71"
280
+ ax.text(0.05,0.97,st,transform=ax.transAxes,color=fg,fontsize=10,va="top",fontfamily="monospace",
281
+ bbox=dict(boxstyle="round,pad=0.8",facecolor=pb,edgecolor=bc,linewidth=2.5))
282
+ i1=mk(pv,"Velocity Profile"); i2=mk(ps,"Wall Shear Stress")
283
+ i3=mk(psc,"Velocity vs Shear"); i4=mk(psum,"Clinical Summary")
284
+ ai=""
285
+ if GROQ_KEY:
286
+ try:
287
+ client=Groq(api_key=GROQ_KEY)
288
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
289
+ messages=[{"role":"system","content":"PIV expert SJSU CardioLab. Analyze PIV stats give clinical interpretation."},
290
+ {"role":"user","content":"PIV data from 27mm SJM Regent MHV 70bpm 5L/min:"+chr(10)+df.describe().to_string()[:600]}],max_tokens=300)
291
+ ai=chr(10)+"━"*20+chr(10)+"AI:"+chr(10)+resp.choices[0].message.content
292
+ except: pass
293
+ return i1,i2,i3,i4,"PIV LOADED: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
294
+ except Exception as e: return None,None,None,None,"Error: "+str(e)
295
 
296
  def analyze_tgt_csv(file, theme="White"):
297
+ if file is None: return None,None,None,None,"Upload a TGT CSV file first."
 
298
  try:
299
  df = pd.read_csv(file.name)
300
  cols = [c.lower().strip() for c in df.columns]
301
  df.columns = cols
302
  num_cols = df.select_dtypes(include=[np.number]).columns.tolist()
303
+ bg="#ffffff" if theme=="White" else "#0a1628"
304
+ fg="#1a202c" if theme=="White" else "white"
305
+ gc="#e2e8f0" if theme=="White" else "#2d4a8a"
306
+ ac="#4a5568" if theme=="White" else "#a8b2d8"
307
+ pb="#f7fafc" if theme=="White" else "#132340"
308
+ tc=next((c for c in cols if "time" in c or "min" in c),None)
309
+ tatc=next((c for c in cols if "tat" in c),num_cols[0] if num_cols else None)
310
+ pfc=next((c for c in cols if "pf" in c),num_cols[1] if len(num_cols)>1 else None)
311
+ hc=next((c for c in cols if "hemo" in c or "hgb" in c),num_cols[2] if len(num_cols)>2 else None)
312
+ plc=next((c for c in cols if "platelet" in c or "plt" in c),num_cols[3] if len(num_cols)>3 else None)
313
+ xv=df[tc] if tc else range(len(df))
314
+ def mk(dc,color,yl,lim,ll,title,bar=False):
315
+ fig2,ax=plt.subplots(figsize=(8,5))
316
+ fig2.patch.set_facecolor(bg); ax.set_facecolor(pb)
317
+ if dc and dc in df.columns:
318
+ xp=df[tc].values if tc else range(len(df)); yp=df[dc].values
319
+ if bar:
320
+ bs=ax.bar(range(len(yp)),yp,color=color,alpha=0.85,edgecolor=bg,width=0.6)
321
+ for b,v in zip(bs,yp): ax.text(b.get_x()+b.get_width()/2,b.get_height()+0.5,str(round(v,1)),ha="center",va="bottom",color=fg,fontsize=10,fontweight="bold")
322
+ else:
323
+ ax.plot(xp,yp,color=color,linewidth=3,marker="o",markersize=8)
324
+ ax.fill_between(xp,yp,alpha=0.2,color=color)
325
+ for xi,yi in zip(xp,yp): ax.annotate(str(round(yi,1)),(xi,yi),textcoords="offset points",xytext=(0,10),ha="center",color=fg,fontsize=10,fontweight="bold")
326
+ ax.axhline(y=lim,color="#f59e0b",linestyle="--",linewidth=2.5,label=ll)
327
+ ax.legend(fontsize=10,labelcolor=fg,facecolor=pb)
328
+ ax.set_ylabel(yl,color=ac,fontsize=11); ax.set_xlabel(tc or "Sample",color=ac,fontsize=11)
329
+ mv=round(float(np.max(yp)),2); st="HIGH" if mv>lim else "NORMAL"
330
+ ax.set_title(title+chr(10)+"Max: "+str(mv)+" Status: "+st,color=fg,fontweight="bold",fontsize=12)
331
+ ax.tick_params(colors=ac,labelsize=10); ax.grid(True,alpha=0.3,color=gc,linestyle="--")
332
+ for sp in ["top","right"]: ax.spines[sp].set_visible(False)
333
+ for sp in ["bottom","left"]: ax.spines[sp].set_color(gc)
334
+ plt.tight_layout()
335
+ buf2=io.BytesIO(); plt.savefig(buf2,format="png",facecolor=bg,bbox_inches="tight",dpi=130); buf2.seek(0)
336
+ res=Image.open(buf2).copy(); plt.close(); return res
337
+ i1=mk(tatc,"#e63946","TAT (ng/mL)",8,"Normal: 8 ng/mL","Thrombin-Antithrombin TAT")
338
+ i2=mk(pfc,"#4361ee","PF1.2 (nmol/L)",2.0,"Normal: 2.0","Prothrombin Fragment PF1.2")
339
+ i3=mk(hc,"#2ecc71","Free Hemoglobin (mg/L)",20,"Normal: 20 mg/L","Free Hemoglobin Hemolysis",bar=True)
340
+ i4=mk(plc,"#e67e22","Platelet Count (10Β³/ΞΌL)",150,"Normal min: 150","Platelet Count")
341
+ ai=""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
  if GROQ_KEY:
343
  try:
344
+ client=Groq(api_key=GROQ_KEY)
345
+ resp=client.chat.completions.create(model="llama-3.3-70b-versatile",
346
+ messages=[{"role":"system","content":"Hematology expert SJSU CardioLab. Analyze TGT data give thrombogenicity risk LOW MODERATE or HIGH. Normal: TAT<8, PF1.2<2.0, Hemo<20, Plt>150."},
347
+ {"role":"user","content":"TGT from 27mm SJM Regent MHV:"+chr(10)+df.describe().to_string()[:600]}],max_tokens=300)
348
+ ai=chr(10)+"━"*20+chr(10)+"AI:"+chr(10)+resp.choices[0].message.content
349
  except: pass
350
+ return i1,i2,i3,i4,"TGT LOADED: "+str(len(df))+" rows | "+", ".join(df.columns.tolist())+ai
351
+ except Exception as e: return None,None,None,None,"Error: "+str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  def generate_image(prompt):
354
  if not prompt.strip(): return None,"Enter description.","";
355
  if not HF_TOKEN: return None,"Add HF_TOKEN to Space secrets.","";
356
  try:
357
+ enhanced,desc=prompt,""
358
  if GROQ_KEY:
359
  try:
360
  client=Groq(api_key=GROQ_KEY)
 
385
  risk=sum([float(t)>15,float(p)>2.0,float(h)>50,float(pl)<150])
386
  return "TAT:"+str(t)+" PF1.2:"+str(p)+chr(10)+"Hemo:"+str(h)+" Plt:"+str(pl)+chr(10)+"Time:"+str(tm)+"min"+chr(10)+"RESULT: "+("HIGH RISK" if risk>=3 else "MODERATE" if risk>=2 else "LOW RISK")
387
 
388
+ # ── UI ─────────────────────────────────────────────────────────────
389
  with gr.Blocks(title="CardioLab AI", css=CSS) as demo:
390
  gr.HTML('''<div style="background:linear-gradient(135deg,#1a237e,#b71c1c);padding:20px;text-align:center;border-radius:12px 12px 0 0"><div style="font-size:2.5em;font-weight:900;color:#fff;letter-spacing:3px">CardioLab AI</div></div>''')
391
 
392
  with gr.Tabs():
393
+
394
  with gr.Tab("Chat"):
395
+ gr.Markdown("### Chat with memory β€” saves conversations like ChatGPT")
396
  with gr.Row():
397
+ with gr.Column(scale=3):
398
+ chatbot = gr.Chatbot(label="", height=420)
399
+ with gr.Row():
400
+ msg_box = gr.Textbox(placeholder="Ask about CardioLab research...", label="", lines=2, scale=4)
401
+ with gr.Column(scale=1, min_width=80):
402
+ send_btn = gr.Button("Send", variant="primary")
403
+ clear_btn = gr.Button("Clear", variant="secondary")
404
+ with gr.Column(scale=1, min_width=200):
405
+ gr.Markdown("### Saved Sessions")
406
+ session_dropdown = gr.Dropdown(
407
+ choices=get_session_list(),
408
+ label="Load a saved session",
409
+ interactive=True
410
+ )
411
+ load_btn = gr.Button("Load Session", variant="primary")
412
+ session_status = gr.Textbox(label="Status", lines=1, interactive=False)
413
+ gr.Markdown("### Save Current Chat")
414
+ session_name_box = gr.Textbox(label="Session name", placeholder="e.g. TGT Research May 2026")
415
+ save_btn = gr.Button("Save Chat", variant="primary")
416
+ delete_btn = gr.Button("Delete Session", variant="secondary")
417
+
418
  send_btn.click(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
419
  msg_box.submit(research_chat, inputs=[msg_box, chatbot], outputs=[msg_box, chatbot])
420
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg_box])
421
+ save_btn.click(save_session, inputs=[chatbot, session_name_box], outputs=[session_status, session_dropdown])
422
+ load_btn.click(load_session, inputs=session_dropdown, outputs=[chatbot, session_status])
423
+ delete_btn.click(delete_session, inputs=session_dropdown, outputs=[session_status, session_dropdown])
424
 
425
  with gr.Tab("Voice"):
426
  voice_chatbot = gr.Chatbot(label="", height=320)
 
440
  search_input.submit(quick_search, inputs=search_input, outputs=search_output)
441
 
442
  with gr.Tab("PIV CSV"):
443
+ gr.Markdown("### Upload PIV CSV β€” 4 separate charts + AI analysis")
 
444
  with gr.Row():
445
+ piv_file = gr.File(label="UPLOAD PIV CSV", file_types=[".csv"], scale=3)
446
+ piv_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
447
  piv_btn = gr.Button("Analyze PIV Data", variant="primary")
448
+ piv_result = gr.Textbox(label="AI Analysis", lines=5)
 
449
  with gr.Row():
450
+ piv_c1 = gr.Image(label="Velocity Profile", type="pil")
451
+ piv_c2 = gr.Image(label="Shear Stress", type="pil")
452
  with gr.Row():
453
+ piv_c3 = gr.Image(label="Velocity vs Shear", type="pil")
454
+ piv_c4 = gr.Image(label="Clinical Summary", type="pil")
455
+ piv_btn.click(analyze_piv_csv, inputs=[piv_file,piv_theme], outputs=[piv_c1,piv_c2,piv_c3,piv_c4,piv_result])
456
 
457
  with gr.Tab("TGT CSV"):
458
+ gr.Markdown("### Upload TGT CSV β€” blood biomarker charts + thrombogenicity assessment")
 
459
  with gr.Row():
460
+ tgt_file = gr.File(label="UPLOAD TGT CSV", file_types=[".csv"], scale=3)
461
+ tgt_theme = gr.Radio(["White","Dark"], value="White", label="Theme", scale=1)
462
  tgt_btn = gr.Button("Analyze TGT Data", variant="primary")
463
+ tgt_result = gr.Textbox(label="AI Assessment", lines=5)
 
464
  with gr.Row():
465
+ tgt_c1 = gr.Image(label="TAT Over Time", type="pil")
466
+ tgt_c2 = gr.Image(label="PF1.2 Over Time", type="pil")
467
  with gr.Row():
468
+ tgt_c3 = gr.Image(label="Free Hemoglobin", type="pil")
469
+ tgt_c4 = gr.Image(label="Platelet Count", type="pil")
470
+ tgt_btn.click(analyze_tgt_csv, inputs=[tgt_file,tgt_theme], outputs=[tgt_c1,tgt_c2,tgt_c3,tgt_c4,tgt_result])
471
 
472
  with gr.Tab("uPAD Photo"):
473
+ gr.Markdown("### Upload uPAD Photo β€” Instant CKD diagnosis")
474
  with gr.Row():
475
  with gr.Column():
476
  photo_input = gr.Image(label="Upload uPAD Photo", type="numpy", height=280)
477
  analyze_btn = gr.Button("Analyze uPAD", variant="primary")
478
  with gr.Column():
479
+ photo_img = gr.Image(label="Detection Zone", type="pil", height=280)
480
  photo_text = gr.Textbox(label="CKD Result", lines=10)
481
  analyze_btn.click(analyze_upad_photo, inputs=photo_input, outputs=[photo_img, photo_text])
482
 
483
  with gr.Tab("AI Image"):
484
  with gr.Row():
485
+ img_prompt = gr.Textbox(placeholder="e.g. bileaflet heart valve | uPAD device | Arduino TGT", label="Describe image", lines=2, scale=4)
486
  with gr.Column(scale=1):
487
  img_btn = gr.Button("Generate", variant="primary")
488
  img_status = gr.Textbox(label="Status", lines=1)
489
  img_desc = gr.Textbox(label="AI Description", lines=2, interactive=False)
490
  img_output = gr.Image(label="Generated Image", type="pil", height=380)
491
+ img_btn.click(generate_image, inputs=img_prompt, outputs=[img_output,img_status,img_desc])
492
 
493
  with gr.Tab("PIV Manual"):
494
  with gr.Row():
495
  with gr.Column():
496
+ v=gr.Number(label="Max Velocity m/s",value=1.8)
497
+ s=gr.Number(label="Wall Shear Stress Pa",value=6.5)
498
+ h=gr.Number(label="Heart Rate bpm",value=72)
499
+ piv_out=gr.Textbox(label="Result",lines=4)
500
+ gr.Button("Analyze PIV",variant="primary").click(piv_manual,inputs=[v,s,h],outputs=piv_out)
501
 
502
  with gr.Tab("TGT Manual"):
503
  with gr.Row():
504
  with gr.Column():
505
+ t1=gr.Number(label="TAT ng/mL",value=18)
506
+ t2=gr.Number(label="PF1.2",value=2.5)
507
+ t3=gr.Number(label="Hemoglobin mg/L",value=60)
508
+ t4=gr.Number(label="Platelets",value=140)
509
+ t5=gr.Number(label="Time min",value=40)
510
+ out2=gr.Textbox(label="Result",lines=6)
511
+ gr.Button("Analyze TGT",variant="primary").click(tgt_manual,inputs=[t1,t2,t3,t4,t5],outputs=out2)
512
 
513
  with gr.Tab("uPAD Manual"):
514
  with gr.Row():
515
  with gr.Column():
516
+ r=gr.Number(label="R value",value=210)
517
+ g=gr.Number(label="G value",value=140)
518
+ b=gr.Number(label="B value",value=80)
519
+ out3=gr.Textbox(label="Result",lines=4)
520
+ gr.Button("Analyze",variant="primary").click(
521
+ lambda r,g,b:"Creatinine: "+str(max(0,round(0.02*(r-b)-0.5,2)))+" mg/dL"+chr(10)+("Normal" if max(0,round(0.02*(r-b)-0.5,2))<1.2 else "Borderline" if max(0,round(0.02*(r-b)-0.5,2))<1.5 else "CKD Stage 2+"),
522
+ inputs=[r,g,b],outputs=out3)
523
 
524
  demo.launch()