vettri06's picture
Rename app to Document_intelligence
039f7ec
Raw History Blame Contribute Delete
15.6 kB
import React, { useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { Activity, BarChart3, CheckCircle2, ChevronRight, Clock3, Download, FileChartColumn, FileText, Layers3, LoaderCircle, RefreshCcw, Search, ShieldCheck, Sparkles, Upload, X, AlertTriangle } from "lucide-react";
import "./styles.css";
const API = "";
const MODELS = ["xgboost", "logistic_regression", "random_forest", "linear_svm", "sentence_embedding_lr"];
async function jsonRequest(url, options) {
const response = await fetch(`${API}${url}`, options);
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body.detail || body.error || `Request failed (${response.status})`);
return body;
}
function App() {
const [view, setView] = useState("analysis");
const [inputMode, setInputMode] = useState("upload");
const [samples, setSamples] = useState([]);
const [sampleId, setSampleId] = useState("");
const [file, setFile] = useState(null);
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [batchFiles, setBatchFiles] = useState([]);
const [batchResults, setBatchResults] = useState([]);
const [analyticsModel, setAnalyticsModel] = useState("xgboost");
const [analytics, setAnalytics] = useState(null);
const fileInput = useRef(null);
useEffect(() => { jsonRequest("/api/samples").then(setSamples).catch((e) => setError(e.message)); }, []);
useEffect(() => { if (view === "analytics") jsonRequest(`/api/analytics?model=${analyticsModel}`).then(setAnalytics).catch((e) => setError(e.message)); }, [view, analyticsModel]);
const activeSample = samples.find((item) => item.id === sampleId);
const previewUrl = file ? URL.createObjectURL(file) : sampleId ? `/api/samples/${sampleId}/preview` : "";
async function analyze() {
setError(""); setResult(null); setLoading(true);
try {
if (inputMode === "upload") {
if (!file) throw new Error("Choose a PNG, JPG, JPEG, or PDF document first.");
const data = new FormData(); data.append("file", file);
setResult(await jsonRequest("/api/process", { method: "POST", body: data }));
} else {
if (!sampleId) throw new Error("Select a sample document first.");
setResult(await jsonRequest(`/api/process-sample/${sampleId}`, { method: "POST" }));
}
} catch (e) { setError(e.message); } finally { setLoading(false); }
}
async function runBatch() {
if (!batchFiles.length) return setError("Choose at least one document for batch processing.");
setError(""); setLoading(true);
try { const data = new FormData(); batchFiles.forEach((item) => data.append("files", item)); setBatchResults(await jsonRequest("/api/process-batch", { method: "POST", body: data })); }
catch (e) { setError(e.message); } finally { setLoading(false); }
}
function resetAnalysis() { setFile(null); setSampleId(""); setResult(null); setError(""); }
return <div className="shell">
<header className="topbar">
<div className="brand"><div className="brand-mark"><ShieldCheck size={20}/></div><div><strong>Document_intelligence</strong><span>Document intelligence studio</span></div></div>
<div className="topbar-status"><span className="live-dot"/> Pipeline online <span className="divider"/> CPU inference</div>
</header>
<div className="workspace">
<aside className="sidebar">
<div className="eyebrow">Workspace</div>
<nav>
<NavButton icon={<FileText size={17}/>} label="Document analysis" active={view === "analysis"} onClick={() => setView("analysis")}/>
<NavButton icon={<Layers3 size={17}/>} label="Batch processing" active={view === "batch"} onClick={() => setView("batch")}/>
<NavButton icon={<BarChart3 size={17}/>} label="Model analytics" active={view === "analytics"} onClick={() => setView("analytics")}/>
</nav>
<div className="sidebar-bottom"><div className="mini-label">System status</div><div className="system-line"><span className="live-dot"/> API ready</div><div className="system-line muted"><Activity size={14}/> OCR + classification</div></div>
</aside>
<main className="main">
{error && <div className="alert error"><AlertTriangle size={17}/><span>{error}</span><button onClick={() => setError("")} aria-label="Dismiss"><X size={16}/></button></div>}
{view === "analysis" && <AnalysisView {...{inputMode, setInputMode, samples, sampleId, setSampleId, activeSample, file, setFile, previewUrl, result, loading, analyze, resetAnalysis, fileInput}}/>}
{view === "batch" && <BatchView {...{batchFiles, setBatchFiles, batchResults, runBatch, loading}}/>}
{view === "analytics" && <AnalyticsView {...{analyticsModel, setAnalyticsModel, analytics}}/>}
</main>
</div>
</div>;
}
function NavButton({ icon, label, active, onClick }) { return <button className={`nav-button ${active ? "active" : ""}`} onClick={onClick}>{icon}<span>{label}</span>{active && <ChevronRight size={15}/>}</button>; }
function AnalysisView({ inputMode, setInputMode, samples, sampleId, setSampleId, activeSample, file, setFile, previewUrl, result, loading, analyze, resetAnalysis, fileInput }) {
return <>
<PageHeading kicker="Document analysis" title="Classify, extract, decide" description="Turn an uploaded document into structured fields, confidence scores, and a clear routing decision." action={<button className="ghost-button" onClick={resetAnalysis}><RefreshCcw size={16}/> Reset</button>}/>
<section className="control-strip">
<div className="segmented"><button className={inputMode === "upload" ? "selected" : ""} onClick={() => setInputMode("upload")}><Upload size={15}/> Upload document</button><button className={inputMode === "sample" ? "selected" : ""} onClick={() => setInputMode("sample")}><Sparkles size={15}/> Use sample</button></div>
{inputMode === "sample" && <select value={sampleId} onChange={(e) => setSampleId(e.target.value)}><option value="">Select a sample document</option>{samples.map((item) => <option key={item.id} value={item.id}>{item.label}</option>)}</select>}
</section>
<div className="analysis-grid">
<section className="panel document-panel">
<div className="panel-heading"><div><span className="panel-kicker">Input document</span><h2>{file?.name || activeSample?.label || "No document selected"}</h2></div>{(file || activeSample) && <span className="ready-badge"><CheckCircle2 size={14}/> Ready</span>}</div>
{inputMode === "upload" && <div className="dropzone" onClick={() => fileInput.current?.click()} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); setFile(e.dataTransfer.files[0]); }}><input ref={fileInput} type="file" accept=".png,.jpg,.jpeg,.pdf" onChange={(e) => setFile(e.target.files[0])}/><Upload size={24}/><strong>{file ? "Replace document" : "Choose a document"}</strong><span>PNG, JPG, JPEG, or PDF · maximum 10 MB</span></div>}
{inputMode === "sample" && <div className="sample-note"><Sparkles size={18}/><div><strong>{activeSample?.label || "Choose a sample above"}</strong><span>{activeSample?.description || "Use a curated document to check the pipeline without uploading a file."}</span></div></div>}
{previewUrl && (file?.type?.startsWith("image/") || activeSample) ? <div className="preview-wrap"><img src={previewUrl} alt="Document preview"/></div> : previewUrl ? <div className="pdf-preview"><FileText size={32}/><span>PDF document selected</span></div> : <div className="empty-preview"><FileChartColumn size={28}/><span>Preview appears here after selecting a document</span></div>}
<button className="primary-button analyze-button" disabled={loading} onClick={analyze}>{loading ? <><LoaderCircle className="spin" size={17}/> Analyzing document...</> : <><Search size={17}/> Analyze document</>}</button>
</section>
<ResultsPanel result={result} loading={loading}/>
</div>
</>;
}
function ResultsPanel({ result, loading }) {
if (loading) return <section className="panel result-panel loading-panel"><LoaderCircle className="spin big" size={28}/><h2>Processing document</h2><p>OCR, layout classification, and field extraction are running.</p><div className="progress-line"><span/></div></section>;
if (!result) return <section className="panel result-panel empty-results"><div className="empty-icon"><Activity size={22}/></div><span className="panel-kicker">Analysis output</span><h2>Ready for processing</h2><p>Select a document and run the pipeline to see classification, extracted fields, and routing confidence.</p></section>;
const cls = result.doc_class.replaceAll("_", " ");
return <section className="panel result-panel"><div className="panel-heading"><div><span className="panel-kicker">Analysis output</span><h2>Document decision</h2></div><span className={`class-badge ${result.needs_review ? "review" : "accepted"}`}>{result.needs_review ? "Review" : "Accepted"}</span></div><div className="classification"><div><span>Predicted class</span><strong>{cls}</strong></div><div className="confidence"><span>Class confidence</span><strong>{(result.class_confidence * 100).toFixed(1)}%</strong><div className="meter"><i style={{width: `${result.class_confidence * 100}%`}}/></div></div></div><div className="stat-row"><Stat icon={<FileText size={16}/>} label="OCR words" value={result.ocr_token_count}/><Stat icon={<Clock3 size={16}/>} label="Latency" value={`${result.processing_time_ms} ms`}/></div><div className="section-title">Structured fields</div><div className="field-list">{Object.entries(result.fields || {}).map(([key, value]) => <div className="field-row" key={key}><span>{key.replaceAll("_", " ")}</span><strong>{value ?? "Not detected"}</strong><em>{result.field_confidences?.[key] ? `${(result.field_confidences[key] * 100).toFixed(0)}%` : "—"}</em></div>)}</div><div className={`route-note ${result.needs_review ? "warn" : "good"}`}><ShieldCheck size={17}/><span>{result.routing_reason || (result.needs_review ? "Manual review recommended" : "Confidence thresholds satisfied")}</span></div><div className="download-row"><button className="secondary-button" onClick={() => downloadJson(result)}><Download size={15}/> JSON</button><button className="secondary-button" onClick={() => downloadCsv(result)}><Download size={15}/> CSV</button></div></section>;
}
function Stat({ icon, label, value }) { return <div className="stat"><span>{icon}{label}</span><strong>{value}</strong></div>; }
function BatchView({ batchFiles, setBatchFiles, batchResults, runBatch, loading }) { const input = useRef(null); return <><PageHeading kicker="Batch processing" title="Process a document queue" description="Run the same classification and extraction pipeline across multiple documents."/><section className="panel batch-panel"><div className="panel-heading"><div><span className="panel-kicker">Batch input</span><h2>{batchFiles.length ? `${batchFiles.length} documents selected` : "Select documents"}</h2></div><button className="secondary-button" onClick={() => input.current?.click()}><Upload size={15}/> Add files</button><input ref={input} hidden type="file" multiple accept=".png,.jpg,.jpeg,.pdf" onChange={(e) => setBatchFiles([...e.target.files])}/></div><div className="batch-list">{batchFiles.length ? batchFiles.map((file) => <div className="batch-item" key={file.name}><FileText size={17}/><span>{file.name}</span><small>{(file.size / 1024).toFixed(0)} KB</small></div>) : <div className="empty-preview compact"><Layers3 size={26}/><span>Your selected documents will appear here.</span></div>}</div><button className="primary-button" disabled={loading || !batchFiles.length} onClick={runBatch}>{loading ? <><LoaderCircle className="spin" size={17}/> Running batch...</> : <><Activity size={17}/> Execute batch analysis</>}</button></section>{batchResults.length > 0 && <section className="panel table-panel"><div className="panel-heading"><div><span className="panel-kicker">Batch output</span><h2>Processing summary</h2></div></div><div className="table-scroll"><table><thead><tr><th>Document</th><th>Class</th><th>Confidence</th><th>Decision</th><th>Words</th><th>Latency</th></tr></thead><tbody>{batchResults.map((item) => <tr key={item.file_name}><td>{item.file_name}</td><td>{item.error ? <span className="error-text">Error</span> : item.doc_class}</td><td>{item.confidence ? `${(item.confidence * 100).toFixed(1)}%` : "—"}</td><td>{item.error ? item.error : item.needs_review ? "Review" : "Accepted"}</td><td>{item.words ?? "—"}</td><td>{item.latency_ms ? `${item.latency_ms} ms` : "—"}</td></tr>)}</tbody></table></div></section>}</>; }
function AnalyticsView({ analyticsModel, setAnalyticsModel, analytics }) { return <><PageHeading kicker="Model analytics" title="Benchmark the decision engine" description="Inspect champion model metrics and confusion matrices from the training artifacts."/><section className="analytics-toolbar"><label>Model<select value={analyticsModel} onChange={(e) => setAnalyticsModel(e.target.value)}>{MODELS.map((model) => <option key={model}>{model}</option>)}</select></label></section><div className="analytics-grid"><section className="panel"><div className="panel-heading"><div><span className="panel-kicker">Confusion matrix</span><h2>{analyticsModel.replaceAll("_", " ")}</h2></div></div>{analytics?.matrix_url ? <img className="matrix" src={analytics.matrix_url} alt="Normalized confusion matrix"/> : <div className="empty-preview compact">Matrix artifact not found.</div>}</section><section className="panel"><div className="panel-heading"><div><span className="panel-kicker">Performance report</span><h2>Classification metrics</h2></div></div><MetricsTable data={analytics?.metrics}/></section></div></>; }
function MetricsTable({ data }) { if (!data) return <div className="empty-preview compact">Metrics are not available.</div>; return <div className="table-scroll"><table><thead><tr><th>Class</th><th>Precision</th><th>Recall</th><th>F1</th><th>Support</th></tr></thead><tbody>{Object.entries(data).map(([key, value]) => typeof value === "object" ? <tr key={key}><td>{key}</td><td>{fmt(value.precision)}</td><td>{fmt(value.recall)}</td><td>{fmt(value["f1-score"])}</td><td>{value.support ?? "—"}</td></tr> : null)}</tbody></table></div>; }
function fmt(value) { return typeof value === "number" ? value.toFixed(3) : "—"; }
function PageHeading({ kicker, title, description, action }) { return <div className="page-heading"><div><span className="eyebrow">{kicker}</span><h1>{title}</h1><p>{description}</p></div>{action}</div>; }
function downloadJson(result) { downloadBlob(JSON.stringify(result, null, 2), `${result.doc_id}_results.json`, "application/json"); }
function downloadCsv(result) { const rows = [["Field Name", "Extracted Value", "Confidence"], ...Object.entries(result.fields || {}).map(([key, value]) => [key, value ?? "", result.field_confidences?.[key] ?? ""])]; downloadBlob(rows.map((row) => row.map((cell) => `"${String(cell).replaceAll('"', '""')}"`).join(",")).join("\n"), `${result.doc_id}_fields.csv`, "text/csv"); }
function downloadBlob(content, name, type) { const url = URL.createObjectURL(new Blob([content], {type})); const anchor = document.createElement("a"); anchor.href = url; anchor.download = name; anchor.click(); URL.revokeObjectURL(url); }
createRoot(document.getElementById("root")).render(<App/>);