698 lines
32 KiB
TypeScript
698 lines
32 KiB
TypeScript
"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<HistoryItem[]>([]);
|
|
const [selectedItem, setSelectedItem] = useState<HistoryItem | null>(null);
|
|
const [selectedDetails, setSelectedDetails] = useState<any>(null);
|
|
const [selectedLogs, setSelectedLogs] = useState<any>(null);
|
|
const [activeStep, setActiveStep] = useState<number>(0);
|
|
const [uploading, setUploading] = useState<boolean>(false);
|
|
const [dragActive, setDragActive] = useState<boolean>(false);
|
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
|
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: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Uploaded Document File Info</h4>
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs space-y-2 text-slate-300">
|
|
<p><span className="text-slate-500">Filename:</span> {selectedItem?.filename}</p>
|
|
<p><span className="text-slate-500">File Size:</span> {formatBytes(selectedItem?.size || 0)}</p>
|
|
<p><span className="text-slate-500">Upload Date:</span> {selectedItem?.uploadTime ? new Date(selectedItem.uploadTime).toLocaleString() : "N/A"}</p>
|
|
<p><span className="text-slate-500">File Hash:</span> {selectedItem?.metadata?.fileHash || "Processed content hash saved"}</p>
|
|
<p><span className="text-slate-500">Is Sample DO:</span> {selectedItem?.isSample ? "True" : "False"}</p>
|
|
</div>
|
|
{selectedItem?.filename && (
|
|
<div>
|
|
<h4 className="text-sm font-semibold text-teal-400 mb-2">Original Document Source View</h4>
|
|
<div className="border border-slate-700 rounded-lg overflow-hidden max-h-96 flex justify-center bg-slate-950">
|
|
<img
|
|
src={selectedItem.isSample ? `/do-pfm/${selectedItem.filename}` : `/api/history?file=${encodeURIComponent(selectedItem.filename)}`}
|
|
alt="Original Document"
|
|
className="object-contain max-h-96 w-full"
|
|
onError={(e) => {
|
|
// Fallback if direct image access requires alternative endpoint
|
|
(e.target as HTMLImageElement).src = `https://placehold.co/600x800/1e293b/f8fafc?text=${encodeURIComponent(selectedItem.filename)}`;
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: `2. vLLM Prompt ("Before")`,
|
|
description: `Inspect prompt parameters & system role (${vllmCallsCount} call${vllmCallsCount > 1 ? "s" : ""})`,
|
|
content: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Payload Sent to vLLM Server</h4>
|
|
{selectedLogs?.vllm_calls && selectedLogs.vllm_calls.length > 0 ? (
|
|
selectedLogs.vllm_calls.map((call: any, idx: number) => (
|
|
<div key={idx} className="space-y-2 border-b border-slate-700/40 pb-4 last:border-0 last:pb-0">
|
|
<div className="flex justify-between items-center text-xs text-slate-400">
|
|
<span className="bg-slate-800 text-teal-300 px-2 py-0.5 rounded-full font-semibold">Call #{idx + 1}</span>
|
|
<span>{new Date(call.timestamp).toLocaleTimeString()}</span>
|
|
</div>
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs overflow-auto max-h-96 text-slate-300">
|
|
<pre>{JSON.stringify(call.request, null, 2)}</pre>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 text-xs text-slate-400 italic">
|
|
No vLLM API calls logged. (If this is a sample/re-indexed document, logs may not be recorded yet).
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: `3. vLLM Response ("After")`,
|
|
description: "Inspect raw output returned by VLM",
|
|
content: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Raw Text Generated by vLLM (PaddleOCR-VL)</h4>
|
|
{selectedLogs?.vllm_calls && selectedLogs.vllm_calls.length > 0 ? (
|
|
selectedLogs.vllm_calls.map((call: any, idx: number) => (
|
|
<div key={idx} className="space-y-2 border-b border-slate-700/40 pb-4 last:border-0 last:pb-0">
|
|
<div className="flex justify-between items-center text-xs text-slate-400">
|
|
<span className="bg-slate-800 text-teal-300 px-2 py-0.5 rounded-full font-semibold">Response #{idx + 1}</span>
|
|
<span>{new Date(call.timestamp).toLocaleTimeString()}</span>
|
|
</div>
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs overflow-auto max-h-96 text-slate-300">
|
|
<pre>{JSON.stringify(call.response, null, 2)}</pre>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 text-xs text-slate-400 italic">
|
|
No vLLM API responses logged.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: "4. Raw OCR Layout Markdown",
|
|
description: "View parsed layout markdown structure",
|
|
content: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Document Layout Parsing Markdown Output</h4>
|
|
{selectedDetails?.result || selectedLogs?.ocr_raw ? (
|
|
<div className="space-y-2">
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs overflow-auto max-h-96 text-slate-300">
|
|
<pre className="whitespace-pre-wrap">
|
|
{(() => {
|
|
const res = selectedDetails?.result || selectedLogs?.ocr_raw;
|
|
const page0 = res?.layoutParsingResults?.[0] || {};
|
|
return page0?.markdown?.text || "No markdown text output found in results.";
|
|
})()}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 text-xs text-slate-400 italic">
|
|
No raw layout results parsed.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: "5. Stage 1 Extracted Fields",
|
|
description: "View output of regex schema matching",
|
|
content: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Regex Extracted Fields (Metadata & Items)</h4>
|
|
{selectedLogs?.stage_1_output ? (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs overflow-auto max-h-96 text-slate-300">
|
|
<pre>{JSON.stringify(selectedLogs.stage_1_output, null, 2)}</pre>
|
|
</div>
|
|
) : (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 text-xs text-slate-400 italic">
|
|
No Stage 1 log output saved.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: "6. Stage 2 Sanitized Output",
|
|
description: "View results after validation checks",
|
|
content: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Sanitized & Validated Metadata (Final database fields)</h4>
|
|
{selectedLogs?.stage_2_output ? (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs overflow-auto max-h-96 text-slate-300">
|
|
<pre>{JSON.stringify(selectedLogs.stage_2_output, null, 2)}</pre>
|
|
</div>
|
|
) : (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 text-xs text-slate-400 italic">
|
|
No Stage 2 log output saved.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: "7. Client Gateway Response",
|
|
description: "Inspect JSON payload sent to frontends",
|
|
content: (
|
|
<div className="space-y-4">
|
|
<h4 className="text-sm font-semibold text-teal-400">Final Gateway JSON Response</h4>
|
|
{selectedLogs?.frontend_response || selectedDetails ? (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 font-mono text-xs overflow-auto max-h-96 text-slate-300">
|
|
<pre>
|
|
{JSON.stringify(
|
|
selectedLogs?.frontend_response || {
|
|
filename: selectedItem?.filename,
|
|
result: selectedDetails
|
|
},
|
|
null,
|
|
2
|
|
)}
|
|
</pre>
|
|
</div>
|
|
) : (
|
|
<div className="bg-slate-900/60 border border-slate-700/50 rounded-lg p-4 text-xs text-slate-400 italic">
|
|
No response payload logged.
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
};
|
|
|
|
const steps = getSteps();
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col bg-slate-950 text-slate-100 min-h-screen">
|
|
{/* Sleek Top Navigation Bar */}
|
|
<header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-md px-6 py-4 flex justify-between items-center sticky top-0 z-50">
|
|
<div className="flex items-center space-x-3">
|
|
<div className="h-8 w-8 bg-gradient-to-tr from-teal-500 to-indigo-600 rounded-lg flex items-center justify-center font-bold text-white shadow-lg shadow-teal-500/20">
|
|
P
|
|
</div>
|
|
<div>
|
|
<h1 className="text-md font-bold bg-clip-text text-transparent bg-gradient-to-r from-white via-slate-200 to-slate-400">
|
|
Prima Fresh Mart AI OCR Gateway
|
|
</h1>
|
|
<p className="text-[10px] text-teal-400 font-mono tracking-wider uppercase">
|
|
Pipeline Process Layers Dashboard
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status Indicators */}
|
|
<div className="flex items-center space-x-4 text-xs">
|
|
<div className="flex items-center space-x-2 bg-slate-800/80 px-3 py-1 rounded-full border border-slate-700/40">
|
|
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
<span className="text-slate-400 font-mono">DB: Online</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2 bg-slate-800/80 px-3 py-1 rounded-full border border-slate-700/40">
|
|
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
<span className="text-slate-400 font-mono">vLLM: Connected</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Layout Grid */}
|
|
<main className="flex-1 grid grid-cols-1 lg:grid-cols-12 gap-6 p-6 overflow-hidden">
|
|
|
|
{/* LEFT COLUMN: Upload Panel & History List (4 Cols) */}
|
|
<section className="lg:col-span-4 flex flex-col space-y-6 overflow-y-auto max-h-[calc(100vh-120px)] pr-2">
|
|
|
|
{/* Upload Zone Card */}
|
|
<div
|
|
className={`border-2 border-dashed rounded-2xl p-6 transition-all duration-300 cursor-pointer flex flex-col items-center justify-center ${
|
|
dragActive
|
|
? "border-teal-400 bg-teal-950/20 shadow-md shadow-teal-500/10"
|
|
: "border-slate-800 hover:border-slate-700 bg-slate-900/40 hover:bg-slate-900/60"
|
|
}`}
|
|
onDragEnter={handleDrag}
|
|
onDragOver={handleDrag}
|
|
onDragLeave={handleDrag}
|
|
onDrop={handleDrop}
|
|
onClick={triggerFileInput}
|
|
>
|
|
<input
|
|
type="file"
|
|
ref={fileInputRef}
|
|
onChange={handleFileChange}
|
|
accept="image/*"
|
|
className="hidden"
|
|
/>
|
|
{uploading ? (
|
|
<div className="flex flex-col items-center py-4 space-y-3">
|
|
<div className="h-8 w-8 border-4 border-t-teal-400 border-slate-700 rounded-full animate-spin"></div>
|
|
<p className="text-xs text-teal-400 font-mono">Running OCR Pipeline...</p>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-4 space-y-3">
|
|
<div className="text-3xl text-slate-400">📤</div>
|
|
<div>
|
|
<p className="text-sm font-semibold text-slate-200">Upload Delivery Order Image</p>
|
|
<p className="text-xs text-slate-500 mt-1">Drag and drop file, or click to browse</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* History List Card */}
|
|
<div className="bg-slate-900/30 border border-slate-850 rounded-2xl p-5 flex flex-col flex-grow min-h-[350px]">
|
|
<h3 className="text-sm font-bold text-slate-400 font-mono uppercase tracking-wider mb-4">
|
|
Upload History
|
|
</h3>
|
|
|
|
<div className="space-y-3 overflow-y-auto max-h-[450px] flex-grow pr-1">
|
|
{history.length === 0 ? (
|
|
<div className="text-center py-8 text-xs text-slate-500 italic">
|
|
No uploads processed yet.
|
|
</div>
|
|
) : (
|
|
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 (
|
|
<div
|
|
key={item.id}
|
|
className={`p-3.5 rounded-xl border transition-all duration-200 cursor-pointer flex justify-between items-start ${
|
|
isSelected
|
|
? "bg-slate-900 border-teal-500/60 shadow-lg shadow-teal-500/5"
|
|
: "bg-slate-900/50 border-slate-800 hover:border-slate-700"
|
|
}`}
|
|
onClick={() => handleSelectItem(item)}
|
|
>
|
|
<div className="space-y-1.5 min-w-0 flex-1">
|
|
<p className="text-xs font-semibold text-slate-200 truncate pr-2" title={item.filename}>
|
|
{item.filename.substring(item.filename.indexOf("-") + 1)}
|
|
</p>
|
|
<div className="flex items-center space-x-2 text-[10px] text-slate-500 font-mono">
|
|
<span>{dateFormatted}</span>
|
|
<span>•</span>
|
|
<span>{formatBytes(item.size)}</span>
|
|
</div>
|
|
{item.metadata && (
|
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
|
{item.metadata.noPO && item.metadata.noPO !== "Not Found" && (
|
|
<span className="bg-teal-950/50 text-teal-400 border border-teal-900/60 px-1.5 py-0.5 rounded text-[9px] font-mono font-semibold">
|
|
{item.metadata.noPO}
|
|
</span>
|
|
)}
|
|
{item.totalItems > 0 && (
|
|
<span className="bg-slate-800 text-slate-300 px-1.5 py-0.5 rounded text-[9px] font-mono">
|
|
{item.totalItems} Items
|
|
</span>
|
|
)}
|
|
{item.flaggedItems > 0 && (
|
|
<span className="bg-rose-950/30 text-rose-400 border border-rose-900/40 px-1.5 py-0.5 rounded text-[9px] font-mono font-semibold">
|
|
{item.flaggedItems} Warning
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={(e) => handleDeleteItem(e, item)}
|
|
className="text-slate-600 hover:text-rose-400 p-1.5 rounded-lg hover:bg-rose-950/20 transition-colors"
|
|
title="Delete document"
|
|
>
|
|
🗑️
|
|
</button>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* RIGHT COLUMN: Interactive Stepper & Document Viewer (8 Cols) */}
|
|
<section className="lg:col-span-8 flex flex-col space-y-6 overflow-y-auto max-h-[calc(100vh-120px)]">
|
|
{errorMsg && (
|
|
<div className="bg-rose-950/30 border border-rose-800/40 p-4 rounded-xl text-xs text-rose-400 flex items-start space-x-2">
|
|
<span className="text-base leading-none">⚠️</span>
|
|
<span>{errorMsg}</span>
|
|
</div>
|
|
)}
|
|
|
|
{!selectedItem ? (
|
|
<div className="flex-grow bg-slate-900/20 border border-slate-900/60 rounded-2xl p-12 flex flex-col justify-center items-center text-center">
|
|
<div className="text-5xl mb-4 text-slate-700 animate-pulse">📁</div>
|
|
<h3 className="text-md font-bold text-slate-400">No Document Selected</h3>
|
|
<p className="text-xs text-slate-500 max-w-sm mt-2">
|
|
Upload a new Delivery Order image, or select one from the history panel to inspect its processing stages.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
|
|
{/* Document Overview Metadata Card */}
|
|
<div className="bg-slate-900/40 border border-slate-850 rounded-2xl p-6 shadow-xl">
|
|
<div className="flex justify-between items-start border-b border-slate-800 pb-4 mb-4">
|
|
<div>
|
|
<h3 className="text-md font-bold text-slate-100 flex items-center space-x-2">
|
|
<span>📄</span>
|
|
<span className="truncate max-w-md">{selectedItem.filename.substring(selectedItem.filename.indexOf("-") + 1)}</span>
|
|
</h3>
|
|
<p className="text-xs text-slate-400 mt-1">Parsed Overview results from Postgres Database</p>
|
|
</div>
|
|
{selectedItem.isSample && (
|
|
<span className="bg-blue-950/40 text-blue-400 border border-blue-900/40 px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wider font-mono">
|
|
Sample DO
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{selectedDetails?.result?.metadata || selectedItem.metadata ? (
|
|
(() => {
|
|
const meta = selectedDetails?.result?.metadata || selectedItem.metadata;
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs font-mono">
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">PO Number</p>
|
|
<p className="text-slate-200 font-semibold">{meta.noPO || "Not Found"}</p>
|
|
</div>
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">SO Number</p>
|
|
<p className="text-slate-200 font-semibold">{meta.noSO || "Not Found"}</p>
|
|
</div>
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">DO Number</p>
|
|
<p className="text-slate-200 font-semibold">{meta.noDO || "Not Found"}</p>
|
|
</div>
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850 md:col-span-3">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">Order Untuk (Store)</p>
|
|
<p className="text-slate-200 font-semibold text-sm">{meta.orderUntuk || "Not Found"}</p>
|
|
<p className="text-slate-400 mt-1 text-[10px] truncate">{meta.alamat || ""}</p>
|
|
</div>
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">Date</p>
|
|
<p className="text-slate-200 font-semibold">{meta.tanggal || "Not Found"}</p>
|
|
</div>
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">Truck Plate</p>
|
|
<p className="text-slate-200 font-semibold">{meta.platTruk || "Not Found"}</p>
|
|
</div>
|
|
<div className="bg-slate-950/40 p-3 rounded-lg border border-slate-850">
|
|
<p className="text-slate-500 uppercase text-[9px] tracking-wider mb-1 font-bold">Total SKUs</p>
|
|
<p className="text-slate-200 font-semibold">{selectedDetails?.items?.length || 0} SKU Items</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})()
|
|
) : (
|
|
<p className="text-xs text-slate-500 italic">No metadata available</p>
|
|
)}
|
|
|
|
{/* Parsed Items List */}
|
|
{selectedDetails?.items && selectedDetails.items.length > 0 && (
|
|
<div className="mt-5 border-t border-slate-800 pt-4">
|
|
<h4 className="text-xs font-bold font-mono text-slate-400 uppercase mb-2">Parsed SKU Table Rows</h4>
|
|
<div className="overflow-x-auto border border-slate-850 rounded-xl">
|
|
<table className="min-w-full text-xs text-left text-slate-300">
|
|
<thead className="bg-slate-950/60 font-mono text-[9px] uppercase text-slate-500 border-b border-slate-800">
|
|
<tr>
|
|
<th className="px-4 py-2">No</th>
|
|
<th className="px-4 py-2">SKU Code</th>
|
|
<th className="px-4 py-2">Item Description</th>
|
|
<th className="px-4 py-2 text-right">Qty</th>
|
|
<th className="px-4 py-2 text-right">Price/Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-800/40 bg-slate-950/10">
|
|
{selectedDetails.items.map((item: any, idx: number) => {
|
|
const isFlagged = selectedDetails.flagged?.[idx];
|
|
const remark = selectedDetails.remarks?.[idx];
|
|
return (
|
|
<tr key={idx} className={isFlagged ? "bg-rose-950/10" : "hover:bg-slate-900/20"}>
|
|
<td className="px-4 py-2 text-slate-500">{idx + 1}</td>
|
|
<td className={`px-4 py-2 font-mono font-semibold ${isFlagged ? "text-rose-400" : "text-teal-400"}`}>
|
|
{item.kodeBarang}
|
|
</td>
|
|
<td className="px-4 py-2 pr-6">
|
|
<div>
|
|
<p className="font-semibold text-slate-200">{item.namaBarang}</p>
|
|
{remark && <p className="text-[10px] text-rose-400 italic mt-0.5">{remark}</p>}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-2 text-right font-mono font-semibold">{item.banyak}</td>
|
|
<td className="px-4 py-2 text-right font-mono text-slate-400">{item.jumlah}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* TIMELINE / STEPPER LAYERS CARD */}
|
|
<div className="bg-slate-900/40 border border-slate-850 rounded-2xl p-6 shadow-xl flex flex-col space-y-6">
|
|
<div className="border-b border-slate-800 pb-3">
|
|
<h3 className="text-md font-bold text-slate-100 flex items-center space-x-2">
|
|
<span>🔬</span>
|
|
<span>AI OCR Pipeline Process Layers</span>
|
|
</h3>
|
|
<p className="text-xs text-slate-500 mt-1">Interactive vertical timeline inspecting how data flows from original image to clean JSON response</p>
|
|
</div>
|
|
|
|
{/* Process Timeline Stepper Container */}
|
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-6">
|
|
|
|
{/* Stepper Steps (4 Cols) */}
|
|
<div className="md:col-span-4 flex flex-col space-y-2 border-r border-slate-800/60 pr-4">
|
|
{steps.map((step, idx) => {
|
|
const isActive = activeStep === idx;
|
|
return (
|
|
<div
|
|
key={idx}
|
|
className={`p-3 rounded-xl cursor-pointer transition-all duration-200 border text-left ${
|
|
isActive
|
|
? "bg-slate-900 border-teal-500 text-slate-200 shadow-md shadow-teal-500/5"
|
|
: "bg-slate-950/20 border-transparent text-slate-500 hover:text-slate-300"
|
|
}`}
|
|
onClick={() => setActiveStep(idx)}
|
|
>
|
|
<p className="text-xs font-bold font-mono tracking-wide">{step.title}</p>
|
|
<p className="text-[10px] font-normal truncate mt-0.5">{step.description}</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Step Detail Content Panel (8 Cols) */}
|
|
<div className="md:col-span-8 bg-slate-950/40 border border-slate-850 rounded-xl p-5 min-h-[350px]">
|
|
<div className="flex justify-between items-center mb-4 border-b border-slate-800/80 pb-2">
|
|
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-400 font-mono">
|
|
Layer Detail view
|
|
</h4>
|
|
<span className="text-[10px] bg-slate-800 text-slate-400 px-2 py-0.5 rounded-full font-mono font-semibold">
|
|
Layer {activeStep + 1} of 7
|
|
</span>
|
|
</div>
|
|
|
|
<div className="text-xs text-slate-200">
|
|
{steps[activeStep].content}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|