github-actions[bot]
commited on
Commit
·
060dc2a
1
Parent(s):
a571c24
Sync from GitHub: 922d61677657398e61342f3cabff773c13c26de4
Browse files- .gitignore +4 -0
- Dockerfile +13 -2
- README.md +146 -24
- app.py +85 -2
- build-frontend.bat +47 -0
- build-frontend.sh +45 -0
- config.py +1 -1
- frontend/.gitignore +41 -0
- frontend/README.md +110 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +27 -0
- frontend/postcss.config.js +6 -0
- frontend/src/App.jsx +194 -0
- frontend/src/components/FileUpload.jsx +177 -0
- frontend/src/components/ProgressIndicator.jsx +97 -0
- frontend/src/components/ResultCard.jsx +240 -0
- frontend/src/index.css +38 -0
- frontend/src/main.jsx +10 -0
- frontend/src/utils/api.js +68 -0
- frontend/src/utils/fileConverter.js +97 -0
- frontend/tailwind.config.js +26 -0
- frontend/vite.config.js +19 -0
- setup.bat +53 -0
- start.bat +26 -0
.gitignore
CHANGED
|
@@ -27,6 +27,10 @@ htmlcov/
|
|
| 27 |
.mypy_cache/
|
| 28 |
.ipynb_checkpoints/
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
*.md
|
| 31 |
!README_git.md
|
| 32 |
!README.md
|
|
|
|
| 27 |
.mypy_cache/
|
| 28 |
.ipynb_checkpoints/
|
| 29 |
|
| 30 |
+
# Frontend development files
|
| 31 |
+
frontend/node_modules/
|
| 32 |
+
frontend/.env.local
|
| 33 |
+
|
| 34 |
*.md
|
| 35 |
!README_git.md
|
| 36 |
!README.md
|
Dockerfile
CHANGED
|
@@ -2,7 +2,7 @@ FROM python:3.10-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
# Install system dependencies
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
git \
|
| 8 |
libgl1 \
|
|
@@ -11,6 +11,9 @@ RUN apt-get update && apt-get install -y \
|
|
| 11 |
libxext6 \
|
| 12 |
libxrender-dev \
|
| 13 |
libgomp1 \
|
|
|
|
|
|
|
|
|
|
| 14 |
&& rm -rf /var/lib/apt/lists/*
|
| 15 |
|
| 16 |
# Copy requirements first for better caching
|
|
@@ -19,7 +22,15 @@ COPY requirements.txt .
|
|
| 19 |
# Install Python dependencies
|
| 20 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 21 |
|
| 22 |
-
# Copy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
COPY config.py .
|
| 24 |
COPY model_manager.py .
|
| 25 |
COPY inference.py .
|
|
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Install system dependencies including Node.js
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
git \
|
| 8 |
libgl1 \
|
|
|
|
| 11 |
libxext6 \
|
| 12 |
libxrender-dev \
|
| 13 |
libgomp1 \
|
| 14 |
+
curl \
|
| 15 |
+
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
| 16 |
+
&& apt-get install -y nodejs \
|
| 17 |
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
|
| 19 |
# Copy requirements first for better caching
|
|
|
|
| 22 |
# Install Python dependencies
|
| 23 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 24 |
|
| 25 |
+
# Copy frontend files and build
|
| 26 |
+
COPY frontend/package*.json frontend/
|
| 27 |
+
WORKDIR /app/frontend
|
| 28 |
+
RUN npm install
|
| 29 |
+
COPY frontend/ .
|
| 30 |
+
RUN npm run build
|
| 31 |
+
|
| 32 |
+
# Copy backend application files
|
| 33 |
+
WORKDIR /app
|
| 34 |
COPY config.py .
|
| 35 |
COPY model_manager.py .
|
| 36 |
COPY inference.py .
|
README.md
CHANGED
|
@@ -9,42 +9,164 @@ license: mit
|
|
| 9 |
app_port: 7860
|
| 10 |
---
|
| 11 |
|
| 12 |
-
# Invoice Information Extractor
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
## Features
|
| 17 |
|
| 18 |
-
|
| 19 |
-
-
|
| 20 |
-
-
|
| 21 |
-
-
|
| 22 |
-
-
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
- **Qwen2.5-VL-7B**: Vision-language model for text extraction (4-bit quantized)
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
```python
|
| 37 |
-
import requests
|
| 38 |
|
| 39 |
-
|
| 40 |
-
files
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
```
|
| 45 |
|
| 46 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
```bash
|
| 48 |
-
|
| 49 |
-
|
| 50 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
app_port: 7860
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Invoice Information Extractor 🧾
|
| 13 |
|
| 14 |
+
A complete full-stack application for extracting information from invoices using AI. Features a modern React frontend and powerful FastAPI backend with YOLOv8 and PaddleOCR.
|
| 15 |
|
| 16 |
+
## ✨ Features
|
| 17 |
|
| 18 |
+
### Frontend (React + Vite)
|
| 19 |
+
- 📁 **Drag-and-drop file upload** with multi-file support
|
| 20 |
+
- 📄 **PDF to Image conversion** (automatically converts multi-page PDFs)
|
| 21 |
+
- 🔄 **Batch processing** with real-time progress tracking
|
| 22 |
+
- 🎨 **Visual detection** of signatures and stamps with bounding boxes
|
| 23 |
+
- 📱 **Responsive design** that works on all devices
|
| 24 |
+
- ⚡ **Fast and modern** UI with Tailwind CSS
|
| 25 |
|
| 26 |
+
### Backend (FastAPI + AI Models)
|
| 27 |
+
- 🤖 **YOLOv8 object detection** for signatures and stamps
|
| 28 |
+
- 📝 **PaddleOCR** for text extraction
|
| 29 |
+
- 🚀 **High-performance API** with async support
|
| 30 |
+
- 📊 **Batch processing** capabilities
|
| 31 |
+
- 🔒 **CORS enabled** for frontend integration
|
| 32 |
+
- 📚 **Interactive API docs** at /docs
|
| 33 |
|
| 34 |
+
## 🎯 Quick Start
|
|
|
|
| 35 |
|
| 36 |
+
### Option 1: Automated Setup (Windows)
|
| 37 |
|
| 38 |
+
Run the automated setup script:
|
| 39 |
+
```bash
|
| 40 |
+
setup.bat
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Then start both servers:
|
| 44 |
+
```bash
|
| 45 |
+
start.bat
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Option 2: Manual Setup
|
| 49 |
+
|
| 50 |
+
#### Backend Setup
|
| 51 |
+
```bash
|
| 52 |
+
# Install dependencies
|
| 53 |
+
pip install -r requirements.txt
|
| 54 |
+
|
| 55 |
+
# Start the server
|
| 56 |
+
python app.py
|
| 57 |
+
```
|
| 58 |
+
Backend runs on: http://localhost:7860
|
| 59 |
|
| 60 |
+
#### Frontend Setup
|
| 61 |
+
```bash
|
| 62 |
+
# Navigate to frontend
|
| 63 |
+
cd frontend
|
| 64 |
+
|
| 65 |
+
# Install dependencies
|
| 66 |
+
npm install
|
| 67 |
+
|
| 68 |
+
# Create environment file
|
| 69 |
+
cp .env.example .env
|
| 70 |
+
|
| 71 |
+
# Start development server
|
| 72 |
+
npm run dev
|
| 73 |
+
```
|
| 74 |
+
Frontend runs on: http://localhost:3000
|
| 75 |
|
| 76 |
+
## 🖥️ Usage
|
|
|
|
|
|
|
| 77 |
|
| 78 |
+
1. **Open the application** at http://localhost:3000
|
| 79 |
+
2. **Upload files** by dragging and dropping or clicking to browse
|
| 80 |
+
- Supports: JPEG, PNG, GIF, WEBP images and PDF files
|
| 81 |
+
3. **Click "Process"** to start extraction
|
| 82 |
+
4. **View results** with:
|
| 83 |
+
- Extracted text from the invoice
|
| 84 |
+
- Visual detection of signatures (red boxes)
|
| 85 |
+
- Visual detection of stamps (blue boxes)
|
| 86 |
+
- Coordinates and metadata
|
| 87 |
+
|
| 88 |
+
## 🔌 API Endpoints
|
| 89 |
+
|
| 90 |
+
### POST /process-invoice
|
| 91 |
+
Process a single invoice image
|
| 92 |
+
|
| 93 |
+
**Request:**
|
| 94 |
+
```bash
|
| 95 |
+
curl -X POST "http://localhost:7860/process-invoice" \
|
| 96 |
+
-F "file=@invoice.jpg"
|
| 97 |
+
```
|
| 98 |
|
| 99 |
+
**Response:**
|
| 100 |
+
```json
|
| 101 |
+
{
|
| 102 |
+
"extracted_text": "Invoice details...",
|
| 103 |
+
"signature_coords": [[x1, y1, x2, y2]],
|
| 104 |
+
"stamp_coords": [[x1, y1, x2, y2]],
|
| 105 |
+
"doc_id": "invoice",
|
| 106 |
+
"processing_time": 2.5
|
| 107 |
+
}
|
| 108 |
```
|
| 109 |
|
| 110 |
+
### GET /docs
|
| 111 |
+
Interactive API documentation (Swagger UI)
|
| 112 |
+
|
| 113 |
+
### GET /health
|
| 114 |
+
Health check endpoint
|
| 115 |
+
|
| 116 |
+
## 🛠️ Technology Stack
|
| 117 |
+
|
| 118 |
+
### Frontend
|
| 119 |
+
- **React 18** - Modern UI library
|
| 120 |
+
- **Vite** - Lightning-fast build tool
|
| 121 |
+
- **Tailwind CSS** - Utility-first CSS
|
| 122 |
+
- **PDF.js** - PDF rendering
|
| 123 |
+
- **Axios** - HTTP client
|
| 124 |
+
|
| 125 |
+
### Backend
|
| 126 |
+
- **FastAPI** - Modern Python web framework
|
| 127 |
+
- **YOLOv8** - State-of-the-art object detection
|
| 128 |
+
- **PaddleOCR** - Multilingual OCR
|
| 129 |
+
- **Uvicorn** - ASGI server
|
| 130 |
+
|
| 131 |
+
## 📖 Documentation
|
| 132 |
+
|
| 133 |
+
- [Setup Guide](SETUP_GUIDE.md) - Detailed setup instructions
|
| 134 |
+
- [Frontend Architecture](frontend/ARCHITECTURE.md) - Frontend technical details
|
| 135 |
+
- [API Documentation](http://localhost:7860/docs) - Interactive API docs (when running)
|
| 136 |
+
|
| 137 |
+
## 🚀 Deployment
|
| 138 |
+
|
| 139 |
+
### Hugging Face Spaces
|
| 140 |
+
|
| 141 |
+
The application is fully configured for deployment to Hugging Face Spaces:
|
| 142 |
+
|
| 143 |
+
1. **Build frontend**:
|
| 144 |
+
```bash
|
| 145 |
+
build-frontend.bat
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
2. **Push to GitHub** (auto-deploys via GitHub Actions):
|
| 149 |
+
```bash
|
| 150 |
+
git add .
|
| 151 |
+
git commit -m "Deploy to HF"
|
| 152 |
+
git push origin main
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
See [HF_DEPLOYMENT_READY.md](HF_DEPLOYMENT_READY.md) and [DEPLOYMENT.md](DEPLOYMENT.md) for detailed instructions.
|
| 156 |
+
|
| 157 |
+
### Docker
|
| 158 |
+
|
| 159 |
```bash
|
| 160 |
+
docker build -t invoice-extractor .
|
| 161 |
+
docker run -p 7860:7860 invoice-extractor
|
| 162 |
```
|
| 163 |
+
|
| 164 |
+
The Dockerfile automatically builds the frontend during the build process.
|
| 165 |
+
|
| 166 |
+
## 📄 License
|
| 167 |
+
|
| 168 |
+
This project is licensed under the MIT License.
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
**Made with ❤️ using AI and modern web technologies**
|
app.py
CHANGED
|
@@ -4,7 +4,8 @@ Provides REST API for invoice processing
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Form
|
| 7 |
-
from fastapi.responses import JSONResponse
|
|
|
|
| 8 |
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
from contextlib import asynccontextmanager
|
| 10 |
from typing import Optional
|
|
@@ -56,10 +57,21 @@ app.add_middleware(
|
|
| 56 |
allow_headers=["*"],
|
| 57 |
)
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
@app.get("/")
|
| 61 |
async def root():
|
| 62 |
-
"""Root endpoint - API information"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
return {
|
| 64 |
"name": API_TITLE,
|
| 65 |
"version": API_VERSION,
|
|
@@ -67,6 +79,7 @@ async def root():
|
|
| 67 |
"models_loaded": model_manager.is_loaded(),
|
| 68 |
"endpoints": {
|
| 69 |
"health": "/health",
|
|
|
|
| 70 |
"extract": "/extract (POST)",
|
| 71 |
"docs": "/docs"
|
| 72 |
}
|
|
@@ -184,6 +197,76 @@ async def extract_invoice(
|
|
| 184 |
file.file.close()
|
| 185 |
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
@app.post("/extract_batch")
|
| 188 |
async def extract_batch(
|
| 189 |
files: list[UploadFile] = File(..., description="Multiple invoice images")
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from fastapi import FastAPI, File, UploadFile, HTTPException, Form
|
| 7 |
+
from fastapi.responses import JSONResponse, FileResponse
|
| 8 |
+
from fastapi.staticfiles import StaticFiles
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
from contextlib import asynccontextmanager
|
| 11 |
from typing import Optional
|
|
|
|
| 57 |
allow_headers=["*"],
|
| 58 |
)
|
| 59 |
|
| 60 |
+
# Mount frontend static files if they exist
|
| 61 |
+
frontend_dist = os.path.join(os.path.dirname(__file__), "frontend", "dist")
|
| 62 |
+
if os.path.exists(frontend_dist):
|
| 63 |
+
app.mount("/assets", StaticFiles(directory=os.path.join(frontend_dist, "assets")), name="assets")
|
| 64 |
+
print(f"📂 Serving frontend from: {frontend_dist}")
|
| 65 |
+
|
| 66 |
|
| 67 |
@app.get("/")
|
| 68 |
async def root():
|
| 69 |
+
"""Root endpoint - Serve frontend or API information"""
|
| 70 |
+
frontend_index = os.path.join(os.path.dirname(__file__), "frontend", "dist", "index.html")
|
| 71 |
+
if os.path.exists(frontend_index):
|
| 72 |
+
return FileResponse(frontend_index)
|
| 73 |
+
|
| 74 |
+
# Fallback to API information
|
| 75 |
return {
|
| 76 |
"name": API_TITLE,
|
| 77 |
"version": API_VERSION,
|
|
|
|
| 79 |
"models_loaded": model_manager.is_loaded(),
|
| 80 |
"endpoints": {
|
| 81 |
"health": "/health",
|
| 82 |
+
"process": "/process-invoice (POST)",
|
| 83 |
"extract": "/extract (POST)",
|
| 84 |
"docs": "/docs"
|
| 85 |
}
|
|
|
|
| 197 |
file.file.close()
|
| 198 |
|
| 199 |
|
| 200 |
+
@app.post("/process-invoice")
|
| 201 |
+
async def process_invoice(
|
| 202 |
+
file: UploadFile = File(..., description="Invoice image file")
|
| 203 |
+
):
|
| 204 |
+
"""
|
| 205 |
+
Process a single invoice and return extracted information
|
| 206 |
+
Simplified endpoint for frontend integration
|
| 207 |
+
|
| 208 |
+
**Parameters:**
|
| 209 |
+
- **file**: Invoice image file (required)
|
| 210 |
+
|
| 211 |
+
**Returns:**
|
| 212 |
+
- JSON with extracted_text, signature_coords, stamp_coords
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
# Validate file type
|
| 216 |
+
if file.content_type and not file.content_type.startswith("image/"):
|
| 217 |
+
raise HTTPException(
|
| 218 |
+
status_code=400,
|
| 219 |
+
detail="File must be an image"
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
# Check if models are loaded
|
| 223 |
+
if not model_manager.is_loaded():
|
| 224 |
+
raise HTTPException(
|
| 225 |
+
status_code=503,
|
| 226 |
+
detail="Models not loaded. Please wait for server initialization."
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
temp_file = None
|
| 230 |
+
try:
|
| 231 |
+
# Save uploaded file to temporary location
|
| 232 |
+
suffix = os.path.splitext(file.filename)[1] if file.filename else '.jpg'
|
| 233 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp:
|
| 234 |
+
temp_file = temp.name
|
| 235 |
+
shutil.copyfileobj(file.file, temp)
|
| 236 |
+
|
| 237 |
+
# Use filename as doc_id
|
| 238 |
+
doc_id = os.path.splitext(file.filename)[0] if file.filename else "invoice"
|
| 239 |
+
|
| 240 |
+
# Process invoice
|
| 241 |
+
result = InferenceProcessor.process_invoice(temp_file, doc_id)
|
| 242 |
+
|
| 243 |
+
# Return simplified response matching frontend expectations
|
| 244 |
+
return JSONResponse(content={
|
| 245 |
+
"extracted_text": result.get("extracted_text", ""),
|
| 246 |
+
"signature_coords": result.get("signature_coords", []),
|
| 247 |
+
"stamp_coords": result.get("stamp_coords", []),
|
| 248 |
+
"doc_id": result.get("doc_id", doc_id),
|
| 249 |
+
"processing_time": result.get("processing_time_sec", 0)
|
| 250 |
+
}, media_type="application/json; charset=utf-8")
|
| 251 |
+
|
| 252 |
+
except Exception as e:
|
| 253 |
+
raise HTTPException(
|
| 254 |
+
status_code=500,
|
| 255 |
+
detail=f"Error processing invoice: {str(e)}"
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
finally:
|
| 259 |
+
# Clean up temporary file
|
| 260 |
+
if temp_file and os.path.exists(temp_file):
|
| 261 |
+
try:
|
| 262 |
+
os.unlink(temp_file)
|
| 263 |
+
except:
|
| 264 |
+
pass
|
| 265 |
+
|
| 266 |
+
# Close uploaded file
|
| 267 |
+
file.file.close()
|
| 268 |
+
|
| 269 |
+
|
| 270 |
@app.post("/extract_batch")
|
| 271 |
async def extract_batch(
|
| 272 |
files: list[UploadFile] = File(..., description="Multiple invoice images")
|
build-frontend.bat
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo ============================================
|
| 3 |
+
echo Building Frontend for Production
|
| 4 |
+
echo ============================================
|
| 5 |
+
echo.
|
| 6 |
+
|
| 7 |
+
cd frontend
|
| 8 |
+
|
| 9 |
+
echo [1/2] Installing dependencies...
|
| 10 |
+
call npm install
|
| 11 |
+
if errorlevel 1 (
|
| 12 |
+
echo ERROR: Failed to install dependencies
|
| 13 |
+
pause
|
| 14 |
+
exit /b 1
|
| 15 |
+
)
|
| 16 |
+
echo ✓ Dependencies installed
|
| 17 |
+
echo.
|
| 18 |
+
|
| 19 |
+
echo [2/2] Building frontend...
|
| 20 |
+
call npm run build
|
| 21 |
+
if errorlevel 1 (
|
| 22 |
+
echo ERROR: Build failed
|
| 23 |
+
pause
|
| 24 |
+
exit /b 1
|
| 25 |
+
)
|
| 26 |
+
echo ✓ Frontend built successfully
|
| 27 |
+
echo.
|
| 28 |
+
|
| 29 |
+
cd ..
|
| 30 |
+
|
| 31 |
+
echo ============================================
|
| 32 |
+
echo Build Complete!
|
| 33 |
+
echo ============================================
|
| 34 |
+
echo.
|
| 35 |
+
echo Frontend built to: frontend/dist/
|
| 36 |
+
echo.
|
| 37 |
+
echo Ready to push to Hugging Face!
|
| 38 |
+
echo.
|
| 39 |
+
echo Next steps:
|
| 40 |
+
echo 1. Commit changes: git add . && git commit -m "Build frontend"
|
| 41 |
+
echo 2. Push to GitHub: git push origin main
|
| 42 |
+
echo 3. GitHub Actions will auto-deploy to HF
|
| 43 |
+
echo.
|
| 44 |
+
echo Or push directly to HF:
|
| 45 |
+
echo git push hf main
|
| 46 |
+
echo ============================================
|
| 47 |
+
pause
|
build-frontend.sh
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pre-deployment Build Script
|
| 2 |
+
|
| 3 |
+
echo "============================================"
|
| 4 |
+
echo "Building Frontend for Production"
|
| 5 |
+
echo "============================================"
|
| 6 |
+
echo ""
|
| 7 |
+
|
| 8 |
+
cd frontend
|
| 9 |
+
|
| 10 |
+
echo "[1/2] Installing dependencies..."
|
| 11 |
+
npm install
|
| 12 |
+
if [ $? -ne 0 ]; then
|
| 13 |
+
echo "ERROR: Failed to install dependencies"
|
| 14 |
+
exit 1
|
| 15 |
+
fi
|
| 16 |
+
echo "✓ Dependencies installed"
|
| 17 |
+
echo ""
|
| 18 |
+
|
| 19 |
+
echo "[2/2] Building frontend..."
|
| 20 |
+
npm run build
|
| 21 |
+
if [ $? -ne 0 ]; then
|
| 22 |
+
echo "ERROR: Build failed"
|
| 23 |
+
exit 1
|
| 24 |
+
fi
|
| 25 |
+
echo "✓ Frontend built successfully"
|
| 26 |
+
echo ""
|
| 27 |
+
|
| 28 |
+
cd ..
|
| 29 |
+
|
| 30 |
+
echo "============================================"
|
| 31 |
+
echo "Build Complete!"
|
| 32 |
+
echo "============================================"
|
| 33 |
+
echo ""
|
| 34 |
+
echo "Frontend built to: frontend/dist/"
|
| 35 |
+
echo ""
|
| 36 |
+
echo "Ready to push to Hugging Face!"
|
| 37 |
+
echo ""
|
| 38 |
+
echo "Next steps:"
|
| 39 |
+
echo " 1. Commit changes: git add . && git commit -m 'Build frontend'"
|
| 40 |
+
echo " 2. Push to GitHub: git push origin main"
|
| 41 |
+
echo " 3. GitHub Actions will auto-deploy to HF"
|
| 42 |
+
echo ""
|
| 43 |
+
echo "Or push directly to HF:"
|
| 44 |
+
echo " git push hf main"
|
| 45 |
+
echo "============================================"
|
config.py
CHANGED
|
@@ -34,7 +34,7 @@ HP_VALID_RANGE = (20, 120)
|
|
| 34 |
ASSET_COST_VALID_RANGE = (100_000, 3_000_000)
|
| 35 |
|
| 36 |
# Cost calculation
|
| 37 |
-
COST_PER_GPU_HOUR = 0.
|
| 38 |
|
| 39 |
# API settings
|
| 40 |
API_TITLE = "Invoice Information Extractor API"
|
|
|
|
| 34 |
ASSET_COST_VALID_RANGE = (100_000, 3_000_000)
|
| 35 |
|
| 36 |
# Cost calculation
|
| 37 |
+
COST_PER_GPU_HOUR = 0.6 # USD
|
| 38 |
|
| 39 |
# API settings
|
| 40 |
API_TITLE = "Invoice Information Extractor API"
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dependencies
|
| 2 |
+
node_modules/
|
| 3 |
+
|
| 4 |
+
# Build outputs
|
| 5 |
+
dist/
|
| 6 |
+
build/
|
| 7 |
+
|
| 8 |
+
# Environment files
|
| 9 |
+
.env
|
| 10 |
+
.env.local
|
| 11 |
+
.env.*.local
|
| 12 |
+
|
| 13 |
+
# Logs
|
| 14 |
+
npm-debug.log*
|
| 15 |
+
yarn-debug.log*
|
| 16 |
+
yarn-error.log*
|
| 17 |
+
pnpm-debug.log*
|
| 18 |
+
|
| 19 |
+
# Editor directories and files
|
| 20 |
+
.vscode/*
|
| 21 |
+
!.vscode/extensions.json
|
| 22 |
+
.idea
|
| 23 |
+
*.suo
|
| 24 |
+
*.ntvs*
|
| 25 |
+
*.njsproj
|
| 26 |
+
*.sln
|
| 27 |
+
*.sw?
|
| 28 |
+
|
| 29 |
+
# OS files
|
| 30 |
+
.DS_Store
|
| 31 |
+
Thumbs.db
|
| 32 |
+
|
| 33 |
+
# Testing
|
| 34 |
+
coverage/
|
| 35 |
+
.nyc_output/
|
| 36 |
+
|
| 37 |
+
# Cache
|
| 38 |
+
.cache/
|
| 39 |
+
.parcel-cache/
|
| 40 |
+
.env.example
|
| 41 |
+
.env.production
|
frontend/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Invoice Extractor Frontend
|
| 2 |
+
|
| 3 |
+
A modern, interactive React frontend for the Invoice Information Extractor application.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- 📁 **Multiple File Upload**: Support for images (JPEG, PNG, GIF, WEBP) and PDF files
|
| 8 |
+
- 🔄 **PDF Conversion**: Automatic PDF to image conversion in the browser
|
| 9 |
+
- 📊 **Batch Processing**: Process multiple files sequentially with real-time progress
|
| 10 |
+
- 🎨 **Visual Detection**: Display bounding boxes for detected signatures and stamps
|
| 11 |
+
- 📱 **Responsive Design**: Works seamlessly on desktop and mobile devices
|
| 12 |
+
- ⚡ **Real-time Updates**: See results as they are processed
|
| 13 |
+
|
| 14 |
+
## Installation
|
| 15 |
+
|
| 16 |
+
1. Install dependencies:
|
| 17 |
+
```bash
|
| 18 |
+
cd frontend
|
| 19 |
+
npm install
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
2. Create environment file:
|
| 23 |
+
```bash
|
| 24 |
+
cp .env.example .env
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
3. Update `.env` with your backend API URL (default: http://localhost:8000)
|
| 28 |
+
|
| 29 |
+
## Development
|
| 30 |
+
|
| 31 |
+
Start the development server:
|
| 32 |
+
```bash
|
| 33 |
+
npm run dev
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
The app will be available at `http://localhost:3000`
|
| 37 |
+
|
| 38 |
+
## Building for Production
|
| 39 |
+
|
| 40 |
+
```bash
|
| 41 |
+
npm run build
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
The built files will be in the `dist` directory.
|
| 45 |
+
|
| 46 |
+
## Preview Production Build
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
npm run preview
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Technology Stack
|
| 53 |
+
|
| 54 |
+
- **React 18** - UI framework
|
| 55 |
+
- **Vite** - Build tool and dev server
|
| 56 |
+
- **Tailwind CSS** - Styling
|
| 57 |
+
- **PDF.js** - PDF rendering and conversion
|
| 58 |
+
- **Axios** - HTTP client
|
| 59 |
+
- **Lucide React** - Icon library
|
| 60 |
+
|
| 61 |
+
## Usage
|
| 62 |
+
|
| 63 |
+
1. **Upload Files**: Drag and drop or click to select images/PDFs
|
| 64 |
+
2. **Process**: Click "Process" button to send files to the backend
|
| 65 |
+
3. **View Results**: See extracted text, signatures, and stamps with visual indicators
|
| 66 |
+
4. **Batch Processing**: Multiple files are processed one by one with progress tracking
|
| 67 |
+
|
| 68 |
+
## API Integration
|
| 69 |
+
|
| 70 |
+
The frontend expects the following backend endpoints:
|
| 71 |
+
|
| 72 |
+
- `POST /process-invoice` - Process a single invoice image
|
| 73 |
+
- Request: FormData with 'file' field
|
| 74 |
+
- Response: JSON with extracted_text, signature_coords, stamp_coords
|
| 75 |
+
|
| 76 |
+
## Features in Detail
|
| 77 |
+
|
| 78 |
+
### PDF to Image Conversion
|
| 79 |
+
- Converts multi-page PDFs to individual images
|
| 80 |
+
- Each page is processed separately
|
| 81 |
+
- High-quality conversion using PDF.js
|
| 82 |
+
|
| 83 |
+
### Bounding Box Visualization
|
| 84 |
+
- Red boxes for signatures
|
| 85 |
+
- Blue boxes for stamps
|
| 86 |
+
- Drawn directly on the canvas over the image
|
| 87 |
+
|
| 88 |
+
### Batch Processing
|
| 89 |
+
- Sequential processing to avoid overwhelming the backend
|
| 90 |
+
- Real-time progress updates
|
| 91 |
+
- Individual error handling per file
|
| 92 |
+
|
| 93 |
+
## Customization
|
| 94 |
+
|
| 95 |
+
### Styling
|
| 96 |
+
Edit `tailwind.config.js` to customize colors and theme.
|
| 97 |
+
|
| 98 |
+
### API Endpoint
|
| 99 |
+
Update `VITE_API_URL` in `.env` file.
|
| 100 |
+
|
| 101 |
+
### Supported File Types
|
| 102 |
+
Modify the `accept` attribute in FileUpload.jsx and validation logic.
|
| 103 |
+
|
| 104 |
+
## Troubleshooting
|
| 105 |
+
|
| 106 |
+
**CORS Issues**: Make sure your backend allows requests from the frontend origin.
|
| 107 |
+
|
| 108 |
+
**PDF Not Loading**: Check that PDF.js worker is properly loaded (check browser console).
|
| 109 |
+
|
| 110 |
+
**API Errors**: Verify backend is running and accessible at the configured URL.
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Invoice Information Extractor</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "invoice-extractor-frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.2.0",
|
| 13 |
+
"react-dom": "^18.2.0",
|
| 14 |
+
"axios": "^1.6.2",
|
| 15 |
+
"pdfjs-dist": "^3.11.174",
|
| 16 |
+
"lucide-react": "^0.294.0"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@types/react": "^18.2.43",
|
| 20 |
+
"@types/react-dom": "^18.2.17",
|
| 21 |
+
"@vitejs/plugin-react": "^4.2.1",
|
| 22 |
+
"autoprefixer": "^10.4.16",
|
| 23 |
+
"postcss": "^8.4.32",
|
| 24 |
+
"tailwindcss": "^3.3.6",
|
| 25 |
+
"vite": "^5.0.8"
|
| 26 |
+
}
|
| 27 |
+
}
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { FileText, AlertCircle } from 'lucide-react';
|
| 3 |
+
import FileUpload from './components/FileUpload';
|
| 4 |
+
import ProgressIndicator from './components/ProgressIndicator';
|
| 5 |
+
import ResultCard from './components/ResultCard';
|
| 6 |
+
import { convertFileToImages, dataUrlToBlob } from './utils/fileConverter';
|
| 7 |
+
import { processBatchInvoices } from './utils/api';
|
| 8 |
+
|
| 9 |
+
function App() {
|
| 10 |
+
const [processing, setProcessing] = useState(false);
|
| 11 |
+
const [results, setResults] = useState([]);
|
| 12 |
+
const [progress, setProgress] = useState({ total: 0, completed: 0, current: '' });
|
| 13 |
+
const [error, setError] = useState(null);
|
| 14 |
+
const [imageDataMap, setImageDataMap] = useState({});
|
| 15 |
+
|
| 16 |
+
const handleFilesSelected = async (files) => {
|
| 17 |
+
setProcessing(true);
|
| 18 |
+
setResults([]);
|
| 19 |
+
setError(null);
|
| 20 |
+
setImageDataMap({});
|
| 21 |
+
|
| 22 |
+
try {
|
| 23 |
+
// Step 1: Convert all files to images
|
| 24 |
+
const allImages = [];
|
| 25 |
+
const imageData = {};
|
| 26 |
+
|
| 27 |
+
for (const file of files) {
|
| 28 |
+
try {
|
| 29 |
+
const images = await convertFileToImages(file);
|
| 30 |
+
images.forEach((img, idx) => {
|
| 31 |
+
const key = `${file.name}_${idx}`;
|
| 32 |
+
allImages.push({
|
| 33 |
+
blob: dataUrlToBlob(img.dataUrl),
|
| 34 |
+
filename: img.filename,
|
| 35 |
+
originalFile: img.originalFile,
|
| 36 |
+
pageNumber: img.pageNumber,
|
| 37 |
+
key: key
|
| 38 |
+
});
|
| 39 |
+
imageData[key] = img.dataUrl;
|
| 40 |
+
});
|
| 41 |
+
} catch (err) {
|
| 42 |
+
console.error(`Error converting ${file.name}:`, err);
|
| 43 |
+
setError(`Failed to convert ${file.name}: ${err.message}`);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
setImageDataMap(imageData);
|
| 48 |
+
setProgress({ total: allImages.length, completed: 0, current: '' });
|
| 49 |
+
|
| 50 |
+
// Step 2: Process all images through the API
|
| 51 |
+
await processBatchInvoices(allImages, (index, result) => {
|
| 52 |
+
// Update progress
|
| 53 |
+
setProgress(prev => ({
|
| 54 |
+
...prev,
|
| 55 |
+
completed: index + 1,
|
| 56 |
+
current: index < allImages.length - 1 ? allImages[index + 1].filename : ''
|
| 57 |
+
}));
|
| 58 |
+
|
| 59 |
+
// Add result
|
| 60 |
+
setResults(prev => [...prev, result]);
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
} catch (err) {
|
| 64 |
+
console.error('Processing error:', err);
|
| 65 |
+
setError(`Processing failed: ${err.message}`);
|
| 66 |
+
} finally {
|
| 67 |
+
setProcessing(false);
|
| 68 |
+
setProgress(prev => ({ ...prev, current: '' }));
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
<div className="min-h-screen py-8 px-4 sm:px-6 lg:px-8">
|
| 74 |
+
<div className="max-w-7xl mx-auto">
|
| 75 |
+
{/* Header */}
|
| 76 |
+
<div className="text-center mb-8">
|
| 77 |
+
<div className="flex items-center justify-center mb-4">
|
| 78 |
+
<div className="p-3 bg-white rounded-2xl shadow-lg">
|
| 79 |
+
<FileText className="w-12 h-12 text-primary-600" />
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
<h1 className="text-4xl font-bold text-white mb-2 drop-shadow-lg">
|
| 83 |
+
Invoice Information Extractor
|
| 84 |
+
</h1>
|
| 85 |
+
<p className="text-lg text-white/90 drop-shadow">
|
| 86 |
+
Extract text, detect signatures and stamps from invoices using AI
|
| 87 |
+
</p>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
{/* Main Content */}
|
| 91 |
+
<div className="space-y-6">
|
| 92 |
+
{/* Upload Section */}
|
| 93 |
+
<div className="glass-morphism p-6">
|
| 94 |
+
<FileUpload onFilesSelected={handleFilesSelected} disabled={processing} />
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
{/* Error Display */}
|
| 98 |
+
{error && (
|
| 99 |
+
<div className="bg-red-50 border-2 border-red-200 rounded-xl p-4 flex items-start">
|
| 100 |
+
<AlertCircle className="w-6 h-6 text-red-600 mr-3 flex-shrink-0 mt-0.5" />
|
| 101 |
+
<div>
|
| 102 |
+
<h3 className="text-red-800 font-semibold mb-1">Error</h3>
|
| 103 |
+
<p className="text-red-700 text-sm">{error}</p>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
)}
|
| 107 |
+
|
| 108 |
+
{/* Progress Indicator */}
|
| 109 |
+
{processing && (
|
| 110 |
+
<ProgressIndicator
|
| 111 |
+
total={progress.total}
|
| 112 |
+
completed={progress.completed}
|
| 113 |
+
current={progress.current}
|
| 114 |
+
results={results}
|
| 115 |
+
/>
|
| 116 |
+
)}
|
| 117 |
+
|
| 118 |
+
{/* Results Section */}
|
| 119 |
+
{results.length > 0 && (
|
| 120 |
+
<div className="space-y-4">
|
| 121 |
+
<div className="flex items-center justify-between bg-white rounded-xl p-4 shadow-md">
|
| 122 |
+
<h2 className="text-2xl font-bold text-gray-800 flex items-center">
|
| 123 |
+
<FileText className="w-6 h-6 mr-2 text-primary-600" />
|
| 124 |
+
Processing Results
|
| 125 |
+
</h2>
|
| 126 |
+
<span className="text-sm font-medium text-gray-600 bg-primary-50 px-3 py-1 rounded-full">
|
| 127 |
+
{results.length} {results.length === 1 ? 'Document' : 'Documents'}
|
| 128 |
+
</span>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
{results.map((result, index) => {
|
| 132 |
+
const imageKey = `${result.originalFile}_${index}`;
|
| 133 |
+
return (
|
| 134 |
+
<ResultCard
|
| 135 |
+
key={index}
|
| 136 |
+
result={result}
|
| 137 |
+
imageData={imageDataMap[imageKey] || imageDataMap[Object.keys(imageDataMap)[index]]}
|
| 138 |
+
/>
|
| 139 |
+
);
|
| 140 |
+
})}
|
| 141 |
+
</div>
|
| 142 |
+
)}
|
| 143 |
+
|
| 144 |
+
{/* Info Cards */}
|
| 145 |
+
{!processing && results.length === 0 && (
|
| 146 |
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
| 147 |
+
<div className="glass-morphism p-6 text-center">
|
| 148 |
+
<div className="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
| 149 |
+
<FileText className="w-6 h-6 text-primary-600" />
|
| 150 |
+
</div>
|
| 151 |
+
<h3 className="font-semibold text-gray-800 mb-2">Multiple Formats</h3>
|
| 152 |
+
<p className="text-sm text-gray-600">
|
| 153 |
+
Upload images or PDFs. PDFs are automatically converted to images.
|
| 154 |
+
</p>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div className="glass-morphism p-6 text-center">
|
| 158 |
+
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
| 159 |
+
<svg className="w-6 h-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 160 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
| 161 |
+
</svg>
|
| 162 |
+
</div>
|
| 163 |
+
<h3 className="font-semibold text-gray-800 mb-2">Batch Processing</h3>
|
| 164 |
+
<p className="text-sm text-gray-600">
|
| 165 |
+
Process multiple documents at once and see results one by one.
|
| 166 |
+
</p>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div className="glass-morphism p-6 text-center">
|
| 170 |
+
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
| 171 |
+
<svg className="w-6 h-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 172 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
| 173 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
| 174 |
+
</svg>
|
| 175 |
+
</div>
|
| 176 |
+
<h3 className="font-semibold text-gray-800 mb-2">Visual Detection</h3>
|
| 177 |
+
<p className="text-sm text-gray-600">
|
| 178 |
+
Automatically detect and highlight signatures and stamps on documents.
|
| 179 |
+
</p>
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
)}
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
{/* Footer */}
|
| 186 |
+
<div className="mt-12 text-center text-white/80 text-sm">
|
| 187 |
+
<p>Powered by AI • Secure Processing • Fast & Accurate</p>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
export default App;
|
frontend/src/components/FileUpload.jsx
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useCallback, useState } from 'react';
|
| 2 |
+
import { Upload, FileText, Image as ImageIcon, X } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const FileUpload = ({ onFilesSelected, disabled }) => {
|
| 5 |
+
const [dragActive, setDragActive] = useState(false);
|
| 6 |
+
const [selectedFiles, setSelectedFiles] = useState([]);
|
| 7 |
+
|
| 8 |
+
const handleDrag = useCallback((e) => {
|
| 9 |
+
e.preventDefault();
|
| 10 |
+
e.stopPropagation();
|
| 11 |
+
if (e.type === 'dragenter' || e.type === 'dragover') {
|
| 12 |
+
setDragActive(true);
|
| 13 |
+
} else if (e.type === 'dragleave') {
|
| 14 |
+
setDragActive(false);
|
| 15 |
+
}
|
| 16 |
+
}, []);
|
| 17 |
+
|
| 18 |
+
const handleDrop = useCallback((e) => {
|
| 19 |
+
e.preventDefault();
|
| 20 |
+
e.stopPropagation();
|
| 21 |
+
setDragActive(false);
|
| 22 |
+
|
| 23 |
+
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
| 24 |
+
handleFiles(Array.from(e.dataTransfer.files));
|
| 25 |
+
}
|
| 26 |
+
}, []);
|
| 27 |
+
|
| 28 |
+
const handleFiles = (files) => {
|
| 29 |
+
const validFiles = files.filter(file => {
|
| 30 |
+
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'];
|
| 31 |
+
return validTypes.includes(file.type);
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
if (validFiles.length !== files.length) {
|
| 35 |
+
alert('Some files were skipped. Only images (JPEG, PNG, GIF, WEBP) and PDF files are supported.');
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
setSelectedFiles(prev => [...prev, ...validFiles]);
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
const handleFileInput = (e) => {
|
| 42 |
+
if (e.target.files && e.target.files.length > 0) {
|
| 43 |
+
handleFiles(Array.from(e.target.files));
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const removeFile = (index) => {
|
| 48 |
+
setSelectedFiles(prev => prev.filter((_, i) => i !== index));
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
const handleProcess = () => {
|
| 52 |
+
if (selectedFiles.length > 0) {
|
| 53 |
+
onFilesSelected(selectedFiles);
|
| 54 |
+
setSelectedFiles([]);
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
const formatFileSize = (bytes) => {
|
| 59 |
+
if (bytes === 0) return '0 Bytes';
|
| 60 |
+
const k = 1024;
|
| 61 |
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
| 62 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 63 |
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
| 64 |
+
};
|
| 65 |
+
|
| 66 |
+
return (
|
| 67 |
+
<div className="w-full space-y-4">
|
| 68 |
+
{/* Upload Zone */}
|
| 69 |
+
<div
|
| 70 |
+
className={`upload-zone relative border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${
|
| 71 |
+
dragActive ? 'drag-active' : ''
|
| 72 |
+
} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
|
| 73 |
+
onDragEnter={handleDrag}
|
| 74 |
+
onDragLeave={handleDrag}
|
| 75 |
+
onDragOver={handleDrag}
|
| 76 |
+
onDrop={handleDrop}
|
| 77 |
+
onClick={() => !disabled && document.getElementById('fileInput').click()}
|
| 78 |
+
>
|
| 79 |
+
<input
|
| 80 |
+
id="fileInput"
|
| 81 |
+
type="file"
|
| 82 |
+
multiple
|
| 83 |
+
accept="image/*,.pdf"
|
| 84 |
+
onChange={handleFileInput}
|
| 85 |
+
className="hidden"
|
| 86 |
+
disabled={disabled}
|
| 87 |
+
/>
|
| 88 |
+
|
| 89 |
+
<div className="flex flex-col items-center justify-center space-y-4">
|
| 90 |
+
<div className="p-4 bg-primary-50 rounded-full">
|
| 91 |
+
<Upload className="w-12 h-12 text-primary-500" />
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div>
|
| 95 |
+
<p className="text-lg font-semibold text-gray-700 mb-1">
|
| 96 |
+
Drop your files here or click to browse
|
| 97 |
+
</p>
|
| 98 |
+
<p className="text-sm text-gray-500">
|
| 99 |
+
Support for images (JPEG, PNG, GIF, WEBP) and PDF files
|
| 100 |
+
</p>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<div className="flex items-center gap-2 text-xs text-gray-400">
|
| 104 |
+
<ImageIcon className="w-4 h-4" />
|
| 105 |
+
<span>Multiple files supported</span>
|
| 106 |
+
<span>•</span>
|
| 107 |
+
<FileText className="w-4 h-4" />
|
| 108 |
+
<span>PDF to image conversion</span>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
{/* Selected Files List */}
|
| 114 |
+
{selectedFiles.length > 0 && (
|
| 115 |
+
<div className="space-y-3">
|
| 116 |
+
<div className="flex items-center justify-between">
|
| 117 |
+
<h3 className="text-sm font-semibold text-gray-700">
|
| 118 |
+
Selected Files ({selectedFiles.length})
|
| 119 |
+
</h3>
|
| 120 |
+
<button
|
| 121 |
+
onClick={() => setSelectedFiles([])}
|
| 122 |
+
className="text-xs text-red-500 hover:text-red-700 font-medium"
|
| 123 |
+
>
|
| 124 |
+
Clear All
|
| 125 |
+
</button>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<div className="space-y-2 max-h-64 overflow-y-auto">
|
| 129 |
+
{selectedFiles.map((file, index) => (
|
| 130 |
+
<div
|
| 131 |
+
key={index}
|
| 132 |
+
className="flex items-center justify-between bg-white p-3 rounded-lg border border-gray-200 hover:border-primary-300 transition-colors"
|
| 133 |
+
>
|
| 134 |
+
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
| 135 |
+
<div className="flex-shrink-0">
|
| 136 |
+
{file.type === 'application/pdf' ? (
|
| 137 |
+
<FileText className="w-8 h-8 text-red-500" />
|
| 138 |
+
) : (
|
| 139 |
+
<ImageIcon className="w-8 h-8 text-blue-500" />
|
| 140 |
+
)}
|
| 141 |
+
</div>
|
| 142 |
+
<div className="flex-1 min-w-0">
|
| 143 |
+
<p className="text-sm font-medium text-gray-900 truncate">
|
| 144 |
+
{file.name}
|
| 145 |
+
</p>
|
| 146 |
+
<p className="text-xs text-gray-500">
|
| 147 |
+
{formatFileSize(file.size)}
|
| 148 |
+
{file.type === 'application/pdf' && ' • Will be converted to images'}
|
| 149 |
+
</p>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
<button
|
| 153 |
+
onClick={() => removeFile(index)}
|
| 154 |
+
className="ml-2 p-1 hover:bg-red-50 rounded-full transition-colors"
|
| 155 |
+
title="Remove file"
|
| 156 |
+
>
|
| 157 |
+
<X className="w-5 h-5 text-red-500" />
|
| 158 |
+
</button>
|
| 159 |
+
</div>
|
| 160 |
+
))}
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<button
|
| 164 |
+
onClick={handleProcess}
|
| 165 |
+
disabled={disabled}
|
| 166 |
+
className="w-full bg-gradient-to-r from-primary-500 to-primary-600 text-white font-semibold py-3 px-6 rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
|
| 167 |
+
>
|
| 168 |
+
<Upload className="w-5 h-5" />
|
| 169 |
+
<span>Process {selectedFiles.length} File{selectedFiles.length > 1 ? 's' : ''}</span>
|
| 170 |
+
</button>
|
| 171 |
+
</div>
|
| 172 |
+
)}
|
| 173 |
+
</div>
|
| 174 |
+
);
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
export default FileUpload;
|
frontend/src/components/ProgressIndicator.jsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Loader2, CheckCircle, XCircle } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
const ProgressIndicator = ({ total, completed, current, results }) => {
|
| 5 |
+
const progress = total > 0 ? (completed / total) * 100 : 0;
|
| 6 |
+
const successCount = results.filter(r => r.success).length;
|
| 7 |
+
const errorCount = results.filter(r => !r.success).length;
|
| 8 |
+
|
| 9 |
+
return (
|
| 10 |
+
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
| 11 |
+
<div className="space-y-4">
|
| 12 |
+
<div className="flex items-center justify-between">
|
| 13 |
+
<h3 className="text-lg font-semibold text-gray-800 flex items-center">
|
| 14 |
+
<Loader2 className="w-5 h-5 mr-2 text-primary-500 animate-spin" />
|
| 15 |
+
Processing Files
|
| 16 |
+
</h3>
|
| 17 |
+
<span className="text-sm font-medium text-gray-600">
|
| 18 |
+
{completed} / {total}
|
| 19 |
+
</span>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
{/* Progress Bar */}
|
| 23 |
+
<div className="relative">
|
| 24 |
+
<div className="overflow-hidden h-3 text-xs flex rounded-full bg-gray-200">
|
| 25 |
+
<div
|
| 26 |
+
style={{ width: `${progress}%` }}
|
| 27 |
+
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-gradient-to-r from-primary-500 to-primary-600 transition-all duration-500"
|
| 28 |
+
/>
|
| 29 |
+
</div>
|
| 30 |
+
<div className="mt-2 text-xs text-gray-500 text-center">
|
| 31 |
+
{Math.round(progress)}% Complete
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
{/* Current Processing */}
|
| 36 |
+
{current && (
|
| 37 |
+
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
| 38 |
+
<p className="text-sm text-blue-800 flex items-center">
|
| 39 |
+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
| 40 |
+
Currently processing: <span className="font-medium ml-1">{current}</span>
|
| 41 |
+
</p>
|
| 42 |
+
</div>
|
| 43 |
+
)}
|
| 44 |
+
|
| 45 |
+
{/* Statistics */}
|
| 46 |
+
<div className="grid grid-cols-3 gap-3 pt-2">
|
| 47 |
+
<div className="text-center">
|
| 48 |
+
<div className="text-2xl font-bold text-gray-800">{total}</div>
|
| 49 |
+
<div className="text-xs text-gray-500">Total</div>
|
| 50 |
+
</div>
|
| 51 |
+
<div className="text-center">
|
| 52 |
+
<div className="text-2xl font-bold text-green-600 flex items-center justify-center">
|
| 53 |
+
{successCount}
|
| 54 |
+
{successCount > 0 && <CheckCircle className="w-5 h-5 ml-1" />}
|
| 55 |
+
</div>
|
| 56 |
+
<div className="text-xs text-gray-500">Success</div>
|
| 57 |
+
</div>
|
| 58 |
+
<div className="text-center">
|
| 59 |
+
<div className="text-2xl font-bold text-red-600 flex items-center justify-center">
|
| 60 |
+
{errorCount}
|
| 61 |
+
{errorCount > 0 && <XCircle className="w-5 h-5 ml-1" />}
|
| 62 |
+
</div>
|
| 63 |
+
<div className="text-xs text-gray-500">Errors</div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
{/* Results List */}
|
| 68 |
+
{results.length > 0 && (
|
| 69 |
+
<div className="border-t pt-4 mt-4">
|
| 70 |
+
<h4 className="text-sm font-semibold text-gray-700 mb-3">Processing Status</h4>
|
| 71 |
+
<div className="space-y-2 max-h-48 overflow-y-auto">
|
| 72 |
+
{results.map((result, index) => (
|
| 73 |
+
<div
|
| 74 |
+
key={index}
|
| 75 |
+
className={`flex items-center justify-between p-2 rounded ${
|
| 76 |
+
result.success ? 'bg-green-50' : 'bg-red-50'
|
| 77 |
+
}`}
|
| 78 |
+
>
|
| 79 |
+
<span className="text-sm text-gray-700 truncate flex-1">
|
| 80 |
+
{result.filename}
|
| 81 |
+
</span>
|
| 82 |
+
{result.success ? (
|
| 83 |
+
<CheckCircle className="w-4 h-4 text-green-600 flex-shrink-0 ml-2" />
|
| 84 |
+
) : (
|
| 85 |
+
<XCircle className="w-4 h-4 text-red-600 flex-shrink-0 ml-2" />
|
| 86 |
+
)}
|
| 87 |
+
</div>
|
| 88 |
+
))}
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
)}
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
);
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
export default ProgressIndicator;
|
frontend/src/components/ResultCard.jsx
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect, useState } from 'react';
|
| 2 |
+
|
| 3 |
+
const ResultCard = ({ result, imageData }) => {
|
| 4 |
+
const canvasRef = useRef(null);
|
| 5 |
+
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
| 6 |
+
|
| 7 |
+
useEffect(() => {
|
| 8 |
+
if (!imageData || !canvasRef.current) return;
|
| 9 |
+
|
| 10 |
+
const canvas = canvasRef.current;
|
| 11 |
+
const ctx = canvas.getContext('2d');
|
| 12 |
+
const img = new Image();
|
| 13 |
+
|
| 14 |
+
img.onload = () => {
|
| 15 |
+
// Set canvas size to match container while maintaining aspect ratio
|
| 16 |
+
const maxWidth = 800;
|
| 17 |
+
const maxHeight = 600;
|
| 18 |
+
let width = img.width;
|
| 19 |
+
let height = img.height;
|
| 20 |
+
|
| 21 |
+
if (width > maxWidth) {
|
| 22 |
+
height = (height * maxWidth) / width;
|
| 23 |
+
width = maxWidth;
|
| 24 |
+
}
|
| 25 |
+
if (height > maxHeight) {
|
| 26 |
+
width = (width * maxHeight) / height;
|
| 27 |
+
height = maxHeight;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
canvas.width = width;
|
| 31 |
+
canvas.height = height;
|
| 32 |
+
setDimensions({ width, height });
|
| 33 |
+
|
| 34 |
+
// Draw image
|
| 35 |
+
ctx.drawImage(img, 0, 0, width, height);
|
| 36 |
+
|
| 37 |
+
// Calculate scale factors
|
| 38 |
+
const scaleX = width / img.width;
|
| 39 |
+
const scaleY = height / img.height;
|
| 40 |
+
|
| 41 |
+
// Draw bounding boxes for signature
|
| 42 |
+
if (result.signature_coords && result.signature_coords.length > 0) {
|
| 43 |
+
ctx.strokeStyle = '#ef4444';
|
| 44 |
+
ctx.lineWidth = 3;
|
| 45 |
+
ctx.setLineDash([5, 5]);
|
| 46 |
+
|
| 47 |
+
result.signature_coords.forEach(coords => {
|
| 48 |
+
const [x1, y1, x2, y2] = coords;
|
| 49 |
+
ctx.strokeRect(
|
| 50 |
+
x1 * scaleX,
|
| 51 |
+
y1 * scaleY,
|
| 52 |
+
(x2 - x1) * scaleX,
|
| 53 |
+
(y2 - y1) * scaleY
|
| 54 |
+
);
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
// Add label
|
| 58 |
+
ctx.fillStyle = '#ef4444';
|
| 59 |
+
ctx.font = 'bold 14px Arial';
|
| 60 |
+
ctx.fillText('Signature', result.signature_coords[0][0] * scaleX, result.signature_coords[0][1] * scaleY - 5);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Draw bounding boxes for stamp
|
| 64 |
+
if (result.stamp_coords && result.stamp_coords.length > 0) {
|
| 65 |
+
ctx.strokeStyle = '#3b82f6';
|
| 66 |
+
ctx.lineWidth = 3;
|
| 67 |
+
ctx.setLineDash([5, 5]);
|
| 68 |
+
|
| 69 |
+
result.stamp_coords.forEach(coords => {
|
| 70 |
+
const [x1, y1, x2, y2] = coords;
|
| 71 |
+
ctx.strokeRect(
|
| 72 |
+
x1 * scaleX,
|
| 73 |
+
y1 * scaleY,
|
| 74 |
+
(x2 - x1) * scaleX,
|
| 75 |
+
(y2 - y1) * scaleY
|
| 76 |
+
);
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
// Add label
|
| 80 |
+
ctx.fillStyle = '#3b82f6';
|
| 81 |
+
ctx.font = 'bold 14px Arial';
|
| 82 |
+
ctx.fillText('Stamp', result.stamp_coords[0][0] * scaleX, result.stamp_coords[0][1] * scaleY - 5);
|
| 83 |
+
}
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
img.src = imageData;
|
| 87 |
+
}, [imageData, result]);
|
| 88 |
+
|
| 89 |
+
if (!result.success) {
|
| 90 |
+
return (
|
| 91 |
+
<div className="bg-red-50 border-2 border-red-200 rounded-xl p-6 mb-4">
|
| 92 |
+
<div className="flex items-start">
|
| 93 |
+
<div className="flex-shrink-0">
|
| 94 |
+
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 95 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
| 96 |
+
</svg>
|
| 97 |
+
</div>
|
| 98 |
+
<div className="ml-3">
|
| 99 |
+
<h3 className="text-sm font-medium text-red-800">Processing Error</h3>
|
| 100 |
+
<div className="mt-2 text-sm text-red-700">
|
| 101 |
+
<p><strong>File:</strong> {result.filename}</p>
|
| 102 |
+
<p><strong>Error:</strong> {result.error}</p>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
return (
|
| 111 |
+
<div className="bg-white rounded-xl shadow-lg overflow-hidden mb-6 border border-gray-200">
|
| 112 |
+
{/* Header */}
|
| 113 |
+
<div className="bg-gradient-to-r from-primary-500 to-primary-600 px-6 py-4">
|
| 114 |
+
<h3 className="text-lg font-semibold text-white flex items-center">
|
| 115 |
+
<svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 116 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 117 |
+
</svg>
|
| 118 |
+
{result.filename}
|
| 119 |
+
</h3>
|
| 120 |
+
{result.pageNumber && (
|
| 121 |
+
<p className="text-primary-100 text-sm mt-1">Page {result.pageNumber}</p>
|
| 122 |
+
)}
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 p-6">
|
| 126 |
+
{/* Image with bounding boxes */}
|
| 127 |
+
<div className="space-y-4">
|
| 128 |
+
<h4 className="text-md font-semibold text-gray-700 flex items-center">
|
| 129 |
+
<svg className="w-5 h-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 130 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
| 131 |
+
</svg>
|
| 132 |
+
Document Preview
|
| 133 |
+
</h4>
|
| 134 |
+
<div className="relative bg-gray-50 rounded-lg p-4 flex justify-center items-center">
|
| 135 |
+
<canvas ref={canvasRef} className="max-w-full h-auto rounded shadow-md" />
|
| 136 |
+
</div>
|
| 137 |
+
<div className="flex gap-4 text-sm">
|
| 138 |
+
{result.signature_coords && result.signature_coords.length > 0 && (
|
| 139 |
+
<div className="flex items-center">
|
| 140 |
+
<div className="w-4 h-4 border-2 border-red-500 mr-2"></div>
|
| 141 |
+
<span className="text-gray-600">Signature Detected</span>
|
| 142 |
+
</div>
|
| 143 |
+
)}
|
| 144 |
+
{result.stamp_coords && result.stamp_coords.length > 0 && (
|
| 145 |
+
<div className="flex items-center">
|
| 146 |
+
<div className="w-4 h-4 border-2 border-blue-500 mr-2"></div>
|
| 147 |
+
<span className="text-gray-600">Stamp Detected</span>
|
| 148 |
+
</div>
|
| 149 |
+
)}
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
{/* Extracted Information */}
|
| 154 |
+
<div className="space-y-4">
|
| 155 |
+
<h4 className="text-md font-semibold text-gray-700 flex items-center">
|
| 156 |
+
<svg className="w-5 h-5 mr-2 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 157 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 158 |
+
</svg>
|
| 159 |
+
Extracted Information
|
| 160 |
+
</h4>
|
| 161 |
+
|
| 162 |
+
<div className="bg-gray-50 rounded-lg p-4 space-y-3">
|
| 163 |
+
<div className="bg-white rounded-lg p-4 shadow-sm">
|
| 164 |
+
<h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Extracted Text</h5>
|
| 165 |
+
<div className="text-sm text-gray-800 whitespace-pre-wrap max-h-96 overflow-y-auto font-mono bg-gray-50 p-3 rounded border border-gray-200">
|
| 166 |
+
{result.extracted_text || 'No text extracted'}
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
{/* Detection Status */}
|
| 171 |
+
<div className="grid grid-cols-2 gap-3">
|
| 172 |
+
<div className="bg-white rounded-lg p-4 shadow-sm">
|
| 173 |
+
<div className="flex items-center justify-between">
|
| 174 |
+
<span className="text-sm font-medium text-gray-600">Signature</span>
|
| 175 |
+
{result.signature_coords && result.signature_coords.length > 0 ? (
|
| 176 |
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
| 177 |
+
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
| 178 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
| 179 |
+
</svg>
|
| 180 |
+
Detected
|
| 181 |
+
</span>
|
| 182 |
+
) : (
|
| 183 |
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
| 184 |
+
Not Found
|
| 185 |
+
</span>
|
| 186 |
+
)}
|
| 187 |
+
</div>
|
| 188 |
+
</div>
|
| 189 |
+
|
| 190 |
+
<div className="bg-white rounded-lg p-4 shadow-sm">
|
| 191 |
+
<div className="flex items-center justify-between">
|
| 192 |
+
<span className="text-sm font-medium text-gray-600">Stamp</span>
|
| 193 |
+
{result.stamp_coords && result.stamp_coords.length > 0 ? (
|
| 194 |
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
| 195 |
+
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
| 196 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
| 197 |
+
</svg>
|
| 198 |
+
Detected
|
| 199 |
+
</span>
|
| 200 |
+
) : (
|
| 201 |
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
| 202 |
+
Not Found
|
| 203 |
+
</span>
|
| 204 |
+
)}
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
</div>
|
| 208 |
+
|
| 209 |
+
{/* Coordinates Info */}
|
| 210 |
+
{(result.signature_coords?.length > 0 || result.stamp_coords?.length > 0) && (
|
| 211 |
+
<div className="bg-white rounded-lg p-4 shadow-sm">
|
| 212 |
+
<h5 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">Coordinates</h5>
|
| 213 |
+
<div className="text-xs space-y-2">
|
| 214 |
+
{result.signature_coords?.length > 0 && (
|
| 215 |
+
<div>
|
| 216 |
+
<span className="font-medium text-red-600">Signature:</span>
|
| 217 |
+
<code className="ml-2 text-gray-700">
|
| 218 |
+
{JSON.stringify(result.signature_coords)}
|
| 219 |
+
</code>
|
| 220 |
+
</div>
|
| 221 |
+
)}
|
| 222 |
+
{result.stamp_coords?.length > 0 && (
|
| 223 |
+
<div>
|
| 224 |
+
<span className="font-medium text-blue-600">Stamp:</span>
|
| 225 |
+
<code className="ml-2 text-gray-700">
|
| 226 |
+
{JSON.stringify(result.stamp_coords)}
|
| 227 |
+
</code>
|
| 228 |
+
</div>
|
| 229 |
+
)}
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
)}
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
);
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
export default ResultCard;
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
body {
|
| 6 |
+
margin: 0;
|
| 7 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
| 8 |
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
| 9 |
+
sans-serif;
|
| 10 |
+
-webkit-font-smoothing: antialiased;
|
| 11 |
+
-moz-osx-font-smoothing: grayscale;
|
| 12 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 13 |
+
min-height: 100vh;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.glass-morphism {
|
| 17 |
+
background: rgba(255, 255, 255, 0.95);
|
| 18 |
+
backdrop-filter: blur(10px);
|
| 19 |
+
border-radius: 20px;
|
| 20 |
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
| 21 |
+
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.upload-zone {
|
| 25 |
+
border: 2px dashed #cbd5e1;
|
| 26 |
+
transition: all 0.3s ease;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.upload-zone:hover {
|
| 30 |
+
border-color: #0ea5e9;
|
| 31 |
+
background-color: #f0f9ff;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.upload-zone.drag-active {
|
| 35 |
+
border-color: #0ea5e9;
|
| 36 |
+
background-color: #e0f2fe;
|
| 37 |
+
transform: scale(1.02);
|
| 38 |
+
}
|
frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.jsx'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
frontend/src/utils/api.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios from 'axios';
|
| 2 |
+
|
| 3 |
+
// Use same origin in production (when deployed), localhost in development
|
| 4 |
+
const API_BASE_URL = import.meta.env.VITE_API_URL || window.location.origin;
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Process a single invoice image
|
| 8 |
+
* @param {Blob} imageBlob - Image blob
|
| 9 |
+
* @param {string} filename - Original filename
|
| 10 |
+
* @returns {Promise<Object>} Processed result
|
| 11 |
+
*/
|
| 12 |
+
export async function processSingleInvoice(imageBlob, filename) {
|
| 13 |
+
const formData = new FormData();
|
| 14 |
+
formData.append('file', imageBlob, filename);
|
| 15 |
+
|
| 16 |
+
const response = await axios.post(`${API_BASE_URL}/process-invoice`, formData, {
|
| 17 |
+
headers: {
|
| 18 |
+
'Content-Type': 'multipart/form-data',
|
| 19 |
+
},
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
return response.data;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Process multiple invoices in batch
|
| 27 |
+
* @param {Array} images - Array of {blob, filename} objects
|
| 28 |
+
* @param {Function} onProgress - Progress callback (index, result)
|
| 29 |
+
* @returns {Promise<Array>} Array of results
|
| 30 |
+
*/
|
| 31 |
+
export async function processBatchInvoices(images, onProgress) {
|
| 32 |
+
const results = [];
|
| 33 |
+
|
| 34 |
+
for (let i = 0; i < images.length; i++) {
|
| 35 |
+
try {
|
| 36 |
+
const result = await processSingleInvoice(images[i].blob, images[i].filename);
|
| 37 |
+
const resultWithMetadata = {
|
| 38 |
+
...result,
|
| 39 |
+
filename: images[i].filename,
|
| 40 |
+
originalFile: images[i].originalFile,
|
| 41 |
+
pageNumber: images[i].pageNumber,
|
| 42 |
+
index: i,
|
| 43 |
+
success: true
|
| 44 |
+
};
|
| 45 |
+
results.push(resultWithMetadata);
|
| 46 |
+
|
| 47 |
+
if (onProgress) {
|
| 48 |
+
onProgress(i, resultWithMetadata);
|
| 49 |
+
}
|
| 50 |
+
} catch (error) {
|
| 51 |
+
const errorResult = {
|
| 52 |
+
filename: images[i].filename,
|
| 53 |
+
originalFile: images[i].originalFile,
|
| 54 |
+
pageNumber: images[i].pageNumber,
|
| 55 |
+
index: i,
|
| 56 |
+
success: false,
|
| 57 |
+
error: error.response?.data?.detail || error.message
|
| 58 |
+
};
|
| 59 |
+
results.push(errorResult);
|
| 60 |
+
|
| 61 |
+
if (onProgress) {
|
| 62 |
+
onProgress(i, errorResult);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
return results;
|
| 68 |
+
}
|
frontend/src/utils/fileConverter.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as pdfjsLib from 'pdfjs-dist';
|
| 2 |
+
|
| 3 |
+
// Configure PDF.js worker
|
| 4 |
+
pdfjsLib.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjsLib.version}/pdf.worker.min.js`;
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Convert PDF to images
|
| 8 |
+
* @param {File} file - PDF file
|
| 9 |
+
* @returns {Promise<Array>} Array of image data URLs
|
| 10 |
+
*/
|
| 11 |
+
export async function pdfToImages(file) {
|
| 12 |
+
const images = [];
|
| 13 |
+
const arrayBuffer = await file.arrayBuffer();
|
| 14 |
+
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
| 15 |
+
|
| 16 |
+
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
| 17 |
+
const page = await pdf.getPage(pageNum);
|
| 18 |
+
const viewport = page.getViewport({ scale: 2.0 });
|
| 19 |
+
|
| 20 |
+
const canvas = document.createElement('canvas');
|
| 21 |
+
const context = canvas.getContext('2d');
|
| 22 |
+
canvas.height = viewport.height;
|
| 23 |
+
canvas.width = viewport.width;
|
| 24 |
+
|
| 25 |
+
await page.render({
|
| 26 |
+
canvasContext: context,
|
| 27 |
+
viewport: viewport
|
| 28 |
+
}).promise;
|
| 29 |
+
|
| 30 |
+
images.push({
|
| 31 |
+
dataUrl: canvas.toDataURL('image/jpeg', 0.95),
|
| 32 |
+
pageNumber: pageNum
|
| 33 |
+
});
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return images;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Convert image file to data URL
|
| 41 |
+
* @param {File} file - Image file
|
| 42 |
+
* @returns {Promise<string>} Image data URL
|
| 43 |
+
*/
|
| 44 |
+
export function imageToDataUrl(file) {
|
| 45 |
+
return new Promise((resolve, reject) => {
|
| 46 |
+
const reader = new FileReader();
|
| 47 |
+
reader.onload = (e) => resolve(e.target.result);
|
| 48 |
+
reader.onerror = reject;
|
| 49 |
+
reader.readAsDataURL(file);
|
| 50 |
+
});
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
/**
|
| 54 |
+
* Convert file to processable format
|
| 55 |
+
* @param {File} file - Input file
|
| 56 |
+
* @returns {Promise<Array>} Array of {dataUrl, filename, pageNumber} objects
|
| 57 |
+
*/
|
| 58 |
+
export async function convertFileToImages(file) {
|
| 59 |
+
const fileType = file.type;
|
| 60 |
+
|
| 61 |
+
if (fileType === 'application/pdf') {
|
| 62 |
+
const pdfImages = await pdfToImages(file);
|
| 63 |
+
return pdfImages.map(img => ({
|
| 64 |
+
dataUrl: img.dataUrl,
|
| 65 |
+
filename: `${file.name}_page_${img.pageNumber}`,
|
| 66 |
+
pageNumber: img.pageNumber,
|
| 67 |
+
originalFile: file.name
|
| 68 |
+
}));
|
| 69 |
+
} else if (fileType.startsWith('image/')) {
|
| 70 |
+
const dataUrl = await imageToDataUrl(file);
|
| 71 |
+
return [{
|
| 72 |
+
dataUrl,
|
| 73 |
+
filename: file.name,
|
| 74 |
+
pageNumber: 1,
|
| 75 |
+
originalFile: file.name
|
| 76 |
+
}];
|
| 77 |
+
} else {
|
| 78 |
+
throw new Error('Unsupported file type. Please upload an image or PDF file.');
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Data URL to Blob conversion
|
| 84 |
+
* @param {string} dataUrl - Data URL
|
| 85 |
+
* @returns {Blob} Blob object
|
| 86 |
+
*/
|
| 87 |
+
export function dataUrlToBlob(dataUrl) {
|
| 88 |
+
const arr = dataUrl.split(',');
|
| 89 |
+
const mime = arr[0].match(/:(.*?);/)[1];
|
| 90 |
+
const bstr = atob(arr[1]);
|
| 91 |
+
let n = bstr.length;
|
| 92 |
+
const u8arr = new Uint8Array(n);
|
| 93 |
+
while (n--) {
|
| 94 |
+
u8arr[n] = bstr.charCodeAt(n);
|
| 95 |
+
}
|
| 96 |
+
return new Blob([u8arr], { type: mime });
|
| 97 |
+
}
|
frontend/tailwind.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: [
|
| 4 |
+
"./index.html",
|
| 5 |
+
"./src/**/*.{js,ts,jsx,tsx}",
|
| 6 |
+
],
|
| 7 |
+
theme: {
|
| 8 |
+
extend: {
|
| 9 |
+
colors: {
|
| 10 |
+
primary: {
|
| 11 |
+
50: '#f0f9ff',
|
| 12 |
+
100: '#e0f2fe',
|
| 13 |
+
200: '#bae6fd',
|
| 14 |
+
300: '#7dd3fc',
|
| 15 |
+
400: '#38bdf8',
|
| 16 |
+
500: '#0ea5e9',
|
| 17 |
+
600: '#0284c7',
|
| 18 |
+
700: '#0369a1',
|
| 19 |
+
800: '#075985',
|
| 20 |
+
900: '#0c4a6e',
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
},
|
| 25 |
+
plugins: [],
|
| 26 |
+
}
|
frontend/vite.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins: [react()],
|
| 6 |
+
server: {
|
| 7 |
+
port: 3000,
|
| 8 |
+
proxy: {
|
| 9 |
+
'/process-invoice': {
|
| 10 |
+
target: 'http://localhost:8000',
|
| 11 |
+
changeOrigin: true,
|
| 12 |
+
},
|
| 13 |
+
'/batch-process': {
|
| 14 |
+
target: 'http://localhost:8000',
|
| 15 |
+
changeOrigin: true,
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
})
|
setup.bat
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo ============================================
|
| 3 |
+
echo Invoice Extractor - Complete Setup
|
| 4 |
+
echo ============================================
|
| 5 |
+
echo.
|
| 6 |
+
|
| 7 |
+
echo [1/4] Installing Backend Dependencies...
|
| 8 |
+
pip install -r requirements.txt
|
| 9 |
+
if errorlevel 1 (
|
| 10 |
+
echo ERROR: Failed to install backend dependencies
|
| 11 |
+
pause
|
| 12 |
+
exit /b 1
|
| 13 |
+
)
|
| 14 |
+
echo ✓ Backend dependencies installed
|
| 15 |
+
echo.
|
| 16 |
+
|
| 17 |
+
echo [2/4] Installing Frontend Dependencies...
|
| 18 |
+
cd frontend
|
| 19 |
+
call npm install
|
| 20 |
+
if errorlevel 1 (
|
| 21 |
+
echo ERROR: Failed to install frontend dependencies
|
| 22 |
+
pause
|
| 23 |
+
exit /b 1
|
| 24 |
+
)
|
| 25 |
+
echo ✓ Frontend dependencies installed
|
| 26 |
+
echo.
|
| 27 |
+
|
| 28 |
+
echo [3/4] Setting up Frontend Environment...
|
| 29 |
+
if not exist .env (
|
| 30 |
+
copy .env.example .env
|
| 31 |
+
echo ✓ Created .env file
|
| 32 |
+
) else (
|
| 33 |
+
echo ✓ .env file already exists
|
| 34 |
+
)
|
| 35 |
+
cd ..
|
| 36 |
+
echo.
|
| 37 |
+
|
| 38 |
+
echo [4/4] Setup Complete!
|
| 39 |
+
echo.
|
| 40 |
+
echo ============================================
|
| 41 |
+
echo Ready to start the application!
|
| 42 |
+
echo ============================================
|
| 43 |
+
echo.
|
| 44 |
+
echo To start the backend:
|
| 45 |
+
echo python app.py
|
| 46 |
+
echo.
|
| 47 |
+
echo To start the frontend:
|
| 48 |
+
echo cd frontend
|
| 49 |
+
echo npm run dev
|
| 50 |
+
echo.
|
| 51 |
+
echo Then open http://localhost:3000 in your browser
|
| 52 |
+
echo ============================================
|
| 53 |
+
pause
|
start.bat
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
echo ============================================
|
| 3 |
+
echo Starting Invoice Extractor Application
|
| 4 |
+
echo ============================================
|
| 5 |
+
echo.
|
| 6 |
+
|
| 7 |
+
echo Starting Backend Server...
|
| 8 |
+
start "Backend Server" cmd /k "python app.py"
|
| 9 |
+
timeout /t 5 /nobreak > nul
|
| 10 |
+
|
| 11 |
+
echo Starting Frontend Development Server...
|
| 12 |
+
start "Frontend Server" cmd /k "cd frontend && npm run dev"
|
| 13 |
+
|
| 14 |
+
echo.
|
| 15 |
+
echo ============================================
|
| 16 |
+
echo Both servers are starting!
|
| 17 |
+
echo ============================================
|
| 18 |
+
echo.
|
| 19 |
+
echo Backend: http://localhost:7860
|
| 20 |
+
echo Frontend: http://localhost:3000
|
| 21 |
+
echo.
|
| 22 |
+
echo Press any key to stop both servers...
|
| 23 |
+
pause > nul
|
| 24 |
+
|
| 25 |
+
taskkill /FI "WindowTitle eq Backend Server*" /T /F
|
| 26 |
+
taskkill /FI "WindowTitle eq Frontend Server*" /T /F
|