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 (
);
};
// ─────────────────────────────────────────────────────────
// SCORE BADGE
// ─────────────────────────────────────────────────────────
const ScoreBadge = ({ score }) => {
const pct = parseFloat(score) * 100;
const color = pct >= 60 ? "#22c55e" : pct >= 35 ? "#f59e0b" : "#ef4444";
return (
ROUGE-L: {pct.toFixed(1)}%
);
};
// ─────────────────────────────────────────────────────────
// OUTPUT CARD
// ─────────────────────────────────────────────────────────
const OutputCard = ({ title, icon, content, badge, accent, loading, theme }) => (
{/* Accent top bar */}
{/* Card header */}
{icon}
{title}
{badge &&
{badge}
}
{/* Body */}
{loading ? (
{[0, 0.2, 0.4].map((d, i) => (
●
))}
Generating…
) : content ? (
{content}
) : (
Awaiting input…
)}
);
// ─────────────────────────────────────────────────────────
// 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 (
{/* ── GLOBAL STYLES ── */}
{/* ═══════════════════════════════════
HEADER
═══════════════════════════════════ */}
{/* ═══════════════════════════════════
MAIN GRID
═══════════════════════════════════ */}
{/* ══ LEFT PANEL ══ */}
{/* ══ RIGHT PANEL ══ */}
📊 Pipeline Outputs
{/* SFT */}
}
/>
{/* Reward */}
Reward: {rewardScore}
)}
/>
{/* PPO */}
}
/>
{/* ROUGE-L comparison */}
{groundTruth && sftOutput && ppoOutput && (
📈 ROUGE-L Comparison vs Ground Truth
{[
{ label: "SFT (Original)", score: rougeSFT, color: "#3b82f6" },
{ label: "PPO (Final)", score: rougePPO, color: "#22c55e" },
].map(({ label, score, color }) => (
{label}
{(parseFloat(score) * 100).toFixed(1)}%
{parseFloat(rougePPO) > parseFloat(rougeSFT) && label.includes("PPO") && (
▲ +{((parseFloat(rougePPO) - parseFloat(rougeSFT)) * 100).toFixed(1)}% improvement
)}
))}
)}
{/* 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
═══════════════════════════════════ */}
);
}