BioStack / src /App.js
AE-Shree
Deploy BioStack RLHF Medical Demo
c934b38
import { useState, useRef, useCallback } from "react";
// ─────────────────────────────────────────────────────────
// THEME DEFINITIONS
// ─────────────────────────────────────────────────────────
const THEMES = {
dark: {
name: "dark",
bg: "#070d14",
surface: "#0f1923",
surfaceAlt: "#0a1520",
border: "#1e2d3d",
borderFocus: "#3b82f6",
text: "#e2e8f0",
textMuted: "#4a6080",
textSub: "#94a3b8",
textBody: "#cbd5e1",
inputBg: "#0a1520",
uploadBg: "#0a1520",
uploadHover: "#1e2d3d",
scrollTrack: "#0f1923",
scrollThumb: "#1e3a5f",
infoBg: "#0a1520",
emptyColor: "#1e3a5f",
cardBg: "#0f1923",
barTrack: "#1e2d3d",
btnDisabled: "#1e2d3d",
btnDisabledTxt:"#4a6080",
headerShadow: "none",
cardShadow: "none",
},
light: {
name: "light",
bg: "#f0f4f8",
surface: "#ffffff",
surfaceAlt: "#f8fafc",
border: "#d1dde9",
borderFocus: "#2563eb",
text: "#0f172a",
textMuted: "#64748b",
textSub: "#475569",
textBody: "#334155",
inputBg: "#ffffff",
uploadBg: "#f8fafc",
uploadHover: "#e2eaf4",
scrollTrack: "#e2e8f0",
scrollThumb: "#94a3b8",
infoBg: "#f1f5f9",
emptyColor: "#94a3b8",
cardBg: "#ffffff",
barTrack: "#e2e8f0",
btnDisabled: "#e2e8f0",
btnDisabledTxt:"#94a3b8",
headerShadow: "0 1px 6px rgba(0,0,0,0.07)",
cardShadow: "0 1px 4px rgba(0,0,0,0.06)",
},
};
// ─────────────────────────────────────────────────────────
// ROUGE-L CALCULATION
// ─────────────────────────────────────────────────────────
const ROUGE_L = (hyp, ref) => {
if (!hyp || !ref) return 0;
const hypW = hyp.toLowerCase().split(/\s+/);
const refW = ref.toLowerCase().split(/\s+/);
const m = hypW.length, n = refW.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++)
dp[i][j] = hypW[i-1] === refW[j-1]
? dp[i-1][j-1] + 1
: Math.max(dp[i-1][j], dp[i][j-1]);
const lcs = dp[m][n];
const prec = lcs / m, rec = lcs / n;
if (prec + rec === 0) return 0;
return ((2 * prec * rec) / (prec + rec)).toFixed(4);
};
// ─────────────────────────────────────────────────────────
// THEME TOGGLE
// ─────────────────────────────────────────────────────────
const ThemeToggle = ({ theme, onToggle }) => {
const isDark = theme.name === "dark";
return (
<button
onClick={onToggle}
title={`Switch to ${isDark ? "light" : "dark"} mode`}
style={{
display: "flex", alignItems: "center", gap: 9,
background: isDark ? "#1a2d42" : "#e8f0fa",
border: `1px solid ${isDark ? "#2d4a6a" : "#c4d4e8"}`,
borderRadius: 24, padding: "6px 14px 6px 8px",
cursor: "pointer", color: theme.text,
fontSize: 12, fontWeight: 600, letterSpacing: 0.4,
transition: "background .3s, border-color .3s",
}}
>
{/* Toggle track */}
<div style={{
width: 42, height: 23, borderRadius: 12,
background: isDark ? "#1d4ed8" : "#f59e0b",
position: "relative", flexShrink: 0,
transition: "background .35s ease",
}}>
{/* Thumb */}
<div style={{
position: "absolute",
top: "50%", transform: "translateY(-50%)",
left: isDark ? 22 : 3,
width: 17, height: 17, borderRadius: "50%",
background: "#fff",
boxShadow: "0 1px 5px rgba(0,0,0,0.25)",
display: "grid", placeItems: "center",
fontSize: 9,
transition: "left .28s ease",
}}>
{isDark ? "πŸŒ™" : "β˜€οΈ"}
</div>
</div>
<span style={{ color: theme.textMuted, userSelect: "none" }}>
{isDark ? "Dark" : "Light"}
</span>
</button>
);
};
// ─────────────────────────────────────────────────────────
// SCORE BADGE
// ─────────────────────────────────────────────────────────
const ScoreBadge = ({ score }) => {
const pct = parseFloat(score) * 100;
const color = pct >= 60 ? "#22c55e" : pct >= 35 ? "#f59e0b" : "#ef4444";
return (
<span style={{
background: color + "22", color,
border: `1px solid ${color}55`,
borderRadius: 6, padding: "2px 10px",
fontFamily: "monospace", fontWeight: 700, fontSize: 13,
}}>
ROUGE-L: {pct.toFixed(1)}%
</span>
);
};
// ─────────────────────────────────────────────────────────
// OUTPUT CARD
// ─────────────────────────────────────────────────────────
const OutputCard = ({ title, icon, content, badge, accent, loading, theme }) => (
<div style={{
background: theme.cardBg,
border: `1px solid ${accent}33`,
borderRadius: 14, padding: 20,
position: "relative", overflow: "hidden",
boxShadow: theme.cardShadow,
transition: "background .3s, box-shadow .3s",
}}>
{/* Accent top bar */}
<div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 3, background: accent }} />
{/* Card header */}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<span style={{ fontSize: 18 }}>{icon}</span>
<span style={{
fontFamily: "'Courier New', monospace",
fontSize: 11, fontWeight: 700, color: accent,
textTransform: "uppercase", letterSpacing: 2,
}}>{title}</span>
{badge && <div style={{ marginLeft: "auto" }}>{badge}</div>}
</div>
{/* Body */}
{loading ? (
<div style={{ display: "flex", gap: 6, alignItems: "center", color: theme.textMuted, fontSize: 13 }}>
{[0, 0.2, 0.4].map((d, i) => (
<span key={i} style={{ animation: `pulse 1s infinite ${d}s` }}>●</span>
))}
<span style={{ marginLeft: 8 }}>Generating…</span>
</div>
) : content ? (
<p style={{ margin: 0, fontSize: 14, lineHeight: 1.78, color: theme.textBody, fontFamily: "Georgia, serif" }}>
{content}
</p>
) : (
<p style={{ margin: 0, fontSize: 13, color: theme.textMuted, fontStyle: "italic" }}>
Awaiting input…
</p>
)}
</div>
);
// ─────────────────────────────────────────────────────────
// MAIN APP
// ─────────────────────────────────────────────────────────
export default function App() {
const [themeName, setThemeName] = useState("dark");
const theme = THEMES[themeName];
const toggleTheme = () => setThemeName(t => t === "dark" ? "light" : "dark");
const [image, setImage] = useState(null);
const [imageFile, setImageFile] = useState(null);
const [groundTruth, setGroundTruth] = useState("");
const [sftOutput, setSftOutput] = useState("");
const [rewardOutput, setRewardOutput] = useState("");
const [ppoOutput, setPpoOutput] = useState("");
const [rewardScore, setRewardScore] = useState(null);
const [loading, setLoading] = useState(false);
const [dragging, setDragging] = useState(false);
const fileRef = useRef();
const runInference = async () => {
if (!image || !imageFile) return;
setLoading(true);
setSftOutput(""); setRewardOutput(""); setPpoOutput(""); setRewardScore(null);
// Use relative URLs for Hugging Face Spaces (no base URL needed)
const BASE = "";
try {
// 1. SFT
const sftForm = new FormData();
sftForm.append("file", imageFile);
const sftRes = await fetch(`${BASE}/sft`, { method: "POST", body: sftForm });
const sftData = await sftRes.json();
setSftOutput(sftData.report);
// 2. Reward
const rmForm = new FormData();
rmForm.append("file", imageFile);
const rmRes = await fetch(`${BASE}/reward`, { method: "POST", body: rmForm });
const rmData = await rmRes.json();
setRewardOutput(rmData.feedback);
setRewardScore(rmData.score.toFixed(2));
// 3. PPO
const ppoForm = new FormData();
ppoForm.append("file", imageFile);
const ppoRes = await fetch(`${BASE}/ppo`, { method: "POST", body: ppoForm });
const ppoData = await ppoRes.json();
setPpoOutput(ppoData.report);
} catch (err) {
console.error("Inference error:", err);
setSftOutput("⚠️ Could not connect to server. Please check your connection.");
}
setLoading(false);
};
const handleFile = (file) => {
if (!file || !file.type.startsWith("image/")) return;
setImageFile(file);
const reader = new FileReader();
reader.onload = e => setImage(e.target.result);
reader.readAsDataURL(file);
};
const onDrop = useCallback((e) => {
e.preventDefault(); setDragging(false);
handleFile(e.dataTransfer.files[0]);
}, []);
const clearAll = () => {
setImage(null); setImageFile(null);
setSftOutput(""); setRewardOutput(""); setPpoOutput(""); setRewardScore(null);
if (fileRef.current) fileRef.current.value = "";
};
const rougeSFT = groundTruth && sftOutput ? ROUGE_L(sftOutput, groundTruth) : null;
const rougePPO = groundTruth && ppoOutput ? ROUGE_L(ppoOutput, groundTruth) : null;
return (
<div style={{
minHeight: "100vh",
background: theme.bg,
color: theme.text,
fontFamily: "system-ui, -apple-system, sans-serif",
transition: "background .3s ease, color .3s ease",
}}>
{/* ── GLOBAL STYLES ── */}
<style>{`
@keyframes pulse { 0%,100%{opacity:.3} 50%{opacity:1} }
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
* { box-sizing: border-box; margin: 0; padding: 0; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: ${theme.scrollTrack}; }
::-webkit-scrollbar-thumb { background: ${theme.scrollThumb}; border-radius: 3px; }
textarea { font-family: system-ui, sans-serif; }
textarea:focus {
outline: none;
border-color: ${theme.borderFocus} !important;
box-shadow: 0 0 0 3px ${theme.borderFocus}22;
}
button { transition: all .22s ease !important; }
button:not(:disabled):hover { filter: brightness(1.1); transform: translateY(-1px); }
button:not(:disabled):active { transform: translateY(0px); filter: brightness(0.97); }
`}</style>
{/* ═══════════════════════════════════
HEADER
═══════════════════════════════════ */}
<header style={{
borderBottom: `2px solid ${theme.border}`,
padding: "20px 32px",
display: "flex", alignItems: "center", gap: 18,
background: `linear-gradient(135deg, ${theme.surface} 0%, ${theme.surfaceAlt} 100%)`,
boxShadow: "0 4px 20px rgba(0,0,0,0.1)",
transition: "background .3s, border-color .3s, box-shadow .3s",
position: "sticky", top: 0, zIndex: 100,
backdropFilter: "blur(10px)",
WebkitBackdropFilter: "blur(10px)",
}}>
{/* Logo mark */}
<div style={{
width: 52, height: 52, flexShrink: 0,
background: "linear-gradient(135deg,#3b82f6,#06b6d4)",
borderRadius: 14, display: "grid", placeItems: "center",
fontSize: 24, boxShadow: "0 4px 16px rgba(59,130,246,0.4)",
border: "2px solid rgba(255,255,255,0.2)",
}}>🫁</div>
{/* Titles */}
<div>
<div style={{
fontWeight: 900,
fontSize: 22,
letterSpacing: -0.8,
color: theme.text,
textShadow: theme.name === "dark" ? "0 1px 2px rgba(0,0,0,0.3)" : "none",
marginBottom: 4
}}>
BioStack
</div>
<div style={{
fontSize: 12,
color: theme.textMuted,
letterSpacing: 1.2,
textTransform: "uppercase",
fontWeight: 600,
opacity: 0.9
}}>
RLHF Based Medical Report Generation
</div>
</div>
<div style={{ flex: 1 }} />
{/* πŸŒ™/β˜€οΈ Toggle */}
<ThemeToggle theme={theme} onToggle={toggleTheme} />
</header>
{/* ═══════════════════════════════════
MAIN GRID
═══════════════════════════════════ */}
<div style={{
display: "grid",
gridTemplateColumns: "370px 1fr",
height: "calc(100vh - 155px)", // Further decreased for optimal fit
overflow: "hidden", // Prevent any overflow
}}>
{/* ══ LEFT PANEL ══ */}
<aside style={{
borderRight: `1px solid ${theme.border}`,
padding: "15px 18px", // Reduced padding
display: "flex", flexDirection: "column", gap: 14, // Reduced gap
background: theme.surface,
overflowY: "auto",
transition: "background .3s, border-color .3s",
height: "100%", // Ensure full height
}}>
<div style={{ fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: 2, fontWeight: 700 }}>
πŸ“€ Input
</div>
{/* Upload zone */}
<div>
<label style={{
fontSize: 11, color: theme.textMuted,
textTransform: "uppercase", letterSpacing: 1.5,
fontWeight: 700, display: "block", marginBottom: 8,
}}>Chest X-Ray Image</label>
<div
onClick={() => fileRef.current.click()}
onDragOver={e => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={onDrop}
style={{
border: `2px dashed ${dragging ? "#3b82f6" : image ? "#22c55e66" : theme.border}`,
borderRadius: 14, minHeight: 190,
display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center",
cursor: "pointer",
background: dragging ? theme.uploadHover : theme.uploadBg,
transition: "all .22s", overflow: "hidden",
}}
>
{image ? (
<img src={image} alt="X-Ray" style={{
width: "100%", height: "100%",
objectFit: "contain", maxHeight: 240, borderRadius: 12,
}} />
) : (
<>
<div style={{ fontSize: 40, marginBottom: 10, opacity: 0.65 }}>🩻</div>
<div style={{ fontSize: 13, color: theme.textMuted, textAlign: "center", lineHeight: 1.65 }}>
Click or drag & drop<br />
<span style={{ fontSize: 11, color: theme.emptyColor }}>PNG, JPG, DICOM supported</span>
</div>
</>
)}
</div>
<input ref={fileRef} type="file" accept="image/*" style={{ display: "none" }}
onChange={e => handleFile(e.target.files[0])} />
</div>
{/* Clear button */}
{image && (
<button onClick={clearAll} style={{
background: "transparent",
border: `1px solid ${theme.border}`,
color: theme.textMuted,
borderRadius: 8, padding: "5px 14px",
cursor: "pointer", fontSize: 12, alignSelf: "flex-start",
}}>
βœ• Clear Image
</button>
)}
{/* Divider */}
<div style={{ height: 1, background: theme.border }} />
{/* Ground truth */}
<div>
<label style={{
fontSize: 11, color: theme.textMuted,
textTransform: "uppercase", letterSpacing: 1.5,
fontWeight: 700, display: "block", marginBottom: 8,
}}>
πŸ“‹ Ground Truth{" "}
<span style={{ color: theme.emptyColor, textTransform: "none", letterSpacing: 0, fontWeight: 400, fontSize: 11 }}>
(optional)
</span>
</label>
<textarea
value={groundTruth}
onChange={e => setGroundTruth(e.target.value)}
placeholder="Paste the radiologist ground truth report here to see ROUGE-L scores…"
rows={5}
style={{
width: "100%",
background: theme.inputBg,
border: `1px solid ${theme.border}`,
borderRadius: 10,
color: theme.text,
padding: "10px 12px",
fontSize: 13, resize: "vertical", lineHeight: 1.65,
transition: "border .2s, box-shadow .2s, background .3s, color .3s",
}}
/>
</div>
{/* Run button */}
<button
onClick={runInference}
disabled={!image || loading}
style={{
background: image && !loading
? "linear-gradient(135deg,#1d4ed8,#0891b2)"
: theme.btnDisabled,
color: image && !loading ? "#fff" : theme.btnDisabledTxt,
border: "none", borderRadius: 12,
padding: "13px 20px",
fontSize: 14, fontWeight: 700,
cursor: image && !loading ? "pointer" : "not-allowed",
letterSpacing: 0.5, width: "100%",
boxShadow: image && !loading ? "0 4px 16px rgba(29,78,216,0.38)" : "none",
}}>
{loading ? "⏳ Running Pipeline…" : "β–Ά Run RLHF Pipeline"}
</button>
</aside>
{/* ══ RIGHT PANEL ══ */}
<main style={{
padding: "18px 22px", // Reduced padding
display: "flex", flexDirection: "column", gap: 16, // Reduced gap
overflowY: "auto",
background: theme.bg,
transition: "background .3s",
height: "100%", // Ensure full height
}}>
<div style={{ fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: 2, fontWeight: 700 }}>
πŸ“Š Pipeline Outputs
</div>
{/* SFT */}
<div style={{ animation: sftOutput ? "fadeIn .4s ease" : "none" }}>
<OutputCard theme={theme} title="SFT Model Output β€” Original" icon="🧠" accent="#3b82f6"
content={sftOutput} loading={loading && !sftOutput}
badge={rougeSFT !== null && <ScoreBadge score={rougeSFT} />}
/>
</div>
{/* Reward */}
<div style={{ animation: rewardOutput ? "fadeIn .4s ease" : "none" }}>
<OutputCard theme={theme} title="Reward Model Output" icon="βš–οΈ" accent="#f59e0b"
content={rewardOutput} loading={loading && sftOutput && !rewardOutput}
badge={rewardScore && (
<span style={{
background: "#f59e0b22", color: "#f59e0b",
border: "1px solid #f59e0b55",
borderRadius: 6, padding: "2px 10px",
fontFamily: "monospace", fontWeight: 700, fontSize: 13,
}}>
Reward: {rewardScore}
</span>
)}
/>
</div>
{/* PPO */}
<div style={{ animation: ppoOutput ? "fadeIn .4s ease" : "none" }}>
<OutputCard theme={theme} title="PPO Final Model Output" icon="🎯" accent="#22c55e"
content={ppoOutput} loading={loading && rewardOutput && !ppoOutput}
badge={rougePPO !== null && <ScoreBadge score={rougePPO} />}
/>
</div>
{/* ROUGE-L comparison */}
{groundTruth && sftOutput && ppoOutput && (
<div style={{
background: theme.surface,
border: `1px solid ${theme.border}`,
borderRadius: 14, padding: 20,
animation: "fadeIn .5s ease",
boxShadow: theme.cardShadow,
transition: "background .3s, border-color .3s",
}}>
<div style={{ fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: 2, fontWeight: 700, marginBottom: 16 }}>
πŸ“ˆ ROUGE-L Comparison vs Ground Truth
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
{[
{ label: "SFT (Original)", score: rougeSFT, color: "#3b82f6" },
{ label: "PPO (Final)", score: rougePPO, color: "#22c55e" },
].map(({ label, score, color }) => (
<div key={label} style={{
background: theme.surfaceAlt, borderRadius: 10, padding: 16,
border: `1px solid ${color}33`,
transition: "background .3s",
}}>
<div style={{ fontSize: 12, color: theme.textMuted, marginBottom: 8 }}>{label}</div>
<div style={{ fontSize: 28, fontWeight: 800, color, fontFamily: "monospace" }}>
{(parseFloat(score) * 100).toFixed(1)}%
</div>
<div style={{ marginTop: 10, height: 5, background: theme.barTrack, borderRadius: 3 }}>
<div style={{
width: `${parseFloat(score) * 100}%`, height: "100%",
background: color, borderRadius: 3,
transition: "width 1.2s ease",
}} />
</div>
{parseFloat(rougePPO) > parseFloat(rougeSFT) && label.includes("PPO") && (
<div style={{ fontSize: 11, color: "#22c55e", marginTop: 7, fontWeight: 600 }}>
β–² +{((parseFloat(rougePPO) - parseFloat(rougeSFT)) * 100).toFixed(1)}% improvement
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Ground truth reference */}
{groundTruth && (
<div style={{
background: theme.surface,
border: `1px solid ${theme.border}`,
borderRadius: 14, padding: 20,
boxShadow: theme.cardShadow,
transition: "background .3s, border-color .3s",
}}>
<div style={{ fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: 2, fontWeight: 700, marginBottom: 10 }}>
πŸ“‹ Ground Truth Reference
</div>
<p style={{ fontSize: 14, lineHeight: 1.78, color: theme.textSub, fontFamily: "Georgia, serif" }}>
{groundTruth}
</p>
</div>
)}
{/* Empty state */}
{!image && !loading && (
<div style={{
flex: 1, display: "flex", flexDirection: "column",
alignItems: "center", justifyContent: "center",
color: theme.emptyColor, gap: 14, paddingTop: 60,
}}>
<div style={{ fontSize: 64, opacity: 0.35 }}>🩻</div>
<div style={{ fontSize: 14, fontWeight: 600, opacity: 0.45 }}>Upload a chest X-ray to begin</div>
<div style={{ fontSize: 12, opacity: 0.3 }}>Results will appear here after running the pipeline</div>
</div>
)}
</main>
</div>
{/* ═══════════════════════════════════
FOOTER
═══════════════════════════════════ */}
<footer style={{
borderTop: `2px solid ${theme.border}`,
padding: "18px 32px",
background: `linear-gradient(135deg, ${theme.surfaceAlt} 0%, ${theme.surface} 100%)`,
textAlign: "center",
fontSize: 13,
color: theme.textMuted,
fontWeight: 600,
letterSpacing: 0.5,
transition: "background .3s, border-color .3s, color .3s",
position: "relative",
boxShadow: "0 -2px 10px rgba(0,0,0,0.05)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
}}>
<div style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 8,
opacity: 0.8
}}>
<span style={{ fontSize: 16 }}>Β©</span>
<span>2026 BioStack. All rights reserved.</span>
</div>
</footer>
</div>
);
}