simikkk commited on
Commit
12a1cdc
·
verified ·
1 Parent(s): db3789c

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile.txt +15 -0
  2. app.py +1450 -0
  3. 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 &middot; &lt;4s per invoice &middot; 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 &lt;your-api-key&gt;" \
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
+ &copy; 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
+ # ============================================================