Spaces:
Sleeping
Sleeping
| 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)", | |
| }, | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 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", | |
| }}> | |
| <div style={{ | |
| width: 42, height: 23, borderRadius: 12, | |
| background: isDark ? "#1d4ed8" : "#f59e0b", | |
| position: "relative", flexShrink: 0, | |
| transition: "background .35s ease", | |
| }}> | |
| <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> | |
| ); | |
| }; | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // HF JUDGE SCORE BADGE (replaces ROUGE-L badge) | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const HFJudgeBadge = ({ 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, | |
| }}>HF Judge: {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", | |
| }}> | |
| <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 3, background: accent }} /> | |
| <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> | |
| {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> | |
| ); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // REWARD BREAKDOWN CARD β mirrors inference.py output exactly | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const RewardBar = ({ label, score, color, desc, theme }) => { | |
| const pct = Math.round(parseFloat(score) * 100); | |
| return ( | |
| <div style={{ marginBottom: 10 }}> | |
| <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 3 }}> | |
| <span style={{ fontSize: 12, color: theme.textSub, fontWeight: 600 }}>{label}</span> | |
| <span style={{ fontSize: 12, fontFamily: "monospace", color, fontWeight: 700 }}> | |
| {parseFloat(score).toFixed(4)} | |
| </span> | |
| </div> | |
| <div style={{ height: 8, background: theme.barTrack, borderRadius: 4 }}> | |
| <div style={{ | |
| width: `${pct}%`, height: 8, borderRadius: 4, | |
| background: color, transition: "width 1s ease", | |
| }} /> | |
| </div> | |
| <div style={{ fontSize: 11, color: theme.textMuted, marginTop: 2 }}>{desc}</div> | |
| </div> | |
| ); | |
| }; | |
| const RewardBreakdownCard = ({ breakdown, label, accent, theme }) => { | |
| if (!breakdown) return null; | |
| const totalPct = Math.round(parseFloat(breakdown.total) * 100); | |
| const totalColor = totalPct >= 60 ? "#22c55e" : totalPct >= 40 ? "#f59e0b" : "#ef4444"; | |
| return ( | |
| <div style={{ | |
| background: theme.cardBg, border: `1px solid ${accent}33`, | |
| borderRadius: 14, padding: 20, position: "relative", overflow: "hidden", | |
| boxShadow: theme.cardShadow, transition: "background .3s", | |
| animation: "fadeIn .4s ease", | |
| }}> | |
| <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 3, background: accent }} /> | |
| {/* Header */} | |
| <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }}> | |
| <div style={{ display: "flex", alignItems: "center", gap: 8 }}> | |
| <span style={{ fontSize: 18 }}>π</span> | |
| <span style={{ | |
| fontFamily: "'Courier New', monospace", fontSize: 11, | |
| fontWeight: 700, color: accent, textTransform: "uppercase", letterSpacing: 2, | |
| }}> | |
| {label} Reward Breakdown | |
| </span> | |
| </div> | |
| {/* Total score pill */} | |
| <span style={{ | |
| background: totalColor + "22", color: totalColor, | |
| border: `1px solid ${totalColor}55`, | |
| borderRadius: 6, padding: "3px 12px", | |
| fontFamily: "monospace", fontWeight: 700, fontSize: 14, | |
| }}> | |
| TOTAL: {parseFloat(breakdown.total).toFixed(4)} | |
| </span> | |
| </div> | |
| {/* Component bars */} | |
| <RewardBar label="Contrastive" score={breakdown.contrastive} color="#3498db" desc="Beats BM25 retrieval baseline" theme={theme} /> | |
| <RewardBar label="ROUGE-L" score={breakdown.rouge_l} color="#9b59b6" desc="Surface overlap with reference" theme={theme} /> | |
| <RewardBar label="Negation Safety" score={breakdown.negation_safety} color="#e67e22" desc="No hallucinated negations" theme={theme} /> | |
| <RewardBar label="HF Judge" score={breakdown.hf_judge} color="#1abc9c" desc={`BiomedBERT sim (${parseFloat(breakdown.biomed_sim).toFixed(3)}) + NLI quality (${parseFloat(breakdown.nli_quality).toFixed(3)})`} theme={theme} /> | |
| {/* Total bar */} | |
| <div style={{ borderTop: `1px solid ${theme.border}`, paddingTop: 10, marginTop: 6 }}> | |
| <div style={{ display: "flex", justifyContent: "space-between", marginBottom: 3 }}> | |
| <span style={{ fontSize: 12, color: theme.text, fontWeight: 700 }}>TOTAL</span> | |
| <span style={{ fontSize: 13, fontFamily: "monospace", color: totalColor, fontWeight: 700 }}> | |
| {parseFloat(breakdown.total).toFixed(4)} / 1.0 | |
| </span> | |
| </div> | |
| <div style={{ height: 10, background: theme.barTrack, borderRadius: 5 }}> | |
| <div style={{ | |
| width: `${totalPct}%`, height: 10, borderRadius: 5, | |
| background: totalColor, transition: "width 1.2s ease", | |
| }} /> | |
| </div> | |
| <div style={{ fontSize: 11, color: theme.textMuted, marginTop: 2 }}> | |
| Weighted average (0.25 Γ each component) | |
| </div> | |
| </div> | |
| </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 [grpoOutput, setGrpoOutput] = useState(""); | |
| const [rewardScore, setRewardScore] = useState(null); | |
| // ββ NEW: HF Judge scores from server (replaces client-side ROUGE-L) ββ | |
| const [sftHFJudge, setSftHFJudge] = useState(null); | |
| const [grpoHFJudge, setGrpoHFJudge] = useState(null); | |
| const [loading, setLoading] = useState(false); | |
| const [dragging, setDragging] = useState(false); | |
| const fileRef = useRef(); | |
| // ββ Pipeline βββββββββββββββββββββββββββββββββββββββββ | |
| const runInference = async () => { | |
| if (!image || !imageFile) return; | |
| setLoading(true); | |
| setSftOutput(""); setRewardOutput(""); setGrpoOutput(""); | |
| setRewardScore(null); | |
| // ββ Reset HF Judge scores ββ | |
| setSftHFJudge(null); | |
| setGrpoHFJudge(null); | |
| 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 β send ground truth for full breakdown + hf_judge | |
| const rmForm = new FormData(); | |
| rmForm.append("file", imageFile); | |
| rmForm.append("ground_truth", groundTruth); | |
| const rmRes = await fetch(`${BASE}/reward`, { method: "POST", body: rmForm }); | |
| const rmData = await rmRes.json(); | |
| setRewardOutput(rmData.feedback); | |
| setRewardScore(parseFloat(rmData.score).toFixed(2)); | |
| // ββ Capture SFT HF Judge score from server ββ | |
| if (rmData.hf_judge !== null && rmData.hf_judge !== undefined) { | |
| setSftHFJudge(rmData.hf_judge); | |
| } | |
| // 3. GRPO + its reward breakdown + hf_judge | |
| const grpoForm = new FormData(); | |
| grpoForm.append("file", imageFile); | |
| grpoForm.append("ground_truth", groundTruth); | |
| const grpoRes = await fetch(`${BASE}/grpo_reward`, { method: "POST", body: grpoForm }); | |
| const grpoData = await grpoRes.json(); | |
| setGrpoOutput(grpoData.report); | |
| // ββ Capture GRPO HF Judge score from server ββ | |
| if (grpoData.hf_judge !== null && grpoData.hf_judge !== undefined) { | |
| setGrpoHFJudge(grpoData.hf_judge); | |
| } | |
| } catch (err) { | |
| console.error("Inference error:", err); | |
| setSftOutput("β οΈ Could not connect to server. Please check your connection."); | |
| } | |
| setLoading(false); | |
| }; | |
| // ββ File handling βββββββββββββββββββββββββββββββββββββ | |
| 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]); | |
| }, []); // eslint-disable-line | |
| const clearAll = () => { | |
| setImage(null); setImageFile(null); | |
| setSftOutput(""); setRewardOutput(""); setGrpoOutput(""); | |
| setRewardScore(null); | |
| setSftHFJudge(null); | |
| setGrpoHFJudge(null); | |
| if (fileRef.current) fileRef.current.value = ""; | |
| }; | |
| 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", | |
| }}> | |
| <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 ; } | |
| 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)", | |
| }}> | |
| <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> | |
| <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, | |
| }}>NeMo Gym Based Medical Report Generation</div> | |
| </div> | |
| <div style={{ flex: 1 }} /> | |
| <ThemeToggle theme={theme} onToggle={toggleTheme} /> | |
| </header> | |
| {/* βββ MAIN GRID βββ */} | |
| <div style={{ | |
| display: "grid", gridTemplateColumns: "370px 1fr", | |
| height: "calc(100vh - 155px)", overflow: "hidden", | |
| }}> | |
| {/* ββ LEFT PANEL ββ */} | |
| <aside style={{ | |
| borderRight: `1px solid ${theme.border}`, padding: "15px 18px", | |
| display: "flex", flexDirection: "column", gap: 14, | |
| background: theme.surface, overflowY: "auto", | |
| transition: "background .3s, border-color .3s", height: "100%", | |
| }}> | |
| <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> | |
| {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> | |
| )} | |
| {/* 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 }}> | |
| </span> | |
| </label> | |
| <textarea | |
| value={groundTruth} | |
| onChange={e => setGroundTruth(e.target.value)} | |
| placeholder="Paste the radiologist ground truth report here" | |
| 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", | |
| }} | |
| /> | |
| {groundTruth && ( | |
| <div style={{ | |
| marginTop: 6, fontSize: 11, color: "#22c55e", | |
| display: "flex", alignItems: "center", gap: 4, | |
| }}> | |
| β Ground Truth Provided β HF Judge scores will be computed | |
| </div> | |
| )} | |
| </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 NeMo Gym Pipeline"} | |
| </button> | |
| </aside> | |
| {/* ββ RIGHT PANEL ββ */} | |
| <main style={{ | |
| padding: "18px 22px", display: "flex", flexDirection: "column", gap: 16, | |
| overflowY: "auto", background: theme.bg, | |
| transition: "background .3s", height: "100%", | |
| }}> | |
| <div style={{ fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: 2, fontWeight: 700 }}> | |
| π Pipeline Outputs | |
| </div> | |
| {/* SFT output β badge shows HF Judge score from server */} | |
| <div style={{ animation: sftOutput ? "fadeIn .4s ease" : "none" }}> | |
| <OutputCard | |
| theme={theme} title="SFT Model Output" icon="π§ " accent="#3b82f6" | |
| content={sftOutput} loading={loading && !sftOutput} | |
| badge={sftHFJudge !== null && <HFJudgeBadge score={sftHFJudge} />} | |
| /> | |
| </div> | |
| {/* Reward output card */} | |
| <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 && ( | |
| <div style={{ | |
| background: "#f59e0b22", color: "#f59e0b", | |
| border: "1px solid #f59e0b55", | |
| borderRadius: 6, padding: "6px 10px", | |
| fontFamily: "monospace", fontWeight: 700, fontSize: 12, | |
| display: "flex", flexDirection: "column", gap: 2, | |
| }}> | |
| <div> | |
| Reward: {rewardScore} | |
| </div> | |
| <div style={{ fontSize: 11, fontWeight: 600, opacity: 0.9 }}> | |
| (0.25 Γ contrastive + 0.25 Γ ROUGE-L + 0.25 Γ negation_safety + 0.25 Γ hf_judge) | |
| </div> | |
| </div> | |
| )} | |
| /> | |
| </div> | |
| {/* GRPO output β badge shows HF Judge score from server */} | |
| <div style={{ animation: grpoOutput ? "fadeIn .4s ease" : "none" }}> | |
| <OutputCard | |
| theme={theme} title="NeMo Gym Output" icon="π―" accent="#22c55e" | |
| content={grpoOutput} loading={loading && rewardOutput && !grpoOutput} | |
| badge={grpoHFJudge !== null && <HFJudgeBadge score={grpoHFJudge} />} | |
| /> | |
| </div> | |
| {/* ββ HF JUDGE COMPARISON (replaces ROUGE-L comparison) ββ */} | |
| {groundTruth && sftOutput && grpoOutput && sftHFJudge !== null && grpoHFJudge !== null && ( | |
| <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: 6 }}> | |
| π€ HF Judge Score Comparison vs Ground Truth | |
| </div> | |
| <div style={{ fontSize: 11, color: theme.textMuted, marginBottom: 16 }}> | |
| BiomedBERT semantic similarity + NLI quality β computed by the server reward model | |
| </div> | |
| <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}> | |
| {[ | |
| { label: "SFT (Original)", score: sftHFJudge, color: "#3b82f6" }, | |
| { label: "GRPO (Final)", score: grpoHFJudge, color: "#22c55e" }, | |
| ].map(({ label, score, color }) => { | |
| const pct = (parseFloat(score) * 100).toFixed(1); | |
| const isGRPO = label.includes("GRPO"); | |
| const sftPct = parseFloat(sftHFJudge) * 100; | |
| const grpoPct = parseFloat(grpoHFJudge) * 100; | |
| const improved = isGRPO && grpoPct > sftPct; | |
| const delta = (grpoPct - sftPct).toFixed(1); | |
| return ( | |
| <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" }}> | |
| {pct}% | |
| </div> | |
| <div style={{ fontSize: 11, color: theme.textMuted, marginBottom: 10 }}> | |
| raw: {parseFloat(score).toFixed(4)} | |
| </div> | |
| <div style={{ height: 5, background: theme.barTrack, borderRadius: 3 }}> | |
| <div style={{ | |
| width: `${pct}%`, height: "100%", | |
| background: color, borderRadius: 3, transition: "width 1.2s ease", | |
| }} /> | |
| </div> | |
| {improved && ( | |
| <div style={{ fontSize: 11, color: "#22c55e", marginTop: 7, fontWeight: 600 }}> | |
| β² +{delta}% improvement | |
| </div> | |
| )} | |
| {isGRPO && !improved && grpoPct < sftPct && ( | |
| <div style={{ fontSize: 11, color: "#f59e0b", marginTop: 7, fontWeight: 600 }}> | |
| βΌ {delta}% vs SFT | |
| </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", | |
| 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> | |
| ); | |
| } |