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 ═══════════════════════════════════ */}
{/* Logo mark */}
🫁
{/* Titles */}
BioStack
RLHF Based Medical Report Generation
{/* 🌙/☀️ Toggle */}
{/* ═══════════════════════════════════ MAIN GRID ═══════════════════════════════════ */}
{/* ══ LEFT PANEL ══ */}