"use client"; import React, { useState, useEffect, useRef } from "react"; interface HistoryItem { id: number; filename: string; uploadTime: string; size: number; parsed: boolean; isSample: boolean; metadata: any; totalItems: number; flaggedItems: number; } export default function Home() { const [history, setHistory] = useState([]); const [selectedItem, setSelectedItem] = useState(null); const [selectedDetails, setSelectedDetails] = useState(null); const [selectedLogs, setSelectedLogs] = useState(null); const [activeStep, setActiveStep] = useState(0); const [uploading, setUploading] = useState(false); const [dragActive, setDragActive] = useState(false); const [errorMsg, setErrorMsg] = useState(null); const fileInputRef = useRef(null); // Fetch history list const fetchHistory = async () => { try { const res = await fetch("/api/history?all=true"); if (res.ok) { const data = await res.json(); setHistory(data.history || []); } } catch (err) { console.error("Failed to fetch history:", err); } }; useEffect(() => { fetchHistory(); }, []); // Fetch details and logs for a selected document const handleSelectItem = async (item: HistoryItem) => { setSelectedItem(item); setSelectedDetails(null); setSelectedLogs(null); setErrorMsg(null); setActiveStep(0); try { // 1. Fetch items and doc details const detailsRes = await fetch(`/api/history?file=${encodeURIComponent(item.filename)}`); if (detailsRes.ok) { const detailsData = await detailsRes.json(); setSelectedDetails(detailsData); } else { throw new Error("Failed to load document details"); } // 2. Fetch processing logs const logsRes = await fetch(`/api/documents/${item.id}/logs`); if (logsRes.ok) { const logsData = await logsRes.json(); setSelectedLogs(logsData.processing_logs); } } catch (err: any) { console.error("Error loading document:", err); setErrorMsg(err.message || "Error loading document details"); } }; // Handle Drag & Drop events const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") { setDragActive(true); } else if (e.type === "dragleave") { setDragActive(false); } }; const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setDragActive(false); if (e.dataTransfer.files && e.dataTransfer.files[0]) { await handleUploadFile(e.dataTransfer.files[0]); } }; const handleFileChange = async (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { await handleUploadFile(e.target.files[0]); } }; const handleUploadFile = async (file: File) => { setUploading(true); setErrorMsg(null); try { const formData = new FormData(); formData.append("file", file); console.log("Uploading file to gateway..."); const res = await fetch("/api/upload", { method: "POST", body: formData, }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Failed to process image"); } const data = await res.json(); console.log("Upload response received:", data); // Refresh list and select new document await fetchHistory(); // Attempt to find the newly uploaded item in history list // Next.js history is sorted descending, so it will likely be first const updatedRes = await fetch("/api/history?all=true"); if (updatedRes.ok) { const updatedData = await updatedRes.json(); const newHistory = updatedData.history || []; setHistory(newHistory); const match = newHistory.find((h: HistoryItem) => h.filename === data.filename); if (match) { handleSelectItem(match); } } } catch (err: any) { console.error("Upload failed:", err); setErrorMsg(err.message || "Failed to upload and process file."); } finally { setUploading(false); } }; const triggerFileInput = () => { fileInputRef.current?.click(); }; const handleDeleteItem = async (e: React.MouseEvent, item: HistoryItem) => { e.stopPropagation(); if (!confirm(`Are you sure you want to delete ${item.filename}?`)) return; try { const res = await fetch("/api/history", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: item.filename }) }); if (res.ok) { if (selectedItem?.id === item.id) { setSelectedItem(null); setSelectedDetails(null); setSelectedLogs(null); } fetchHistory(); } } catch (err) { console.error("Failed to delete document:", err); } }; // Format bytes to human readable format const formatBytes = (bytes: number) => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; // Setup Stepper Steps const getSteps = () => { const vllmCallsCount = selectedLogs?.vllm_calls?.length || 0; return [ { title: "1. Upload & Preprocess", description: "Verify document metadata & files", content: (

Uploaded Document File Info

Filename: {selectedItem?.filename}

File Size: {formatBytes(selectedItem?.size || 0)}

Upload Date: {selectedItem?.uploadTime ? new Date(selectedItem.uploadTime).toLocaleString() : "N/A"}

File Hash: {selectedItem?.metadata?.fileHash || "Processed content hash saved"}

Is Sample DO: {selectedItem?.isSample ? "True" : "False"}

{selectedItem?.filename && (

Original Document Source View

Original Document { // Fallback if direct image access requires alternative endpoint (e.target as HTMLImageElement).src = `https://placehold.co/600x800/1e293b/f8fafc?text=${encodeURIComponent(selectedItem.filename)}`; }} />
)}
) }, { title: `2. vLLM Prompt ("Before")`, description: `Inspect prompt parameters & system role (${vllmCallsCount} call${vllmCallsCount > 1 ? "s" : ""})`, content: (

Payload Sent to vLLM Server

{selectedLogs?.vllm_calls && selectedLogs.vllm_calls.length > 0 ? ( selectedLogs.vllm_calls.map((call: any, idx: number) => (
Call #{idx + 1} {new Date(call.timestamp).toLocaleTimeString()}
{JSON.stringify(call.request, null, 2)}
)) ) : (
No vLLM API calls logged. (If this is a sample/re-indexed document, logs may not be recorded yet).
)}
) }, { title: `3. vLLM Response ("After")`, description: "Inspect raw output returned by VLM", content: (

Raw Text Generated by vLLM (PaddleOCR-VL)

{selectedLogs?.vllm_calls && selectedLogs.vllm_calls.length > 0 ? ( selectedLogs.vllm_calls.map((call: any, idx: number) => (
Response #{idx + 1} {new Date(call.timestamp).toLocaleTimeString()}
{JSON.stringify(call.response, null, 2)}
)) ) : (
No vLLM API responses logged.
)}
) }, { title: "4. Raw OCR Layout Markdown", description: "View parsed layout markdown structure", content: (

Document Layout Parsing Markdown Output

{selectedDetails?.result || selectedLogs?.ocr_raw ? (
                    {(() => {
                      const res = selectedDetails?.result || selectedLogs?.ocr_raw;
                      const page0 = res?.layoutParsingResults?.[0] || {};
                      return page0?.markdown?.text || "No markdown text output found in results.";
                    })()}
                  
) : (
No raw layout results parsed.
)}
) }, { title: "5. Stage 1 Extracted Fields", description: "View output of regex schema matching", content: (

Regex Extracted Fields (Metadata & Items)

{selectedLogs?.stage_1_output ? (
{JSON.stringify(selectedLogs.stage_1_output, null, 2)}
) : (
No Stage 1 log output saved.
)}
) }, { title: "6. Stage 2 Sanitized Output", description: "View results after validation checks", content: (

Sanitized & Validated Metadata (Final database fields)

{selectedLogs?.stage_2_output ? (
{JSON.stringify(selectedLogs.stage_2_output, null, 2)}
) : (
No Stage 2 log output saved.
)}
) }, { title: "7. Client Gateway Response", description: "Inspect JSON payload sent to frontends", content: (

Final Gateway JSON Response

{selectedLogs?.frontend_response || selectedDetails ? (
                  {JSON.stringify(
                    selectedLogs?.frontend_response || {
                      filename: selectedItem?.filename,
                      result: selectedDetails
                    }, 
                    null, 
                    2
                  )}
                
) : (
No response payload logged.
)}
) } ]; }; const steps = getSteps(); return (
{/* Sleek Top Navigation Bar */}
P

Prima Fresh Mart AI OCR Gateway

Pipeline Process Layers Dashboard

{/* Status Indicators */}
DB: Online
vLLM: Connected
{/* Main Layout Grid */}
{/* LEFT COLUMN: Upload Panel & History List (4 Cols) */}
{/* Upload Zone Card */}
{uploading ? (

Running OCR Pipeline...

) : (
📤

Upload Delivery Order Image

Drag and drop file, or click to browse

)}
{/* History List Card */}

Upload History

{history.length === 0 ? (
No uploads processed yet.
) : ( history.map((item) => { const isSelected = selectedItem?.id === item.id; const dateFormatted = new Date(item.uploadTime).toLocaleDateString("id-ID", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" }); return (
handleSelectItem(item)} >

{item.filename.substring(item.filename.indexOf("-") + 1)}

{dateFormatted} • {formatBytes(item.size)}
{item.metadata && (
{item.metadata.noPO && item.metadata.noPO !== "Not Found" && ( {item.metadata.noPO} )} {item.totalItems > 0 && ( {item.totalItems} Items )} {item.flaggedItems > 0 && ( {item.flaggedItems} Warning )}
)}
); }) )}
{/* RIGHT COLUMN: Interactive Stepper & Document Viewer (8 Cols) */}
{errorMsg && (
⚠️ {errorMsg}
)} {!selectedItem ? (
📁

No Document Selected

Upload a new Delivery Order image, or select one from the history panel to inspect its processing stages.

) : (
{/* Document Overview Metadata Card */}

📄 {selectedItem.filename.substring(selectedItem.filename.indexOf("-") + 1)}

Parsed Overview results from Postgres Database

{selectedItem.isSample && ( Sample DO )}
{selectedDetails?.result?.metadata || selectedItem.metadata ? ( (() => { const meta = selectedDetails?.result?.metadata || selectedItem.metadata; return (

PO Number

{meta.noPO || "Not Found"}

SO Number

{meta.noSO || "Not Found"}

DO Number

{meta.noDO || "Not Found"}

Order Untuk (Store)

{meta.orderUntuk || "Not Found"}

{meta.alamat || ""}

Date

{meta.tanggal || "Not Found"}

Truck Plate

{meta.platTruk || "Not Found"}

Total SKUs

{selectedDetails?.items?.length || 0} SKU Items

); })() ) : (

No metadata available

)} {/* Parsed Items List */} {selectedDetails?.items && selectedDetails.items.length > 0 && (

Parsed SKU Table Rows

{selectedDetails.items.map((item: any, idx: number) => { const isFlagged = selectedDetails.flagged?.[idx]; const remark = selectedDetails.remarks?.[idx]; return ( ); })}
No SKU Code Item Description Qty Price/Total
{idx + 1} {item.kodeBarang}

{item.namaBarang}

{remark &&

{remark}

}
{item.banyak} {item.jumlah}
)}
{/* TIMELINE / STEPPER LAYERS CARD */}

🔬 AI OCR Pipeline Process Layers

Interactive vertical timeline inspecting how data flows from original image to clean JSON response

{/* Process Timeline Stepper Container */}
{/* Stepper Steps (4 Cols) */}
{steps.map((step, idx) => { const isActive = activeStep === idx; return (
setActiveStep(idx)} >

{step.title}

{step.description}

); })}
{/* Step Detail Content Panel (8 Cols) */}

Layer Detail view

Layer {activeStep + 1} of 7
{steps[activeStep].content}
)}
); }