feat: Add OCR Processing Layers Viewer UI and log proxy middleware
This commit is contained in:
1 parent
54b46fc35f
commit
aa3233e411
11 files changed
+964
-30
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { query } from "../../../../../db";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> | { id: string } }
|
||||
) {
|
||||
try {
|
||||
// Handle both Promise and synchronous params for Next.js version compatibility
|
||||
const resolvedParams = await params;
|
||||
const { id } = resolvedParams;
|
||||
const docId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(docId)) {
|
||||
return NextResponse.json({ error: "Invalid document ID" }, { status: 400 });
|
||||
}
|
||||
|
||||
const res = await query(
|
||||
"SELECT filename, processing_logs FROM documents WHERE id = $1",
|
||||
[docId]
|
||||
);
|
||||
|
||||
if (res.rowCount === 0 || !res.rows[0]) {
|
||||
return NextResponse.json({ error: "Document not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
filename: res.rows[0].filename,
|
||||
processing_logs: res.rows[0].processing_logs || null
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching document logs:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import { query, cleanupAndReindexItems, resolveStoreFromText } from "../../../db
|
||||
import { parseDOMetadata, sanitizeParsedMetadata } from "../../../utils/parser";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
let safeFile = "";
|
||||
try {
|
||||
const { filename } = await req.json();
|
||||
if (!filename) {
|
||||
return NextResponse.json({ error: "Filename is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const safeFile = path.basename(filename);
|
||||
safeFile = path.basename(filename);
|
||||
|
||||
// 1. Resolve path to the public assets folder or fallback to /uploads
|
||||
let filePath = path.join(process.cwd(), "public", "do-pfm", safeFile);
|
||||
|
||||
@@ -3,7 +3,8 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
import { query, resolveStoreFromText } from "../../../db";
|
||||
import { parseDOMetadata } from "../../../utils/parser";
|
||||
import { parseDOMetadata, sanitizeParsedMetadata } from "../../../utils/parser";
|
||||
import { startActiveLog, getActiveLog, clearActiveLog } from "../../../utils/active-log";
|
||||
|
||||
const UPLOADS_DIR = "/uploads";
|
||||
|
||||
@@ -75,6 +76,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const pipelineUrl = process.env.PIPELINE_URL || "http://paddleocr-pipeline-api:8090/layout-parsing";
|
||||
console.log(`Forwarding uploaded file ${filename} to pipeline: ${pipelineUrl}`);
|
||||
startActiveLog(filename);
|
||||
|
||||
const response = await fetch(pipelineUrl, {
|
||||
method: "POST",
|
||||
@@ -85,6 +87,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
clearActiveLog();
|
||||
const errText = await response.text();
|
||||
return NextResponse.json({ error: `Pipeline API error: ${errText}` }, { status: response.status });
|
||||
}
|
||||
@@ -131,19 +134,46 @@ export async function POST(req: NextRequest) {
|
||||
(docMetadata as any).orderUntuk = resolvedStore.orderUntuk;
|
||||
(docMetadata as any).alamat = resolvedStore.alamat;
|
||||
|
||||
// Stage 2 Filtering: Sanitize parsed metadata
|
||||
const sanitizedMetadata = sanitizeParsedMetadata(docMetadata as any);
|
||||
|
||||
// Construct client response representation
|
||||
const wrappedResult = {
|
||||
errorCode: 0,
|
||||
errorMsg: "Success",
|
||||
result: pipelineResult
|
||||
};
|
||||
const clientResponse = {
|
||||
filename,
|
||||
result: wrappedResult
|
||||
};
|
||||
|
||||
// Retrieve and finalize active log data
|
||||
const activeLog = getActiveLog();
|
||||
let logsPayload: any = null;
|
||||
if (activeLog && activeLog.filename === filename) {
|
||||
activeLog.ocr_raw = pipelineResult;
|
||||
activeLog.stage_1_output = docMetadata;
|
||||
activeLog.stage_2_output = sanitizedMetadata;
|
||||
activeLog.frontend_response = clientResponse;
|
||||
logsPayload = activeLog;
|
||||
}
|
||||
clearActiveLog();
|
||||
|
||||
const insertDocRes = await query(`
|
||||
INSERT INTO documents (filename, upload_time, size, parsed, metadata, layout_parsing_result, is_sample, file_hash)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
INSERT INTO documents (filename, upload_time, size, parsed, metadata, layout_parsing_result, is_sample, file_hash, processing_logs)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id
|
||||
`, [
|
||||
filename,
|
||||
new Date(),
|
||||
buffer.length,
|
||||
true,
|
||||
JSON.stringify(docMetadata),
|
||||
JSON.stringify(sanitizedMetadata),
|
||||
JSON.stringify(pipelineResult),
|
||||
false,
|
||||
fileHash
|
||||
fileHash,
|
||||
logsPayload ? JSON.stringify(logsPayload) : null
|
||||
]);
|
||||
|
||||
const docId = insertDocRes.rows[0].id;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { logVllmCall } from "../../../../utils/active-log";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return handleProxy(req);
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return handleProxy(req);
|
||||
}
|
||||
|
||||
export async function PUT(req: NextRequest) {
|
||||
return handleProxy(req);
|
||||
}
|
||||
|
||||
export async function DELETE(req: NextRequest) {
|
||||
return handleProxy(req);
|
||||
}
|
||||
|
||||
async function handleProxy(req: NextRequest) {
|
||||
try {
|
||||
const pathname = req.nextUrl.pathname;
|
||||
// Extract everything after /api/vllm-proxy
|
||||
const relPath = pathname.replace(/^\/api\/vllm-proxy/, "");
|
||||
|
||||
// The real vLLM server is at paddleocr-vllm-server:8118 inside docker compose
|
||||
const realBaseUrl = process.env.VLLM_SERVER_REAL_URL || "http://paddleocr-vllm-server:8118";
|
||||
|
||||
// Construct the destination URL
|
||||
const destUrl = `${realBaseUrl}${relPath}${req.nextUrl.search}`;
|
||||
|
||||
console.log(`[vllm-proxy] Routing request from ${pathname} to ${destUrl}`);
|
||||
|
||||
// Read the request body if present
|
||||
let reqBody: any = null;
|
||||
let reqBodyBuffer: Buffer | null = null;
|
||||
|
||||
if (req.body) {
|
||||
const arrayBuffer = await req.arrayBuffer();
|
||||
reqBodyBuffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const contentType = req.headers.get("content-type") || "";
|
||||
if (contentType.includes("application/json")) {
|
||||
try {
|
||||
reqBody = JSON.parse(reqBodyBuffer.toString("utf-8"));
|
||||
} catch (e) {
|
||||
console.warn("[vllm-proxy] Failed to parse request body as JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct headers, filtering out headers that might cause issues (e.g. Host)
|
||||
const headers = new Headers();
|
||||
req.headers.forEach((value, key) => {
|
||||
if (key.toLowerCase() !== "host" && key.toLowerCase() !== "content-length") {
|
||||
headers.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// Make the actual call to the real vLLM server
|
||||
const forwardResponse = await fetch(destUrl, {
|
||||
method: req.method,
|
||||
headers: headers,
|
||||
body: reqBodyBuffer ? new Uint8Array(reqBodyBuffer) : null,
|
||||
// @ts-ignore
|
||||
duplex: "half"
|
||||
});
|
||||
|
||||
// Read the response content
|
||||
const resBodyBuffer = Buffer.from(await forwardResponse.arrayBuffer());
|
||||
let resBody: any = null;
|
||||
|
||||
const resContentType = forwardResponse.headers.get("content-type") || "";
|
||||
if (resContentType.includes("application/json")) {
|
||||
try {
|
||||
resBody = JSON.parse(resBodyBuffer.toString("utf-8"));
|
||||
} catch (e) {
|
||||
console.warn("[vllm-proxy] Failed to parse response body as JSON:", e);
|
||||
}
|
||||
} else {
|
||||
resBody = resBodyBuffer.toString("utf-8");
|
||||
}
|
||||
|
||||
// Log the interaction if it looks like a completion call
|
||||
if (pathname.includes("/chat/completions") || pathname.includes("/completions")) {
|
||||
// Make a clean copy of the request to log (hiding huge base64 images if they clutter logs)
|
||||
const cleanReq = sanitizeLogPayload(reqBody);
|
||||
logVllmCall(cleanReq, resBody);
|
||||
}
|
||||
|
||||
// Return the response back to pipeline-api
|
||||
const responseHeaders = new Headers();
|
||||
forwardResponse.headers.forEach((value, key) => {
|
||||
responseHeaders.set(key, value);
|
||||
});
|
||||
|
||||
return new Response(resBodyBuffer, {
|
||||
status: forwardResponse.status,
|
||||
statusText: forwardResponse.statusText,
|
||||
headers: responseHeaders
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[vllm-proxy] Error forwarding request:", error);
|
||||
return NextResponse.json({ error: "Failed to proxy request to vLLM server" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to keep log sizes reasonable by truncating huge base64 image strings
|
||||
function sanitizeLogPayload(payload: any): any {
|
||||
if (!payload) return payload;
|
||||
try {
|
||||
const copy = JSON.parse(JSON.stringify(payload));
|
||||
if (copy.messages && Array.isArray(copy.messages)) {
|
||||
for (const msg of copy.messages) {
|
||||
if (msg.content && Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "image_url" && part.image_url && part.image_url.url) {
|
||||
const url = part.image_url.url;
|
||||
if (url.startsWith("data:") && url.length > 200) {
|
||||
part.image_url.url = url.substring(0, 100) + "...[TRUNCATED BASE64]..." + url.substring(url.length - 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
} catch (e) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
--background: #0f172a;
|
||||
--foreground: #f8fafc;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI OCR Delivery Order",
|
||||
description: "Generated by create next app",
|
||||
@@ -25,9 +14,10 @@ export default function RootLayout({
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
className="h-full antialiased text-slate-100 bg-slate-950"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<body className="min-h-full flex flex-col font-sans">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,697 @@
|
||||
export const dynamic = "force-dynamic";
|
||||
"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 style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem', textAlign: 'center' }}>
|
||||
<h1>Prima Fresh Mart OCR API Gateway</h1>
|
||||
<p>Status: Running</p>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -19,15 +19,17 @@ export async function initDb(pool: Pool) {
|
||||
metadata JSONB,
|
||||
layout_parsing_result JSONB,
|
||||
is_sample BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
file_hash VARCHAR(64)
|
||||
file_hash VARCHAR(64),
|
||||
processing_logs JSONB
|
||||
);
|
||||
`);
|
||||
|
||||
// Ensure file_hash exists on existing tables
|
||||
// Ensure file_hash and processing_logs exist on existing tables
|
||||
try {
|
||||
await pool.query("ALTER TABLE documents ADD COLUMN IF NOT EXISTS file_hash VARCHAR(64);");
|
||||
await pool.query("ALTER TABLE documents ADD COLUMN IF NOT EXISTS processing_logs JSONB;");
|
||||
} catch (alterErr) {
|
||||
console.error("Failed to alter documents table for file_hash:", alterErr);
|
||||
console.error("Failed to alter documents table for schema upgrade:", alterErr);
|
||||
}
|
||||
|
||||
await pool.query(`
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Active Log Tracker for OCR Processing Layers
|
||||
|
||||
export interface VllmCall {
|
||||
request: any;
|
||||
response: any;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface ActiveUploadLog {
|
||||
filename: string;
|
||||
vllm_calls: VllmCall[];
|
||||
ocr_raw?: any;
|
||||
stage_1_output?: any;
|
||||
stage_2_output?: any;
|
||||
frontend_response?: any;
|
||||
}
|
||||
|
||||
// Store active log in global context to persist across Next.js hot-reloads
|
||||
const globalForActiveLog = global as unknown as {
|
||||
activeLog: ActiveUploadLog | null;
|
||||
};
|
||||
|
||||
export function startActiveLog(filename: string) {
|
||||
globalForActiveLog.activeLog = {
|
||||
filename,
|
||||
vllm_calls: []
|
||||
};
|
||||
console.log(`[ActiveLog] Started tracking log for ${filename}`);
|
||||
}
|
||||
|
||||
export function logVllmCall(request: any, response: any) {
|
||||
if (globalForActiveLog.activeLog) {
|
||||
globalForActiveLog.activeLog.vllm_calls.push({
|
||||
request,
|
||||
response,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
console.log(`[ActiveLog] Logged vLLM call for ${globalForActiveLog.activeLog.filename} (total calls: ${globalForActiveLog.activeLog.vllm_calls.length})`);
|
||||
} else {
|
||||
console.log("[ActiveLog] Warning: Attempted to log vLLM call but no active log session is running.");
|
||||
}
|
||||
}
|
||||
|
||||
export function getActiveLog(): ActiveUploadLog | null {
|
||||
return globalForActiveLog.activeLog;
|
||||
}
|
||||
|
||||
export function clearActiveLog() {
|
||||
globalForActiveLog.activeLog = null;
|
||||
console.log("[ActiveLog] Cleared active log tracking context");
|
||||
}
|
||||
Reference in new issue
Block a user