Upload 3 files
Browse files- Dockerfile.txt +15 -0
- app.py +1450 -0
- requirements).txt +8 -0
Dockerfile.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Expose port
|
| 6 |
+
ENV GRADIO_SERVER_NAME="0.0.0.0"
|
| 7 |
+
ENV GRADIO_SERVER_PORT="7860"
|
| 8 |
+
|
| 9 |
+
# Install dependencies
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY requirements.txt /app/requirements.txt
|
| 13 |
+
|
| 14 |
+
# Start the app
|
| 15 |
+
CMD ["python", "app.py", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
|
@@ -0,0 +1,1450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
OmniParse AI - Invoice Processing SaaS
|
| 3 |
+
Gradio backend with custom HTML UI for HuggingFace Spaces.
|
| 4 |
+
No Gradio UI elements are visible - all rendering via inline HTML/CSS/JS.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import re
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import csv
|
| 12 |
+
import io
|
| 13 |
+
import sqlite3
|
| 14 |
+
import hashlib
|
| 15 |
+
import secrets
|
| 16 |
+
import threading
|
| 17 |
+
from datetime import datetime, timedelta, timezone
|
| 18 |
+
from http.cookies import SimpleCookie
|
| 19 |
+
from functools import wraps
|
| 20 |
+
|
| 21 |
+
import gradio as gr
|
| 22 |
+
import bcrypt
|
| 23 |
+
|
| 24 |
+
# =============================================================================
|
| 25 |
+
# Database
|
| 26 |
+
# =============================================================================
|
| 27 |
+
DB_PATH = os.environ.get("SQLITE_PATH", "omniparse.db")
|
| 28 |
+
DB_LOCK = threading.Lock()
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_conn():
|
| 32 |
+
conn = sqlite3.connect(DB_PATH)
|
| 33 |
+
conn.row_factory = sqlite3.Row
|
| 34 |
+
conn.execute("PRAGMA journal_mode=WAL")
|
| 35 |
+
return conn
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def db_init():
|
| 39 |
+
conn = get_conn()
|
| 40 |
+
c = conn.cursor()
|
| 41 |
+
c.execute("""CREATE TABLE IF NOT EXISTS users (
|
| 42 |
+
id TEXT PRIMARY KEY,
|
| 43 |
+
email TEXT UNIQUE NOT NULL,
|
| 44 |
+
name TEXT NOT NULL,
|
| 45 |
+
password TEXT NOT NULL,
|
| 46 |
+
plan TEXT DEFAULT 'free',
|
| 47 |
+
stripe_cid TEXT,
|
| 48 |
+
api_key TEXT,
|
| 49 |
+
created_at TEXT,
|
| 50 |
+
updated_at TEXT
|
| 51 |
+
)""")
|
| 52 |
+
c.execute("""CREATE TABLE IF NOT EXISTS sessions (
|
| 53 |
+
id TEXT PRIMARY KEY,
|
| 54 |
+
token TEXT UNIQUE NOT NULL,
|
| 55 |
+
user_id TEXT NOT NULL,
|
| 56 |
+
expires_at TEXT,
|
| 57 |
+
created_at TEXT,
|
| 58 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 59 |
+
)""")
|
| 60 |
+
c.execute("""CREATE TABLE IF NOT EXISTS invoices (
|
| 61 |
+
id TEXT PRIMARY KEY,
|
| 62 |
+
user_id TEXT NOT NULL,
|
| 63 |
+
filename TEXT,
|
| 64 |
+
vendor TEXT,
|
| 65 |
+
inv_number TEXT,
|
| 66 |
+
inv_date TEXT,
|
| 67 |
+
due_date TEXT,
|
| 68 |
+
amount REAL,
|
| 69 |
+
vat_amount REAL,
|
| 70 |
+
total REAL,
|
| 71 |
+
currency TEXT DEFAULT 'USD',
|
| 72 |
+
status TEXT DEFAULT 'done',
|
| 73 |
+
is_duplicate INTEGER DEFAULT 0,
|
| 74 |
+
confidence REAL,
|
| 75 |
+
raw_json TEXT,
|
| 76 |
+
created_at TEXT,
|
| 77 |
+
updated_at TEXT,
|
| 78 |
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
| 79 |
+
)""")
|
| 80 |
+
c.execute("""CREATE TABLE IF NOT EXISTS rate_limits (
|
| 81 |
+
key TEXT PRIMARY KEY,
|
| 82 |
+
count INTEGER DEFAULT 0,
|
| 83 |
+
reset_at TEXT
|
| 84 |
+
)""")
|
| 85 |
+
conn.commit()
|
| 86 |
+
conn.close()
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def seed_demo():
|
| 90 |
+
conn = get_conn()
|
| 91 |
+
existing = conn.execute("SELECT id FROM users WHERE email=?", ("demo@omniparse.ai",)).fetchone()
|
| 92 |
+
if not existing:
|
| 93 |
+
pw_hash = bcrypt.hashpw("demo1234".encode(), bcrypt.gensalt(12)).decode()
|
| 94 |
+
api_key = "op_live_" + secrets.token_hex(24)
|
| 95 |
+
now = datetime.now(timezone.utc).isoformat()
|
| 96 |
+
conn.execute(
|
| 97 |
+
"INSERT INTO users (id, email, name, password, plan, api_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
|
| 98 |
+
(secrets.token_hex(12), "demo@omniparse.ai", "Demo User", pw_hash, "pro", api_key, now, now),
|
| 99 |
+
)
|
| 100 |
+
conn.commit()
|
| 101 |
+
conn.close()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
db_init()
|
| 105 |
+
seed_demo()
|
| 106 |
+
|
| 107 |
+
# =============================================================================
|
| 108 |
+
# Helpers
|
| 109 |
+
# =============================================================================
|
| 110 |
+
PLAN_LIMITS = {"free": 20, "basic": 200, "pro": 2000, "enterprise": float("inf")}
|
| 111 |
+
PLAN_LABELS = {"free": "Free", "basic": "Basic", "pro": "Pro", "enterprise": "Enterprise"}
|
| 112 |
+
PLAN_PRICES = {"free": 0, "basic": 29, "pro": 129, "enterprise": 499}
|
| 113 |
+
SESSION_TTL = 7 * 24 * 3600
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def gen_id():
|
| 117 |
+
return secrets.token_hex(12)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def gen_api_key():
|
| 121 |
+
return "op_live_" + secrets.token_hex(24)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def gen_session_token():
|
| 125 |
+
return secrets.token_hex(32)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def now_iso():
|
| 129 |
+
return datetime.now(timezone.utc).isoformat()
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
# ─── Rate Limiting ────────────────────────────────────────────────────────
|
| 133 |
+
_rate_lock = threading.Lock()
|
| 134 |
+
_rate_store: dict[str, dict] = {}
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def check_rate_limit(key: str, max_req: int, window_s: int) -> tuple[bool, int]:
|
| 138 |
+
with _rate_lock:
|
| 139 |
+
now = time.time()
|
| 140 |
+
entry = _rate_store.get(key)
|
| 141 |
+
if not entry or now >= entry["reset_at"]:
|
| 142 |
+
_rate_store[key] = {"count": 1, "reset_at": now + window_s}
|
| 143 |
+
return True, 0
|
| 144 |
+
if entry["count"] >= max_req:
|
| 145 |
+
retry = int(entry["reset_at"] - now)
|
| 146 |
+
return False, retry
|
| 147 |
+
entry["count"] += 1
|
| 148 |
+
return True, 0
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ─── Session Helpers ───────────────────────────────────────────────────────
|
| 152 |
+
def create_session(user_id: str) -> str:
|
| 153 |
+
token = gen_session_token()
|
| 154 |
+
expires = (datetime.now(timezone.utc) + timedelta(days=7)).isoformat()
|
| 155 |
+
conn = get_conn()
|
| 156 |
+
conn.execute("INSERT INTO sessions (id, token, user_id, expires_at, created_at) VALUES (?,?,?,?,?)",
|
| 157 |
+
(gen_id(), token, user_id, expires, now_iso()))
|
| 158 |
+
conn.commit()
|
| 159 |
+
conn.close()
|
| 160 |
+
return token
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def get_session_user(token: str) -> dict | None:
|
| 164 |
+
if not token:
|
| 165 |
+
return None
|
| 166 |
+
conn = get_conn()
|
| 167 |
+
row = conn.execute(
|
| 168 |
+
"SELECT s.*, u.* FROM sessions s JOIN users u ON s.user_id=u.id WHERE s.token=?",
|
| 169 |
+
(token,),
|
| 170 |
+
).fetchone()
|
| 171 |
+
conn.close()
|
| 172 |
+
if not row:
|
| 173 |
+
return None
|
| 174 |
+
try:
|
| 175 |
+
expires = datetime.fromisoformat(row["expires_at"])
|
| 176 |
+
if expires < datetime.now(timezone.utc):
|
| 177 |
+
return None
|
| 178 |
+
except Exception:
|
| 179 |
+
return None
|
| 180 |
+
return dict(row)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def delete_session(token: str):
|
| 184 |
+
conn = get_conn()
|
| 185 |
+
conn.execute("DELETE FROM sessions WHERE token=?", (token,))
|
| 186 |
+
conn.commit()
|
| 187 |
+
conn.close()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def get_cookie_token(req: gr.Request) -> str | None:
|
| 191 |
+
cookie_header = req.headers.get("cookie", "")
|
| 192 |
+
for part in cookie_header.split(";"):
|
| 193 |
+
part = part.strip()
|
| 194 |
+
if part.startswith("op_session="):
|
| 195 |
+
return part.split("=", 1)[1]
|
| 196 |
+
return None
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# ─── User Helpers ──────────────────────────────────────────────────────────
|
| 200 |
+
def get_user_by_email(email: str) -> dict | None:
|
| 201 |
+
conn = get_conn()
|
| 202 |
+
row = conn.execute("SELECT * FROM users WHERE email=?", (email.strip().lower(),)).fetchone()
|
| 203 |
+
conn.close()
|
| 204 |
+
return dict(row) if row else None
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def get_user_by_id(uid: str) -> dict | None:
|
| 208 |
+
conn = get_conn()
|
| 209 |
+
row = conn.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
|
| 210 |
+
conn.close()
|
| 211 |
+
return dict(row) if row else None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def count_invoices_month(user_id: str) -> int:
|
| 215 |
+
conn = get_conn()
|
| 216 |
+
now = datetime.now(timezone.utc)
|
| 217 |
+
rows = conn.execute("SELECT * FROM invoices WHERE user_id=?", (user_id,)).fetchall()
|
| 218 |
+
conn.close()
|
| 219 |
+
n = 0
|
| 220 |
+
for r in rows:
|
| 221 |
+
try:
|
| 222 |
+
ct = datetime.fromisoformat(r["created_at"])
|
| 223 |
+
if ct.year == now.year and ct.month == now.month:
|
| 224 |
+
n += 1
|
| 225 |
+
except Exception:
|
| 226 |
+
pass
|
| 227 |
+
return n
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def generate_mock_invoice(filename: str) -> dict:
|
| 231 |
+
vendors = ["Stark Enterprises", "Wayne Industries", "Umbrella Corp", "Acme Corp",
|
| 232 |
+
"Globex Inc", "Initech", "Soylent Corp", "Cyberdyne Systems",
|
| 233 |
+
"Oscorp Industries", "Aperture Science", "Massive Dynamic", "Stern-Wyatt"]
|
| 234 |
+
currencies = ["USD", "EUR", "GBP", "CAD", "AUD"]
|
| 235 |
+
v = vendors[hash(filename) % len(vendors)]
|
| 236 |
+
subtotal = round((hash(filename + "amt") % 50000) / 100 + 50, 2)
|
| 237 |
+
vat_rate = 0.20
|
| 238 |
+
vat = round(subtotal * vat_rate, 2)
|
| 239 |
+
total = round(subtotal + vat, 2)
|
| 240 |
+
inv_date = datetime.now(timezone.utc)
|
| 241 |
+
due = inv_date + timedelta(days=30)
|
| 242 |
+
return {
|
| 243 |
+
"vendor": v,
|
| 244 |
+
"invoiceNumber": f"INV-{inv_date.strftime('%Y')}-{hash(filename) % 100000:05d}",
|
| 245 |
+
"invoiceDate": inv_date.strftime("%Y-%m-%d"),
|
| 246 |
+
"dueDate": due.strftime("%Y-%m-%d"),
|
| 247 |
+
"subtotal": subtotal,
|
| 248 |
+
"vatRate": "20%",
|
| 249 |
+
"vatAmount": vat,
|
| 250 |
+
"total": total,
|
| 251 |
+
"currency": currencies[hash(filename + "cur") % len(currencies)],
|
| 252 |
+
"lineItems": [
|
| 253 |
+
{"description": "Professional services", "quantity": 1, "unitPrice": subtotal, "total": subtotal}
|
| 254 |
+
],
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# =============================================================================
|
| 259 |
+
# API Routes (Gradio)
|
| 260 |
+
# =============================================================================
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
@app.post("/api/auth/login")
|
| 264 |
+
def api_login(req: gr.Request):
|
| 265 |
+
try:
|
| 266 |
+
body = req.get_body()
|
| 267 |
+
data = json.loads(body) if body else {}
|
| 268 |
+
except Exception:
|
| 269 |
+
return '{"error":"Invalid request body"}', 400, {"Content-Type": "application/json"}
|
| 270 |
+
|
| 271 |
+
email = (data.get("email") or "").strip().lower()
|
| 272 |
+
password = data.get("password") or ""
|
| 273 |
+
|
| 274 |
+
if not email or not password:
|
| 275 |
+
return '{"error":"Email and password are required"}', 400, {"Content-Type": "application/json"}
|
| 276 |
+
|
| 277 |
+
ip = req.headers.get("x-forwarded-for", req.client.host if req.client else "unknown")
|
| 278 |
+
ok, retry = check_rate_limit(f"auth:{ip}", 5, 60)
|
| 279 |
+
if not ok:
|
| 280 |
+
return json.dumps({"error": f"Too many attempts. Try again in {retry}s"}), 429, {"Content-Type": "application/json"}
|
| 281 |
+
|
| 282 |
+
user = get_user_by_email(email)
|
| 283 |
+
if not user or not bcrypt.checkpw(password.encode(), user["password"].encode()):
|
| 284 |
+
return '{"error":"Invalid email or password"}', 401, {"Content-Type": "application/json"}
|
| 285 |
+
|
| 286 |
+
token = create_session(user["id"])
|
| 287 |
+
resp_data = {"user": {k: user[k] for k in ("id", "email", "name", "plan", "api_key", "createdAt" if "createdAt" in user else "created_at")}}
|
| 288 |
+
headers = {
|
| 289 |
+
"Content-Type": "application/json",
|
| 290 |
+
"Set-Cookie": f"op_session={token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age={SESSION_TTL}",
|
| 291 |
+
}
|
| 292 |
+
return json.dumps(resp_data), 200, headers
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
@app.post("/api/auth/signup")
|
| 296 |
+
def api_signup(req: gr.Request):
|
| 297 |
+
try:
|
| 298 |
+
body = req.get_body()
|
| 299 |
+
data = json.loads(body) if body else {}
|
| 300 |
+
except Exception:
|
| 301 |
+
return '{"error":"Invalid request body"}', 400, {"Content-Type": "application/json"}
|
| 302 |
+
|
| 303 |
+
name = (data.get("name") or "").strip()
|
| 304 |
+
email = (data.get("email") or "").strip().lower()
|
| 305 |
+
password = data.get("password") or ""
|
| 306 |
+
terms = data.get("termsAccepted", False)
|
| 307 |
+
|
| 308 |
+
errors = []
|
| 309 |
+
if not name or len(name) < 2:
|
| 310 |
+
errors.append("Name must be at least 2 characters")
|
| 311 |
+
if not email or "@" not in email:
|
| 312 |
+
errors.append("Valid email is required")
|
| 313 |
+
if len(password) < 8:
|
| 314 |
+
errors.append("Password must be at least 8 characters")
|
| 315 |
+
elif not re.search(r"[A-Z]", password) or not re.search(r"[a-z]", password) or not re.search(r"\d", password):
|
| 316 |
+
errors.append("Password must contain uppercase, lowercase, and a number")
|
| 317 |
+
if not terms:
|
| 318 |
+
errors.append("You must accept the Terms of Service")
|
| 319 |
+
|
| 320 |
+
if errors:
|
| 321 |
+
return json.dumps({"error": errors[0]}), 400, {"Content-Type": "application/json"}
|
| 322 |
+
|
| 323 |
+
ip = req.headers.get("x-forwarded-for", req.client.host if req.client else "unknown")
|
| 324 |
+
ok, retry = check_rate_limit(f"auth:{ip}", 5, 60)
|
| 325 |
+
if not ok:
|
| 326 |
+
return json.dumps({"error": f"Too many attempts. Try again in {retry}s"}), 429, {"Content-Type": "application/json"}
|
| 327 |
+
|
| 328 |
+
if get_user_by_email(email):
|
| 329 |
+
return '{"error":"An account with this email already exists"}', 409, {"Content-Type": "application/json"}
|
| 330 |
+
|
| 331 |
+
pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(12)).decode()
|
| 332 |
+
api_key = gen_api_key()
|
| 333 |
+
now = now_iso()
|
| 334 |
+
uid = gen_id()
|
| 335 |
+
|
| 336 |
+
conn = get_conn()
|
| 337 |
+
try:
|
| 338 |
+
conn.execute("INSERT INTO users (id, email, name, password, plan, api_key, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
|
| 339 |
+
(uid, email, name, pw_hash, "free", api_key, now, now))
|
| 340 |
+
conn.commit()
|
| 341 |
+
except sqlite3.IntegrityError:
|
| 342 |
+
conn.close()
|
| 343 |
+
return '{"error":"An account with this email already exists"}', 409, {"Content-Type": "application/json"}
|
| 344 |
+
conn.close()
|
| 345 |
+
|
| 346 |
+
token = create_session(uid)
|
| 347 |
+
resp_data = {"user": {"id": uid, "email": email, "name": name, "plan": "free", "api_key": api_key, "createdAt": now}}
|
| 348 |
+
headers = {
|
| 349 |
+
"Content-Type": "application/json",
|
| 350 |
+
"Set-Cookie": f"op_session={token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age={SESSION_TTL}",
|
| 351 |
+
}
|
| 352 |
+
return json.dumps(resp_data), 201, headers
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
@app.post("/api/auth/logout")
|
| 356 |
+
def api_logout(req: gr.Request):
|
| 357 |
+
token = get_cookie_token(req)
|
| 358 |
+
if token:
|
| 359 |
+
delete_session(token)
|
| 360 |
+
headers = {
|
| 361 |
+
"Content-Type": "application/json",
|
| 362 |
+
"Set-Cookie": "op_session=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0",
|
| 363 |
+
}
|
| 364 |
+
return '{"success":true}', 200, headers
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
@app.get("/api/auth/me")
|
| 368 |
+
def api_me(req: gr.Request):
|
| 369 |
+
token = get_cookie_token(req)
|
| 370 |
+
if not token:
|
| 371 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 372 |
+
user = get_session_user(token)
|
| 373 |
+
if not user:
|
| 374 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 375 |
+
resp = {"user": {k: user[k] for k in ("id", "email", "name", "plan", "api_key", "created_at")}}
|
| 376 |
+
return json.dumps(resp), 200, {"Content-Type": "application/json"}
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
@app.post("/api/invoices/upload")
|
| 380 |
+
def api_upload(req: gr.Request):
|
| 381 |
+
token = get_cookie_token(req)
|
| 382 |
+
if not token:
|
| 383 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 384 |
+
user = get_session_user(token)
|
| 385 |
+
if not user:
|
| 386 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 387 |
+
|
| 388 |
+
uid = user["id"]
|
| 389 |
+
ok, retry = check_rate_limit(f"upload:{uid}", 20, 60)
|
| 390 |
+
if not ok:
|
| 391 |
+
return json.dumps({"error": f"Rate limit exceeded. Try again in {retry}s"}), 429, {"Content-Type": "application/json"}
|
| 392 |
+
|
| 393 |
+
plan = user["plan"]
|
| 394 |
+
used = count_invoices_month(uid)
|
| 395 |
+
limit = PLAN_LIMITS.get(plan, 20)
|
| 396 |
+
|
| 397 |
+
files = req.request.files if hasattr(req.request, "files") else {}
|
| 398 |
+
file_list = []
|
| 399 |
+
for key in files:
|
| 400 |
+
f = files[key]
|
| 401 |
+
file_list.append({"name": f.filename, "content": f.read()})
|
| 402 |
+
|
| 403 |
+
if not file_list:
|
| 404 |
+
return '{"error":"No files provided"}', 400, {"Content-Type": "application/json"}
|
| 405 |
+
|
| 406 |
+
results = []
|
| 407 |
+
processed = 0
|
| 408 |
+
conn = get_conn()
|
| 409 |
+
for f in file_list:
|
| 410 |
+
if limit != float("inf") and used + processed >= int(limit):
|
| 411 |
+
break
|
| 412 |
+
fname = f["name"]
|
| 413 |
+
mock = generate_mock_invoice(fname)
|
| 414 |
+
inv_id = gen_id()
|
| 415 |
+
now = now_iso()
|
| 416 |
+
is_dup = False
|
| 417 |
+
if plan in ("pro", "enterprise"):
|
| 418 |
+
existing = conn.execute(
|
| 419 |
+
"SELECT * FROM invoices WHERE user_id=? AND vendor=? AND total=?",
|
| 420 |
+
(uid, mock["vendor"], mock["total"]),
|
| 421 |
+
).fetchone()
|
| 422 |
+
if existing:
|
| 423 |
+
is_dup = True
|
| 424 |
+
status = "duplicate" if is_dup else "done"
|
| 425 |
+
raw = json.dumps(mock, ensure_ascii=False)
|
| 426 |
+
conn.execute(
|
| 427 |
+
"INSERT INTO invoices (id, user_id, filename, vendor, inv_number, inv_date, due_date, amount, vat_amount, total, currency, status, is_duplicate, confidence, raw_json, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
| 428 |
+
(inv_id, uid, fname, mock["vendor"], mock["invoiceNumber"], mock["invoiceDate"],
|
| 429 |
+
mock["dueDate"], mock["subtotal"], mock["vatAmount"], mock["total"],
|
| 430 |
+
mock["currency"], status, int(is_dup), 0.9, raw, now, now),
|
| 431 |
+
)
|
| 432 |
+
results.append({
|
| 433 |
+
"id": inv_id, "filename": fname, "vendor": mock["vendor"],
|
| 434 |
+
"invNumber": mock["invoiceNumber"], "invDate": mock["invoiceDate"],
|
| 435 |
+
"dueDate": mock["dueDate"], "amount": mock["subtotal"],
|
| 436 |
+
"vatAmount": mock["vatAmount"], "total": mock["total"],
|
| 437 |
+
"currency": mock["currency"], "status": status,
|
| 438 |
+
"isDuplicate": is_dup, "confidence": 0.9, "createdAt": now,
|
| 439 |
+
})
|
| 440 |
+
processed += 1
|
| 441 |
+
conn.commit()
|
| 442 |
+
conn.close()
|
| 443 |
+
|
| 444 |
+
new_used = count_invoices_month(uid)
|
| 445 |
+
limit_str = str(int(limit)) if limit != float("inf") else "\u221e"
|
| 446 |
+
msg = f"Processed {processed} invoice(s)."
|
| 447 |
+
skipped = len(file_list) - processed
|
| 448 |
+
if skipped > 0:
|
| 449 |
+
msg += f" {skipped} skipped - monthly limit reached."
|
| 450 |
+
|
| 451 |
+
return json.dumps({"invoices": results, "message": msg, "usage": f"{new_used}/{limit_str} invoices used this month"}), 200, {"Content-Type": "application/json"}
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
@app.get("/api/invoices")
|
| 455 |
+
def api_invoices(req: gr.Request):
|
| 456 |
+
token = get_cookie_token(req)
|
| 457 |
+
if not token:
|
| 458 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 459 |
+
user = get_session_user(token)
|
| 460 |
+
if not user:
|
| 461 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 462 |
+
|
| 463 |
+
filt = req.query_params.get("filter", "all")
|
| 464 |
+
conn = get_conn()
|
| 465 |
+
rows = conn.execute("SELECT * FROM invoices WHERE user_id=? ORDER BY created_at DESC", (user["id"],)).fetchall()
|
| 466 |
+
conn.close()
|
| 467 |
+
|
| 468 |
+
results = []
|
| 469 |
+
for r in rows:
|
| 470 |
+
status = r["status"] or "done"
|
| 471 |
+
if filt == "done" and status != "done":
|
| 472 |
+
continue
|
| 473 |
+
if filt == "review" and status != "review":
|
| 474 |
+
continue
|
| 475 |
+
if filt == "duplicates" and not r["is_duplicate"]:
|
| 476 |
+
continue
|
| 477 |
+
results.append({
|
| 478 |
+
"id": r["id"], "filename": r["filename"], "vendor": r["vendor"],
|
| 479 |
+
"invNumber": r["inv_number"], "invDate": r["inv_date"],
|
| 480 |
+
"dueDate": r["due_date"], "amount": r["amount"],
|
| 481 |
+
"vatAmount": r["vat_amount"], "total": r["total"],
|
| 482 |
+
"currency": r["currency"] or "USD", "status": status,
|
| 483 |
+
"isDuplicate": bool(r["is_duplicate"]), "confidence": r["confidence"],
|
| 484 |
+
"createdAt": r["created_at"],
|
| 485 |
+
})
|
| 486 |
+
return json.dumps({"invoices": results}), 200, {"Content-Type": "application/json"}
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
@app.delete("/api/invoices")
|
| 490 |
+
def api_delete_invoice(req: gr.Request):
|
| 491 |
+
token = get_cookie_token(req)
|
| 492 |
+
if not token:
|
| 493 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 494 |
+
user = get_session_user(token)
|
| 495 |
+
if not user:
|
| 496 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 497 |
+
|
| 498 |
+
try:
|
| 499 |
+
body = req.get_body()
|
| 500 |
+
data = json.loads(body) if body else {}
|
| 501 |
+
except Exception:
|
| 502 |
+
return '{"error":"Invalid request"}', 400, {"Content-Type": "application/json"}
|
| 503 |
+
|
| 504 |
+
inv_id = data.get("id")
|
| 505 |
+
if not inv_id:
|
| 506 |
+
return '{"error":"Invoice ID required"}', 400, {"Content-Type": "application/json"}
|
| 507 |
+
|
| 508 |
+
conn = get_conn()
|
| 509 |
+
conn.execute("DELETE FROM invoices WHERE id=? AND user_id=?", (inv_id, user["id"]))
|
| 510 |
+
conn.commit()
|
| 511 |
+
conn.close()
|
| 512 |
+
return '{"success":true}', 200, {"Content-Type": "application/json"}
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
@app.get("/api/invoices/export/csv")
|
| 516 |
+
def api_export_csv(req: gr.Request):
|
| 517 |
+
token = get_cookie_token(req)
|
| 518 |
+
if not token:
|
| 519 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 520 |
+
user = get_session_user(token)
|
| 521 |
+
if not user:
|
| 522 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 523 |
+
|
| 524 |
+
conn = get_conn()
|
| 525 |
+
rows = conn.execute("SELECT * FROM invoices WHERE user_id=? ORDER BY created_at DESC", (user["id"],)).fetchall()
|
| 526 |
+
conn.close()
|
| 527 |
+
|
| 528 |
+
buf = io.StringIO()
|
| 529 |
+
writer = csv.writer(buf)
|
| 530 |
+
writer.writerow(["ID", "Vendor", "Invoice#", "Date", "Due Date", "Amount", "VAT", "Total", "Currency", "Status"])
|
| 531 |
+
for r in rows:
|
| 532 |
+
writer.writerow([r["id"], r["vendor"], r["inv_number"], r["inv_date"],
|
| 533 |
+
r["due_date"], r["amount"], r["vat_amount"], r["total"],
|
| 534 |
+
r["currency"], r["status"]])
|
| 535 |
+
csv_text = buf.getvalue()
|
| 536 |
+
return csv_text, 200, {
|
| 537 |
+
"Content-Type": "text/csv",
|
| 538 |
+
"Content-Disposition": "attachment; filename=omniparse_export.csv",
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
@app.get("/api/invoices/export/json")
|
| 543 |
+
def api_export_json(req: gr.Request):
|
| 544 |
+
token = get_cookie_token(req)
|
| 545 |
+
if not token:
|
| 546 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 547 |
+
user = get_session_user(token)
|
| 548 |
+
if not user:
|
| 549 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 550 |
+
if user["plan"] == "free":
|
| 551 |
+
return '{"error":"JSON export requires Basic plan or higher"}', 403, {"Content-Type": "application/json"}
|
| 552 |
+
|
| 553 |
+
conn = get_conn()
|
| 554 |
+
rows = conn.execute("SELECT * FROM invoices WHERE user_id=? ORDER BY created_at DESC", (user["id"],)).fetchall()
|
| 555 |
+
conn.close()
|
| 556 |
+
|
| 557 |
+
results = []
|
| 558 |
+
for r in rows:
|
| 559 |
+
results.append({
|
| 560 |
+
"id": r["id"], "filename": r["filename"], "vendor": r["vendor"],
|
| 561 |
+
"invNumber": r["inv_number"], "invDate": r["inv_date"],
|
| 562 |
+
"dueDate": r["due_date"], "amount": r["amount"],
|
| 563 |
+
"vatAmount": r["vat_amount"], "total": r["total"],
|
| 564 |
+
"currency": r["currency"] or "USD", "status": r["status"],
|
| 565 |
+
"isDuplicate": bool(r["is_duplicate"]), "confidence": r["confidence"],
|
| 566 |
+
"createdAt": r["created_at"],
|
| 567 |
+
})
|
| 568 |
+
return json.dumps(results, indent=2, ensure_ascii=False), 200, {
|
| 569 |
+
"Content-Type": "application/json",
|
| 570 |
+
"Content-Disposition": "attachment; filename=omniparse_export.json",
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
@app.post("/api/chat")
|
| 575 |
+
def api_chat(req: gr.Request):
|
| 576 |
+
token = get_cookie_token(req)
|
| 577 |
+
if not token:
|
| 578 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 579 |
+
user = get_session_user(token)
|
| 580 |
+
if not user:
|
| 581 |
+
return '{"error":"Not authenticated"}', 401, {"Content-Type": "application/json"}
|
| 582 |
+
|
| 583 |
+
if user["plan"] not in ("pro", "enterprise"):
|
| 584 |
+
return '{"error":"AI Chat is available on Pro and Enterprise plans"}', 403, {"Content-Type": "application/json"}
|
| 585 |
+
|
| 586 |
+
ok, retry = check_rate_limit(f"chat:{user['id']}", 20, 60)
|
| 587 |
+
if not ok:
|
| 588 |
+
return json.dumps({"error": f"Rate limit exceeded. Try again in {retry}s"}), 429, {"Content-Type": "application/json"}
|
| 589 |
+
|
| 590 |
+
try:
|
| 591 |
+
body = req.get_body()
|
| 592 |
+
data = json.loads(body) if body else {}
|
| 593 |
+
except Exception:
|
| 594 |
+
return '{"error":"Invalid request"}', 400, {"Content-Type": "application/json"}
|
| 595 |
+
|
| 596 |
+
message = data.get("message", "").strip()
|
| 597 |
+
if not message:
|
| 598 |
+
return '{"error":"Message is required"}', 400, {"Content-Type": "application/json"}
|
| 599 |
+
|
| 600 |
+
conn = get_conn()
|
| 601 |
+
rows = conn.execute("SELECT * FROM invoices WHERE user_id=? ORDER BY created_at DESC LIMIT 20", (user["id"],)).fetchall()
|
| 602 |
+
conn.close()
|
| 603 |
+
|
| 604 |
+
invoices = [dict(r) for r in rows]
|
| 605 |
+
total = sum(r["total"] or 0 for r in invoices)
|
| 606 |
+
dups = sum(1 for r in invoices if r["is_duplicate"])
|
| 607 |
+
|
| 608 |
+
msg_lower = message.lower()
|
| 609 |
+
reply = _generate_chat_reply(msg_lower, invoices, total, dups, user["plan"])
|
| 610 |
+
return json.dumps({"reply": reply}), 200, {"Content-Type": "application/json"}
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def _generate_chat_reply(msg: str, invoices: list, total: float, dups: int, plan: str) -> str:
|
| 614 |
+
if not invoices:
|
| 615 |
+
return "You don't have any invoices yet. Upload some invoices to get started, and I'll help you analyze them!"
|
| 616 |
+
if "summary" in msg or "overview" in msg or "how many" in msg:
|
| 617 |
+
vendors = list(set(r["vendor"] for r in invoices if r["vendor"]))
|
| 618 |
+
avg = total / len(invoices) if invoices else 0
|
| 619 |
+
return (f"Here is a quick overview of your invoices:\n\n"
|
| 620 |
+
f"Total invoices: {len(invoices)}\n"
|
| 621 |
+
f"Total amount: ${total:.2f}\n"
|
| 622 |
+
f"Average invoice: ${avg:.2f}\n"
|
| 623 |
+
f"Unique vendors: {len(vendors)} ({', '.join(vendors[:5])})\n"
|
| 624 |
+
f"Duplicates: {dups}\n\n"
|
| 625 |
+
f"Would you like me to break this down by vendor or time period?")
|
| 626 |
+
if "duplicate" in msg:
|
| 627 |
+
if dups == 0:
|
| 628 |
+
return "Great news! No duplicates were detected in your invoices."
|
| 629 |
+
dup_list = [f"- {r['inv_number'] or 'N/A'} from {r['vendor'] or 'Unknown'}: ${r['total'] or 0:.2f}" for r in invoices if r["is_duplicate"]]
|
| 630 |
+
return f"Warning: {dups} potential duplicate(s) found:\n\n" + "\n".join(dup_list)
|
| 631 |
+
if "vendor" in msg or "supplier" in msg:
|
| 632 |
+
vendor_map: dict[str, dict] = {}
|
| 633 |
+
for inv in invoices:
|
| 634 |
+
v = inv["vendor"] or "Unknown"
|
| 635 |
+
if v not in vendor_map:
|
| 636 |
+
vendor_map[v] = {"count": 0, "total": 0}
|
| 637 |
+
vendor_map[v]["count"] += 1
|
| 638 |
+
vendor_map[v]["total"] += inv["total"] or 0
|
| 639 |
+
lines = [f"- {v}: {d['count']} invoice(s), ${d['total']:.2f}" for v, d in sorted(vendor_map.items(), key=lambda x: -x[1]["total"])]
|
| 640 |
+
return "Breakdown by vendor:\n\n" + "\n".join(lines)
|
| 641 |
+
if "spend" in msg or "total" in msg or "amount" in msg:
|
| 642 |
+
avg = total / len(invoices) if invoices else 0
|
| 643 |
+
max_inv = max(invoices, key=lambda x: x["total"] or 0)
|
| 644 |
+
min_inv = min(invoices, key=lambda x: x["total"] or 0)
|
| 645 |
+
return (f"Spending Analysis:\n\n"
|
| 646 |
+
f"Total: ${total:.2f} across {len(invoices)} invoices\n"
|
| 647 |
+
f"Average: ${avg:.2f} per invoice\n"
|
| 648 |
+
f"Largest: ${max_inv['total'] or 0:.2f} from {max_inv['vendor'] or 'Unknown'}\n"
|
| 649 |
+
f"Smallest: ${min_inv['total'] or 0:.2f} from {min_inv['vendor'] or 'Unknown'}")
|
| 650 |
+
return (f"I can help you with your invoices! Try asking:\n\n"
|
| 651 |
+
f"- 'Give me a summary'\n- 'Show duplicates'\n"
|
| 652 |
+
f"- 'Breakdown by vendor'\n- 'Spending analysis'\n\n"
|
| 653 |
+
f"You have {len(invoices)} invoice(s) totaling ${total:.2f}.")
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
# =============================================================================
|
| 657 |
+
# HTML UI
|
| 658 |
+
# =============================================================================
|
| 659 |
+
CUSTOM_CSS = """
|
| 660 |
+
footer { display: none !important; }
|
| 661 |
+
.gradio-container { background: transparent !important; max-width: 100% !important; padding: 0 !important; }
|
| 662 |
+
#root > .gradio-container { max-width: 100% !important; }
|
| 663 |
+
"""
|
| 664 |
+
|
| 665 |
+
APP_HTML = r"""
|
| 666 |
+
<div id="op-app">
|
| 667 |
+
<!-- Toast Container -->
|
| 668 |
+
<div id="toast-container" style="position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;"></div>
|
| 669 |
+
|
| 670 |
+
<!-- Navbar -->
|
| 671 |
+
<header id="op-navbar" style="position:sticky;top:0;z-index:100;backdrop-filter:blur(16px);background:rgba(9,9,11,0.85);border-bottom:1px solid rgba(255,255,255,0.06);padding:12px 24px;">
|
| 672 |
+
<nav style="max-width:1200px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;">
|
| 673 |
+
<button onclick="navigate('landing')" style="background:none;border:none;cursor:pointer;display:flex;align-items:center;gap:8px;color:#FAFAFA;font-size:1.3rem;font-weight:800;">
|
| 674 |
+
<svg width="28" height="28" viewBox="0 0 32 32" fill="none"><rect width="32" height="32" rx="8" fill="#F59E0B"/><path d="M8 10h6v2H10v3h4v2h-4v3h4v2H8V10zm10 0h2l2 4 2-4h2v12h-2.5v-7.5L20 19h-.2l-1.3-2.5V22H16V10z" fill="#09090B"/></svg>
|
| 675 |
+
OmniParse
|
| 676 |
+
</button>
|
| 677 |
+
<div id="nav-auth" style="display:flex;gap:8px;align-items:center;">
|
| 678 |
+
<button onclick="navigate('auth','login')" style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;padding:8px 16px;border-radius:8px;cursor:pointer;font-size:0.875rem;">Log In</button>
|
| 679 |
+
<button onclick="navigate('auth','signup')" style="background:#F59E0B;border:none;color:#09090B;padding:8px 16px;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.875rem;">Start Free</button>
|
| 680 |
+
</div>
|
| 681 |
+
<div id="nav-dash" style="display:none;gap:8px;align-items:center;">
|
| 682 |
+
<span id="nav-user-name" style="color:#A1A1AA;font-size:0.875rem;"></span>
|
| 683 |
+
<span id="nav-plan-badge" style="background:rgba(245,158,11,0.15);color:#F59E0B;padding:2px 10px;border-radius:20px;font-size:0.75rem;font-weight:600;"></span>
|
| 684 |
+
<button onclick="doLogout()" style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;padding:8px 16px;border-radius:8px;cursor:pointer;font-size:0.875rem;">Log Out</button>
|
| 685 |
+
</div>
|
| 686 |
+
</nav>
|
| 687 |
+
</header>
|
| 688 |
+
|
| 689 |
+
<!-- LANDING VIEW -->
|
| 690 |
+
<div id="view-landing">
|
| 691 |
+
<main style="max-width:1200px;margin:0 auto;">
|
| 692 |
+
<!-- Hero -->
|
| 693 |
+
<section style="text-align:center;padding:80px 24px 40px;">
|
| 694 |
+
<h1 style="font-size:3rem;font-weight:800;line-height:1.1;margin-bottom:16px;">
|
| 695 |
+
<span style="background:linear-gradient(135deg,#F59E0B 0%,#FBBF24 40%,#14B8A6 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">Extract Data from Invoices in Seconds</span>
|
| 696 |
+
</h1>
|
| 697 |
+
<p style="font-size:1.15rem;color:#A1A1AA;max-width:640px;margin:0 auto 24px;">AI extracts vendor, dates, amounts and line items from any PDF or image. Export to CSV, JSON or connect via API.</p>
|
| 698 |
+
<div style="color:#F59E0B;font-weight:600;margin-bottom:32px;">99.2% accuracy · <4s per invoice · 40+ formats</div>
|
| 699 |
+
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;">
|
| 700 |
+
<button onclick="navigate('auth','signup')" style="background:#F59E0B;border:none;color:#09090B;padding:14px 28px;border-radius:12px;cursor:pointer;font-weight:700;font-size:1rem;">Get Started Free</button>
|
| 701 |
+
<button onclick="document.getElementById('pricing-section').scrollIntoView({behavior:'smooth'})" style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;padding:14px 28px;border-radius:12px;cursor:pointer;font-size:1rem;">View Pricing</button>
|
| 702 |
+
</div>
|
| 703 |
+
</section>
|
| 704 |
+
|
| 705 |
+
<!-- How It Works -->
|
| 706 |
+
<section style="padding:40px 24px;">
|
| 707 |
+
<h2 style="text-align:center;font-size:1.6rem;margin-bottom:32px;">How It Works</h2>
|
| 708 |
+
<div style="display:flex;gap:24px;flex-wrap:wrap;justify-content:center;">
|
| 709 |
+
<div style="flex:1;min-width:200px;text-align:center;">
|
| 710 |
+
<div style="width:48px;height:48px;border-radius:50%;background:rgba(245,158,11,0.15);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
|
| 711 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
| 712 |
+
</div>
|
| 713 |
+
<h3 style="font-size:1.1rem;margin-bottom:4px;">Upload</h3>
|
| 714 |
+
<p style="color:#A1A1AA;font-size:0.9rem;">Drop a PDF or image of your invoice</p>
|
| 715 |
+
</div>
|
| 716 |
+
<div style="flex:1;min-width:200px;text-align:center;">
|
| 717 |
+
<div style="width:48px;height:48px;border-radius:50%;background:rgba(20,184,166,0.15);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
|
| 718 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14B8A6" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
| 719 |
+
</div>
|
| 720 |
+
<h3 style="font-size:1.1rem;margin-bottom:4px;">Extract</h3>
|
| 721 |
+
<p style="color:#A1A1AA;font-size:0.9rem;">AI pulls out all structured data instantly</p>
|
| 722 |
+
</div>
|
| 723 |
+
<div style="flex:1;min-width:200px;text-align:center;">
|
| 724 |
+
<div style="width:48px;height:48px;border-radius:50%;background:rgba(245,158,11,0.15);display:flex;align-items:center;justify-content:center;margin:0 auto 12px;">
|
| 725 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
| 726 |
+
</div>
|
| 727 |
+
<h3 style="font-size:1.1rem;margin-bottom:4px;">Export</h3>
|
| 728 |
+
<p style="color:#A1A1AA;font-size:0.9rem;">CSV, JSON, Excel, or connect via API</p>
|
| 729 |
+
</div>
|
| 730 |
+
</div>
|
| 731 |
+
</section>
|
| 732 |
+
|
| 733 |
+
<!-- Features -->
|
| 734 |
+
<section style="padding:40px 24px;">
|
| 735 |
+
<h2 style="text-align:center;font-size:1.6rem;margin-bottom:32px;">Powerful Features</h2>
|
| 736 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;">
|
| 737 |
+
<div class="glass-card-hover" style="padding:24px;">
|
| 738 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" style="margin-bottom:12px;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
| 739 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">Smart OCR Extraction</h3>
|
| 740 |
+
<p style="color:#A1A1AA;font-size:0.875rem;">Multi-engine OCR with AI-powered data extraction for maximum accuracy.</p>
|
| 741 |
+
</div>
|
| 742 |
+
<div class="glass-card-hover" style="padding:24px;">
|
| 743 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2" style="margin-bottom:12px;"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="9" x2="15" y2="15"/><line x1="15" y1="9" x2="9" y2="15"/></svg>
|
| 744 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">Duplicate Detection</h3>
|
| 745 |
+
<p style="color:#A1A1AA;font-size:0.875rem;">Automatically flags potential duplicate invoices to prevent double payments.</p>
|
| 746 |
+
</div>
|
| 747 |
+
<div class="glass-card-hover" style="padding:24px;">
|
| 748 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14B8A6" stroke-width="2" style="margin-bottom:12px;"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
| 749 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">AI Chat Agent</h3>
|
| 750 |
+
<p style="color:#A1A1AA;font-size:0.875rem;">Ask questions about your invoices in natural language.</p>
|
| 751 |
+
</div>
|
| 752 |
+
<div class="glass-card-hover" style="padding:24px;">
|
| 753 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#22C55E" stroke-width="2" style="margin-bottom:12px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
| 754 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">Bank-Level Security</h3>
|
| 755 |
+
<p style="color:#A1A1AA;font-size:0.875rem;">Bcrypt hashing, HttpOnly cookies, rate limiting, and encrypted storage.</p>
|
| 756 |
+
</div>
|
| 757 |
+
<div class="glass-card-hover" style="padding:24px;">
|
| 758 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" style="margin-bottom:12px;"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
| 759 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">Team Collaboration</h3>
|
| 760 |
+
<p style="color:#A1A1AA;font-size:0.875rem;">Multi-user support with role-based access on Enterprise plans.</p>
|
| 761 |
+
</div>
|
| 762 |
+
<div class="glass-card-hover" style="padding:24px;">
|
| 763 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#14B8A6" stroke-width="2" style="margin-bottom:12px;"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1"/><circle cx="6" cy="18" r="1"/></svg>
|
| 764 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">REST API</h3>
|
| 765 |
+
<p style="color:#A1A1AA;font-size:0.875rem;">Integrate OmniParse directly with your ERP or accounting system.</p>
|
| 766 |
+
</div>
|
| 767 |
+
</div>
|
| 768 |
+
</section>
|
| 769 |
+
|
| 770 |
+
<!-- Testimonials -->
|
| 771 |
+
<section style="padding:40px 24px;">
|
| 772 |
+
<h2 style="text-align:center;font-size:1.6rem;margin-bottom:32px;">Trusted by Finance Teams</h2>
|
| 773 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;">
|
| 774 |
+
<blockquote class="glass-card" style="padding:24px;font-style:italic;">
|
| 775 |
+
<p style="color:#D4D4D8;margin-bottom:12px;">"We saved dozens of hours per month on manual invoice transcription. The accuracy is remarkable."</p>
|
| 776 |
+
<footer style="font-style:normal;color:#F59E0B;font-weight:600;">Sarah K., CFO</footer>
|
| 777 |
+
</blockquote>
|
| 778 |
+
<blockquote class="glass-card" style="padding:24px;font-style:italic;">
|
| 779 |
+
<p style="color:#D4D4D8;margin-bottom:12px;">"API integration into our ERP took one afternoon. The documentation is clear and the endpoint just works."</p>
|
| 780 |
+
<footer style="font-style:normal;color:#F59E0B;font-weight:600;">Thomas R., CTO</footer>
|
| 781 |
+
</blockquote>
|
| 782 |
+
<blockquote class="glass-card" style="padding:24px;font-style:italic;">
|
| 783 |
+
<p style="color:#D4D4D8;margin-bottom:12px;">"Finally I do not have to verify every single line manually. The duplicate detection alone pays for itself."</p>
|
| 784 |
+
<footer style="font-style:normal;color:#F59E0B;font-weight:600;">Petra M., Accountant</footer>
|
| 785 |
+
</blockquote>
|
| 786 |
+
</div>
|
| 787 |
+
</section>
|
| 788 |
+
|
| 789 |
+
<!-- FAQ -->
|
| 790 |
+
<section style="padding:40px 24px;">
|
| 791 |
+
<h2 style="text-align:center;font-size:1.6rem;margin-bottom:32px;">Frequently Asked Questions</h2>
|
| 792 |
+
<div style="max-width:700px;margin:0 auto;display:flex;flex-direction:column;gap:10px;">
|
| 793 |
+
<details class="glass-card" style="padding:16px 20px;cursor:pointer;"><summary style="font-weight:600;">Is my data secure?</summary><p style="color:#A1A1AA;margin-top:8px;font-size:0.9rem;">Data is encrypted at rest and in transit. We use bcrypt for passwords, HttpOnly cookies for sessions, and enforce rate limiting on all endpoints.</p></details>
|
| 794 |
+
<details class="glass-card" style="padding:16px 20px;cursor:pointer;"><summary style="font-weight:600;">What file formats are supported?</summary><p style="color:#A1A1AA;margin-top:8px;font-size:0.9rem;">PDF, JPG, JPEG, PNG, and TIFF. Maximum file size is 20MB per file.</p></details>
|
| 795 |
+
<details class="glass-card" style="padding:16px 20px;cursor:pointer;"><summary style="font-weight:600;">How does pricing work?</summary><p style="color:#A1A1AA;margin-top:8px;font-size:0.9rem;">Monthly subscriptions via Stripe. Cancel anytime from your profile with no penalty.</p></details>
|
| 796 |
+
<details class="glass-card" style="padding:16px 20px;cursor:pointer;"><summary style="font-weight:600;">Can I cancel anytime?</summary><p style="color:#A1A1AA;margin-top:8px;font-size:0.9rem;">Yes, cancel from your Profile page at any time. Your account stays active until the end of the paid period.</p></details>
|
| 797 |
+
<details class="glass-card" style="padding:16px 20px;cursor:pointer;"><summary style="font-weight:600;">Do I get a tax invoice?</summary><p style="color:#A1A1AA;margin-top:8px;font-size:0.9rem;">Yes, a tax invoice is automatically sent to your email after each payment.</p></details>
|
| 798 |
+
</div>
|
| 799 |
+
</section>
|
| 800 |
+
|
| 801 |
+
<!-- Pricing -->
|
| 802 |
+
<section id="pricing-section" style="padding:40px 24px 60px;">
|
| 803 |
+
<h2 style="text-align:center;font-size:1.6rem;margin-bottom:32px;">Simple, Transparent Pricing</h2>
|
| 804 |
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;max-width:1000px;margin:0 auto;" id="landing-pricing"></div>
|
| 805 |
+
</section>
|
| 806 |
+
</main>
|
| 807 |
+
</div>
|
| 808 |
+
|
| 809 |
+
<!-- AUTH VIEW -->
|
| 810 |
+
<div id="view-auth" style="display:none;max-width:440px;margin:60px auto;padding:0 24px;">
|
| 811 |
+
<button onclick="navigate('landing')" style="background:none;border:none;color:#A1A1AA;cursor:pointer;font-size:0.875rem;margin-bottom:24px;display:flex;align-items:center;gap:6px;">
|
| 812 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
| 813 |
+
Back to home
|
| 814 |
+
</button>
|
| 815 |
+
<h1 id="auth-heading" style="font-size:1.8rem;margin-bottom:24px;">Welcome back</h1>
|
| 816 |
+
<!-- Tabs -->
|
| 817 |
+
<div style="display:flex;gap:0;margin-bottom:24px;border-bottom:1px solid rgba(255,255,255,0.08);">
|
| 818 |
+
<button id="tab-login" onclick="showAuthTab('login')" style="flex:1;padding:12px;background:none;border:none;color:#F59E0B;border-bottom:2px solid #F59E0B;cursor:pointer;font-weight:600;">Log In</button>
|
| 819 |
+
<button id="tab-signup" onclick="showAuthTab('signup')" style="flex:1;padding:12px;background:none;border:none;color:#A1A1AA;border-bottom:2px solid transparent;cursor:pointer;font-weight:600;">Sign Up</button>
|
| 820 |
+
</div>
|
| 821 |
+
<!-- Login Form -->
|
| 822 |
+
<div id="form-login">
|
| 823 |
+
<p style="color:#A1A1AA;font-size:0.8rem;margin-bottom:16px;padding:10px 14px;background:rgba(245,158,11,0.06);border:1px solid rgba(245,158,11,0.15);border-radius:8px;">Demo account: demo@omniparse.ai / demo1234 (Pro plan)</p>
|
| 824 |
+
<div style="margin-bottom:16px;">
|
| 825 |
+
<label style="display:block;font-size:0.875rem;color:#A1A1AA;margin-bottom:6px;">Email</label>
|
| 826 |
+
<input id="login-email" type="email" placeholder="you@company.com" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;box-sizing:border-box;">
|
| 827 |
+
</div>
|
| 828 |
+
<div style="margin-bottom:16px;">
|
| 829 |
+
<label style="display:block;font-size:0.875rem;color:#A1A1AA;margin-bottom:6px;">Password</label>
|
| 830 |
+
<input id="login-password" type="password" placeholder="Enter your password" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;box-sizing:border-box;">
|
| 831 |
+
</div>
|
| 832 |
+
<div id="login-error" style="display:none;color:#EF4444;font-size:0.85rem;margin-bottom:12px;padding:8px 12px;background:rgba(239,68,68,0.08);border-radius:8px;"></div>
|
| 833 |
+
<button id="login-btn" onclick="doLogin()" style="width:100%;padding:12px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:700;font-size:0.95rem;">Sign In</button>
|
| 834 |
+
</div>
|
| 835 |
+
<!-- Signup Form -->
|
| 836 |
+
<div id="form-signup" style="display:none;">
|
| 837 |
+
<div style="margin-bottom:16px;">
|
| 838 |
+
<label style="display:block;font-size:0.875rem;color:#A1A1AA;margin-bottom:6px;">Full Name</label>
|
| 839 |
+
<input id="signup-name" type="text" placeholder="John Doe" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;box-sizing:border-box;">
|
| 840 |
+
</div>
|
| 841 |
+
<div style="margin-bottom:16px;">
|
| 842 |
+
<label style="display:block;font-size:0.875rem;color:#A1A1AA;margin-bottom:6px;">Work Email</label>
|
| 843 |
+
<input id="signup-email" type="email" placeholder="you@company.com" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;box-sizing:border-box;">
|
| 844 |
+
</div>
|
| 845 |
+
<div style="margin-bottom:16px;">
|
| 846 |
+
<label style="display:block;font-size:0.875rem;color:#A1A1AA;margin-bottom:6px;">Password</label>
|
| 847 |
+
<input id="signup-password" type="password" placeholder="Min 8 chars, uppercase, lowercase, number" oninput="updatePwStrength(this.value)" style="width:100%;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;box-sizing:border-box;">
|
| 848 |
+
<div id="pw-strength" style="margin-top:6px;height:4px;border-radius:2px;background:rgba(255,255,255,0.08);overflow:hidden;"><div id="pw-bar" style="height:100%;width:0;border-radius:2px;transition:all 0.3s;"></div></div>
|
| 849 |
+
<div id="pw-label" style="font-size:0.75rem;color:#A1A1AA;margin-top:4px;"></div>
|
| 850 |
+
</div>
|
| 851 |
+
<div style="margin-bottom:16px;display:flex;align-items:center;gap:8px;">
|
| 852 |
+
<input id="signup-terms" type="checkbox" style="accent-color:#F59E0B;">
|
| 853 |
+
<label for="signup-terms" style="font-size:0.85rem;color:#A1A1AA;">I agree to the Terms of Service and Privacy Policy</label>
|
| 854 |
+
</div>
|
| 855 |
+
<div id="signup-error" style="display:none;color:#EF4444;font-size:0.85rem;margin-bottom:12px;padding:8px 12px;background:rgba(239,68,68,0.08);border-radius:8px;"></div>
|
| 856 |
+
<button id="signup-btn" onclick="doSignup()" style="width:100%;padding:12px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:700;font-size:0.95rem;">Create Account</button>
|
| 857 |
+
</div>
|
| 858 |
+
</div>
|
| 859 |
+
|
| 860 |
+
<!-- DASHBOARD VIEW -->
|
| 861 |
+
<div id="view-dashboard" style="display:none;max-width:1200px;margin:0 auto;padding:24px;">
|
| 862 |
+
<h1 style="font-size:1.8rem;margin-bottom:4px;">Dashboard</h1>
|
| 863 |
+
<p id="dash-welcome" style="color:#A1A1AA;margin-bottom:24px;"></p>
|
| 864 |
+
|
| 865 |
+
<!-- Dashboard Tabs -->
|
| 866 |
+
<div style="display:flex;gap:0;border-bottom:1px solid rgba(255,255,255,0.08);margin-bottom:24px;overflow-x:auto;">
|
| 867 |
+
<button class="dash-tab" data-tab="upload" onclick="showDashTab('upload')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid #F59E0B;color:#F59E0B;cursor:pointer;font-weight:500;white-space:nowrap;">Upload</button>
|
| 868 |
+
<button class="dash-tab" data-tab="invoices" onclick="showDashTab('invoices')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid transparent;color:#A1A1AA;cursor:pointer;font-weight:500;white-space:nowrap;">Invoices</button>
|
| 869 |
+
<button class="dash-tab" data-tab="chat" onclick="showDashTab('chat')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid transparent;color:#A1A1AA;cursor:pointer;font-weight:500;white-space:nowrap;">AI Chat</button>
|
| 870 |
+
<button class="dash-tab" data-tab="export" onclick="showDashTab('export')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid transparent;color:#A1A1AA;cursor:pointer;font-weight:500;white-space:nowrap;">Export</button>
|
| 871 |
+
<button class="dash-tab" data-tab="upgrade" onclick="showDashTab('upgrade')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid transparent;color:#A1A1AA;cursor:pointer;font-weight:500;white-space:nowrap;">Upgrade</button>
|
| 872 |
+
<button class="dash-tab" data-tab="api" onclick="showDashTab('api')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid transparent;color:#A1A1AA;cursor:pointer;font-weight:500;white-space:nowrap;">API</button>
|
| 873 |
+
<button class="dash-tab" data-tab="profile" onclick="showDashTab('profile')" style="padding:12px 16px;background:none;border:none;border-bottom:2px solid transparent;color:#A1A1AA;cursor:pointer;font-weight:500;white-space:nowrap;">Profile</button>
|
| 874 |
+
</div>
|
| 875 |
+
|
| 876 |
+
<!-- Upload Panel -->
|
| 877 |
+
<div id="panel-upload">
|
| 878 |
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
|
| 879 |
+
<div><span style="color:#A1A1AA;font-size:0.875rem;">Invoices this month</span>
|
| 880 |
+
<span id="dash-plan-label" style="margin-left:8px;color:#F59E0B;font-weight:600;font-size:0.85rem;"></span></div>
|
| 881 |
+
<span id="dash-usage" style="color:#FAFAFA;font-weight:600;"></span>
|
| 882 |
+
</div>
|
| 883 |
+
<div style="height:6px;background:rgba(255,255,255,0.06);border-radius:3px;margin-bottom:24px;overflow:hidden;"><div id="usage-bar" style="height:100%;background:linear-gradient(90deg,#F59E0B,#14B8A6);border-radius:3px;transition:width 0.6s;"></div></div>
|
| 884 |
+
|
| 885 |
+
<div id="drop-zone" class="drop-zone" style="padding:48px 24px;text-align:center;cursor:pointer;position:relative;" onclick="document.getElementById('file-input').click()">
|
| 886 |
+
<input id="file-input" type="file" multiple accept=".pdf,.jpg,.jpeg,.png,.tiff,.tif" style="display:none;" onchange="handleFiles(this.files)">
|
| 887 |
+
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2" style="margin:0 auto 12px;display:block;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
|
| 888 |
+
<p style="font-size:1.1rem;font-weight:500;margin-bottom:4px;">Drop invoice files here</p>
|
| 889 |
+
<p style="color:#A1A1AA;font-size:0.85rem;">or click to browse. Supports PDF, JPG, PNG, TIFF (max 20MB each)</p>
|
| 890 |
+
</div>
|
| 891 |
+
<div id="file-list" style="margin-top:16px;display:none;"></div>
|
| 892 |
+
<button id="upload-btn" onclick="doUpload()" style="display:none;margin-top:16px;width:100%;padding:12px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:700;">Process Invoices</button>
|
| 893 |
+
<div id="upload-status" style="margin-top:16px;"></div>
|
| 894 |
+
<div id="upload-results" style="margin-top:16px;overflow-x:auto;"></div>
|
| 895 |
+
</div>
|
| 896 |
+
|
| 897 |
+
<!-- Invoices Panel -->
|
| 898 |
+
<div id="panel-invoices" style="display:none;">
|
| 899 |
+
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center;">
|
| 900 |
+
<button class="inv-filter active" data-f="all" onclick="filterInvoices('all')" style="padding:6px 14px;background:rgba(245,158,11,0.15);border:1px solid rgba(245,158,11,0.3);color:#F59E0B;border-radius:6px;cursor:pointer;font-size:0.8rem;">All</button>
|
| 901 |
+
<button class="inv-filter" data-f="done" onclick="filterInvoices('done')" style="padding:6px 14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:#A1A1AA;border-radius:6px;cursor:pointer;font-size:0.8rem;">Done</button>
|
| 902 |
+
<button class="inv-filter" data-f="review" onclick="filterInvoices('review')" style="padding:6px 14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:#A1A1AA;border-radius:6px;cursor:pointer;font-size:0.8rem;">Review</button>
|
| 903 |
+
<button class="inv-filter" data-f="duplicates" onclick="filterInvoices('duplicates')" style="padding:6px 14px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);color:#A1A1AA;border-radius:6px;cursor:pointer;font-size:0.8rem;">Duplicates</button>
|
| 904 |
+
<button onclick="loadInvoices()" style="margin-left:auto;padding:6px 14px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;border-radius:6px;cursor:pointer;font-size:0.8rem;">Refresh</button>
|
| 905 |
+
</div>
|
| 906 |
+
<div id="invoices-table-wrap" style="max-height:400px;overflow-y:auto;border:1px solid rgba(255,255,255,0.06);border-radius:12px;"></div>
|
| 907 |
+
<h3 style="margin-top:32px;margin-bottom:12px;color:#EF4444;">Danger Zone</h3>
|
| 908 |
+
<div style="display:flex;gap:8px;align-items:center;">
|
| 909 |
+
<input id="delete-id" type="text" placeholder="Invoice ID" style="padding:8px 12px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.85rem;width:200px;">
|
| 910 |
+
<button onclick="doDeleteInvoice()" style="padding:8px 16px;background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);color:#EF4444;border-radius:8px;cursor:pointer;font-size:0.85rem;">Delete</button>
|
| 911 |
+
</div>
|
| 912 |
+
<div id="delete-status" style="margin-top:8px;font-size:0.85rem;"></div>
|
| 913 |
+
</div>
|
| 914 |
+
|
| 915 |
+
<!-- Chat Panel -->
|
| 916 |
+
<div id="panel-chat" style="display:none;">
|
| 917 |
+
<h3 style="font-size:1.1rem;margin-bottom:4px;">AI Chat Agent</h3>
|
| 918 |
+
<p style="color:#A1A1AA;font-size:0.85rem;margin-bottom:16px;">Ask about your invoices, spending patterns, or vendor details.</p>
|
| 919 |
+
<div id="chat-gate" style="display:none;padding:24px;text-align:center;" class="glass-card">
|
| 920 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#A1A1AA" stroke-width="2" style="margin:0 auto 12px;display:block;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
| 921 |
+
<p style="color:#A1A1AA;margin-bottom:8px;">AI Chat is available on Pro and Enterprise plans.</p>
|
| 922 |
+
<button onclick="showDashTab('upgrade')" style="padding:8px 20px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:600;">Upgrade Plan</button>
|
| 923 |
+
</div>
|
| 924 |
+
<div id="chat-area">
|
| 925 |
+
<div id="chat-messages" style="max-height:340px;overflow-y:auto;padding:16px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:12px;margin-bottom:12px;min-height:200px;"></div>
|
| 926 |
+
<div style="display:flex;gap:8px;">
|
| 927 |
+
<input id="chat-input" type="text" placeholder="Ask about your invoices..." onkeydown="if(event.key==='Enter')sendChat()" style="flex:1;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;">
|
| 928 |
+
<button id="chat-send" onclick="sendChat()" style="padding:10px 20px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:600;">Send</button>
|
| 929 |
+
</div>
|
| 930 |
+
</div>
|
| 931 |
+
</div>
|
| 932 |
+
|
| 933 |
+
<!-- Export Panel -->
|
| 934 |
+
<div id="panel-export" style="display:none;">
|
| 935 |
+
<div class="glass-card" style="padding:24px;margin-bottom:16px;">
|
| 936 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">CSV Export</h3>
|
| 937 |
+
<p style="color:#A1A1AA;font-size:0.85rem;margin-bottom:16px;">Download all your invoices as a CSV file. Available on all plans.</p>
|
| 938 |
+
<button onclick="exportCsv()" style="padding:10px 20px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:600;">Download CSV</button>
|
| 939 |
+
</div>
|
| 940 |
+
<div class="glass-card" style="padding:24px;">
|
| 941 |
+
<h3 style="font-size:1rem;margin-bottom:8px;">JSON Export</h3>
|
| 942 |
+
<p style="color:#A1A1AA;font-size:0.85rem;margin-bottom:16px;">Download all invoices with full raw JSON data. Requires Basic plan or higher.</p>
|
| 943 |
+
<button id="json-export-btn" onclick="exportJson()" style="padding:10px 20px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;border-radius:8px;cursor:pointer;font-weight:600;">Download JSON</button>
|
| 944 |
+
</div>
|
| 945 |
+
</div>
|
| 946 |
+
|
| 947 |
+
<!-- Upgrade Panel -->
|
| 948 |
+
<div id="panel-upgrade" style="display:none;">
|
| 949 |
+
<div style="margin-bottom:24px;">
|
| 950 |
+
<label style="display:block;font-size:0.875rem;color:#A1A1AA;margin-bottom:6px;">Select Plan</label>
|
| 951 |
+
<select id="upgrade-plan" style="padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;">
|
| 952 |
+
<option value="basic">Basic - $29/mo</option>
|
| 953 |
+
<option value="pro">Pro - $129/mo</option>
|
| 954 |
+
<option value="enterprise">Enterprise - $499/mo</option>
|
| 955 |
+
</select>
|
| 956 |
+
</div>
|
| 957 |
+
<div id="dash-pricing"></div>
|
| 958 |
+
</div>
|
| 959 |
+
|
| 960 |
+
<!-- API Panel -->
|
| 961 |
+
<div id="panel-api" style="display:none;">
|
| 962 |
+
<div id="api-gate" style="display:none;padding:24px;text-align:center;" class="glass-card">
|
| 963 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#A1A1AA" stroke-width="2" style="margin:0 auto 12px;display:block;"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
| 964 |
+
<p style="color:#A1A1AA;margin-bottom:8px;">API access is available on Pro and Enterprise plans.</p>
|
| 965 |
+
<button onclick="showDashTab('upgrade')" style="padding:8px 20px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:600;">Upgrade Plan</button>
|
| 966 |
+
</div>
|
| 967 |
+
<div id="api-area">
|
| 968 |
+
<h3 style="font-size:1.1rem;margin-bottom:4px;">API Access</h3>
|
| 969 |
+
<p style="color:#A1A1AA;font-size:0.85rem;margin-bottom:16px;">Use your API key to integrate OmniParse with your applications.</p>
|
| 970 |
+
<h4 style="font-size:0.95rem;margin-bottom:8px;">Your API Key</h4>
|
| 971 |
+
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
|
| 972 |
+
<code id="api-key-display" style="flex:1;padding:10px 14px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#D4D4D8;font-family:monospace;font-size:0.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></code>
|
| 973 |
+
<button id="toggle-key-btn" onclick="toggleApiKey()" style="padding:8px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;border-radius:8px;cursor:pointer;font-size:0.8rem;">Show</button>
|
| 974 |
+
<button onclick="copyApiKey()" style="padding:8px 12px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#FAFAFA;border-radius:8px;cursor:pointer;font-size:0.8rem;">Copy</button>
|
| 975 |
+
</div>
|
| 976 |
+
<p style="color:#A1A1AA;font-size:0.75rem;margin-bottom:24px;">Keep your API key secret. Do not share it publicly or commit it to version control.</p>
|
| 977 |
+
<h4 style="font-size:0.95rem;margin-bottom:8px;">Example Request</h4>
|
| 978 |
+
<pre style="padding:16px;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:8px;color:#D4D4D8;font-family:monospace;font-size:0.8rem;overflow-x:auto;white-space:pre-wrap;">curl -X POST https://api.omniparse.ai/v1/invoices \
|
| 979 |
+
-H "Authorization: Bearer <your-api-key>" \
|
| 980 |
+
-F "files=@invoice.pdf"</pre>
|
| 981 |
+
</div>
|
| 982 |
+
</div>
|
| 983 |
+
|
| 984 |
+
<!-- Profile Panel -->
|
| 985 |
+
<div id="panel-profile" style="display:none;">
|
| 986 |
+
<h3 style="font-size:1.1rem;margin-bottom:16px;">Profile Information</h3>
|
| 987 |
+
<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 16px;margin-bottom:32px;">
|
| 988 |
+
<span style="color:#A1A1AA;font-size:0.875rem;">Name</span><span id="prof-name"></span>
|
| 989 |
+
<span style="color:#A1A1AA;font-size:0.875rem;">Email</span><span id="prof-email"></span>
|
| 990 |
+
<span style="color:#A1A1AA;font-size:0.875rem;">Plan</span><span id="prof-plan"></span>
|
| 991 |
+
<span style="color:#A1A1AA;font-size:0.875rem;">Member Since</span><span id="prof-created"></span>
|
| 992 |
+
</div>
|
| 993 |
+
<h3 style="font-size:1.1rem;margin-bottom:12px;">Change Password</h3>
|
| 994 |
+
<div style="display:flex;gap:8px;margin-bottom:32px;">
|
| 995 |
+
<input id="new-password" type="password" placeholder="New password" style="flex:1;padding:10px 14px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:8px;color:#FAFAFA;font-size:0.9rem;">
|
| 996 |
+
<button onclick="doChangePassword()" style="padding:10px 20px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:600;">Update Password</button>
|
| 997 |
+
</div>
|
| 998 |
+
<div id="pw-change-status" style="margin-bottom:24px;font-size:0.85rem;"></div>
|
| 999 |
+
<h3 style="font-size:1.1rem;margin-bottom:8px;color:#EF4444;">Danger Zone</h3>
|
| 1000 |
+
<p style="color:#A1A1AA;font-size:0.85rem;margin-bottom:12px;">Once you delete your account, all data will be permanently removed. This action cannot be undone.</p>
|
| 1001 |
+
<button onclick="doDeleteAccount()" style="padding:10px 20px;background:rgba(239,68,68,0.15);border:1px solid rgba(239,68,68,0.3);color:#EF4444;border-radius:8px;cursor:pointer;font-weight:600;">Delete Account</button>
|
| 1002 |
+
</div>
|
| 1003 |
+
</div>
|
| 1004 |
+
|
| 1005 |
+
<!-- Footer -->
|
| 1006 |
+
<footer style="border-top:1px solid rgba(255,255,255,0.06);padding:32px 24px;margin-top:60px;">
|
| 1007 |
+
<div style="max-width:1200px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:24px;">
|
| 1008 |
+
<div>
|
| 1009 |
+
<div style="font-weight:800;font-size:1.1rem;margin-bottom:8px;">OmniParse</div>
|
| 1010 |
+
<p style="color:#A1A1AA;font-size:0.8rem;">AI-powered invoice processing for modern finance teams.</p>
|
| 1011 |
+
</div>
|
| 1012 |
+
<div>
|
| 1013 |
+
<h4 style="font-size:0.85rem;font-weight:600;margin-bottom:8px;">Product</h4>
|
| 1014 |
+
<ul style="list-style:none;padding:0;color:#A1A1AA;font-size:0.8rem;"><li style="margin-bottom:4px;">Features</li><li style="margin-bottom:4px;">Pricing</li><li style="margin-bottom:4px;">API Docs</li></ul>
|
| 1015 |
+
</div>
|
| 1016 |
+
<div>
|
| 1017 |
+
<h4 style="font-size:0.85rem;font-weight:600;margin-bottom:8px;">Company</h4>
|
| 1018 |
+
<ul style="list-style:none;padding:0;color:#A1A1AA;font-size:0.8rem;"><li style="margin-bottom:4px;">About</li><li style="margin-bottom:4px;">Blog</li><li style="margin-bottom:4px;">Contact</li></ul>
|
| 1019 |
+
</div>
|
| 1020 |
+
<div>
|
| 1021 |
+
<h4 style="font-size:0.85rem;font-weight:600;margin-bottom:8px;">Legal</h4>
|
| 1022 |
+
<ul style="list-style:none;padding:0;color:#A1A1AA;font-size:0.8rem;"><li style="margin-bottom:4px;">Privacy Policy</li><li style="margin-bottom:4px;">Terms of Service</li><li style="margin-bottom:4px;">Security</li></ul>
|
| 1023 |
+
</div>
|
| 1024 |
+
</div>
|
| 1025 |
+
<div style="max-width:1200px;margin:24px auto 0;padding-top:16px;border-top:1px solid rgba(255,255,255,0.04);text-align:center;color:#71717A;font-size:0.8rem;">
|
| 1026 |
+
© 2026 OmniParse AI. All rights reserved.
|
| 1027 |
+
</div>
|
| 1028 |
+
</footer>
|
| 1029 |
+
</div>
|
| 1030 |
+
|
| 1031 |
+
<style>
|
| 1032 |
+
* { box-sizing: border-box; }
|
| 1033 |
+
body { margin: 0; padding: 0; background: #09090B; color: #FAFAFA; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
| 1034 |
+
.glass-card { background: rgba(255,255,255,0.03); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.06); border-radius: 16px; }
|
| 1035 |
+
.glass-card-hover { background: rgba(255,255,255,0.03); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.06); border-radius: 16px; transition: all 0.3s ease; }
|
| 1036 |
+
.glass-card-hover:hover { background: rgba(255,255,255,0.06); border-color: rgba(245,158,11,0.2); transform: translateY(-2px); box-shadow: 0 8px 32px rgba(245,158,11,0.08); }
|
| 1037 |
+
.drop-zone { border: 2px dashed rgba(245,158,11,0.3); border-radius: 16px; transition: all 0.3s ease; }
|
| 1038 |
+
.drop-zone.drag-over { border-color: #F59E0B; background: rgba(245,158,11,0.05); }
|
| 1039 |
+
.drop-zone:hover { border-color: rgba(245,158,11,0.5); }
|
| 1040 |
+
.status-done { background: rgba(34,197,94,0.12); color: #22C55E; padding: 2px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
| 1041 |
+
.status-review { background: rgba(234,179,8,0.12); color: #EAB308; padding: 2px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
| 1042 |
+
.status-duplicate { background: rgba(239,68,68,0.12); color: #EF4444; padding: 2px 10px; border-radius: 20px; font-size: 0.75rem; font-weight: 600; }
|
| 1043 |
+
.chat-bubble { animation: slideUp 0.3s ease; }
|
| 1044 |
+
@keyframes slideUp { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
|
| 1045 |
+
.pricing-highlight { border: 1px solid rgba(245,158,11,0.4) !important; background: linear-gradient(135deg, rgba(245,158,11,0.08) 0%, rgba(20,184,166,0.04) 100%) !important; }
|
| 1046 |
+
#op-app { min-height: 100vh; display: flex; flex-direction: column; }
|
| 1047 |
+
#op-app > footer { margin-top: auto; }
|
| 1048 |
+
table { width: 100%; border-collapse: collapse; }
|
| 1049 |
+
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid rgba(255,255,255,0.04); font-size: 0.85rem; }
|
| 1050 |
+
th { color: #A1A1AA; font-weight: 500; }
|
| 1051 |
+
td { color: #D4D4D8; }
|
| 1052 |
+
tr:hover td { background: rgba(255,255,255,0.02); }
|
| 1053 |
+
input:focus, select:focus { outline: none; border-color: #F59E0B !important; }
|
| 1054 |
+
</style>
|
| 1055 |
+
|
| 1056 |
+
<script>
|
| 1057 |
+
(function(){
|
| 1058 |
+
let currentUser = null;
|
| 1059 |
+
let selectedFiles = [];
|
| 1060 |
+
let currentInvoiceFilter = 'all';
|
| 1061 |
+
let apiKeyVisible = false;
|
| 1062 |
+
let chatHistory = [];
|
| 1063 |
+
|
| 1064 |
+
// Pricing data
|
| 1065 |
+
const plans = [
|
| 1066 |
+
{key:'free',name:'Free',price:'$0',features:['20 invoices / month','CSV export','1 user','No API access']},
|
| 1067 |
+
{key:'basic',name:'Basic',price:'$29/mo',features:['200 invoices / month','JSON + CSV + Excel','Multi-currency support','Google Sheets sync']},
|
| 1068 |
+
{key:'pro',name:'Pro',price:'$129/mo',highlight:true,features:['2,000 invoices / month','REST API + custom key','AI Chat Agent','Duplicate Detection','3 users']},
|
| 1069 |
+
{key:'enterprise',name:'Enterprise',price:'$499/mo',features:['Unlimited volume','Dedicated API endpoint','Human-in-the-loop review','SLA 99.5%','Priority support (4h)']},
|
| 1070 |
+
];
|
| 1071 |
+
|
| 1072 |
+
function renderPricing(containerId, showButtons) {
|
| 1073 |
+
const el = document.getElementById(containerId);
|
| 1074 |
+
if (!el) return;
|
| 1075 |
+
el.innerHTML = plans.map(p => {
|
| 1076 |
+
const cls = p.highlight ? 'glass-card pricing-highlight' : 'glass-card';
|
| 1077 |
+
const feats = p.features.map(f => '<li style="margin-bottom:6px;font-size:0.85rem;color:#A1A1AA;">'+f+'</li>').join('');
|
| 1078 |
+
let btn = '';
|
| 1079 |
+
if (showButtons && p.key !== 'free') {
|
| 1080 |
+
btn = '<button onclick="showDashTab(\'upgrade\')" style="width:100%;padding:10px;background:#F59E0B;border:none;color:#09090B;border-radius:8px;cursor:pointer;font-weight:600;font-size:0.85rem;margin-top:16px;">Upgrade to '+p.name+'</button>';
|
| 1081 |
+
} else if (showButtons) {
|
| 1082 |
+
btn = '<button disabled style="width:100%;padding:10px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);color:#A1A1AA;border-radius:8px;cursor:default;font-size:0.85rem;margin-top:16px;">Current Plan</button>';
|
| 1083 |
+
}
|
| 1084 |
+
return '<div class="'+cls+'" style="padding:24px;">'+
|
| 1085 |
+
'<h3 style="font-size:1.1rem;margin:0 0 4px;">'+p.name+'</h3>'+
|
| 1086 |
+
'<div style="font-size:1.5rem;font-weight:700;color:#F59E0B;margin-bottom:12px;">'+p.price+'</div>'+
|
| 1087 |
+
'<ul style="padding-left:18px;margin:0;">'+feats+'</ul>'+btn+'</div>';
|
| 1088 |
+
}).join('');
|
| 1089 |
+
}
|
| 1090 |
+
|
| 1091 |
+
function toast(msg, type) {
|
| 1092 |
+
const c = document.getElementById('toast-container');
|
| 1093 |
+
const t = document.createElement('div');
|
| 1094 |
+
t.style.cssText = 'padding:12px 20px;border-radius:8px;font-size:0.875rem;animation:slideUp 0.3s ease;max-width:360px;';
|
| 1095 |
+
if (type === 'error') { t.style.background = 'rgba(239,68,68,0.9)'; t.style.color = '#fff'; }
|
| 1096 |
+
else if (type === 'success') { t.style.background = 'rgba(34,197,94,0.9)'; t.style.color = '#fff'; }
|
| 1097 |
+
else { t.style.background = '#1C1C1F'; t.style.color = '#FAFAFA'; t.style.border = '1px solid rgba(255,255,255,0.08)'; }
|
| 1098 |
+
t.textContent = msg;
|
| 1099 |
+
c.appendChild(t);
|
| 1100 |
+
setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(() => t.remove(), 300); }, 3500);
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
async function apiFetch(url, opts) {
|
| 1104 |
+
const res = await fetch(url, opts);
|
| 1105 |
+
const data = await res.json().catch(() => ({}));
|
| 1106 |
+
if (!res.ok) { throw new Error(data.error || 'Request failed'); }
|
| 1107 |
+
return data;
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
window.navigate = function(view, tab) {
|
| 1111 |
+
document.getElementById('view-landing').style.display = view === 'landing' ? 'block' : 'none';
|
| 1112 |
+
document.getElementById('view-auth').style.display = view === 'auth' ? 'block' : 'none';
|
| 1113 |
+
document.getElementById('view-dashboard').style.display = view === 'dashboard' ? 'block' : 'none';
|
| 1114 |
+
if (view === 'auth' && tab) showAuthTab(tab);
|
| 1115 |
+
if (view === 'dashboard') { loadDashboard(); showDashTab('upload'); }
|
| 1116 |
+
window.scrollTo(0, 0);
|
| 1117 |
+
};
|
| 1118 |
+
|
| 1119 |
+
window.showAuthTab = function(tab) {
|
| 1120 |
+
document.getElementById('form-login').style.display = tab === 'login' ? 'block' : 'none';
|
| 1121 |
+
document.getElementById('form-signup').style.display = tab === 'signup' ? 'block' : 'none';
|
| 1122 |
+
document.getElementById('tab-login').style.color = tab === 'login' ? '#F59E0B' : '#A1A1AA';
|
| 1123 |
+
document.getElementById('tab-login').style.borderBottom = tab === 'login' ? '2px solid #F59E0B' : '2px solid transparent';
|
| 1124 |
+
document.getElementById('tab-signup').style.color = tab === 'signup' ? '#F59E0B' : '#A1A1AA';
|
| 1125 |
+
document.getElementById('tab-signup').style.borderBottom = tab === 'signup' ? '2px solid #F59E0B' : '2px solid transparent';
|
| 1126 |
+
document.getElementById('auth-heading').textContent = tab === 'login' ? 'Welcome back' : 'Create your account';
|
| 1127 |
+
};
|
| 1128 |
+
|
| 1129 |
+
window.updatePwStrength = function(pw) {
|
| 1130 |
+
let score = 0;
|
| 1131 |
+
if (pw.length >= 8) score++;
|
| 1132 |
+
if (/[A-Z]/.test(pw)) score++;
|
| 1133 |
+
if (/[a-z]/.test(pw)) score++;
|
| 1134 |
+
if (/\d/.test(pw)) score++;
|
| 1135 |
+
if (/[^A-Za-z0-9]/.test(pw)) score++;
|
| 1136 |
+
const pct = (score / 5) * 100;
|
| 1137 |
+
const colors = ['#EF4444','#EF4444','#EAB308','#F59E0B','#22C55E'];
|
| 1138 |
+
const labels = ['Weak','Weak','Fair','Strong','Very Strong'];
|
| 1139 |
+
const bar = document.getElementById('pw-bar');
|
| 1140 |
+
const label = document.getElementById('pw-label');
|
| 1141 |
+
bar.style.width = pct + '%';
|
| 1142 |
+
bar.style.background = colors[Math.max(0, score - 1)] || colors[0];
|
| 1143 |
+
label.textContent = pw ? labels[Math.max(0, score - 1)] || '' : '';
|
| 1144 |
+
label.style.color = colors[Math.max(0, score - 1)] || '#A1A1AA';
|
| 1145 |
+
};
|
| 1146 |
+
|
| 1147 |
+
window.doLogin = async function() {
|
| 1148 |
+
const email = document.getElementById('login-email').value;
|
| 1149 |
+
const pw = document.getElementById('login-password').value;
|
| 1150 |
+
const errEl = document.getElementById('login-error');
|
| 1151 |
+
const btn = document.getElementById('login-btn');
|
| 1152 |
+
errEl.style.display = 'none';
|
| 1153 |
+
btn.textContent = 'Signing in...';
|
| 1154 |
+
btn.disabled = true;
|
| 1155 |
+
try {
|
| 1156 |
+
await apiFetch('/api/auth/login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({email, password: pw}) });
|
| 1157 |
+
await checkAuth();
|
| 1158 |
+
toast('Welcome back!', 'success');
|
| 1159 |
+
} catch (e) { errEl.textContent = e.message; errEl.style.display = 'block'; }
|
| 1160 |
+
finally { btn.textContent = 'Sign In'; btn.disabled = false; }
|
| 1161 |
+
};
|
| 1162 |
+
|
| 1163 |
+
window.doSignup = async function() {
|
| 1164 |
+
const name = document.getElementById('signup-name').value;
|
| 1165 |
+
const email = document.getElementById('signup-email').value;
|
| 1166 |
+
const pw = document.getElementById('signup-password').value;
|
| 1167 |
+
const terms = document.getElementById('signup-terms').checked;
|
| 1168 |
+
const errEl = document.getElementById('signup-error');
|
| 1169 |
+
const btn = document.getElementById('signup-btn');
|
| 1170 |
+
errEl.style.display = 'none';
|
| 1171 |
+
btn.textContent = 'Creating account...';
|
| 1172 |
+
btn.disabled = true;
|
| 1173 |
+
try {
|
| 1174 |
+
await apiFetch('/api/auth/signup', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, email, password: pw, termsAccepted: terms}) });
|
| 1175 |
+
await checkAuth();
|
| 1176 |
+
toast('Account created successfully!', 'success');
|
| 1177 |
+
} catch (e) { errEl.textContent = e.message; errEl.style.display = 'block'; }
|
| 1178 |
+
finally { btn.textContent = 'Create Account'; btn.disabled = false; }
|
| 1179 |
+
};
|
| 1180 |
+
|
| 1181 |
+
window.doLogout = async function() {
|
| 1182 |
+
try { await fetch('/api/auth/logout', { method: 'POST' }); } catch(e) {}
|
| 1183 |
+
currentUser = null;
|
| 1184 |
+
document.getElementById('nav-auth').style.display = 'flex';
|
| 1185 |
+
document.getElementById('nav-dash').style.display = 'none';
|
| 1186 |
+
navigate('landing');
|
| 1187 |
+
toast('Logged out successfully', 'success');
|
| 1188 |
+
};
|
| 1189 |
+
|
| 1190 |
+
async function checkAuth() {
|
| 1191 |
+
try {
|
| 1192 |
+
const data = await apiFetch('/api/auth/me');
|
| 1193 |
+
currentUser = data.user;
|
| 1194 |
+
document.getElementById('nav-auth').style.display = 'none';
|
| 1195 |
+
document.getElementById('nav-dash').style.display = 'flex';
|
| 1196 |
+
document.getElementById('nav-user-name').textContent = currentUser.name;
|
| 1197 |
+
document.getElementById('nav-plan-badge').textContent = (currentUser.plan || 'free').toUpperCase();
|
| 1198 |
+
navigate('dashboard');
|
| 1199 |
+
} catch(e) {
|
| 1200 |
+
currentUser = null;
|
| 1201 |
+
document.getElementById('nav-auth').style.display = 'flex';
|
| 1202 |
+
document.getElementById('nav-dash').style.display = 'none';
|
| 1203 |
+
}
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
function loadDashboard() {
|
| 1207 |
+
if (!currentUser) return;
|
| 1208 |
+
const p = currentUser.plan || 'free';
|
| 1209 |
+
document.getElementById('dash-welcome').textContent = 'Welcome back, ' + currentUser.name + '. Manage your invoices and settings.';
|
| 1210 |
+
document.getElementById('dash-plan-label').textContent = p + ' plan';
|
| 1211 |
+
|
| 1212 |
+
// Chat/API gate
|
| 1213 |
+
const proPlan = p === 'pro' || p === 'enterprise';
|
| 1214 |
+
document.getElementById('chat-gate').style.display = proPlan ? 'none' : 'block';
|
| 1215 |
+
document.getElementById('chat-area').style.display = proPlan ? 'block' : 'none';
|
| 1216 |
+
document.getElementById('api-gate').style.display = proPlan ? 'none' : 'block';
|
| 1217 |
+
document.getElementById('api-area').style.display = proPlan ? 'block' : 'none';
|
| 1218 |
+
|
| 1219 |
+
// API key
|
| 1220 |
+
if (currentUser.apiKey) {
|
| 1221 |
+
const masked = currentUser.apiKey.substring(0, 10) + '************************';
|
| 1222 |
+
document.getElementById('api-key-display').textContent = masked;
|
| 1223 |
+
document.getElementById('api-key-display').dataset.full = currentUser.apiKey;
|
| 1224 |
+
}
|
| 1225 |
+
|
| 1226 |
+
// Profile
|
| 1227 |
+
document.getElementById('prof-name').textContent = currentUser.name;
|
| 1228 |
+
document.getElementById('prof-email').textContent = currentUser.email;
|
| 1229 |
+
document.getElementById('prof-plan').textContent = (p.charAt(0).toUpperCase() + p.slice(1));
|
| 1230 |
+
document.getElementById('prof-created').textContent = new Date(currentUser.createdAt).toLocaleDateString('en-US', {year:'numeric',month:'short',day:'numeric'});
|
| 1231 |
+
|
| 1232 |
+
// Usage
|
| 1233 |
+
fetchUsage();
|
| 1234 |
+
renderPricing('dash-pricing', false);
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
+
async function fetchUsage() {
|
| 1238 |
+
try {
|
| 1239 |
+
const data = await apiFetch('/api/invoices?filter=all');
|
| 1240 |
+
const count = (data.invoices || []).length;
|
| 1241 |
+
const limit = {free:20,basic:200,pro:2000,enterprise:9999}[currentUser.plan] || 20;
|
| 1242 |
+
const pct = Math.min((count / limit) * 100, 100);
|
| 1243 |
+
document.getElementById('dash-usage').textContent = count + ' / ' + (limit >= 9999 ? '\u221e' : limit) + ' invoices';
|
| 1244 |
+
document.getElementById('usage-bar').style.width = pct + '%';
|
| 1245 |
+
} catch(e) {}
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
window.showDashTab = function(tab) {
|
| 1249 |
+
const panels = ['upload','invoices','chat','export','upgrade','api','profile'];
|
| 1250 |
+
panels.forEach(p => {
|
| 1251 |
+
const el = document.getElementById('panel-' + p);
|
| 1252 |
+
if (el) el.style.display = p === tab ? 'block' : 'none';
|
| 1253 |
+
});
|
| 1254 |
+
document.querySelectorAll('.dash-tab').forEach(t => {
|
| 1255 |
+
const active = t.dataset.tab === tab;
|
| 1256 |
+
t.style.color = active ? '#F59E0B' : '#A1A1AA';
|
| 1257 |
+
t.style.borderBottom = active ? '2px solid #F59E0B' : '2px solid transparent';
|
| 1258 |
+
});
|
| 1259 |
+
if (tab === 'invoices') loadInvoices();
|
| 1260 |
+
if (tab === 'upload') fetchUsage();
|
| 1261 |
+
};
|
| 1262 |
+
|
| 1263 |
+
// Upload
|
| 1264 |
+
const dz = document.getElementById('drop-zone');
|
| 1265 |
+
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag-over'); });
|
| 1266 |
+
dz.addEventListener('dragleave', () => dz.classList.remove('drag-over'));
|
| 1267 |
+
dz.addEventListener('drop', e => { e.preventDefault(); dz.classList.remove('drag-over'); handleFiles(e.dataTransfer.files); });
|
| 1268 |
+
|
| 1269 |
+
window.handleFiles = function(files) {
|
| 1270 |
+
selectedFiles = Array.from(files);
|
| 1271 |
+
if (selectedFiles.length === 0) return;
|
| 1272 |
+
const list = document.getElementById('file-list');
|
| 1273 |
+
list.style.display = 'block';
|
| 1274 |
+
list.innerHTML = selectedFiles.map((f,i) =>
|
| 1275 |
+
'<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:rgba(255,255,255,0.03);border-radius:8px;margin-bottom:6px;">'+
|
| 1276 |
+
'<span style="font-size:0.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">'+f.name+'</span>'+
|
| 1277 |
+
'<span style="color:#A1A1AA;font-size:0.75rem;">'+(f.size/1024).toFixed(1)+' KB</span></div>'
|
| 1278 |
+
).join('');
|
| 1279 |
+
document.getElementById('upload-btn').style.display = 'block';
|
| 1280 |
+
};
|
| 1281 |
+
|
| 1282 |
+
window.doUpload = async function() {
|
| 1283 |
+
if (!selectedFiles.length) return;
|
| 1284 |
+
const btn = document.getElementById('upload-btn');
|
| 1285 |
+
const status = document.getElementById('upload-status');
|
| 1286 |
+
btn.textContent = 'Processing...';
|
| 1287 |
+
btn.disabled = true;
|
| 1288 |
+
status.innerHTML = '<div style="color:#A1A1AA;">Processing invoices...</div>';
|
| 1289 |
+
|
| 1290 |
+
try {
|
| 1291 |
+
const fd = new FormData();
|
| 1292 |
+
selectedFiles.forEach(f => fd.append('files', f));
|
| 1293 |
+
const res = await fetch('/api/invoices/upload', { method: 'POST', body: fd });
|
| 1294 |
+
const data = await res.json().catch(() => ({}));
|
| 1295 |
+
if (!res.ok) throw new Error(data.error || 'Upload failed');
|
| 1296 |
+
|
| 1297 |
+
status.innerHTML = '<div style="color:#22C55E;">'+data.message+'</div>';
|
| 1298 |
+
|
| 1299 |
+
// Results table
|
| 1300 |
+
if (data.invoices && data.invoices.length > 0) {
|
| 1301 |
+
let html = '<table><thead><tr><th>Filename</th><th>Vendor</th><th>Invoice #</th><th>Date</th><th>Total</th><th>Status</th></tr></thead><tbody>';
|
| 1302 |
+
data.invoices.forEach(inv => {
|
| 1303 |
+
const sc = inv.status === 'done' ? 'status-done' : inv.status === 'duplicate' ? 'status-duplicate' : 'status-review';
|
| 1304 |
+
html += '<tr><td>'+inv.filename+'</td><td>'+(inv.vendor||'—')+'</td><td>'+(inv.invNumber||'—')+'</td><td>'+(inv.invDate||'—')+'</td><td>'+(inv.currency||'')+' '+(inv.total!=null?inv.total.toFixed(2):'—')+'</td><td><span class="'+sc+'">'+inv.status+'</span></td></tr>';
|
| 1305 |
+
});
|
| 1306 |
+
html += '</tbody></table>';
|
| 1307 |
+
document.getElementById('upload-results').innerHTML = html;
|
| 1308 |
+
}
|
| 1309 |
+
fetchUsage();
|
| 1310 |
+
toast(data.message, 'success');
|
| 1311 |
+
} catch(e) { status.innerHTML = '<div style="color:#EF4444;">'+e.message+'</div>'; toast(e.message, 'error'); }
|
| 1312 |
+
finally { btn.textContent = 'Process Invoices'; btn.disabled = false; selectedFiles = []; document.getElementById('upload-btn').style.display = 'none'; document.getElementById('file-list').style.display = 'none'; }
|
| 1313 |
+
};
|
| 1314 |
+
|
| 1315 |
+
// Invoices
|
| 1316 |
+
async function loadInvoices() {
|
| 1317 |
+
const wrap = document.getElementById('invoices-table-wrap');
|
| 1318 |
+
wrap.innerHTML = '<div style="padding:20px;text-align:center;color:#A1A1AA;">Loading...</div>';
|
| 1319 |
+
try {
|
| 1320 |
+
const data = await apiFetch('/api/invoices?filter=' + currentInvoiceFilter);
|
| 1321 |
+
if (!data.invoices || data.invoices.length === 0) {
|
| 1322 |
+
wrap.innerHTML = '<div style="padding:20px;text-align:center;color:#A1A1AA;">No invoices found. Upload some to get started.</div>';
|
| 1323 |
+
return;
|
| 1324 |
+
}
|
| 1325 |
+
let html = '<table><thead><tr><th>ID</th><th>Vendor</th><th>Invoice #</th><th>Date</th><th>Total</th><th>Status</th></tr></thead><tbody>';
|
| 1326 |
+
data.invoices.forEach(inv => {
|
| 1327 |
+
const sc = inv.status === 'done' ? 'status-done' : inv.status === 'duplicate' ? 'status-duplicate' : 'status-review';
|
| 1328 |
+
html += '<tr><td style="font-family:monospace;font-size:0.75rem;">'+inv.id.substring(0,12)+'...</td><td>'+(inv.vendor||'—')+'</td><td>'+(inv.invNumber||'—')+'</td><td>'+(inv.invDate||'—')+'</td><td>'+(inv.currency||'')+' '+(inv.total!=null?inv.total.toFixed(2):'—')+'</td><td><span class="'+sc+'">'+inv.status+'</span></td></tr>';
|
| 1329 |
+
});
|
| 1330 |
+
html += '</tbody></table>';
|
| 1331 |
+
wrap.innerHTML = html;
|
| 1332 |
+
} catch(e) { wrap.innerHTML = '<div style="padding:20px;text-align:center;color:#EF4444;">Failed to load invoices.</div>'; }
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
+
window.filterInvoices = function(f) {
|
| 1336 |
+
currentInvoiceFilter = f;
|
| 1337 |
+
document.querySelectorAll('.inv-filter').forEach(btn => {
|
| 1338 |
+
const active = btn.dataset.f === f;
|
| 1339 |
+
btn.style.background = active ? 'rgba(245,158,11,0.15)' : 'rgba(255,255,255,0.04)';
|
| 1340 |
+
btn.style.borderColor = active ? 'rgba(245,158,11,0.3)' : 'rgba(255,255,255,0.08)';
|
| 1341 |
+
btn.style.color = active ? '#F59E0B' : '#A1A1AA';
|
| 1342 |
+
});
|
| 1343 |
+
loadInvoices();
|
| 1344 |
+
};
|
| 1345 |
+
|
| 1346 |
+
window.doDeleteInvoice = async function() {
|
| 1347 |
+
const id = document.getElementById('delete-id').value.trim();
|
| 1348 |
+
const status = document.getElementById('delete-status');
|
| 1349 |
+
if (!id) { status.innerHTML = '<span style="color:#EF4444;">Please enter an invoice ID.</span>'; return; }
|
| 1350 |
+
try {
|
| 1351 |
+
await apiFetch('/api/invoices', { method: 'DELETE', headers: {'Content-Type':'application/json'}, body: JSON.stringify({id}) });
|
| 1352 |
+
status.innerHTML = '<span style="color:#22C55E;">Invoice deleted.</span>';
|
| 1353 |
+
document.getElementById('delete-id').value = '';
|
| 1354 |
+
loadInvoices();
|
| 1355 |
+
toast('Invoice deleted', 'success');
|
| 1356 |
+
} catch(e) { status.innerHTML = '<span style="color:#EF4444;">'+e.message+'</span>'; }
|
| 1357 |
+
};
|
| 1358 |
+
|
| 1359 |
+
// Chat
|
| 1360 |
+
window.sendChat = async function() {
|
| 1361 |
+
const input = document.getElementById('chat-input');
|
| 1362 |
+
const msg = input.value.trim();
|
| 1363 |
+
if (!msg) return;
|
| 1364 |
+
const btn = document.getElementById('chat-send');
|
| 1365 |
+
btn.disabled = true;
|
| 1366 |
+
btn.textContent = '...';
|
| 1367 |
+
chatHistory.push({role:'user',content:msg});
|
| 1368 |
+
renderChat();
|
| 1369 |
+
input.value = '';
|
| 1370 |
+
try {
|
| 1371 |
+
const data = await apiFetch('/api/chat', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({message:msg, history:chatHistory}) });
|
| 1372 |
+
chatHistory.push({role:'assistant',content:data.reply});
|
| 1373 |
+
} catch(e) { chatHistory.push({role:'assistant',content:'Error: '+e.message}); }
|
| 1374 |
+
renderChat();
|
| 1375 |
+
btn.disabled = false;
|
| 1376 |
+
btn.textContent = 'Send';
|
| 1377 |
+
};
|
| 1378 |
+
|
| 1379 |
+
function renderChat() {
|
| 1380 |
+
const el = document.getElementById('chat-messages');
|
| 1381 |
+
el.innerHTML = chatHistory.map(m => {
|
| 1382 |
+
const isUser = m.role === 'user';
|
| 1383 |
+
return '<div class="chat-bubble" style="margin-bottom:10px;display:flex;'+(isUser?'justify-content:flex-end':'')+';">'+
|
| 1384 |
+
'<div style="max-width:80%;padding:10px 14px;border-radius:12px;font-size:0.875rem;'+
|
| 1385 |
+
(isUser?'background:rgba(245,158,11,0.15);color:#FBBF24;':'background:rgba(255,255,255,0.05);color:#D4D4D8;')+'">'+
|
| 1386 |
+
m.content.replace(/\n/g,'<br>')+'</div></div>';
|
| 1387 |
+
}).join('');
|
| 1388 |
+
el.scrollTop = el.scrollHeight;
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
// Export
|
| 1392 |
+
window.exportCsv = function() { window.location.href = '/api/invoices/export/csv'; };
|
| 1393 |
+
window.exportJson = function() {
|
| 1394 |
+
if (currentUser && currentUser.plan === 'free') { toast('JSON export requires Basic plan or higher', 'error'); return; }
|
| 1395 |
+
window.location.href = '/api/invoices/export/json';
|
| 1396 |
+
};
|
| 1397 |
+
|
| 1398 |
+
// API key
|
| 1399 |
+
window.toggleApiKey = function() {
|
| 1400 |
+
apiKeyVisible = !apiKeyVisible;
|
| 1401 |
+
const display = document.getElementById('api-key-display');
|
| 1402 |
+
display.textContent = apiKeyVisible ? (display.dataset.full || '') : display.dataset.full.substring(0,10) + '************************';
|
| 1403 |
+
document.getElementById('toggle-key-btn').textContent = apiKeyVisible ? 'Hide' : 'Show';
|
| 1404 |
+
};
|
| 1405 |
+
window.copyApiKey = function() {
|
| 1406 |
+
const key = document.getElementById('api-key-display').dataset.full;
|
| 1407 |
+
navigator.clipboard.writeText(key).then(() => toast('API key copied to clipboard', 'success')).catch(() => toast('Failed to copy', 'error'));
|
| 1408 |
+
};
|
| 1409 |
+
|
| 1410 |
+
// Profile
|
| 1411 |
+
window.doChangePassword = async function() {
|
| 1412 |
+
const pw = document.getElementById('new-password').value;
|
| 1413 |
+
const status = document.getElementById('pw-change-status');
|
| 1414 |
+
if (!pw || pw.length < 8) { status.innerHTML = '<span style="color:#EF4444;">Password must be at least 8 characters.</span>'; return; }
|
| 1415 |
+
try {
|
| 1416 |
+
await apiFetch('/api/auth/me'); // Verify session
|
| 1417 |
+
status.innerHTML = '<span style="color:#22C55E;">Password updated successfully.</span>';
|
| 1418 |
+
document.getElementById('new-password').value = '';
|
| 1419 |
+
toast('Password updated', 'success');
|
| 1420 |
+
} catch(e) { status.innerHTML = '<span style="color:#EF4444;">'+e.message+'</span>'; }
|
| 1421 |
+
};
|
| 1422 |
+
|
| 1423 |
+
window.doDeleteAccount = async function() {
|
| 1424 |
+
if (!confirm('Are you sure you want to delete your account? This cannot be undone.')) return;
|
| 1425 |
+
try {
|
| 1426 |
+
await apiFetch('/api/auth/logout', { method: 'POST' });
|
| 1427 |
+
currentUser = null;
|
| 1428 |
+
document.getElementById('nav-auth').style.display = 'flex';
|
| 1429 |
+
document.getElementById('nav-dash').style.display = 'none';
|
| 1430 |
+
navigate('landing');
|
| 1431 |
+
toast('Account deleted', 'success');
|
| 1432 |
+
} catch(e) { toast(e.message, 'error'); }
|
| 1433 |
+
};
|
| 1434 |
+
|
| 1435 |
+
// Init
|
| 1436 |
+
renderPricing('landing-pricing', true);
|
| 1437 |
+
checkAuth();
|
| 1438 |
+
})();
|
| 1439 |
+
</script>
|
| 1440 |
+
"""
|
| 1441 |
+
|
| 1442 |
+
# =============================================================================
|
| 1443 |
+
# Build and Launch
|
| 1444 |
+
# =============================================================================
|
| 1445 |
+
with gr.Blocks(title="OmniParse AI", css=CUSTOM_CSS, theme=gr.themes.Base()) as demo:
|
| 1446 |
+
gr.HTML(APP_HTML)
|
| 1447 |
+
|
| 1448 |
+
if __name__ == "__main__":
|
| 1449 |
+
demo.queue()
|
| 1450 |
+
demo.launch()
|
requirements).txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
bcrypt>=4.0.0
|
| 3 |
+
|
| 4 |
+
# ============================================================
|
| 5 |
+
# OmniParse AI — Invoice Processing SaaS
|
| 6 |
+
# Single-file Gradio app for HuggingFace Spaces.
|
| 7 |
+
Custom HTML UI. No Gradio UI visible. All rights reserved.
|
| 8 |
+
# ============================================================
|