import { useState, useEffect, useRef, useCallback } from "react";
const API = "/api";
/* ═══════════════════════════════════════════════════════════════════════════ */
/* ICONS */
/* ═══════════════════════════════════════════════════════════════════════════ */
const Shield = ({ className = "" }) => (
);
const Search = () => (
);
const Check = () => (
);
const XIcon = () => (
);
const ChevDown = () => (
);
const Zap = () => (
);
const Upload = () => (
);
const FileText = () => (
);
const Trash = () => (
);
const EXAMPLES = [
"When was Python released and who created it?",
"What caused World War I?",
"Tell me about artificial intelligence history.",
"How does the human body work?",
"What is climate change and what causes it?",
"Tell me about the Renaissance period.",
"How did the internet develop?",
"Tell me about quantum physics.",
];
/* ═══════════════════════════════════════════════════════════════════════════ */
/* SMALL COMPONENTS */
/* ═══════════════════════════════════════════════════════════════════════════ */
function Metric({ label, value, color = "text-indigo-400" }) {
return (
);
}
function ClaimCard({ claim }) {
const [open, setOpen] = useState(false);
const ok = claim.is_supported;
return (
setOpen(!open)}>
{ok ? : }
{claim.text}
Similarity: {(claim.similarity_score * 100).toFixed(1)}%
|
Entailment: {claim.entailment_label}
{open && claim.best_evidence && (
Best Evidence
{claim.best_evidence}
)}
);
}
function EvidenceCard({ ev, idx }) {
return (
#{idx + 1} — {ev.source}
Score: {(ev.similarity_score * 100).toFixed(1)}%
{ev.content}
);
}
function ResponseRenderer({ text }) {
// Detect comparison responses and render as table
if (text.startsWith("Comparison between")) {
return ;
}
// Detect list responses (filter results like "Students with % greater than...")
if (/^\d+\s+students\s+have|^Students with .* found\):/i.test(text) || /^\s*\d+\.\s+/m.test(text)) {
return ;
}
// Detect detail responses ("Details for X:" or "For X:")
if (/^(Details for|For )\S/i.test(text)) {
return ;
}
// Default: plain text with line breaks
return {text}
;
}
function ComparisonTable({ text }) {
const lines = text.split("\n").filter(l => l.trim());
// First line: "Comparison between X and Y:"
const header = lines[0];
const nameMatch = header.match(/Comparison between (.+?) and (.+?):/);
const name1 = nameMatch?.[1] || "Student 1";
const name2 = nameMatch?.[2] || "Student 2";
// Parse rows: " Subject: val1 vs val2 marker"
const rows = [];
let summary = [];
for (let i = 1; i < lines.length; i++) {
const vsMatch = lines[i].match(/^\s*(.+?):\s*([\d.]+)\s+vs\s+([\d.]+)\s*(.*)/);
if (vsMatch) {
const [, subject, v1, v2, marker] = vsMatch;
rows.push({ subject: subject.trim(), v1: parseFloat(v1), v2: parseFloat(v2), marker: marker.trim() });
} else if (!lines[i].match(/^Comparison between/)) {
summary.push(lines[i].trim());
}
}
return (
Subject
{name1}
{name2}
Result
{rows.map((r, i) => {
const diff = r.v1 - r.v2;
const cls1 = diff > 0 ? "text-emerald-400 font-semibold" : diff < 0 ? "text-red-400" : "text-slate-300";
const cls2 = diff < 0 ? "text-emerald-400 font-semibold" : diff > 0 ? "text-red-400" : "text-slate-300";
const badge = diff > 0
? +{diff.toFixed(1)}
: diff < 0
? {diff.toFixed(1)}
: Equal ;
return (
{r.subject}
{r.v1}
{r.v2}
{badge}
);
})}
{summary.length > 0 && (
{summary.map((s, i) =>
{s}
)}
)}
);
}
function ListResponse({ text }) {
const lines = text.split("\n").filter(l => l.trim());
const header = lines[0];
const items = lines.slice(1).filter(l => /^\s*\d+\./.test(l));
const rest = lines.slice(1).filter(l => !/^\s*\d+\./.test(l));
return (
{header}
{items.length > 0 && (
#
Name
Value
{items.map((item, i) => {
const m = item.match(/^\s*(\d+)\.\s+(.+?)\s*[—–-]\s*([\d.]+)/);
if (!m) return null;
return (
{m[1]}
{m[2].trim()}
{m[3]}
);
})}
)}
{rest.map((r, i) =>
{r}
)}
);
}
function DetailTable({ text }) {
const lines = text.split("\n").filter(l => l.trim());
const header = lines[0];
const items = lines.slice(1).filter(l => l.includes(":"));
return (
{header}
{items.length > 0 && (
{items.map((item, i) => {
const m = item.match(/^\s*-?\s*(.+?):\s*(.+)/);
if (!m) return null;
return (
{m[1].trim()}
{m[2].trim()}
);
})}
)}
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* TAB: UPLOAD DOCUMENTS */
/* ═══════════════════════════════════════════════════════════════════════════ */
function UploadTab({ onStatusChange, onSwitchToQuery }) {
const [uploading, setUploading] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [dragOver, setDragOver] = useState(false);
const [error, setError] = useState(null);
const fileInput = useRef(null);
// Load already-uploaded files on mount
useEffect(() => {
fetch(`${API}/status`).then(r => r.json()).then(d => {
setUploadedFiles(d.uploaded_files.map(f => ({ name: f, status: "done" })));
}).catch(() => {});
}, []);
const uploadFile = async (file) => {
setError(null);
const ext = file.name.split(".").pop().toLowerCase();
if (!["txt", "pdf", "docx", "xlsx", "xls", "csv"].includes(ext)) {
setError(`Unsupported file: .${ext}. Use .txt, .pdf, .docx, .xlsx, .xls, or .csv`);
return;
}
setUploading(true);
setUploadedFiles(prev => [...prev, { name: file.name, status: "uploading" }]);
const form = new FormData();
form.append("file", file);
try {
const res = await fetch(`${API}/upload`, { method: "POST", body: form });
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.detail || "Upload failed");
}
const data = await res.json();
setUploadedFiles(prev =>
prev.map(f => f.name === file.name ? { ...f, status: "done", chunks: data.chunks_added } : f)
);
onStatusChange?.();
// Auto-switch to query tab after successful upload
setTimeout(() => onSwitchToQuery?.(), 1000);
} catch (e) {
setError(e.message);
setUploadedFiles(prev => prev.filter(f => f.name !== file.name || f.status !== "uploading"));
}
setUploading(false);
};
const handleFiles = (files) => {
Array.from(files).forEach(uploadFile);
};
const handleDrop = (e) => {
e.preventDefault();
setDragOver(false);
handleFiles(e.dataTransfer.files);
};
const clearAll = async () => {
if (!confirm("Delete all uploaded files?")) return;
try {
await fetch(`${API}/clear-uploads`, { method: "POST" });
setUploadedFiles([]);
onStatusChange?.();
} catch (e) { console.error(e); }
};
const deleteFile = async (filename) => {
if (!confirm(`Delete "${filename}"?`)) return;
try {
const res = await fetch(`${API}/delete-file`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename }),
});
if (res.ok) {
setUploadedFiles(prev => prev.filter(f => f.name !== filename));
onStatusChange?.();
}
} catch (e) { console.error(e); }
};
const doneFiles = uploadedFiles.filter(f => f.status === "done");
return (
{/* Drop zone */}
{ e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)}
onDrop={handleDrop}
onClick={() => fileInput.current?.click()}
>
handleFiles(e.target.files)}
/>
{dragOver ? "Drop files here" : "Upload your documents"}
Drag & drop or click to browse — TXT, PDF, DOCX, Excel, CSV
{uploading && (
)}
{error && (
{error}
)}
{/* Uploaded files list */}
{doneFiles.length > 0 && (
Uploaded Documents ({doneFiles.length})
Clear all uploads
{doneFiles.map((f, i) => (
{f.name}
{f.chunks ? `${f.chunks} chunks extracted` : "Processed"}
deleteFile(f.name)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-slate-500 hover:text-red-400 hover:bg-red-950/30 opacity-0 group-hover:opacity-100 transition-all"
title={`Delete ${f.name}`}
>
))}
)}
{/* Hint */}
How it works:
Upload any document (TXT, PDF, DOCX, Excel, or CSV)
The system extracts text and splits it into searchable chunks
Switch to the Query tab and ask questions about your document
Every claim in the answer is verified against your uploaded content
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* TAB: QUERY */
/* ═══════════════════════════════════════════════════════════════════════════ */
function QueryTab({ chunkCount }) {
const [query, setQuery] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [showEvidence, setShowEvidence] = useState(false);
const run = async (q) => {
const text = q || query;
if (!text.trim()) return;
setQuery(text);
setLoading(true);
setResult(null);
try {
const res = await fetch(`${API}/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: text }),
});
setResult(await res.json());
} catch (e) { console.error(e); }
setLoading(false);
};
return (
{/* No docs warning */}
{chunkCount === 0 && (
No documents loaded. Go to Upload tab to add documents first.
)}
{/* Search bar */}
{/* Example chips */}
{EXAMPLES.slice(0, 5).map((ex) => (
run(ex)}
disabled={chunkCount === 0}
className="text-xs bg-slate-800/60 hover:bg-slate-700/60 disabled:opacity-40 border border-slate-700/40 text-slate-300 hover:text-white py-1.5 px-3 rounded-full transition-all"
>
{ex.length > 40 ? ex.slice(0, 38) + "..." : ex}
))}
{/* Loading */}
{loading && (
)}
{/* Results */}
{result && !loading && (
{/* Status banner */}
{result.is_verified ? "✅" : result.supported_claims === 0 ? "❌" : "⚠️"}
{result.is_verified ? "Verified Response" : result.supported_claims === 0 ? "Hallucinated Response" : "Partially Verified"}
{result.supported_claims}/{result.total_claims} claims supported
{(result.support_ratio * 100).toFixed(0)}%
support ratio
{/* Metrics */}
{/* Response */}
Response
{result.elapsed_seconds}s
{/* Progress bar */}
Verification Progress
{result.supported_claims}/{result.total_claims}
{/* Prompt Refinement Suggestion */}
{!result.is_verified && result.total_claims > 0 && (
Prompt Refinement Suggested
The response could not be fully verified. Try refining your query to get better results:
{result.claims.filter(c => !c.is_supported).slice(0, 3).map((c, i) => (
Unverified: {c.text.length > 120 ? c.text.slice(0, 118) + "..." : c.text}
))}
run(`Based on the uploaded document, ${query}`)}
className="text-xs bg-indigo-600/30 hover:bg-indigo-600/50 border border-indigo-500/30 text-indigo-300 py-1.5 px-3 rounded-full transition-all"
>
Try: "Based on the uploaded document, {query.length > 30 ? query.slice(0, 28) + "..." : query}"
run(`Explain in detail: ${query}`)}
className="text-xs bg-indigo-600/30 hover:bg-indigo-600/50 border border-indigo-500/30 text-indigo-300 py-1.5 px-3 rounded-full transition-all"
>
Try: "Explain in detail: {query.length > 30 ? query.slice(0, 28) + "..." : query}"
run(`What does the document say about ${query.replace(/^(what is|explain|describe|tell me about)\s*/i, '')}`)}
className="text-xs bg-indigo-600/30 hover:bg-indigo-600/50 border border-indigo-500/30 text-indigo-300 py-1.5 px-3 rounded-full transition-all"
>
Try: "What does the document say about..."
)}
{/* Claims */}
{result.claims.length > 0 && (
Claims Breakdown
{result.claims.map((c, i) => )}
)}
{/* Evidence */}
{result.evidence.length > 0 && (
setShowEvidence(!showEvidence)} className="flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-slate-400 hover:text-slate-200 transition-colors mb-3">
Retrieved Evidence ({result.evidence.length})
{showEvidence && (
{result.evidence.map((ev, i) => )}
)}
)}
)}
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* TAB: VERIFY CLAIMS */
/* ═══════════════════════════════════════════════════════════════════════════ */
function VerifyTab() {
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const verify = async () => {
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
if (!lines.length) return;
setLoading(true);
setResult(null);
try {
const res = await fetch(`${API}/verify`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ claims: lines }),
});
setResult(await res.json());
} catch (e) { console.error(e); }
setLoading(false);
};
return (
Enter claims to verify (one per line):
{loading ?
: <> Verify Claims>}
{result && (
{result.supported}/{result.total} supported ({(result.ratio * 100).toFixed(0)}%)
{result.results.map((c, i) => )}
)}
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* TAB: ABOUT */
/* ═══════════════════════════════════════════════════════════════════════════ */
function AboutTab() {
return (
How VDHF Works
The Verification-Driven Hallucination Firewall is a post-generation system that detects and blocks
hallucinated content in LLM responses by verifying every factual claim against a trusted knowledge base.
{[
{ n: "1", title: "Retrieve", desc: "Sentence-BERT embeds your query and finds the top-K most relevant document chunks via ChromaDB." },
{ n: "2", title: "Generate", desc: "An LLM (Groq / mock) generates a response grounded in the retrieved evidence context." },
{ n: "3", title: "Extract", desc: "Rule-based decomposition splits the response into atomic, independently verifiable factual claims." },
{ n: "4", title: "Verify", desc: "Each claim is scored for semantic similarity and checked for NLI entailment against evidence." },
{ n: "5", title: "Firewall", desc: "If SupportRatio >= threshold (80%), the response passes. Otherwise, regeneration is triggered." },
{ n: "6", title: "Regenerate", desc: "A refined prompt using only verified evidence is created, and the LLM produces a safer response." },
].map((s) => (
))}
Models & Config
Parameter
Value
{[
["Embedding Model", "all-MiniLM-L6-v2 (Sentence-BERT)"],
["NLI Model", "microsoft/deberta-base-mnli"],
["LLM", "llama-3.3-70b-versatile (Groq)"],
["Similarity Threshold", "0.75"],
["Firewall Threshold", "0.80"],
["Top-K Retrieval", "7"],
["Max Regenerations", "2"],
].map(([k, v]) => (
{k}
{v}
))}
);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* MAIN APP */
/* ═══════════════════════════════════════════════════════════════════════════ */
export default function App() {
const [tab, setTab] = useState("upload");
const [status, setStatus] = useState(null);
const refreshStatus = useCallback(() => {
fetch(`${API}/status`).then((r) => r.json()).then(setStatus).catch(() => {});
}, []);
// Clear uploads on app start (clean slate each session)
useEffect(() => {
fetch(`${API}/clear-uploads`, { method: "POST" }).then(() => refreshStatus()).catch(() => refreshStatus());
}, [refreshStatus]);
const chunkCount = status?.document_chunks ?? 0;
const totalDocs = (status?.documents_loaded?.length ?? 0) + (status?.uploaded_files?.length ?? 0);
const tabs = [
{ id: "upload", label: "Upload", icon: "📁" },
{ id: "query", label: "Query", icon: "🔍" },
{ id: "verify", label: "Verify Claims", icon: "🧪" },
{ id: "about", label: "About", icon: "📖" },
];
return (
{/* ── Hero Header ─── */}
{/* ── Tabs ─── */}
{tabs.map((t) => (
setTab(t.id)}
className={`py-3.5 px-5 text-sm font-medium transition-all border-b-2 ${tab === t.id ? "border-indigo-500 text-indigo-300" : "border-transparent text-slate-400 hover:text-slate-200"}`}
>
{t.icon} {t.label}
))}
{/* ── Content ─── */}
{tab === "upload" && setTab("query")} />}
{tab === "query" && }
{tab === "verify" && }
{tab === "about" && }
{/* ── Footer ─── */}
VDHF — Verification-Driven Hallucination Firewall · Built with React + FastAPI
);
}