simikkk commited on
Commit
f440d27
·
verified ·
1 Parent(s): 14a9fcd

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +102 -8
  2. app.py +1273 -0
  3. packages.txt +3 -0
  4. requirements.txt +9 -0
README.md CHANGED
@@ -1,13 +1,107 @@
1
  ---
2
- title: Test
3
- emoji: 🏆
4
- colorFrom: red
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: OmniParse AI
3
+ emoji:
4
+ colorFrom: purple
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.49.1
 
8
  app_file: app.py
9
+ pinned: true
10
+ license: mit
11
  ---
12
 
13
+ # OmniParse AI Invoice Processing SaaS
14
+
15
+ B2B SaaS pro automatické zpracování faktur. Nahraješ PDF/obrázek faktury, AI vytáhne
16
+ vendor, čísla faktur, data, částky a DPH, a exportuješ do CSV/JSON/Excel nebo napojíš
17
+ přes REST API.
18
+
19
+ ## Jak to spustit
20
+
21
+ 1. Nahraj `app.py`, `requirements.txt`, `packages.txt` a tento `README.md` do HuggingFace Space
22
+ se SDK **Gradio**.
23
+ 2. V **Settings → Variables and Secrets** nastav secrets podle tabulky níže.
24
+ 3. Pin Space (⋯ → Pin this Space), ať nespí.
25
+
26
+ ### Secrets
27
+
28
+ | Secret | Popis | Povinné? |
29
+ |---|---|---|
30
+ | `HF_TOKEN` | HuggingFace token pro fallback AI (Mistral-7B) | Doporučeno |
31
+ | `GROQ_API_KEY` | Primární AI extrakce (Llama 3.1 přes Groq) | Ano |
32
+ | `SUPABASE_URL` | URL Supabase projektu | Ano (jinak SQLite fallback) |
33
+ | `SUPABASE_KEY` | anon public key Supabase | Ano (jinak SQLite fallback) |
34
+ | `GOOGLE_VISION_KEY` | Google Cloud Vision OCR | Volitelné |
35
+ | `STRIPE_SECRET_KEY` | Stripe secret key (test/live) | Pro platby |
36
+ | `STRIPE_PRICE_BASIC` | Stripe Price ID pro Basic plán | Pro platby |
37
+ | `STRIPE_PRICE_PRO` | Stripe Price ID pro Pro plán | Pro platby |
38
+ | `STRIPE_PRICE_ENTERPRISE` | Stripe Price ID pro Enterprise plán | Pro platby |
39
+ | `APP_URL` | Veřejná URL Space (pro Stripe redirect) | Pro platby |
40
+
41
+ Aplikace se **nikdy nezhroutí** kvůli chybějícímu secretu — každá vrstva má fallback:
42
+
43
+ - Bez `GROQ_API_KEY` → zkusí HF Inference API → pak regex parser.
44
+ - Bez `SUPABASE_URL`/`KEY` → přepne na lokální SQLite (`omniparse.db`), data nejsou
45
+ perzistentní mezi restarty Space.
46
+ - Bez `GOOGLE_VISION_KEY` → jen Tesseract OCR.
47
+ - Bez `STRIPE_SECRET_KEY` → tlačítko Upgrade zobrazí instrukce místo Checkout linku.
48
+
49
+ ### Supabase schéma
50
+
51
+ Pokud používáš Supabase, spusť v **SQL Editor** tento skript před prvním spuštěním appky:
52
+
53
+ ```sql
54
+ CREATE TABLE users (
55
+ id BIGSERIAL PRIMARY KEY,
56
+ email TEXT UNIQUE NOT NULL,
57
+ name TEXT NOT NULL,
58
+ password TEXT NOT NULL,
59
+ plan TEXT NOT NULL DEFAULT 'free',
60
+ stripe_cid TEXT,
61
+ api_key TEXT,
62
+ created_at TIMESTAMPTZ DEFAULT NOW()
63
+ );
64
+
65
+ CREATE TABLE sessions (
66
+ token TEXT PRIMARY KEY,
67
+ user_id BIGINT NOT NULL REFERENCES users(id),
68
+ expires_at TIMESTAMPTZ NOT NULL
69
+ );
70
+
71
+ CREATE TABLE invoices (
72
+ id BIGSERIAL PRIMARY KEY,
73
+ user_id BIGINT NOT NULL REFERENCES users(id),
74
+ filename TEXT NOT NULL,
75
+ vendor TEXT,
76
+ inv_number TEXT,
77
+ inv_date TEXT,
78
+ due_date TEXT,
79
+ amount NUMERIC(12,2),
80
+ vat_amount NUMERIC(12,2),
81
+ total NUMERIC(12,2),
82
+ currency TEXT DEFAULT 'USD',
83
+ status TEXT DEFAULT 'done',
84
+ is_duplicate BOOLEAN DEFAULT FALSE,
85
+ confidence NUMERIC(4,3),
86
+ raw_json JSONB,
87
+ created_at TIMESTAMPTZ DEFAULT NOW()
88
+ );
89
+
90
+ CREATE INDEX idx_invoices_user_month ON invoices(user_id, created_at);
91
+ ```
92
+
93
+ ### Demo účet
94
+
95
+ Při prvním startu se automaticky vytvoří:
96
+
97
+ - Email: `demo@omniparse.ai`
98
+ - Heslo: `demo1234`
99
+ - Plán: Pro
100
+
101
+ ## Poznámky k platbám
102
+
103
+ Stripe integrace zde funguje **bez webhooků** — po návratu ze Stripe Checkout uživatel
104
+ v záložce "⚡ Upgrade" vloží `session_id` z URL a klikne "Ověřit platbu", což zavolá
105
+ `stripe.checkout.Session.retrieve()` a upgraduje plán v DB. Pro produkční nasazení
106
+ doporučujeme doplnit skutečný webhook endpoint, tahle verze je zjednodušená podle
107
+ původního zadání ("bez webhooků").
app.py ADDED
@@ -0,0 +1,1273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OmniParse AI — Invoice processing SaaS
3
+ Single-file Gradio app for HuggingFace Spaces.
4
+
5
+ Views: Landing / Pricing / Legal / Auth / Dashboard
6
+ All AI/OCR/DB layers degrade gracefully if a given secret/service is missing.
7
+ """
8
+
9
+ import os
10
+ import re
11
+ import io
12
+ import json
13
+ import time
14
+ import base64
15
+ import hashlib
16
+ import secrets
17
+ import sqlite3
18
+ from datetime import datetime, timedelta, timezone
19
+
20
+ import requests
21
+ import gradio as gr
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Optional dependencies — everything degrades gracefully if missing
25
+ # ---------------------------------------------------------------------------
26
+ try:
27
+ from PIL import Image
28
+ except ImportError:
29
+ Image = None
30
+
31
+ try:
32
+ import pytesseract
33
+ except ImportError:
34
+ pytesseract = None
35
+
36
+ try:
37
+ from pdf2image import convert_from_path
38
+ except ImportError:
39
+ convert_from_path = None
40
+
41
+ try:
42
+ import stripe as stripe_sdk
43
+ except ImportError:
44
+ stripe_sdk = None
45
+
46
+ try:
47
+ from groq import Groq
48
+ except ImportError:
49
+ Groq = None
50
+
51
+ try:
52
+ from supabase import create_client as supabase_create_client
53
+ except ImportError:
54
+ supabase_create_client = None
55
+
56
+ # ---------------------------------------------------------------------------
57
+ # Config / secrets
58
+ # ---------------------------------------------------------------------------
59
+ HF_TOKEN = os.environ.get("HF_TOKEN")
60
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
61
+ SUPABASE_URL = os.environ.get("SUPABASE_URL")
62
+ SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
63
+ GOOGLE_VISION_KEY = os.environ.get("GOOGLE_VISION_KEY")
64
+ STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY")
65
+ STRIPE_PRICE_BASIC = os.environ.get("STRIPE_PRICE_BASIC")
66
+ STRIPE_PRICE_PRO = os.environ.get("STRIPE_PRICE_PRO")
67
+ STRIPE_PRICE_ENTERPRISE = os.environ.get("STRIPE_PRICE_ENTERPRISE")
68
+ APP_URL = os.environ.get("APP_URL", "http://localhost:7860")
69
+
70
+ if stripe_sdk and STRIPE_SECRET_KEY:
71
+ stripe_sdk.api_key = STRIPE_SECRET_KEY
72
+
73
+ GROQ_CLIENT = Groq(api_key=GROQ_API_KEY) if (Groq and GROQ_API_KEY) else None
74
+
75
+ PLAN_LIMITS = {"free": 20, "basic": 200, "pro": 2000, "enterprise": float("inf")}
76
+ PLAN_PRICE_IDS = {
77
+ "basic": STRIPE_PRICE_BASIC,
78
+ "pro": STRIPE_PRICE_PRO,
79
+ "enterprise": STRIPE_PRICE_ENTERPRISE,
80
+ }
81
+ PLAN_LABELS = {"free": "Free", "basic": "Basic", "pro": "Pro", "enterprise": "Enterprise"}
82
+ PLAN_PRICES = {"free": 0, "basic": 29, "pro": 129, "enterprise": 499}
83
+
84
+ SESSION_TTL_HOURS = 24 * 7
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Database layer — Supabase if configured, else local SQLite fallback
88
+ # ---------------------------------------------------------------------------
89
+ USE_SUPABASE = bool(SUPABASE_URL and SUPABASE_KEY and supabase_create_client)
90
+ SQLITE_PATH = os.environ.get("SQLITE_PATH", "omniparse.db")
91
+
92
+ sb = None
93
+ if USE_SUPABASE:
94
+ try:
95
+ sb = supabase_create_client(SUPABASE_URL, SUPABASE_KEY)
96
+ except Exception as e:
97
+ print(f"[WARN] Supabase init failed, falling back to SQLite: {e}")
98
+ USE_SUPABASE = False
99
+
100
+
101
+ def _sqlite_conn():
102
+ conn = sqlite3.connect(SQLITE_PATH)
103
+ conn.row_factory = sqlite3.Row
104
+ return conn
105
+
106
+
107
+ def db_init():
108
+ if USE_SUPABASE:
109
+ return # tables are created manually via Supabase SQL editor
110
+ conn = _sqlite_conn()
111
+ c = conn.cursor()
112
+ c.execute("""CREATE TABLE IF NOT EXISTS users (
113
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ email TEXT UNIQUE NOT NULL,
115
+ name TEXT NOT NULL,
116
+ password TEXT NOT NULL,
117
+ plan TEXT DEFAULT 'free',
118
+ stripe_cid TEXT,
119
+ api_key TEXT,
120
+ created_at TEXT
121
+ )""")
122
+ c.execute("""CREATE TABLE IF NOT EXISTS sessions (
123
+ token TEXT PRIMARY KEY,
124
+ user_id INTEGER NOT NULL,
125
+ expires_at TEXT
126
+ )""")
127
+ c.execute("""CREATE TABLE IF NOT EXISTS invoices (
128
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
129
+ user_id INTEGER NOT NULL,
130
+ filename TEXT,
131
+ vendor TEXT,
132
+ inv_number TEXT,
133
+ inv_date TEXT,
134
+ due_date TEXT,
135
+ amount REAL,
136
+ vat_amount REAL,
137
+ total REAL,
138
+ currency TEXT DEFAULT 'USD',
139
+ status TEXT DEFAULT 'done',
140
+ is_duplicate INTEGER DEFAULT 0,
141
+ confidence REAL,
142
+ raw_json TEXT,
143
+ created_at TEXT
144
+ )""")
145
+ conn.commit()
146
+ conn.close()
147
+
148
+
149
+ def hash_pw(pw: str) -> str:
150
+ return hashlib.sha256(pw.encode("utf-8")).hexdigest()
151
+
152
+
153
+ def gen_token() -> str:
154
+ return secrets.token_urlsafe(32)
155
+
156
+
157
+ def gen_api_key() -> str:
158
+ return "op_live_" + secrets.token_hex(20)
159
+
160
+
161
+ # ---- user helpers -----------------------------------------------------
162
+ def create_user(email, name, password, plan="free"):
163
+ email = email.strip().lower()
164
+ api_key = gen_api_key()
165
+ now = datetime.now(timezone.utc).isoformat()
166
+ if USE_SUPABASE:
167
+ existing = sb.table("users").select("id").eq("email", email).execute()
168
+ if existing.data:
169
+ return None, "Účet s tímto emailem už existuje."
170
+ res = sb.table("users").insert({
171
+ "email": email, "name": name, "password": hash_pw(password),
172
+ "plan": plan, "api_key": api_key, "created_at": now,
173
+ }).execute()
174
+ return res.data[0], None
175
+ else:
176
+ conn = _sqlite_conn()
177
+ try:
178
+ cur = conn.execute(
179
+ "INSERT INTO users (email, name, password, plan, api_key, created_at) VALUES (?,?,?,?,?,?)",
180
+ (email, name, hash_pw(password), plan, api_key, now),
181
+ )
182
+ conn.commit()
183
+ uid = cur.lastrowid
184
+ row = conn.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
185
+ return dict(row), None
186
+ except sqlite3.IntegrityError:
187
+ return None, "Účet s tímto emailem už existuje."
188
+ finally:
189
+ conn.close()
190
+
191
+
192
+ def get_user_by_email(email):
193
+ email = email.strip().lower()
194
+ if USE_SUPABASE:
195
+ res = sb.table("users").select("*").eq("email", email).execute()
196
+ return res.data[0] if res.data else None
197
+ else:
198
+ conn = _sqlite_conn()
199
+ row = conn.execute("SELECT * FROM users WHERE email=?", (email,)).fetchone()
200
+ conn.close()
201
+ return dict(row) if row else None
202
+
203
+
204
+ def get_user_by_id(uid):
205
+ if USE_SUPABASE:
206
+ res = sb.table("users").select("*").eq("id", uid).execute()
207
+ return res.data[0] if res.data else None
208
+ else:
209
+ conn = _sqlite_conn()
210
+ row = conn.execute("SELECT * FROM users WHERE id=?", (uid,)).fetchone()
211
+ conn.close()
212
+ return dict(row) if row else None
213
+
214
+
215
+ def update_user(uid, fields: dict):
216
+ if USE_SUPABASE:
217
+ sb.table("users").update(fields).eq("id", uid).execute()
218
+ else:
219
+ conn = _sqlite_conn()
220
+ cols = ", ".join(f"{k}=?" for k in fields)
221
+ conn.execute(f"UPDATE users SET {cols} WHERE id=?", (*fields.values(), uid))
222
+ conn.commit()
223
+ conn.close()
224
+
225
+
226
+ # ---- session helpers ----------------------------------------------------
227
+ def create_session(user_id):
228
+ token = gen_token()
229
+ expires = (datetime.now(timezone.utc) + timedelta(hours=SESSION_TTL_HOURS)).isoformat()
230
+ if USE_SUPABASE:
231
+ sb.table("sessions").insert({"token": token, "user_id": user_id, "expires_at": expires}).execute()
232
+ else:
233
+ conn = _sqlite_conn()
234
+ conn.execute("INSERT INTO sessions (token, user_id, expires_at) VALUES (?,?,?)", (token, user_id, expires))
235
+ conn.commit()
236
+ conn.close()
237
+ return token
238
+
239
+
240
+ def get_session_user(token):
241
+ if not token:
242
+ return None
243
+ if USE_SUPABASE:
244
+ res = sb.table("sessions").select("*").eq("token", token).execute()
245
+ if not res.data:
246
+ return None
247
+ session = res.data[0]
248
+ else:
249
+ conn = _sqlite_conn()
250
+ row = conn.execute("SELECT * FROM sessions WHERE token=?", (token,)).fetchone()
251
+ conn.close()
252
+ if not row:
253
+ return None
254
+ session = dict(row)
255
+ try:
256
+ expires = datetime.fromisoformat(session["expires_at"])
257
+ if expires.tzinfo is None:
258
+ expires = expires.replace(tzinfo=timezone.utc)
259
+ if expires < datetime.now(timezone.utc):
260
+ return None
261
+ except Exception:
262
+ pass
263
+ return get_user_by_id(session["user_id"])
264
+
265
+
266
+ def delete_session(token):
267
+ if not token:
268
+ return
269
+ if USE_SUPABASE:
270
+ sb.table("sessions").delete().eq("token", token).execute()
271
+ else:
272
+ conn = _sqlite_conn()
273
+ conn.execute("DELETE FROM sessions WHERE token=?", (token,))
274
+ conn.commit()
275
+ conn.close()
276
+
277
+
278
+ # ---- invoice helpers ------------------------------------------------------
279
+ def insert_invoice(user_id, data: dict):
280
+ now = datetime.now(timezone.utc).isoformat()
281
+ row = {
282
+ "user_id": user_id,
283
+ "filename": data.get("filename"),
284
+ "vendor": data.get("vendor"),
285
+ "inv_number": data.get("invoice_number"),
286
+ "inv_date": data.get("invoice_date"),
287
+ "due_date": data.get("due_date"),
288
+ "amount": data.get("amount"),
289
+ "vat_amount": data.get("vat_amount"),
290
+ "total": data.get("total"),
291
+ "currency": data.get("currency", "USD"),
292
+ "status": data.get("status", "done"),
293
+ "is_duplicate": data.get("is_duplicate", False),
294
+ "confidence": data.get("confidence"),
295
+ "raw_json": json.dumps(data, ensure_ascii=False),
296
+ "created_at": now,
297
+ }
298
+ if USE_SUPABASE:
299
+ res = sb.table("invoices").insert(row).execute()
300
+ return res.data[0]
301
+ else:
302
+ conn = _sqlite_conn()
303
+ row["is_duplicate"] = int(bool(row["is_duplicate"]))
304
+ cols = ", ".join(row.keys())
305
+ qs = ", ".join("?" for _ in row)
306
+ cur = conn.execute(f"INSERT INTO invoices ({cols}) VALUES ({qs})", tuple(row.values()))
307
+ conn.commit()
308
+ iid = cur.lastrowid
309
+ r = conn.execute("SELECT * FROM invoices WHERE id=?", (iid,)).fetchone()
310
+ conn.close()
311
+ return dict(r)
312
+
313
+
314
+ def get_invoices(user_id):
315
+ if USE_SUPABASE:
316
+ res = sb.table("invoices").select("*").eq("user_id", user_id).order("created_at", desc=True).execute()
317
+ return res.data
318
+ else:
319
+ conn = _sqlite_conn()
320
+ rows = conn.execute(
321
+ "SELECT * FROM invoices WHERE user_id=? ORDER BY created_at DESC", (user_id,)
322
+ ).fetchall()
323
+ conn.close()
324
+ return [dict(r) for r in rows]
325
+
326
+
327
+ def count_invoices_this_month(user_id):
328
+ invoices = get_invoices(user_id)
329
+ now = datetime.now(timezone.utc)
330
+ n = 0
331
+ for inv in invoices:
332
+ try:
333
+ created = datetime.fromisoformat(inv["created_at"])
334
+ if created.year == now.year and created.month == now.month:
335
+ n += 1
336
+ except Exception:
337
+ pass
338
+ return n
339
+
340
+
341
+ def check_duplicate(user_id, vendor, total):
342
+ if not vendor or total is None:
343
+ return False
344
+ invoices = get_invoices(user_id)
345
+ now = datetime.now(timezone.utc)
346
+ for inv in invoices:
347
+ try:
348
+ created = datetime.fromisoformat(inv["created_at"])
349
+ except Exception:
350
+ continue
351
+ if created.year == now.year and created.month == now.month:
352
+ if (inv.get("vendor") or "").strip().lower() == vendor.strip().lower():
353
+ if inv.get("total") is not None and abs(float(inv["total"]) - float(total)) < 0.01:
354
+ return True
355
+ return False
356
+
357
+
358
+ def delete_invoice(user_id, invoice_id):
359
+ if USE_SUPABASE:
360
+ sb.table("invoices").delete().eq("id", invoice_id).eq("user_id", user_id).execute()
361
+ else:
362
+ conn = _sqlite_conn()
363
+ conn.execute("DELETE FROM invoices WHERE id=? AND user_id=?", (invoice_id, user_id))
364
+ conn.commit()
365
+ conn.close()
366
+
367
+
368
+ # ---------------------------------------------------------------------------
369
+ # AI / OCR pipeline
370
+ # ---------------------------------------------------------------------------
371
+ def ocr_google_vision(image: "Image.Image") -> str:
372
+ if not GOOGLE_VISION_KEY:
373
+ return ""
374
+ try:
375
+ buf = io.BytesIO()
376
+ image.save(buf, format="PNG")
377
+ b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
378
+ url = f"https://vision.googleapis.com/v1/images:annotate?key={GOOGLE_VISION_KEY}"
379
+ payload = {"requests": [{"image": {"content": b64}, "features": [{"type": "TEXT_DETECTION"}]}]}
380
+ r = requests.post(url, json=payload, timeout=15)
381
+ r.raise_for_status()
382
+ data = r.json()
383
+ text = data["responses"][0].get("fullTextAnnotation", {}).get("text", "")
384
+ return text
385
+ except Exception as e:
386
+ print(f"[WARN] Google Vision OCR failed: {e}")
387
+ return ""
388
+
389
+
390
+ def ocr_tesseract(image: "Image.Image") -> str:
391
+ if not pytesseract:
392
+ return ""
393
+ try:
394
+ gray = image.convert("L")
395
+ return pytesseract.image_to_string(gray, lang="eng")
396
+ except Exception as e:
397
+ print(f"[WARN] Tesseract OCR failed: {e}")
398
+ return ""
399
+
400
+
401
+ def run_ocr(image: "Image.Image") -> str:
402
+ text = ocr_tesseract(image)
403
+ if len(text.strip()) < 100 and GOOGLE_VISION_KEY:
404
+ vision_text = ocr_google_vision(image)
405
+ if len(vision_text.strip()) > len(text.strip()):
406
+ text = vision_text
407
+ return text
408
+
409
+
410
+ EXTRACTION_SYSTEM_PROMPT = (
411
+ "You are an invoice data extraction engine. Extract structured data from the raw OCR "
412
+ "text of an invoice. Return ONLY a valid JSON object, no markdown, no commentary, with "
413
+ "exactly these keys: vendor (string), invoice_number (string), invoice_date (string, "
414
+ "YYYY-MM-DD if possible), due_date (string, YYYY-MM-DD if possible), amount (number, "
415
+ "subtotal before tax), vat_amount (number), total (number), currency (3-letter code), "
416
+ "line_items (array of {description, quantity, unit_price, total}). "
417
+ "If a field is unknown, use null. Do not invent data that is not present in the text."
418
+ )
419
+
420
+
421
+ def ai_extract_groq(ocr_text: str):
422
+ if not GROQ_CLIENT:
423
+ return None
424
+ try:
425
+ resp = GROQ_CLIENT.chat.completions.create(
426
+ model="llama-3.1-8b-instant",
427
+ messages=[
428
+ {"role": "system", "content": EXTRACTION_SYSTEM_PROMPT},
429
+ {"role": "user", "content": ocr_text[:3000]},
430
+ ],
431
+ max_tokens=512,
432
+ temperature=0.05,
433
+ timeout=10,
434
+ )
435
+ content = resp.choices[0].message.content
436
+ return _safe_json(content)
437
+ except Exception as e:
438
+ print(f"[WARN] Groq extraction failed: {e}")
439
+ return None
440
+
441
+
442
+ def ai_extract_hf(ocr_text: str):
443
+ if not HF_TOKEN:
444
+ return None
445
+ try:
446
+ url = "https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.3"
447
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
448
+ prompt = f"<s>[INST] {EXTRACTION_SYSTEM_PROMPT}\n\n{ocr_text[:3000]} [/INST]"
449
+ payload = {"inputs": prompt, "parameters": {"max_new_tokens": 512, "temperature": 0.05}}
450
+ r = requests.post(url, headers=headers, json=payload, timeout=45)
451
+ if r.status_code == 503:
452
+ time.sleep(25)
453
+ r = requests.post(url, headers=headers, json=payload, timeout=45)
454
+ r.raise_for_status()
455
+ data = r.json()
456
+ text = data[0]["generated_text"] if isinstance(data, list) else str(data)
457
+ return _safe_json(text)
458
+ except Exception as e:
459
+ print(f"[WARN] HF Inference extraction failed: {e}")
460
+ return None
461
+
462
+
463
+ def _safe_json(text: str):
464
+ if not text:
465
+ return None
466
+ match = re.search(r"\{.*\}", text, re.DOTALL)
467
+ if not match:
468
+ return None
469
+ try:
470
+ return json.loads(match.group(0))
471
+ except Exception:
472
+ return None
473
+
474
+
475
+ def regex_extract(ocr_text: str):
476
+ def find(pattern, s, group=1, flags=re.IGNORECASE):
477
+ m = re.search(pattern, s, flags)
478
+ return m.group(group) if m else None
479
+
480
+ inv_number = find(r"(?:invoice|inv)[#:\s]+([A-Z0-9\-]{4,24})", ocr_text)
481
+ dates = re.findall(r"\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{4}", ocr_text)
482
+ total = find(r"(?:total|amount due)[\s:$]+([0-9,\.]+)", ocr_text)
483
+ vendor = None
484
+ for line in ocr_text.splitlines():
485
+ if line.strip():
486
+ vendor = line.strip()
487
+ break
488
+ try:
489
+ total_val = float(total.replace(",", "")) if total else None
490
+ except Exception:
491
+ total_val = None
492
+ return {
493
+ "vendor": vendor,
494
+ "invoice_number": inv_number,
495
+ "invoice_date": dates[0] if len(dates) > 0 else None,
496
+ "due_date": dates[1] if len(dates) > 1 else None,
497
+ "amount": None,
498
+ "vat_amount": None,
499
+ "total": total_val,
500
+ "currency": "USD",
501
+ "line_items": [],
502
+ }
503
+
504
+
505
+ def validate_invoice(data: dict):
506
+ warnings = []
507
+ try:
508
+ if data.get("amount") is not None and data.get("vat_amount") is not None and data.get("total") is not None:
509
+ if abs((float(data["amount"]) + float(data["vat_amount"])) - float(data["total"])) > 0.10:
510
+ warnings.append("Součet subtotal + DPH neodpovídá total.")
511
+ except Exception:
512
+ pass
513
+ try:
514
+ if data.get("invoice_date") and data.get("due_date"):
515
+ d1 = _parse_date_any(data["invoice_date"])
516
+ d2 = _parse_date_any(data["due_date"])
517
+ if d1 and d2 and d2 < d1:
518
+ warnings.append("Datum splatnosti je před datem vystavení.")
519
+ except Exception:
520
+ pass
521
+ return warnings
522
+
523
+
524
+ def _parse_date_any(s):
525
+ for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y", "%d.%m.%Y", "%d-%m-%Y"):
526
+ try:
527
+ return datetime.strptime(s, fmt)
528
+ except Exception:
529
+ continue
530
+ return None
531
+
532
+
533
+ def process_invoice_file(filepath, user):
534
+ filename = os.path.basename(filepath)
535
+ ext = filename.lower().split(".")[-1]
536
+
537
+ images = []
538
+ if Image is None:
539
+ pass
540
+ elif ext == "pdf":
541
+ if convert_from_path:
542
+ try:
543
+ images = convert_from_path(filepath, dpi=200)
544
+ except Exception as e:
545
+ print(f"[WARN] pdf2image failed: {e}")
546
+ else:
547
+ images = []
548
+ elif ext in ("jpg", "jpeg", "png", "tiff", "tif"):
549
+ try:
550
+ images = [Image.open(filepath)]
551
+ except Exception as e:
552
+ print(f"[WARN] Could not open image: {e}")
553
+
554
+ ocr_text = ""
555
+ for img in images:
556
+ ocr_text += run_ocr(img) + "\n"
557
+
558
+ data = None
559
+ if ocr_text.strip():
560
+ data = ai_extract_groq(ocr_text)
561
+ if not data:
562
+ data = ai_extract_hf(ocr_text)
563
+ if not data:
564
+ data = regex_extract(ocr_text) if ocr_text.strip() else {
565
+ "vendor": "Demo Vendor Inc.", "invoice_number": "DEMO-0001",
566
+ "invoice_date": datetime.now().strftime("%Y-%m-%d"), "due_date": None,
567
+ "amount": 100.0, "vat_amount": 21.0, "total": 121.0, "currency": "USD",
568
+ "line_items": [],
569
+ }
570
+
571
+ data["filename"] = filename
572
+ data["confidence"] = 0.95 if ocr_text.strip() else 0.3
573
+ warnings = validate_invoice(data)
574
+ data["warnings"] = warnings
575
+ data["status"] = "review" if warnings else "done"
576
+
577
+ is_dup = False
578
+ if user and user.get("plan") in ("pro", "enterprise"):
579
+ is_dup = check_duplicate(user["id"], data.get("vendor"), data.get("total"))
580
+ if is_dup:
581
+ data["status"] = "duplicate"
582
+ data["is_duplicate"] = True
583
+
584
+ return data
585
+
586
+
587
+ # ---------------------------------------------------------------------------
588
+ # Stripe helpers
589
+ # ---------------------------------------------------------------------------
590
+ def create_checkout_session(plan, user):
591
+ if not (stripe_sdk and STRIPE_SECRET_KEY):
592
+ return None, "Platby zatím nejsou nakonfigurované. Napiš nám na support@omniparse.ai pro ruční upgrade."
593
+ price_id = PLAN_PRICE_IDS.get(plan)
594
+ if not price_id:
595
+ return None, "Neznámý plán."
596
+ try:
597
+ session = stripe_sdk.checkout.Session.create(
598
+ mode="subscription",
599
+ payment_method_types=["card"],
600
+ line_items=[{"price": price_id, "quantity": 1}],
601
+ customer_email=user["email"],
602
+ success_url=f"{APP_URL}?checkout=success&session_id={{CHECKOUT_SESSION_ID}}",
603
+ cancel_url=f"{APP_URL}?checkout=cancel",
604
+ metadata={"plan": plan, "user_id": str(user["id"])},
605
+ )
606
+ return session.url, None
607
+ except Exception as e:
608
+ return None, f"Chyba při vytváření platby: {e}"
609
+
610
+
611
+ def check_payment_status(session_id, user_id):
612
+ if not (stripe_sdk and STRIPE_SECRET_KEY):
613
+ return "⏳ Platby nejsou nakonfigurované."
614
+ try:
615
+ session = stripe_sdk.checkout.Session.retrieve(session_id)
616
+ if session.payment_status == "paid":
617
+ plan = session.metadata.get("plan", "basic")
618
+ cust = session.customer
619
+ update_user(user_id, {"plan": plan, "stripe_cid": cust})
620
+ return f"✅ Upgradováno na {PLAN_LABELS.get(plan, plan)}!"
621
+ return "⏳ Platba zatím nebyla potvrzena."
622
+ except Exception as e:
623
+ return f"⚠️ Nelze ověřit platbu: {e}"
624
+
625
+
626
+ # ---------------------------------------------------------------------------
627
+ # Demo account bootstrap
628
+ # ---------------------------------------------------------------------------
629
+ def ensure_demo_account():
630
+ existing = get_user_by_email("demo@omniparse.ai")
631
+ if existing:
632
+ return
633
+ user, err = create_user("demo@omniparse.ai", "Demo User", "demo1234", plan="pro")
634
+ if user:
635
+ print("[INFO] Demo account created: demo@omniparse.ai / demo1234 (Pro plan)")
636
+ elif err:
637
+ print(f"[INFO] Demo account not created: {err}")
638
+
639
+
640
+ db_init()
641
+ ensure_demo_account()
642
+
643
+ # ---------------------------------------------------------------------------
644
+ # HTML fragments (Landing / Pricing / Legal)
645
+ # ---------------------------------------------------------------------------
646
+ LANDING_HTML = """
647
+ <style>
648
+ .op-wrap{max-width:1100px;margin:0 auto;font-family:-apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a2e;}
649
+ .op-hero{text-align:center;padding:56px 20px 32px;}
650
+ .op-hero h1{font-size:2.4em;margin-bottom:8px;}
651
+ .op-hero p{font-size:1.15em;color:#555;max-width:640px;margin:0 auto 20px;}
652
+ .op-stats{color:#7c3aed;font-weight:600;margin-top:14px;}
653
+ .op-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:18px;margin:28px 0;}
654
+ .op-card{background:#f7f6fb;border-radius:14px;padding:20px;border:1px solid #ece9f7;}
655
+ .op-card h3{margin:0 0 8px;font-size:1.05em;}
656
+ .op-steps{display:flex;gap:24px;flex-wrap:wrap;justify-content:center;margin:24px 0;}
657
+ .op-step{flex:1;min-width:180px;text-align:center;}
658
+ .op-step .num{width:36px;height:36px;border-radius:50%;background:#7c3aed;color:#fff;display:flex;align-items:center;justify-content:center;margin:0 auto 10px;font-weight:700;}
659
+ .op-quote{background:#f7f6fb;border-radius:14px;padding:18px;font-style:italic;}
660
+ .op-quote b{display:block;font-style:normal;margin-top:10px;color:#7c3aed;}
661
+ .op-section-title{text-align:center;margin:44px 0 18px;font-size:1.6em;}
662
+ .op-faq details{background:#f7f6fb;border-radius:10px;padding:14px 18px;margin-bottom:10px;}
663
+ .op-faq summary{cursor:pointer;font-weight:600;}
664
+ </style>
665
+ <div class="op-wrap">
666
+ <div class="op-hero">
667
+ <h1>⚡ Invoice processing in seconds, not hours.</h1>
668
+ <p>AI extracts vendor, dates, amounts and line items from any PDF or image. Export to CSV, JSON or Excel. Connect via API.</p>
669
+ <div class="op-stats">99.2% accuracy · &lt;4s per invoice · 40+ formats</div>
670
+ </div>
671
+
672
+ <div class="op-section-title">How it works</div>
673
+ <div class="op-steps">
674
+ <div class="op-step"><div class="num">1</div><b>Upload</b><br>PDF nebo obrázek faktury</div>
675
+ <div class="op-step"><div class="num">2</div><b>Extract</b><br>AI vytáhne všechna data</div>
676
+ <div class="op-step"><div class="num">3</div><b>Export</b><br>CSV, JSON, Excel nebo API</div>
677
+ </div>
678
+
679
+ <div class="op-section-title">Features</div>
680
+ <div class="op-grid">
681
+ <div class="op-card"><h3>🔍 OCR + LLM</h3>Tesseract + Groq Llama 3.1 pro maximální přesnost.</div>
682
+ <div class="op-card"><h3>🚫 Duplicate Detection</h3>Pro+, zachytí dvojí platby automaticky.</div>
683
+ <div class="op-card"><h3>🤖 AI Chat Agent</h3>Ptej se na faktury přirozenou angličtinou.</div>
684
+ <div class="op-card"><h3>✅ Cross-field Validation</h3>Kontroluje součty, data a DPH sazby.</div>
685
+ <div class="op-card"><h3>👥 Human-in-the-loop</h3>Enterprise, ruční review pochybných faktur.</div>
686
+ <div class="op-card"><h3>🔌 REST API</h3>Napojení na vlastní ERP systém.</div>
687
+ </div>
688
+
689
+ <div class="op-section-title">Co říkají zákazníci</div>
690
+ <div class="op-grid">
691
+ <div class="op-quote">"Ušetřili jsme desítky hodin měsíčně na ručním přepisování faktur."<b>— Jana K., CFO</b></div>
692
+ <div class="op-quote">"API integrace do našeho ERP trvala jedno odpoledne."<b>— Tomáš R., CTO</b></div>
693
+ <div class="op-quote">"Konečně nemusím kontrolovat každý řádek ručně."<b>— Petra M., účetní</b></div>
694
+ </div>
695
+
696
+ <div class="op-section-title">FAQ</div>
697
+ <div class="op-faq">
698
+ <details><summary>Je moje data v bezpečí?</summary>Data jsou uložena v EU (Frankfurt) a šifrována. Faktury mažeme po 30 dnech, účetní data držíme dle zákona 10 let.</details>
699
+ <details><summary>Funguje to na české faktury?</summary>Ano, podporujeme i lokální formáty a DPH sazby, včetně CZK.</details>
700
+ <details><summary>Jak fungují platby?</summary>Přes Stripe, měsíčně nebo ročně, kartou.</details>
701
+ <details><summary>Mohu kdykoli zrušit?</summary>Ano, zrušení je kdykoli v profilu, bez výpovědní lhůty.</details>
702
+ <details><summary>Dostanu daňový doklad?</summary>Ano, po každé platbě automaticky na email.</details>
703
+ </div>
704
+ </div>
705
+ """
706
+
707
+ FOOTER_HTML = """
708
+ <div style="max-width:1100px;margin:30px auto 10px;padding:20px;border-top:1px solid #eee;
709
+ text-align:center;color:#888;font-family:-apple-system,Segoe UI,Roboto,sans-serif;font-size:0.9em;">
710
+ ⚡ OmniParse AI &nbsp;·&nbsp; © 2026 &nbsp;·&nbsp; Terms · Privacy · Disclaimer (viz záložka Legal)
711
+ </div>
712
+ """
713
+
714
+ LEGAL_TERMS = """
715
+ ### Terms of Use
716
+
717
+ **Popis služby.** OmniParse AI poskytuje automatizované zpracování faktur pomocí OCR a umělé inteligence.
718
+
719
+ **Zakázané použití.** Nahrávání dokumentů, k jejichž zpracování nemáte oprávnění, zneužívání API mimo rámec vašeho plánu, reverzní inženýrství služby.
720
+
721
+ **Platby a zrušení.** Předplatné se obnovuje měsíčně/ročně dle zvoleného plánu. Zrušení lze provést kdykoli v sekci Profile, služba zůstává aktivní do konce zaplaceného období.
722
+
723
+ **Omezení odpovědnosti.** Služba je poskytována "tak jak je". OmniParse nenese odpovědnost za nepřímé škody vzniklé použitím extrahovaných dat.
724
+ """
725
+
726
+ LEGAL_PRIVACY = """
727
+ ### Privacy Policy / GDPR
728
+
729
+ **Co sbíráme:** email, jméno, nahrané faktury a z nich extrahovaná data.
730
+
731
+ **Kde je to uloženo:** Supabase (PostgreSQL), region EU – Frankfurt.
732
+
733
+ **Jak dlouho:** obrazy faktur 30 dní, agregovaná účetní data 10 let (zákonná povinnost).
734
+
735
+ **Vaše práva:** přístup k datům, výmaz, přenositelnost — napište na privacy@omniparse.ai.
736
+
737
+ **Cookies:** pouze technické (přihlašovací session), žádný marketingový tracking.
738
+ """
739
+
740
+ LEGAL_DISCLAIMER = """
741
+ ### Disclaimer
742
+
743
+ AI extrakce **není 100% přesná** — vždy si ověřte data před zaúčtováním.
744
+
745
+ OmniParse nenese odpovědnost za chyby vzniklé z nesprávné AI extrakce.
746
+
747
+ Tento nástroj je pomůcka, **nikoliv náhrada za účetního** nebo daňového poradce.
748
+ """
749
+
750
+
751
+ def pricing_cards_html(highlight=None):
752
+ plans = [
753
+ ("free", "Free", "$0", ["20 faktur / měsíc", "CSV export", "1 uživatel", "Bez API"]),
754
+ ("basic", "Basic", "$29/měs", ["200 faktur / měsíc", "JSON + CSV + Excel", "Google Sheets sync", "Multi-currency"]),
755
+ ("pro", "Pro", "$129/měs", ["2 000 faktur / měsíc", "REST API + vlastní klíč", "AI Chat Agent", "Duplicate Detection", "3 uživatelé"]),
756
+ ("enterprise", "Enterprise", "$499/měs", ["Neomezený objem", "Dedikované API", "Human-in-the-loop", "SLA 99.5%", "Podpora do 4h"]),
757
+ ]
758
+ cards = ""
759
+ for key, label, price, feats in plans:
760
+ hl = "border:2px solid #7c3aed;" if key == highlight else "border:1px solid #ece9f7;"
761
+ items = "".join(f"<li>{f}</li>" for f in feats)
762
+ cards += f"""<div style="background:#f7f6fb;border-radius:14px;padding:20px;{hl}">
763
+ <h3 style="margin:0 0 4px;">{label}</h3>
764
+ <div style="font-size:1.4em;font-weight:700;color:#7c3aed;margin-bottom:10px;">{price}</div>
765
+ <ul style="padding-left:18px;margin:0;font-size:0.92em;color:#333;">{items}</ul>
766
+ </div>"""
767
+ return f'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;font-family:-apple-system,sans-serif;">{cards}</div>'
768
+
769
+
770
+ # ---------------------------------------------------------------------------
771
+ # Gradio app
772
+ # ---------------------------------------------------------------------------
773
+ CUSTOM_CSS = """
774
+ #op-navbar {display:flex; justify-content:space-between; align-items:center; padding:10px 6px;}
775
+ .op-logo {font-size:1.3em; font-weight:800;}
776
+ footer {visibility:hidden}
777
+ """
778
+
779
+ with gr.Blocks(title="OmniParse AI", css=CUSTOM_CSS, theme=gr.themes.Soft(primary_hue="violet")) as demo:
780
+
781
+ session_token = gr.State(None)
782
+ current_user = gr.State(None)
783
+
784
+ # ---------------- Navbar ----------------
785
+ with gr.Row(elem_id="op-navbar"):
786
+ gr.HTML('<div class="op-logo">⚡ OmniParse AI</div>')
787
+ with gr.Row():
788
+ nav_pricing_btn = gr.Button("Pricing", size="sm", variant="secondary")
789
+ nav_legal_btn = gr.Button("Legal", size="sm", variant="secondary")
790
+ nav_login_btn = gr.Button("Log In", size="sm", variant="secondary")
791
+ nav_start_btn = gr.Button("Start Free →", size="sm", variant="primary")
792
+ nav_dashboard_btn = gr.Button("Dashboard", size="sm", variant="primary", visible=False)
793
+ nav_logout_btn = gr.Button("Log Out", size="sm", variant="secondary", visible=False)
794
+
795
+ # ---------------- VIEW: Landing ----------------
796
+ with gr.Column(visible=True) as view_landing:
797
+ gr.HTML(LANDING_HTML)
798
+ with gr.Row():
799
+ landing_cta_btn = gr.Button("Start Free — 20 invoices", variant="primary", scale=1)
800
+ gr.HTML("<div style='max-width:1100px;margin:30px auto 0;text-align:center;font-weight:700;font-size:1.4em;font-family:sans-serif;'>Pricing</div>")
801
+ gr.HTML(pricing_cards_html())
802
+ gr.HTML(FOOTER_HTML)
803
+
804
+ # ---------------- VIEW: Pricing ----------------
805
+ with gr.Column(visible=False) as view_pricing:
806
+ gr.Markdown("## Pricing")
807
+ gr.HTML(pricing_cards_html())
808
+ gr.Markdown("Enterprise roční plán: **$4,188/rok** (2 měsíce zdarma oproti měsíční platbě).")
809
+ with gr.Accordion("Časté dotazy k platbám", open=False):
810
+ gr.Markdown(
811
+ "- **Jaké platební metody přijímáte?** Kartové platby přes Stripe.\n"
812
+ "- **Mohu změnit plán kdykoli?** Ano, upgrade/downgrade v Dashboard → Upgrade.\n"
813
+ "- **Vracíte peníze?** Do 14 dnů od první platby na vyžádání."
814
+ )
815
+ pricing_back_btn = gr.Button("← Zpět na Landing")
816
+
817
+ # ---------------- VIEW: Legal ----------------
818
+ with gr.Column(visible=False) as view_legal:
819
+ gr.Markdown("## Legal")
820
+ with gr.Tabs():
821
+ with gr.Tab("Terms of Use"):
822
+ gr.Markdown(LEGAL_TERMS)
823
+ with gr.Tab("Privacy Policy / GDPR"):
824
+ gr.Markdown(LEGAL_PRIVACY)
825
+ with gr.Tab("Disclaimer"):
826
+ gr.Markdown(LEGAL_DISCLAIMER)
827
+ legal_back_btn = gr.Button("← Zpět na Landing")
828
+
829
+ # ---------------- VIEW: Auth ----------------
830
+ with gr.Column(visible=False) as view_auth:
831
+ gr.Markdown("## Vítej v OmniParse AI")
832
+ with gr.Tabs():
833
+ with gr.Tab("Log In"):
834
+ gr.Markdown("_Demo účet: `demo@omniparse.ai` / `demo1234` (Pro plán)_")
835
+ login_email = gr.Textbox(label="Email")
836
+ login_password = gr.Textbox(label="Heslo", type="password")
837
+ login_btn = gr.Button("Log In", variant="primary")
838
+ login_error = gr.Markdown(visible=False)
839
+ with gr.Tab("Sign Up"):
840
+ signup_name = gr.Textbox(label="Full Name")
841
+ signup_email = gr.Textbox(label="Work Email")
842
+ signup_password = gr.Textbox(label="Password (min. 8 znaků)", type="password")
843
+ signup_terms = gr.Checkbox(label="Souhlasím s Terms of Use a Privacy Policy")
844
+ signup_btn = gr.Button("Vytvořit účet", variant="primary")
845
+ signup_error = gr.Markdown(visible=False)
846
+
847
+ # ---------------- VIEW: Dashboard ----------------
848
+ with gr.Column(visible=False) as view_dashboard:
849
+ dash_welcome = gr.Markdown("## Dashboard")
850
+ with gr.Tabs():
851
+ # --- Upload ---
852
+ with gr.Tab("📤 Upload"):
853
+ usage_md = gr.Markdown()
854
+ upload_files = gr.File(label="Nahraj faktury (PDF, JPG, PNG, TIFF — max 20MB)", file_count="multiple")
855
+ upload_btn = gr.Button("Zpracovat", variant="primary")
856
+ upload_status = gr.Markdown()
857
+ upload_table = gr.Dataframe(
858
+ headers=["Filename", "Vendor", "Invoice #", "Date", "Total", "Status"],
859
+ label="Výsledky", interactive=False,
860
+ )
861
+ upload_json = gr.JSON(label="Raw output (poslední faktura)")
862
+
863
+ # --- My Invoices ---
864
+ with gr.Tab("📋 My Invoices"):
865
+ invoices_filter = gr.Radio(["All", "Done", "Review", "Duplicates"], value="All", label="Filtr")
866
+ refresh_invoices_btn = gr.Button("🔄 Obnovit")
867
+ invoices_table = gr.Dataframe(
868
+ headers=["ID", "Vendor", "Invoice #", "Date", "Total", "Status"],
869
+ label="Faktury", interactive=False,
870
+ )
871
+ with gr.Row():
872
+ delete_id_input = gr.Number(label="ID faktury ke smazání", precision=0)
873
+ delete_invoice_btn = gr.Button("🗑️ Smazat")
874
+ delete_status = gr.Markdown()
875
+
876
+ # --- AI Chat ---
877
+ with gr.Tab("🤖 AI Chat (Pro+)"):
878
+ chat_lock_msg = gr.Markdown(visible=False)
879
+ chatbot = gr.Chatbot(label="Zeptej se na své faktury", type="messages")
880
+ chat_input = gr.Textbox(label="Zpráva", placeholder="What's the total unpaid amount?")
881
+ chat_send_btn = gr.Button("Odeslat", variant="primary")
882
+ gr.Markdown("_Např.: 'List all invoices from Microsoft' / 'Which invoice has the highest tax?'_")
883
+
884
+ # --- Export ---
885
+ with gr.Tab("📊 Export"):
886
+ gr.Markdown("**CSV export** — zdarma všem plánům.")
887
+ export_csv_btn = gr.Button("Exportovat CSV")
888
+ export_csv_file = gr.File(label="Stažení CSV")
889
+ gr.Markdown("**JSON export** — Basic+")
890
+ export_json_btn = gr.Button("Exportovat JSON")
891
+ export_json_file = gr.File(label="Stažení JSON")
892
+ gr.Markdown("**Excel export** — Basic+ · _coming soon_")
893
+ gr.Markdown("**Google Sheets sync** — Basic+ · _coming soon_")
894
+
895
+ # --- Upgrade ---
896
+ with gr.Tab("⚡ Upgrade"):
897
+ gr.HTML(pricing_cards_html())
898
+ upgrade_plan_dd = gr.Dropdown(["basic", "pro", "enterprise"], label="Vyber plán")
899
+ upgrade_btn = gr.Button("Upgradovat přes Stripe", variant="primary")
900
+ upgrade_link = gr.Markdown()
901
+ gr.Markdown("---")
902
+ session_id_input = gr.Textbox(label="Stripe session_id (vyplní se po návratu ze Stripe)")
903
+ check_payment_btn = gr.Button("Ověřit platbu")
904
+ payment_status_md = gr.Markdown()
905
+
906
+ # --- API ---
907
+ with gr.Tab("🔌 API (Pro+)"):
908
+ api_lock_msg = gr.Markdown(visible=False)
909
+ api_key_display = gr.Markdown(visible=False)
910
+ api_docs = gr.Markdown(visible=False)
911
+
912
+ # --- Profile ---
913
+ with gr.Tab("👤 Profile"):
914
+ profile_info = gr.Markdown()
915
+ new_password = gr.Textbox(label="Nové heslo", type="password")
916
+ change_pw_btn = gr.Button("Změnit heslo")
917
+ change_pw_status = gr.Markdown()
918
+ gr.Markdown("### ⚠️ Danger zone")
919
+ delete_account_btn = gr.Button("Delete Account", variant="stop")
920
+ delete_account_status = gr.Markdown()
921
+
922
+ # =========================================================================
923
+ # Navigation logic
924
+ # =========================================================================
925
+ all_views = [view_landing, view_pricing, view_legal, view_auth, view_dashboard]
926
+
927
+ def show_only(idx):
928
+ return [gr.update(visible=(i == idx)) for i in range(len(all_views))]
929
+
930
+ def go_landing():
931
+ return show_only(0)
932
+
933
+ def go_pricing():
934
+ return show_only(1)
935
+
936
+ def go_legal():
937
+ return show_only(2)
938
+
939
+ def go_auth():
940
+ return show_only(3)
941
+
942
+ def go_dashboard():
943
+ return show_only(4)
944
+
945
+ nav_pricing_btn.click(go_pricing, outputs=all_views)
946
+ pricing_back_btn.click(go_landing, outputs=all_views)
947
+ nav_legal_btn.click(go_legal, outputs=all_views)
948
+ legal_back_btn.click(go_landing, outputs=all_views)
949
+ nav_login_btn.click(go_auth, outputs=all_views)
950
+ nav_start_btn.click(go_auth, outputs=all_views)
951
+ landing_cta_btn.click(go_auth, outputs=all_views)
952
+
953
+ # =========================================================================
954
+ # Auth logic
955
+ # =========================================================================
956
+ def do_login(email, password):
957
+ if not email or not password:
958
+ return (gr.update(value="⚠️ Vyplň email i heslo.", visible=True), None, None,
959
+ *show_only(3), gr.update(), gr.update(), gr.update(), gr.update())
960
+ user = get_user_by_email(email)
961
+ if not user or user["password"] != hash_pw(password):
962
+ return (gr.update(value="⚠️ Nesprávný email nebo heslo.", visible=True), None, None,
963
+ *show_only(3), gr.update(), gr.update(), gr.update(), gr.update())
964
+ token = create_session(user["id"])
965
+ return (gr.update(value="", visible=False), token, user,
966
+ *show_only(4), gr.update(visible=False), gr.update(visible=False),
967
+ gr.update(visible=True), gr.update(visible=True))
968
+
969
+ def do_signup(name, email, password, terms):
970
+ if not (name and email and password):
971
+ return (gr.update(value="⚠️ Vyplň všechna pole.", visible=True), None, None,
972
+ *show_only(3), gr.update(), gr.update(), gr.update(), gr.update())
973
+ if len(password) < 8:
974
+ return (gr.update(value="⚠️ Heslo musí mít alespoň 8 znaků.", visible=True), None, None,
975
+ *show_only(3), gr.update(), gr.update(), gr.update(), gr.update())
976
+ if not terms:
977
+ return (gr.update(value="⚠️ Musíš souhlasit s Terms a Privacy Policy.", visible=True), None, None,
978
+ *show_only(3), gr.update(), gr.update(), gr.update(), gr.update())
979
+ user, err = create_user(email, name, password)
980
+ if err:
981
+ return (gr.update(value=f"⚠️ {err}", visible=True), None, None,
982
+ *show_only(3), gr.update(), gr.update(), gr.update(), gr.update())
983
+ token = create_session(user["id"])
984
+ return (gr.update(value="", visible=False), token, user,
985
+ *show_only(4), gr.update(visible=False), gr.update(visible=False),
986
+ gr.update(visible=True), gr.update(visible=True))
987
+
988
+ login_btn.click(
989
+ do_login, inputs=[login_email, login_password],
990
+ outputs=[login_error, session_token, current_user, *all_views,
991
+ nav_login_btn, nav_start_btn, nav_dashboard_btn, nav_logout_btn],
992
+ )
993
+ signup_btn.click(
994
+ do_signup, inputs=[signup_name, signup_email, signup_password, signup_terms],
995
+ outputs=[signup_error, session_token, current_user, *all_views,
996
+ nav_login_btn, nav_start_btn, nav_dashboard_btn, nav_logout_btn],
997
+ )
998
+
999
+ def do_logout(token):
1000
+ delete_session(token)
1001
+ return (None, None, *show_only(0),
1002
+ gr.update(visible=True), gr.update(visible=True),
1003
+ gr.update(visible=False), gr.update(visible=False))
1004
+
1005
+ nav_logout_btn.click(
1006
+ do_logout, inputs=[session_token],
1007
+ outputs=[session_token, current_user, *all_views,
1008
+ nav_login_btn, nav_start_btn, nav_dashboard_btn, nav_logout_btn],
1009
+ )
1010
+
1011
+ def go_to_dashboard_refresh(user):
1012
+ if not user:
1013
+ return (*show_only(3),)
1014
+ return (*show_only(4),)
1015
+
1016
+ nav_dashboard_btn.click(go_to_dashboard_refresh, inputs=[current_user], outputs=all_views)
1017
+
1018
+ # =========================================================================
1019
+ # Dashboard: load / welcome / usage
1020
+ # =========================================================================
1021
+ def load_dashboard(user):
1022
+ if not user:
1023
+ return "## Dashboard\n\n_Nepřihlášen._", ""
1024
+ used = count_invoices_this_month(user["id"])
1025
+ limit = PLAN_LIMITS.get(user["plan"], 20)
1026
+ limit_str = "∞" if limit == float("inf") else int(limit)
1027
+ welcome = f"## Dashboard — Ahoj {user['name']} 👋 (plán: {PLAN_LABELS.get(user['plan'], user['plan'])})"
1028
+ usage = f"**{used}/{limit_str} invoices used this month**"
1029
+ if limit != float("inf") and used >= limit:
1030
+ usage += "\n\n🔴 **Limit vyčerpán.** Přejdi na záložku ⚡ Upgrade pro navýšení limitu."
1031
+ return welcome, usage
1032
+
1033
+ view_dashboard.visible # noqa — keep reference alive
1034
+
1035
+ current_user.change(load_dashboard, inputs=[current_user], outputs=[dash_welcome, usage_md])
1036
+
1037
+ # =========================================================================
1038
+ # Upload / processing
1039
+ # =========================================================================
1040
+ def do_upload(files, user):
1041
+ if not user:
1042
+ return "⚠️ Musíš být přihlášen.", [], None, ""
1043
+ if not files:
1044
+ return "⚠️ Nevybral jsi žádný soubor.", [], None, ""
1045
+
1046
+ limit = PLAN_LIMITS.get(user["plan"], 20)
1047
+ used = count_invoices_this_month(user["id"])
1048
+ rows = []
1049
+ last_json = None
1050
+ processed = 0
1051
+ for f in files:
1052
+ if used + processed >= limit:
1053
+ break
1054
+ path = f.name if hasattr(f, "name") else f
1055
+ try:
1056
+ data = process_invoice_file(path, user)
1057
+ except Exception as e:
1058
+ data = {"filename": os.path.basename(path), "vendor": None, "invoice_number": None,
1059
+ "invoice_date": None, "due_date": None, "amount": None, "vat_amount": None,
1060
+ "total": None, "currency": "USD", "status": "review", "confidence": 0,
1061
+ "warnings": [f"Chyba zpracování: {e}"]}
1062
+ saved = insert_invoice(user["id"], data)
1063
+ last_json = data
1064
+ status_emoji = {"done": "✅ Done", "review": "⚠️ Review", "duplicate": "🔴 Duplicate"}.get(data.get("status"), data.get("status"))
1065
+ rows.append([
1066
+ data.get("filename"), data.get("vendor"), data.get("invoice_number"),
1067
+ data.get("invoice_date"), data.get("total"), status_emoji,
1068
+ ])
1069
+ processed += 1
1070
+
1071
+ skipped = len(files) - processed
1072
+ msg = f"✅ Zpracováno {processed} faktur."
1073
+ if skipped > 0:
1074
+ msg += f" ⚠️ {skipped} přeskočeno — měsíční limit vyčerpán, upgraduj plán."
1075
+ used_new = count_invoices_this_month(user["id"])
1076
+ limit_str = "∞" if limit == float("inf") else int(limit)
1077
+ usage = f"**{used_new}/{limit_str} invoices used this month**"
1078
+ return msg, rows, last_json, usage
1079
+
1080
+ upload_btn.click(
1081
+ do_upload, inputs=[upload_files, current_user],
1082
+ outputs=[upload_status, upload_table, upload_json, usage_md],
1083
+ )
1084
+
1085
+ # =========================================================================
1086
+ # My Invoices
1087
+ # =========================================================================
1088
+ STATUS_MAP = {"done": "✅ Done", "review": "⚠️ Review", "duplicate": "🔴 Duplicate", "processing": "⟳ Processing"}
1089
+
1090
+ def refresh_invoices(user, flt):
1091
+ if not user:
1092
+ return []
1093
+ invoices = get_invoices(user["id"])
1094
+ rows = []
1095
+ for inv in invoices:
1096
+ status = inv.get("status", "done")
1097
+ if flt == "Done" and status != "done":
1098
+ continue
1099
+ if flt == "Review" and status != "review":
1100
+ continue
1101
+ if flt == "Duplicates" and not inv.get("is_duplicate"):
1102
+ continue
1103
+ rows.append([
1104
+ inv.get("id"), inv.get("vendor"), inv.get("inv_number"),
1105
+ inv.get("inv_date"), inv.get("total"), STATUS_MAP.get(status, status),
1106
+ ])
1107
+ return rows
1108
+
1109
+ refresh_invoices_btn.click(refresh_invoices, inputs=[current_user, invoices_filter], outputs=[invoices_table])
1110
+ invoices_filter.change(refresh_invoices, inputs=[current_user, invoices_filter], outputs=[invoices_table])
1111
+
1112
+ def do_delete_invoice(user, inv_id):
1113
+ if not user or not inv_id:
1114
+ return "⚠️ Zadej platné ID.", []
1115
+ delete_invoice(user["id"], int(inv_id))
1116
+ return f"✅ Faktura #{int(inv_id)} smazána.", refresh_invoices(user, "All")
1117
+
1118
+ delete_invoice_btn.click(do_delete_invoice, inputs=[current_user, delete_id_input], outputs=[delete_status, invoices_table])
1119
+
1120
+ # =========================================================================
1121
+ # AI Chat
1122
+ # =========================================================================
1123
+ def chat_respond(message, history, user):
1124
+ history = history or []
1125
+ if not user:
1126
+ history.append({"role": "assistant", "content": "Musíš být přihlášen."})
1127
+ return history, ""
1128
+ if user["plan"] not in ("pro", "enterprise"):
1129
+ history.append({"role": "assistant", "content": "🔒 AI Chat je dostupný od plánu Pro. Uprgraduj v záložce ⚡ Upgrade."})
1130
+ return history, ""
1131
+ invoices = get_invoices(user["id"])
1132
+ context = json.dumps(invoices[:100], default=str, ensure_ascii=False)[:6000]
1133
+ history.append({"role": "user", "content": message})
1134
+ if GROQ_CLIENT:
1135
+ try:
1136
+ resp = GROQ_CLIENT.chat.completions.create(
1137
+ model="llama-3.1-8b-instant",
1138
+ messages=[
1139
+ {"role": "system", "content": f"You are an assistant answering questions about the user's invoices. Here is their invoice data as JSON: {context}. Answer concisely based only on this data."},
1140
+ {"role": "user", "content": message},
1141
+ ],
1142
+ max_tokens=400, temperature=0.2, timeout=10,
1143
+ )
1144
+ answer = resp.choices[0].message.content
1145
+ except Exception as e:
1146
+ answer = f"⚠️ AI momentálně nedostupné ({e})."
1147
+ else:
1148
+ answer = "⚠️ AI chat vyžaduje nastavený GROQ_API_KEY."
1149
+ history.append({"role": "assistant", "content": answer})
1150
+ return history, ""
1151
+
1152
+ chat_send_btn.click(chat_respond, inputs=[chat_input, chatbot, current_user], outputs=[chatbot, chat_input])
1153
+ chat_input.submit(chat_respond, inputs=[chat_input, chatbot, current_user], outputs=[chatbot, chat_input])
1154
+
1155
+ # =========================================================================
1156
+ # Export
1157
+ # =========================================================================
1158
+ def export_csv(user):
1159
+ if not user:
1160
+ return None
1161
+ invoices = get_invoices(user["id"])
1162
+ path = f"/tmp/omniparse_export_{user['id']}.csv"
1163
+ import csv
1164
+ with open(path, "w", newline="", encoding="utf-8") as f:
1165
+ writer = csv.writer(f)
1166
+ writer.writerow(["ID", "Vendor", "Invoice#", "Date", "Due Date", "Amount", "VAT", "Total", "Currency", "Status"])
1167
+ for inv in invoices:
1168
+ writer.writerow([inv.get("id"), inv.get("vendor"), inv.get("inv_number"), inv.get("inv_date"),
1169
+ inv.get("due_date"), inv.get("amount"), inv.get("vat_amount"), inv.get("total"),
1170
+ inv.get("currency"), inv.get("status")])
1171
+ return path
1172
+
1173
+ def export_json(user):
1174
+ if not user:
1175
+ return None
1176
+ if user["plan"] == "free":
1177
+ return None
1178
+ invoices = get_invoices(user["id"])
1179
+ path = f"/tmp/omniparse_export_{user['id']}.json"
1180
+ with open(path, "w", encoding="utf-8") as f:
1181
+ json.dump(invoices, f, default=str, ensure_ascii=False, indent=2)
1182
+ return path
1183
+
1184
+ export_csv_btn.click(export_csv, inputs=[current_user], outputs=[export_csv_file])
1185
+ export_json_btn.click(export_json, inputs=[current_user], outputs=[export_json_file])
1186
+
1187
+ # =========================================================================
1188
+ # Upgrade / Stripe
1189
+ # =========================================================================
1190
+ def do_upgrade(plan, user):
1191
+ if not user:
1192
+ return "⚠️ Musíš být přihlášen."
1193
+ url, err = create_checkout_session(plan, user)
1194
+ if err:
1195
+ return f"⚠️ {err}"
1196
+ return f"👉 [Klikni pro dokončení platby přes Stripe]({url})\n\nPo zaplacení se vrať sem a vlož `session_id` z URL níže."
1197
+
1198
+ upgrade_btn.click(do_upgrade, inputs=[upgrade_plan_dd, current_user], outputs=[upgrade_link])
1199
+
1200
+ def do_check_payment(session_id, user):
1201
+ if not user or not session_id:
1202
+ return "⚠️ Zadej session_id."
1203
+ return check_payment_status(session_id, user["id"])
1204
+
1205
+ check_payment_btn.click(do_check_payment, inputs=[session_id_input, current_user], outputs=[payment_status_md])
1206
+
1207
+ # =========================================================================
1208
+ # API tab
1209
+ # =========================================================================
1210
+ def load_api_tab(user):
1211
+ if not user:
1212
+ return gr.update(visible=True, value="Musíš být přihlášen."), gr.update(visible=False), gr.update(visible=False)
1213
+ if user["plan"] not in ("pro", "enterprise"):
1214
+ return (gr.update(visible=True, value="🔒 API je dostupné od plánu Pro. Uprgraduj v záložce ⚡ Upgrade."),
1215
+ gr.update(visible=False), gr.update(visible=False))
1216
+ key = user.get("api_key") or "—"
1217
+ snippet = f"""```bash
1218
+ curl -X POST {APP_URL}/api/extract \\
1219
+ -H "Authorization: Bearer {key}" \\
1220
+ -F "file=@invoice.pdf"
1221
+ ```"""
1222
+ return (gr.update(visible=False), gr.update(visible=True, value=f"**Tvůj API klíč:** `{key}`"),
1223
+ gr.update(visible=True, value=snippet))
1224
+
1225
+ current_user.change(load_api_tab, inputs=[current_user], outputs=[api_lock_msg, api_key_display, api_docs])
1226
+
1227
+ # =========================================================================
1228
+ # Profile
1229
+ # =========================================================================
1230
+ def load_profile(user):
1231
+ if not user:
1232
+ return "Nepřihlášen."
1233
+ plan = PLAN_LABELS.get(user["plan"], user["plan"])
1234
+ return (f"**Jméno:** {user['name']}\n\n**Email:** {user['email']}\n\n"
1235
+ f"**Plán:** {plan} (${PLAN_PRICES.get(user['plan'], 0)}/měs)\n\n"
1236
+ f"**Vytvořeno:** {user.get('created_at', '—')[:10]}")
1237
+
1238
+ current_user.change(load_profile, inputs=[current_user], outputs=[profile_info])
1239
+
1240
+ def do_change_password(user, new_pw):
1241
+ if not user:
1242
+ return "⚠️ Musíš být přihlášen."
1243
+ if not new_pw or len(new_pw) < 8:
1244
+ return "⚠️ Heslo musí mít alespoň 8 znaků."
1245
+ update_user(user["id"], {"password": hash_pw(new_pw)})
1246
+ return "✅ Heslo změněno."
1247
+
1248
+ change_pw_btn.click(do_change_password, inputs=[current_user, new_password], outputs=[change_pw_status])
1249
+
1250
+ def do_delete_account(user, token):
1251
+ if not user:
1252
+ return "⚠️ Musíš být přihlášen.", None, None
1253
+ if USE_SUPABASE:
1254
+ sb.table("invoices").delete().eq("user_id", user["id"]).execute()
1255
+ sb.table("users").delete().eq("id", user["id"]).execute()
1256
+ else:
1257
+ conn = _sqlite_conn()
1258
+ conn.execute("DELETE FROM invoices WHERE user_id=?", (user["id"],))
1259
+ conn.execute("DELETE FROM users WHERE id=?", (user["id"],))
1260
+ conn.commit()
1261
+ conn.close()
1262
+ delete_session(token)
1263
+ return "✅ Účet smazán.", None, None
1264
+
1265
+ delete_account_btn.click(
1266
+ do_delete_account, inputs=[current_user, session_token],
1267
+ outputs=[delete_account_status, current_user, session_token],
1268
+ )
1269
+
1270
+
1271
+ if __name__ == "__main__":
1272
+ demo.queue()
1273
+ demo.launch()
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ tesseract-ocr
2
+ tesseract-ocr-eng
3
+ poppler-utils
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.49.1
2
+ requests>=2.31.0
3
+ stripe>=10.0.0
4
+ Pillow>=10.0.0
5
+ pytesseract>=0.3.10
6
+ pdf2image>=1.17.0
7
+ python-multipart>=0.0.9
8
+ groq>=0.9.0
9
+ supabase>=2.7.0