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 (
);
};
// ─────────────────────────────────────────────────────────
// 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 (
HF Judge: {pct.toFixed(1)}%
);
};
// ─────────────────────────────────────────────────────────
// OUTPUT CARD
// ─────────────────────────────────────────────────────────
const OutputCard = ({ title, icon, content, badge, accent, loading, theme }) => (
{icon}
{title}
{badge &&
{badge}
}
{loading ? (
{[0, 0.2, 0.4].map((d, i) => (
●
))}
Generating…
) : content ? (
{content}
) : (
Awaiting input…
)}
);
// ─────────────────────────────────────────────────────────
// REWARD BREAKDOWN CARD — mirrors inference.py output exactly
// ─────────────────────────────────────────────────────────
const RewardBar = ({ label, score, color, desc, theme }) => {
const pct = Math.round(parseFloat(score) * 100);
return (
{label}
{parseFloat(score).toFixed(4)}
{desc}
);
};
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 (
{/* Header */}
📊
{label} Reward Breakdown
{/* Total score pill */}
TOTAL: {parseFloat(breakdown.total).toFixed(4)}
{/* Component bars */}
{/* Total bar */}
TOTAL
{parseFloat(breakdown.total).toFixed(4)} / 1.0
Weighted average (0.25 × each component)
);
};
// ─────────────────────────────────────────────────────────
// 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 (
{/* ═══ HEADER ═══ */}
{/* ═══ MAIN GRID ═══ */}
{/* ══ LEFT PANEL ══ */}
{/* ══ RIGHT PANEL ══ */}
📊 Pipeline Outputs
{/* SFT output — badge shows HF Judge score from server */}
}
/>
{/* Reward output card */}
Reward: {rewardScore}
(0.25 × contrastive + 0.25 × ROUGE-L + 0.25 × negation_safety + 0.25 × hf_judge)
)}
/>
{/* GRPO output — badge shows HF Judge score from server */}
}
/>
{/* ── HF JUDGE COMPARISON (replaces ROUGE-L comparison) ── */}
{groundTruth && sftOutput && grpoOutput && sftHFJudge !== null && grpoHFJudge !== null && (
🤖 HF Judge Score Comparison vs Ground Truth
BiomedBERT semantic similarity + NLI quality — computed by the server reward model
{[
{ 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 (
{label}
{pct}%
raw: {parseFloat(score).toFixed(4)}
{improved && (
▲ +{delta}% improvement
)}
{isGRPO && !improved && grpoPct < sftPct && (
▼ {delta}% vs SFT
)}
);
})}
)}
{/* Ground truth reference */}
{groundTruth && (
📋 Ground Truth Reference
{groundTruth}
)}
{/* Empty state */}
{!image && !loading && (
🩻
Upload a chest X-ray to begin
Results will appear here after running the pipeline
)}
{/* ═══ FOOTER ═══ */}
);
}