neuro / src /App.jsx
Narendra5805's picture
first commit
5951777
Raw
History Blame Contribute Delete
19 kB
import { useState, useEffect, useRef } from "react";
import * as pdfjsLib from 'pdfjs-dist';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerSrc;
import "./App.css";
function App() {
// Worker reference
const workerRef = useRef(null);
// Application State
const [modelStatus, setModelStatus] = useState("idle");
const [progressItems, setProgressItems] = useState({});
const [totalProgress, setTotalProgress] = useState(0);
const [errorMsg, setErrorMsg] = useState("");
const [loadTime, setLoadTime] = useState(null);
// Performance metrics
const [loadedDevice, setLoadedDevice] = useState("Detecting...");
const [deviceConfig, setDeviceConfig] = useState("wasm");
const [generationTime, setGenerationTime] = useState(null);
// VLM State
const [imageFile, setImageFile] = useState(null);
const [imagePreviewUrl, setImagePreviewUrl] = useState("");
const [extractedMetadata, setExtractedMetadata] = useState(null);
const [isGenerating, setIsGenerating] = useState(false);
// Initialize Web Worker
useEffect(() => {
workerRef.current = new Worker(new URL("./ai/worker.js", import.meta.url), {
type: "module",
});
const onMessageReceived = (e) => {
const { type, data, error } = e.data;
switch (type) {
case "progress":
setProgressItems((prev) => {
const newItems = { ...prev, [data.file]: data };
const itemsArray = Object.values(newItems);
const loaded = itemsArray.reduce((acc, item) => acc + (item.loaded || 0), 0);
const total = itemsArray.reduce((acc, item) => acc + (item.total || 0), 0);
if (total > 0) {
setTotalProgress((loaded / total) * 100);
}
return newItems;
});
break;
case "ready":
setModelStatus("ready");
setLoadedDevice(data.device === "webgpu" ? "WebGPU" : "WASM CPU");
setLoadTime((performance.now() - loadTime) / 1000);
break;
case "completed":
setExtractedMetadata(data.metadata);
setGenerationTime(data.durationSec);
setIsGenerating(false);
break;
case "error":
console.error("Worker Error:", error);
setErrorMsg(error);
setModelStatus("error");
setIsGenerating(false);
break;
default:
break;
}
};
workerRef.current.onmessage = onMessageReceived;
// Auto-load model on mount (try WebGPU first)
setModelStatus("loading");
setProgressItems({});
setTotalProgress(0);
setErrorMsg("");
setLoadTime(performance.now());
workerRef.current.postMessage({ type: "load", data: { device: "wasm" } });
// Clean up worker on unmount
return () => {
if (workerRef.current) {
workerRef.current.terminate();
}
};
}, []);
function handleDeviceChange(newDevice) {
if (isGenerating || modelStatus === "loading") return;
setDeviceConfig(newDevice);
setModelStatus("loading");
setProgressItems({});
setTotalProgress(0);
setErrorMsg("");
setLoadTime(performance.now());
if (workerRef.current) {
workerRef.current.postMessage({ type: "load", data: { device: newDevice } });
}
}
async function handleClearCache() {
if (window.confirm("Are you sure you want to delete the cached model files? This will require downloading the VLM weights again.")) {
try {
// Clear the custom IndexedDB cache
await new Promise((resolve, reject) => {
const req = indexedDB.deleteDatabase('transformers-cache');
req.onsuccess = resolve;
req.onerror = reject;
req.onblocked = resolve;
});
// Clear fallback Cache API just in case
const cacheKeys = await caches.keys();
for (const key of cacheKeys) {
if (key.includes("onnx") || key.includes("transformers") || key.includes("SmolVLM")) {
await caches.delete(key);
}
}
alert("Successfully cleared cached files from IndexedDB and Cache Storage!");
window.location.reload();
} catch (err) {
alert("Failed to clear cache: " + err.message);
}
}
}
async function handleImageUpload(e) {
const file = e.target.files[0];
if (!file) return;
setImageFile(file);
setExtractedMetadata(null);
setGenerationTime(null);
if (file.type === "application/pdf") {
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport }).promise;
const dataUrl = canvas.toDataURL("image/png");
setImagePreviewUrl(dataUrl);
} catch (err) {
console.error("PDF Parsing error:", err);
alert("Failed to read PDF.");
}
} else {
const reader = new FileReader();
reader.onload = (ev) => {
setImagePreviewUrl(ev.target.result);
};
reader.readAsDataURL(file);
}
}
function handleExtractName() {
if (!imagePreviewUrl || isGenerating || modelStatus !== "ready") return;
setIsGenerating(true);
setExtractedMetadata(null);
setGenerationTime(null);
const img = new Image();
img.onload = () => {
// Crop 1: Top Quarter (25%)
const canvasQuarter = document.createElement("canvas");
const ctxQuarter = canvasQuarter.getContext("2d");
canvasQuarter.width = img.width;
canvasQuarter.height = img.height * 0.25;
ctxQuarter.drawImage(img, 0, 0, img.width, img.height * 0.25, 0, 0, canvasQuarter.width, canvasQuarter.height);
// Crop: Top Half (50%) — enough context for both header info and subject deduction
const canvasHalf = document.createElement("canvas");
const ctxHalf = canvasHalf.getContext("2d");
canvasHalf.width = img.width;
canvasHalf.height = img.height * 0.50;
ctxHalf.drawImage(img, 0, 0, img.width, img.height * 0.50, 0, 0, canvasHalf.width, canvasHalf.height);
const croppedHalf = canvasHalf.toDataURL("image/png");
if (workerRef.current) {
workerRef.current.postMessage({
type: "generate",
data: {
imageFileOrUrlTopHalf: croppedHalf,
requestId: "vlm-extract"
}
});
}
};
img.src = imagePreviewUrl;
}
function handleDownloadRenamed() {
if (!imageFile || !extractedMetadata) return;
// Clean name: remove all spaces, punctuation, leaving only alphanumeric
const cleanName = (extractedMetadata.name || "Unknown")
.replace(/[^a-zA-Z0-9]/g, "");
// Clean roll number: remove any non-alphanumeric characters (like trailing periods)
const cleanRoll = (extractedMetadata.rollNo || "")
.replace(/[^a-zA-Z0-9]/g, "");
// Map subjects to their clean abbreviations (Maths, Bio, Phy, Chem, Eco)
const rawSubject = (extractedMetadata.subject || "").trim().toLowerCase();
let subAbbr = "Sub";
if (rawSubject.includes("math")) subAbbr = "Maths";
else if (rawSubject.includes("bio")) subAbbr = "Bio";
else if (rawSubject.includes("phys")) subAbbr = "Phy";
else if (rawSubject.includes("chem")) subAbbr = "Chem";
else if (rawSubject.includes("econ")) subAbbr = "Eco";
else {
// Fallback: clean the subject string and capitalize it
const cleaned = rawSubject.replace(/[^a-z0-9]/gi, '');
subAbbr = cleaned ? (cleaned.charAt(0).toUpperCase() + cleaned.slice(1)) : "Sub";
}
// Retain original extension (default to .pdf if missing)
const extension = imageFile.name.includes('.') ? imageFile.name.split('.').pop() : 'pdf';
// Format: StudentNameROLL_sub (e.g. AadarshPatel21_Maths.pdf)
const newFilename = `${cleanName}${cleanRoll}_${subAbbr}.${extension}`;
// Create download link
const url = URL.createObjectURL(imageFile);
const a = document.createElement('a');
a.href = url;
a.download = newFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return (
<div className="app-container">
{/* Header Panel */}
<div className="header-section">
<div className="title-group">
<h1>Student Name Extractor</h1>
<p>Powered by SmolVLM-500M-Instruct running {modelStatus === "ready" && loadedDevice === "WebGPU" ? "on GPU via WebGPU" : "on CPU via WebAssembly"}</p>
<div className="backend-control" style={{ marginTop: "10px", display: "flex", gap: "12px", alignItems: "center", flexWrap: "wrap" }}>
<div style={{ display: "flex", gap: "6px", alignItems: "center" }}>
<span style={{ fontSize: "13px", color: "rgba(255, 255, 255, 0.6)" }}>Device: <strong>WebAssembly (CPU)</strong></span>
</div>
<button
onClick={handleClearCache}
disabled={modelStatus === "loading" || isGenerating}
style={{
background: "rgba(239, 68, 68, 0.15)",
border: "1px solid rgba(239, 68, 68, 0.3)",
borderRadius: "6px",
color: "#f87171",
padding: "4px 8px",
fontSize: "12px",
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "4px",
}}
>
🗑️ Clear Storage Cache
</button>
</div>
</div>
<div className="model-badge">
<span className={`pulse-dot ${modelStatus}`}></span>
{modelStatus === "idle" && "Model Not Loaded"}
{modelStatus === "loading" && `Downloading Weights: ${totalProgress.toFixed(0)}%`}
{modelStatus === "ready" && `Model Ready (${loadedDevice})`}
{modelStatus === "error" && "Load Failed"}
</div>
</div>
{/* Main Content Area */}
{modelStatus === "error" && (
<div className="error-banner">
⚠️ <strong>Error loading model:</strong> {errorMsg}
<br />
<button onClick={() => window.location.reload()} style={{ marginTop: "10px", padding: "5px 10px", borderRadius: "4px", background: "rgba(255,255,255,0.1)", color: "white", border: "none", cursor: "pointer" }}>Retry Loading</button>
</div>
)}
{modelStatus !== "ready" && modelStatus !== "error" && (
<div className="glass-panel" style={{ textAlign: "center", padding: "60px 20px" }}>
<h2>Initializing VLM Scanner Engine</h2>
<p style={{ color: "rgba(255,255,255,0.7)", marginTop: "10px", marginBottom: "30px" }}>
Downloading and compiling the SmolVLM-500M-Instruct model. <br />
This will take a few minutes on the first run as it caches the ~800MB weights.
</p>
<div className="progress-container" style={{ width: "80%", margin: "0 auto", height: "12px", background: "rgba(255,255,255,0.1)", borderRadius: "6px", overflow: "hidden" }}>
<div className="progress-bar" style={{ width: `${totalProgress}%`, height: "100%", background: "var(--primary)", transition: "width 0.3s ease" }}></div>
</div>
</div>
)}
{modelStatus === "ready" && (
<div className="vlm-scanner-container" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "20px" }}>
{/* Upload Panel */}
<div className="glass-panel" style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<h3>1. Upload Document</h3>
<p style={{ fontSize: "14px", color: "rgba(255,255,255,0.6)" }}>
Upload an image of a document (PNG, JPG, WebP) containing a student's name.
</p>
<div
style={{
border: "2px dashed rgba(255,255,255,0.2)",
borderRadius: "12px",
padding: "40px",
textAlign: "center",
position: "relative",
cursor: "pointer",
transition: "all 0.2s ease",
backgroundColor: "rgba(0,0,0,0.2)"
}}
onClick={() => document.getElementById("file-upload").click()}
>
<input
id="file-upload"
type="file"
accept="image/*,application/pdf"
onChange={handleImageUpload}
style={{ display: "none" }}
/>
<span style={{ fontSize: "32px", display: "block", marginBottom: "10px" }}>📄</span>
<span style={{ fontWeight: "500" }}>Click to select a document</span>
</div>
{imagePreviewUrl && (
<button
onClick={handleExtractName}
disabled={isGenerating}
className="send-button"
style={{ padding: "14px", width: "100%", fontSize: "16px", borderRadius: "8px", marginTop: "auto" }}
>
{isGenerating ? "Scanning Document..." : "Extract Metadata"}
</button>
)}
</div>
{/* Preview & Results Panel */}
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
<div className="glass-panel" style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "15px" }}>
<h3 style={{ margin: 0 }}>Document Preview</h3>
</div>
<div style={{
flex: 1,
backgroundColor: "rgba(0,0,0,0.3)",
borderRadius: "8px",
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
minHeight: "250px"
}}>
{imagePreviewUrl ? (
<img src={imagePreviewUrl} alt="Document Preview" style={{ maxWidth: "100%", maxHeight: "300px", objectFit: "contain" }} />
) : (
<span style={{ color: "rgba(255,255,255,0.3)", fontSize: "14px" }}>No document selected</span>
)}
</div>
</div>
{/* Document Metadata Panel */}
<div className="glass-panel" style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "15px" }}>
<h3 style={{ margin: 0, fontSize: "14px", letterSpacing: "1px", color: "rgba(255,255,255,0.6)" }}>EXTRACTED METADATA</h3>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
{generationTime && (
<span style={{ fontSize: "12px", color: "rgba(255, 255, 255, 0.4)" }}>
Processed in {generationTime}s
</span>
)}
{isGenerating && <span className="pulse-dot"></span>}
</div>
</div>
<div style={{
padding: "20px",
backgroundColor: "rgba(0,0,0,0.2)",
borderRadius: "8px",
border: "1px solid rgba(255,255,255,0.05)",
textAlign: "left",
flex: 1,
display: "flex",
flexDirection: "column",
gap: "15px"
}}>
{isGenerating ? (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "rgba(255,255,255,0.4)" }}>
Analyzing document structure...
</div>
) : extractedMetadata ? (
<>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)", textTransform: "uppercase" }}>Student Name</span>
<strong style={{ fontSize: "18px", color: "var(--primary)" }}>{extractedMetadata.name || "--"}</strong>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)", textTransform: "uppercase" }}>Subject / Course</span>
<strong style={{ fontSize: "18px", color: "white" }}>{extractedMetadata.subject || "--"}</strong>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<span style={{ fontSize: "12px", color: "rgba(255,255,255,0.5)", textTransform: "uppercase" }}>Roll Number / ID</span>
<strong style={{ fontSize: "18px", color: "white" }}>{extractedMetadata.rollNo || "--"}</strong>
</div>
<div style={{ marginTop: "auto", paddingTop: "20px" }}>
<button
onClick={handleDownloadRenamed}
style={{
width: "100%",
padding: "12px",
backgroundColor: "rgba(16, 185, 129, 0.2)",
color: "#34d399",
border: "1px solid rgba(16, 185, 129, 0.4)",
borderRadius: "6px",
cursor: "pointer",
fontWeight: "bold",
display: "flex",
justifyContent: "center",
alignItems: "center",
gap: "8px"
}}
>
⬇️ Download Renamed File
</button>
</div>
</>
) : (
<div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", color: "rgba(255,255,255,0.4)" }}>
Metadata will appear here
</div>
)}
</div>
</div>
</div>
</div>
)}
</div>
);
}
export default App;