Abhinav633's picture
Update frontend/src/App.jsx
67ffe67 verified
Raw
History Blame Contribute Delete
19.1 kB
import { useState, useRef, useCallback } from "react";
const API_BASE = "";
const SEV_COLOR = { high: "#ff4d6d", medium: "#ff9f1c", low: "#2ec4b6" };
const SEV_BG = { high: "#fff0f3", medium: "#fff8ee", low: "#f0fafa" };
function Badge({ sev }) {
const col = SEV_COLOR[sev] || "#888";
return (
<span style={{
background: `${col}20`, color: col, borderRadius: 4,
padding: "2px 8px", fontSize: 11, fontWeight: 600,
textTransform: "uppercase", marginLeft: 6,
}}>{sev}</span>
);
}
function StatCard({ value, label, color, bg }) {
return (
<div style={{
background: bg, borderLeft: `4px solid ${color}`,
borderRadius: 10, padding: "16px 22px", minWidth: 110,
}}>
<div style={{ fontSize: 36, fontWeight: 800, color }}>{value}</div>
<div style={{ fontSize: 11, color: "#888", textTransform: "uppercase", letterSpacing: ".06em" }}>{label}</div>
</div>
);
}
export default function App() {
const [files, setFiles] = useState([]);
const [dragging, setDragging] = useState(false);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [statusMsg, setStatusMsg] = useState("");
const [results, setResults] = useState(null);
const [error, setError] = useState(null);
const [activeTab, setActiveTab] = useState("ranked");
const fileRef = useRef();
const addFiles = useCallback((incoming) => {
const pdfs = Array.from(incoming).filter(f => f.type === "application/pdf");
if (pdfs.length < incoming.length) {
setError("Only PDF files are supported.");
}
setFiles(prev => {
const existing = new Set(prev.map(f => f.name));
return [...prev, ...pdfs.filter(f => !existing.has(f.name))];
});
}, []);
const removeFile = (name) => setFiles(f => f.filter(x => x.name !== name));
const reset = () => {
setFiles([]); setResults(null); setError(null);
setProgress(0); setStatusMsg(""); setActiveTab("ranked");
};
const analyze = async () => {
if (files.length < 2) { setError("Please upload at least 2 PDF files."); return; }
setError(null); setLoading(true); setProgress(10); setResults(null);
// Fake progress while waiting
const ticker = setInterval(() => {
setProgress(p => p < 85 ? p + 3 : p);
}, 1200);
const msgs = [
"Extracting text from PDFs...",
"Sending to Groq for protocol extraction...",
"Comparing protocols across papers...",
"Generating contradiction report...",
];
let mi = 0;
setStatusMsg(msgs[mi]);
const msgTicker = setInterval(() => {
mi = Math.min(mi + 1, msgs.length - 1);
setStatusMsg(msgs[mi]);
}, 6000);
try {
const form = new FormData();
files.forEach(f => form.append("files", f));
const resp = await fetch(`${API_BASE}/analyze`, { method: "POST", body: form });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || `Server error ${resp.status}`);
}
const data = await resp.json();
setProgress(100);
setStatusMsg("Analysis complete!");
setResults(data);
setActiveTab("ranked");
} catch (e) {
setError(e.message);
} finally {
clearInterval(ticker);
clearInterval(msgTicker);
setLoading(false);
}
};
const downloadReport = () => {
if (!results?.html_report) return;
const blob = new Blob([results.html_report], { type: "text/html" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "protocol_report.html";
a.click();
URL.revokeObjectURL(url);
};
const comp = results?.comparison || {};
const summary = comp.summary || {};
const contras = comp.contradictions || [];
const ranked = comp.ranked_issues || [];
const optimal = comp.optimal_protocol || {};
const paperNames = results?.paper_names || [];
const extracted = results?.extracted || [];
return (
<div style={{ fontFamily: "'Segoe UI', sans-serif", background: "#f7f7fa", minHeight: "100vh", padding: "40px 24px" }}>
<div style={{ maxWidth: 1100, margin: "0 auto" }}>
{/* ── Header ── */}
<div style={{ background: "#1a1a2e", borderRadius: 12, padding: "32px 36px", marginBottom: 28, color: "white" }}>
<div style={{ fontSize: 11, letterSpacing: ".15em", textTransform: "uppercase", color: "#a78bfa", marginBottom: 8 }}>
ContraGenAI · Bioengineering · Reproducibility
</div>
<h1 style={{ fontSize: 28, fontWeight: 800, margin: "0 0 8px" }}>ContraGenAI</h1>
<div style={{ fontSize: 14, color: "#a78bfa", marginBottom: 8 }}>A Protocol Contradiction Detector</div>
<p style={{ color: "#aab", fontSize: 13, lineHeight: 1.6, maxWidth: 620 }}>
Upload 2 or more research papers and detect methodological contradictions across experimental protocols —
reagents, temperatures, cell lines, antibodies, timing and more.
<b style={{ color: "#a78bfa" }}>ContraGenAI</b> · Powered by Groq + LLaMA 3.3 70B
</p>
</div>
{/* ── Upload section ── */}
{!results && (
<div style={{ background: "white", borderRadius: 12, padding: "24px 28px", marginBottom: 20, boxShadow: "0 1px 4px rgba(0,0,0,.07)" }}>
<div style={{ fontSize: 16, fontWeight: 700, borderLeft: "4px solid #7b2d8b", paddingLeft: 12, marginBottom: 18 }}>
Upload Research Papers
</div>
{/* Drop zone */}
<div
onClick={() => fileRef.current?.click()}
onDragOver={e => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
onDrop={e => { e.preventDefault(); setDragging(false); addFiles(e.dataTransfer.files); }}
style={{
border: `2px dashed ${dragging ? "#7b2d8b" : "#ddd"}`,
borderRadius: 10, padding: "36px 24px", textAlign: "center",
cursor: "pointer", background: dragging ? "#f5f0ff" : "#fafafa",
transition: "all .2s", marginBottom: 16,
}}
>
<input ref={fileRef} type="file" multiple accept=".pdf"
style={{ display: "none" }} onChange={e => addFiles(e.target.files)} />
<div style={{ fontSize: 32, marginBottom: 10 }}>📄</div>
<div style={{ fontWeight: 600, color: "#1a1a2e", marginBottom: 4 }}>
Drag and drop PDFs here
</div>
<div style={{ fontSize: 12, color: "#aaa" }}>or click to browse · multiple files supported</div>
</div>
{/* File list */}
{files.length > 0 && (
<div style={{ marginBottom: 20 }}>
{files.map(f => (
<div key={f.name} style={{
display: "flex", alignItems: "center", gap: 12,
background: "#f5f0ff", borderRadius: 6, padding: "8px 14px", marginBottom: 6,
}}>
<span style={{ fontSize: 16 }}>📄</span>
<span style={{ flex: 1, fontSize: 13, color: "#1a1a2e" }}>{f.name}</span>
<span style={{ fontSize: 11, color: "#aaa" }}>{(f.size / 1024).toFixed(0)} KB</span>
<button onClick={() => removeFile(f.name)}
style={{ background: "none", border: "none", cursor: "pointer", color: "#aaa", fontSize: 18, lineHeight: 1 }}>×</button>
</div>
))}
<div style={{ fontSize: 12, color: "#aaa", marginTop: 4 }}>{files.length} file(s) ready</div>
</div>
)}
{/* Error */}
{error && (
<div style={{ background: "#fff0f3", border: "1px solid #ff4d6d40", borderRadius: 8, padding: "12px 16px", color: "#ff4d6d", fontSize: 13, marginBottom: 16 }}>
⚠ {error}
</div>
)}
{/* Progress */}
{loading && (
<div style={{ marginBottom: 20 }}>
<div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, color: "#888", marginBottom: 6 }}>
<span>{statusMsg}</span>
<span>{progress}%</span>
</div>
<div style={{ height: 6, background: "#eee", borderRadius: 3, overflow: "hidden" }}>
<div style={{
height: "100%", borderRadius: 3,
background: "linear-gradient(90deg,#7b2d8b,#2ec4b6)",
width: `${progress}%`, transition: "width .4s ease",
}} />
</div>
</div>
)}
{/* Analyze button */}
<button
onClick={analyze}
disabled={loading || files.length < 2}
style={{
padding: "13px 32px", background: files.length >= 2 && !loading ? "#7b2d8b" : "#ddd",
color: files.length >= 2 && !loading ? "white" : "#aaa",
border: "none", borderRadius: 8, fontWeight: 700, fontSize: 14,
cursor: files.length >= 2 && !loading ? "pointer" : "not-allowed",
transition: "all .2s",
}}
>
{loading ? "Analyzing..." : `Detect Contradictions →`}
</button>
{files.length < 2 && !loading && (
<span style={{ marginLeft: 14, fontSize: 12, color: "#aaa" }}>
Add {2 - files.length} more paper{files.length === 1 ? "" : "s"} to start
</span>
)}
</div>
)}
{/* ── Results ── */}
{results && (
<>
{/* Results header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 20, flexWrap: "wrap", gap: 12 }}>
<div>
<div style={{ fontSize: 22, fontWeight: 800, color: "#1a1a2e" }}>Analysis Results</div>
<div style={{ fontSize: 13, color: "#888", marginTop: 3 }}>
{paperNames.length} papers · {comp.protocol_type || "Protocol"}
</div>
</div>
<div style={{ display: "flex", gap: 10 }}>
<button onClick={downloadReport} style={{
padding: "10px 20px", background: "#2ec4b6", color: "white",
border: "none", borderRadius: 8, fontWeight: 600, fontSize: 13, cursor: "pointer",
}}>⬇ Download Report</button>
<button onClick={reset} style={{
padding: "10px 20px", background: "white", color: "#1a1a2e",
border: "1px solid #ddd", borderRadius: 8, fontWeight: 600, fontSize: 13, cursor: "pointer",
}}>← New Analysis</button>
</div>
</div>
{/* Summary stats */}
<div style={{ display: "flex", gap: 14, flexWrap: "wrap", marginBottom: 20 }}>
<StatCard value={summary.total_contradictions ?? 0} label="Total" color="#7b2d8b" bg="#f5f0ff" />
<StatCard value={summary.high ?? 0} label="High" color="#ff4d6d" bg="#fff0f3" />
<StatCard value={summary.medium ?? 0} label="Medium" color="#ff9f1c" bg="#fff8ee" />
<StatCard value={summary.low ?? 0} label="Low" color="#2ec4b6" bg="#f0fafa" />
</div>
{/* Tab nav */}
<div style={{ display: "flex", gap: 0, borderBottom: "1px solid #eee", marginBottom: 20 }}>
{[
{ id: "ranked", label: "Ranked Issues" },
{ id: "table", label: "Comparison Table" },
{ id: "optimal", label: "Optimal Protocol" },
{ id: "extraction", label: "Extraction Summary" },
].map(t => (
<button key={t.id} onClick={() => setActiveTab(t.id)} style={{
padding: "10px 20px", background: "none", border: "none",
borderBottom: activeTab === t.id ? "2px solid #7b2d8b" : "2px solid transparent",
color: activeTab === t.id ? "#7b2d8b" : "#888",
fontWeight: activeTab === t.id ? 600 : 400,
fontSize: 13, cursor: "pointer", marginBottom: -1,
}}>{t.label}</button>
))}
</div>
{/* Tab: Ranked */}
{activeTab === "ranked" && (
<div style={{ background: "white", borderRadius: 12, padding: "24px 28px", boxShadow: "0 1px 4px rgba(0,0,0,.07)" }}>
{ranked.length === 0
? <p style={{ color: "#aaa" }}>No ranked issues found.</p>
: ranked.map((item, i) => {
const sev = item.severity || "low";
const col = SEV_COLOR[sev] || "#888";
const bg = SEV_BG[sev] || "#fafafa";
return (
<div key={i} style={{
display: "flex", gap: 14, background: bg,
border: `1px solid ${col}30`, borderRadius: 8,
padding: "14px 16px", marginBottom: 10,
}}>
<div style={{ fontSize: 24, fontWeight: 800, color: col, minWidth: 34 }}>#{item.rank}</div>
<div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#1a1a2e" }}>
{item.parameter}<Badge sev={sev} />
</div>
<div style={{ fontSize: 12, color: "#555", marginTop: 4, lineHeight: 1.5 }}>{item.brief}</div>
</div>
</div>
);
})
}
</div>
)}
{/* Tab: Comparison Table */}
{activeTab === "table" && (
<div style={{ background: "white", borderRadius: 12, padding: "24px 28px", boxShadow: "0 1px 4px rgba(0,0,0,.07)", overflowX: "auto" }}>
{contras.length === 0
? <p style={{ color: "#aaa" }}>No contradictions found.</p>
: (
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr>
{["Parameter", "Category", "Severity",
...paperNames.map((n, i) => `Paper ${i + 1}`),
"Impact on Reproducibility"
].map(h => (
<th key={h} style={{
background: "#f5f0ff", fontSize: 11, textTransform: "uppercase",
letterSpacing: ".06em", padding: "10px 14px", textAlign: "left",
}}>{h}</th>
))}
</tr>
</thead>
<tbody>
{contras.map((c, i) => {
const sev = c.severity || "low";
const col = SEV_COLOR[sev] || "#888";
return (
<tr key={i}>
<td style={{ padding: "10px 14px", borderBottom: "1px solid #f0f0f5", fontWeight: 600 }}>{c.parameter}</td>
<td style={{ padding: "10px 14px", borderBottom: "1px solid #f0f0f5", color: "#888", fontSize: 12 }}>{c.category}</td>
<td style={{ padding: "10px 14px", borderBottom: "1px solid #f0f0f5" }}><Badge sev={sev} /></td>
{paperNames.map((_, j) => (
<td key={j} style={{ padding: "10px 14px", borderBottom: "1px solid #f0f0f5", fontFamily: "monospace", fontSize: 12 }}>
{c.values?.[`paper_${j}`] || "—"}
</td>
))}
<td style={{ padding: "10px 14px", borderBottom: "1px solid #f0f0f5", fontSize: 12, color: "#444", lineHeight: 1.5, maxWidth: 260 }}>{c.explanation}</td>
</tr>
);
})}
</tbody>
</table>
)
}
</div>
)}
{/* Tab: Optimal Protocol */}
{activeTab === "optimal" && (
<div style={{ background: "white", borderRadius: 12, padding: "24px 28px", boxShadow: "0 1px 4px rgba(0,0,0,.07)" }}>
<p style={{ fontSize: 13, color: "#444", lineHeight: 1.6, marginBottom: 20 }}>{optimal.rationale}</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(240px,1fr))", gap: 14 }}>
{(optimal.parameters || []).map((p, i) => (
<div key={i} style={{ borderLeft: "3px solid #2ec4b6", paddingLeft: 12 }}>
<div style={{ fontSize: 11, textTransform: "uppercase", color: "#aaa", letterSpacing: ".08em" }}>{p.label}</div>
<div style={{ fontSize: 14, fontWeight: 600, color: "#1a1a2e", margin: "3px 0" }}>{p.value}</div>
<div style={{ fontSize: 12, color: "#666", lineHeight: 1.5 }}>{p.reason}</div>
</div>
))}
</div>
</div>
)}
{/* Tab: Extraction Summary */}
{activeTab === "extraction" && (
<div style={{ background: "white", borderRadius: 12, padding: "24px 28px", boxShadow: "0 1px 4px rgba(0,0,0,.07)" }}>
{extracted.map((e, i) => {
const cats = e.parameters || {};
const total = Object.values(cats).reduce((s, v) => s + (Array.isArray(v) ? v.length : 0), 0);
return (
<div key={i} style={{ background: "#fafafa", border: "1px solid #eee", borderRadius: 8, padding: "14px 16px", marginBottom: 12 }}>
<div style={{ fontWeight: 600, color: "#1a1a2e", marginBottom: 4 }}>Paper {i + 1}: {e.title || e._filename}</div>
<div style={{ fontSize: 12, color: "#888", marginBottom: 6 }}>
{e.authors} &nbsp;|&nbsp; {e.year} &nbsp;|&nbsp; {e.protocol_type}
</div>
<div style={{ fontSize: 12, color: "#7b2d8b", fontWeight: 600 }}>
{total} parameters extracted across {Object.keys(cats).length} categories
</div>
</div>
);
})}
</div>
)}
</>
)}
</div>
</div>
);
}