Spaces:
Sleeping
Sleeping
| 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} | {e.year} | {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> | |
| ); | |
| } | |