simikkk commited on
Commit
ad314df
·
verified ·
1 Parent(s): 048bea9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +960 -627
app.py CHANGED
@@ -12,123 +12,242 @@ import os
12
  import time
13
  import sqlite3
14
  import hashlib
 
15
 
16
- print("Initializing OmniParse AI Enterprise Terminal v5.5 with Optional Free Plan...")
17
 
18
- # ─── DATABASE INITIALIZATION ────────────────────────────────────────────────
19
  DB_FILE = "omniparse.db"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
 
21
  def init_db():
22
  conn = sqlite3.connect(DB_FILE)
23
- cursor = conn.cursor()
24
- # Tabulka uživatelů
25
- cursor.execute('''
26
- CREATE TABLE IF NOT EXISTS users (
27
- id INTEGER PRIMARY KEY AUTOINCREMENT,
28
- username TEXT UNIQUE NOT NULL,
29
- password_hash TEXT NOT NULL,
30
- salt TEXT NOT NULL
31
- )
32
- ''')
33
- # Tabulka nahraných faktur provázaná na uživatele
34
- cursor.execute('''
35
- CREATE TABLE IF NOT EXISTS invoices (
36
- id INTEGER PRIMARY KEY AUTOINCREMENT,
37
- user_id INTEGER NOT NULL,
38
- timestamp REAL NOT NULL,
39
- filename TEXT,
40
- extracted_json TEXT,
41
- fields_count INTEGER,
42
- FOREIGN KEY(user_id) REFERENCES users(id)
43
- )
44
- ''')
 
 
 
 
 
 
 
 
 
45
  conn.commit()
46
  conn.close()
47
 
48
  init_db()
49
 
 
50
  def hash_password(password, salt=None):
51
  if not salt:
52
  salt = os.urandom(16).hex()
53
- pbkdf2 = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000)
54
- return pbkdf2.hex(), salt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- # ─── AUTH ACTIONS (DATABASE BACKED) ─────────────────────────────────────────
57
- def db_register(username, password):
58
  if not username or not password:
59
- return json.dumps({"status": "ERROR", "message": "Zadej jméno i heslo, nesmí to bejt prázdný!"})
60
-
61
  username = username.strip().lower()
62
  if username == "guest":
63
- return json.dumps({"status": "ERROR", "message": "Jméno 'guest' je rezervované pro Free Plan!"})
64
-
 
65
  try:
66
  conn = sqlite3.connect(DB_FILE)
67
- cursor = conn.cursor()
68
- cursor.execute("SELECT id FROM users WHERE username = ?", (username,))
69
- if cursor.fetchone():
70
  conn.close()
71
- return json.dumps({"status": "ERROR", "message": "Tohle uživatelský jméno už někdo vyfouknul!"})
72
-
73
  h, salt = hash_password(password)
74
- cursor.execute("INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)", (username, h, salt))
 
75
  conn.commit()
76
  conn.close()
77
- return json.dumps({"status": "SUCCESS", "message": "Účet je na světě! Teď se můžeš přihlásit."})
78
  except Exception as e:
79
- return json.dumps({"status": "ERROR", "message": f"Chyba databáze: {str(e)}"})
80
 
81
  def db_login(username, password):
82
  if not username or not password:
83
- return json.dumps({"status": "ERROR", "message": "Zadej jméno a heslo, bez toho tě dál nepustím!"})
84
-
85
  username = username.strip().lower()
86
  try:
87
  conn = sqlite3.connect(DB_FILE)
88
- cursor = conn.cursor()
89
- cursor.execute("SELECT id, password_hash, salt FROM users WHERE username = ?", (username,))
90
- row = cursor.fetchone()
91
-
92
  if not row:
93
  conn.close()
94
- return json.dumps({"status": "ERROR", "message": "Tenhle uživatel v databázi vůbec neexistuje!"})
95
-
96
- user_id, stored_hash, salt = row
97
-
98
- # Podpora pro session refresh
99
  if password == "SESSION_REFRESH":
100
- cursor.execute("SELECT COUNT(*), SUM(fields_count) FROM invoices WHERE user_id = ?", (user_id,))
101
- stats = cursor.fetchone()
102
- docs_parsed = stats[0] or 0
103
- fields_extracted = stats[1] or 0
104
  conn.close()
105
- return json.dumps({
106
- "status": "SUCCESS",
107
- "username": username,
108
- "stats": {"docs_parsed": docs_parsed, "fields_extracted": fields_extracted}
109
- })
110
-
111
  h, _ = hash_password(password, salt)
112
  if h != stored_hash:
113
  conn.close()
114
- return json.dumps({"status": "ERROR", "message": "Špatný heslo, zkus to znova!"})
115
-
116
- cursor.execute("SELECT COUNT(*), SUM(fields_count) FROM invoices WHERE user_id = ?", (user_id,))
117
- stats = cursor.fetchone()
118
- docs_parsed = stats[0] or 0
119
- fields_extracted = stats[1] or 0
120
  conn.close()
121
-
122
- return json.dumps({
123
- "status": "SUCCESS",
124
- "username": username,
125
- "stats": {"docs_parsed": docs_parsed, "fields_extracted": fields_extracted}
126
- })
127
  except Exception as e:
128
- return json.dumps({"status": "ERROR", "message": f"Chyba při přihlášení: {str(e)}"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- # ─── CORE PARSER ENGINE ─────────────────────────────────────────────────────
132
  model_name = "naver-clova-ix/donut-base-finetuned-cord-v2"
133
  try:
134
  processor = DonutProcessor.from_pretrained(model_name)
@@ -136,42 +255,61 @@ try:
136
  device = "cuda" if torch.cuda.is_available() else "cpu"
137
  model.to(device)
138
  MODEL_LOADED = True
139
- print(f"Model loaded successfully on {device}")
140
  except Exception as e:
141
  MODEL_LOADED = False
142
- processor = None
143
- model = None
144
  device = "cpu"
145
- print(f"Model load warning (Using Demo Fallback): {e}")
146
 
147
- def clean_and_float(text_value):
148
  try:
149
- cleaned = re.sub(r"[^\d.,]", "", str(text_value)).replace(",", ".")
150
- return float(cleaned)
151
- except Exception:
152
  return 0.0
153
 
 
154
  def parse_invoice(image_b64: str, username: str):
155
  if not image_b64:
156
- return json.dumps({"parser_status": "ERROR", "error_log": "Nebyla přijata žádná obrazová data faktury."})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  try:
159
  header, _, data = image_b64.partition(",")
160
  img_bytes = base64.b64decode(data if data else image_b64)
161
  img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
162
  except Exception as e:
163
- return json.dumps({"parser_status": "ERROR", "error_log": f"Image decode failed: {str(e)}"})
164
 
165
  try:
166
  start_time = time.time()
167
-
168
  if MODEL_LOADED and processor and model:
169
  task_prompt = "<s_cord-v2>"
170
- decoder_input_ids = processor.tokenizer(
171
- task_prompt, add_special_tokens=False, return_tensors="pt"
172
- ).input_ids
173
  pixel_values = processor(img, return_tensors="pt").pixel_values
174
-
175
  outputs = model.generate(
176
  pixel_values.to(device),
177
  decoder_input_ids=decoder_input_ids.to(device),
@@ -182,27 +320,20 @@ def parse_invoice(image_b64: str, username: str):
182
  bad_words_ids=[[processor.tokenizer.unk_token_id]],
183
  return_dict_in_generate=True,
184
  )
185
-
186
  sequence = processor.batch_decode(outputs.sequences)[0]
187
- sequence = sequence.replace(processor.tokenizer.eos_token, "").replace(
188
- processor.tokenizer.pad_token, ""
189
- )
190
- sequence = re.sub(r"<[^>]+>", " ", sequence).strip()
191
-
192
  extracted_fields = {}
193
- lines = re.split(r"[;\n]", sequence)
194
- for line in lines:
195
  if ":" in line:
196
- parts = line.split(":", 1)
197
- key = parts[0].strip().upper().replace(" ", "_")
198
- value = parts[1].strip()
199
- if key and value:
200
- extracted_fields[key] = value
201
-
202
  if not extracted_fields:
203
  extracted_fields["RAW_TEXT"] = sequence.strip()
204
  else:
205
- # Fallback demo data
206
  extracted_fields = {
207
  "STORE_NAME": "TutaGarage Stavby s.r.o.",
208
  "TOTAL": "24500.00",
@@ -214,736 +345,938 @@ def parse_invoice(image_b64: str, username: str):
214
 
215
  elapsed = round(time.time() - start_time, 2)
216
  confidence_score = round(random.uniform(0.95, 0.99), 2)
217
- human_review = confidence_score < 0.96
218
 
219
- invoice_fp = extracted_fields.get("INVOICE_NUMBER", extracted_fields.get("TOTAL", "UNKNOWN"))
220
-
221
- # Kontrola duplicity v rámci přihlášeného uživatele (pokud to není host)
222
  is_duplicate = False
223
- is_guest = (not username or username.strip().lower() == "guest")
224
-
225
- if not is_guest and invoice_fp != "UNKNOWN":
226
  try:
227
  conn = sqlite3.connect(DB_FILE)
228
- cursor = conn.cursor()
229
- cursor.execute("SELECT id FROM users WHERE username = ?", (username.strip().lower(),))
230
- u_row = cursor.fetchone()
231
- if u_row:
232
- user_id = u_row[0]
233
- cursor.execute("SELECT id FROM invoices WHERE user_id = ? AND extracted_json LIKE ?", (user_id, f'%"{invoice_fp}"%'))
234
- if cursor.fetchone():
235
- is_duplicate = True
236
  conn.close()
237
- except Exception:
238
  pass
239
 
240
- subtotal_val = clean_and_float(extracted_fields.get("SUBTOTAL", "0"))
241
- tax_val = clean_and_float(extracted_fields.get("TAX", "0"))
242
- total_val = clean_and_float(extracted_fields.get("TOTAL", "0"))
243
-
244
  validation_status = "PASSED"
245
  if subtotal_val > 0 and total_val > 0:
246
  if abs((subtotal_val + tax_val) - total_val) > 1.0:
247
- validation_status = "WARNING: Math discrepancy detected"
248
 
249
  result = {
250
  "parser_status": "SUCCESS",
251
  "parse_time_seconds": elapsed,
 
252
  "security_and_compliance": {
253
  "confidence_score": confidence_score,
254
- "human_review_required": human_review,
255
  "duplicate_detected": is_duplicate,
256
  "cross_field_validation": validation_status,
257
  },
258
  "extracted_data": extracted_fields,
259
  }
260
 
261
- # Zápis do DB pouze pokud uživatel NENÍ host
262
- if not is_guest:
263
- fields_count = len(extracted_fields)
264
  try:
265
  conn = sqlite3.connect(DB_FILE)
266
- cursor = conn.cursor()
267
- cursor.execute("SELECT id FROM users WHERE username = ?", (username.strip().lower(),))
268
- u_row = cursor.fetchone()
269
- if u_row:
270
- user_id = u_row[0]
271
- cursor.execute(
272
- "INSERT INTO invoices (user_id, timestamp, filename, extracted_json, fields_count) VALUES (?, ?, ?, ?, ?)",
273
- (user_id, time.time(), "uploaded_doc.png", json.dumps(result), fields_count)
274
- )
275
- conn.commit()
276
-
277
- cursor.execute("SELECT COUNT(*), SUM(fields_count) FROM invoices WHERE user_id = ?", (user_id,))
278
- count_row = cursor.fetchone()
279
- result["user_stats"] = {
280
- "docs_parsed": count_row[0] or 0,
281
- "fields_extracted": count_row[1] or 0
282
- }
283
  conn.close()
284
- except Exception as db_err:
285
- print(f"DB Insert failure: {db_err}")
 
 
 
286
  else:
287
- # Pro hosta vrátíme flag, JS si spočítá lokální session statistiky sám
288
- result["user_stats"] = {
289
- "docs_parsed": "GUEST_MODE",
290
- "fields_extracted": "GUEST_MODE"
291
- }
292
-
293
- # Generování CSV exportu
294
- rows = [["Field", "Value"]]
295
- rows.append(["confidence_score", str(confidence_score)])
296
- rows.append(["human_review_required", str(human_review)])
297
- rows.append(["duplicate_detected", str(is_duplicate)])
298
- rows.append(["cross_field_validation", validation_status])
299
- rows.append(["parse_time_seconds", str(elapsed)])
300
- for k, v in extracted_fields.items():
301
- rows.append([k, v])
302
-
303
- csv_path = "/tmp/omniparse_result.csv"
304
- df = pd.DataFrame(rows[1:], columns=rows[0])
305
- df.to_csv(csv_path, index=False)
306
-
307
- with open(csv_path, "rb") as f:
308
- csv_b64 = base64.b64encode(f.read()).decode()
309
-
310
- result["_csv_b64"] = csv_b64
311
  return json.dumps(result)
312
 
313
  except Exception as e:
314
- return json.dumps({"parser_status": "ERROR", "error_log": str(e)})
315
 
316
 
317
- def agent_chat(user_message: str, extracted_json: str) -> str:
318
  if not user_message.strip():
319
  return ""
 
 
 
 
 
 
 
 
 
320
 
321
  try:
322
  data = json.loads(extracted_json) if extracted_json else {}
323
  fields = data.get("extracted_data", {})
324
  compliance = data.get("security_and_compliance", {})
325
- except Exception:
326
  fields = {}
327
  compliance = {}
328
 
329
  if not fields:
330
- return "Nejdřív nahraj a vytěž nějakou fakturu, abych měl z čeho brát data, šéfe."
331
 
332
  msg = user_message.lower()
333
  matched = []
334
  for key, val in fields.items():
335
- clean_key = key.lower().replace("_", " ")
336
- if clean_key in msg or any(word in clean_key for word in msg.split() if len(word) > 3):
337
  matched.append(f"**{key}**: {val}")
338
-
339
  if matched:
340
- return "Našel jsem tyhle shody:\n" + "\n".join(f"• {m}" for m in matched[:5])
341
-
342
- if any(w in msg for w in ["total", "cena", "castka", "suma", "celkem", "amount"]):
343
- total = fields.get("TOTAL", fields.get("TOTAL_PRICE", "nenalezeno"))
344
- subtotal = fields.get("SUBTOTAL", "—")
345
- tax = fields.get("TAX", "—")
346
- return f"💰 **Celková částka**: {total}\n• Základ: {subtotal}\n• Daň/DPH: {tax}"
347
-
348
- if any(w in msg for w in ["vendor", "firma", "kdo", "dodavatel", "spolecnost", "seller", "store"]):
349
- vendor = fields.get("STORE_NAME", fields.get("VENDOR", fields.get("COMPANY", "nenalezeno")))
350
- return f"🏢 **Dodavatel**: {vendor}"
351
-
352
- if any(w in msg for w in ["date", "datum", "kdy", "vystaveni"]):
353
- date = fields.get("DATE", fields.get("INVOICE_DATE", "nenalezeno"))
354
- return f"📅 **Datum dokladu**: {date}"
355
-
356
- if any(w in msg for w in ["confidence", "skore", "spolehlivost", "presnost"]):
357
- score = compliance.get("confidence_score", "—")
358
- return f"🎯 **Spolehlivost modelu**: {score} ({float(score)*100:.0f}% jistota)" if score != "—" else "Skóre nedostupné."
359
-
360
- if any(w in msg for w in ["all", "vsechno", "pole", "data", "vypis"]):
361
- lines = [f"• **{k}**: {v}" for k, v in list(fields.items())[:10]]
362
- return "📋 **Kompletní extrahovaná data (Top 10)**:\n" + "\n".join(lines)
363
-
364
- keys = list(fields.keys())[:6]
365
- return f"Mám vyextrahováno {len(fields)} polí. Zeptej se mě třeba na: {', '.join(keys[:5])}."
366
-
367
-
368
- # ─── GRADIO WRAPPER BRIDGES ─────────────────────────────────────────────────
369
  def gr_parse(image_b64, username):
370
  return parse_invoice(image_b64, username)
371
 
372
- def gr_chat(user_msg, extracted_json):
373
- return agent_chat(user_msg, extracted_json)
374
 
 
 
375
 
376
- # ─── STYLING & INTERACTION LAYER ────────────────────────────────────────────
 
377
  CUSTOM_CSS = """
378
- body { margin: 0 !important; padding: 0 !important; background: #09090b !important; }
379
- .gradio-container { max-width: 100% !important; padding: 0 !important; margin: 0 !important; background: #09090b !important; }
380
- footer { display: none !important; }
381
- #hidden-controls { display: none !important; }
382
  """
383
 
384
- HTML_UI = """
 
385
  <!DOCTYPE html>
386
  <html lang="cs">
387
  <head>
388
  <meta charset="UTF-8"/>
389
  <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
390
  <script src="https://cdn.tailwindcss.com"></script>
391
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
392
  <style>
393
  *{box-sizing:border-box;margin:0;padding:0;}
394
  :root{
395
- --bg: #09090b;
396
- --surface: #111113;
397
- --surface2: #18181b;
398
- --border: #27272a;
399
- --border2: #3f3f46;
400
- --text: #e4e4e7;
401
- --text2: #a1a1aa;
402
- --text3: #71717a;
403
- --text4: #52525b;
404
- --blue: #3b82f6;
405
- --blue-light: #60a5fa;
406
- --blue-dark: #1d4ed8;
407
- --green: #34d399;
408
  }
409
  body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);}
410
  .card{background:var(--surface2);border:1px solid var(--border);border-radius:14px;}
411
- .section-label{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--text4);}
 
412
  .drop-zone{border:1.5px dashed var(--border2);border-radius:12px;transition:all .25s ease;cursor:pointer;}
413
- .drop-zone:hover,.drop-zone.drag-over{border-color:var(--blue);background:rgba(59,130,246,.05);}
414
- .btn-primary{background:linear-gradient(135deg,var(--blue) 0%,var(--blue-dark) 100%);color:#fff;border:none;border-radius:12px;font-weight:600;font-size:14px;cursor:pointer;transition:all .2s ease;}
415
- .btn-primary:hover:not(:disabled){filter:brightness(1.1);transform:translateY(-1px);}
416
- .btn-primary:disabled{opacity:.45;cursor:not-allowed;}
417
- .btn-secondary{background:var(--surface2);border:1px solid var(--border2);color:var(--text2);border-radius:12px;font-size:14px;font-weight:500;cursor:pointer;transition:all .18s ease;}
418
  .btn-secondary:hover{border-color:var(--blue-light);color:var(--text);}
419
- .chat-input{background:var(--surface2);border:1px solid var(--border2);color:var(--text);border-radius:10px;padding:10px 14px;font-size:14px;outline:none;width:100%;}
420
- .chat-input:focus{border-color:var(--blue);}
 
 
 
 
421
  .code-view{background:#0d1117;border:1px solid #21262d;border-radius:10px;font-family:'JetBrains Mono',monospace;}
422
- .dl-card{background:linear-gradient(135deg,rgba(59,130,246,.07) 0%,rgba(29,78,216,.04) 100%);border:1px solid rgba(59,130,246,.2);border-radius:12px;}
423
  .grad{background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}
424
- .spin{animation:spin 0.8s linear infinite;}
 
425
  @keyframes spin{to{transform:rotate(360deg);}}
426
- .toast{animation:slideIn .3s ease;}
427
  @keyframes slideIn{from{transform:translateX(110%);opacity:0;}to{transform:translateX(0);opacity:1;}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  </style>
429
  </head>
430
  <body>
431
 
432
- <div id="toast-root" style="position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;"></div>
433
 
434
- <!-- ═══ 1. AUTHENTICATION OVERLAY SCREEN ═══ -->
435
  <div id="auth-screen" class="fixed inset-0 z-50 flex items-center justify-center bg-[#09090b] p-4">
436
- <div class="card w-full max-w-md p-8 space-y-5">
437
  <div class="text-center space-y-2">
438
- <div class="mx-auto w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center shadow-lg shadow-blue-500/25">
439
  <svg width="22" height="22" viewBox="0 0 16 16" fill="none"><path d="M2 3h12M2 6h8M2 9h10M2 12h6" stroke="white" stroke-width="1.5" stroke-linecap="round"/><circle cx="13" cy="11" r="2.5" stroke="white" stroke-width="1.2"/><path d="M15 13l1.5 1.5" stroke="white" stroke-width="1.2" stroke-linecap="round"/></svg>
440
  </div>
441
- <h2 class="text-2xl font-bold tracking-tight text-white">OmniParse <span class="grad">AI</span></h2>
442
- <p class="text-xs text-zinc-400">Multi-User Document Data-Mining Terminal</p>
443
  </div>
444
-
 
 
 
 
 
 
445
  <div class="space-y-3">
446
  <div>
447
  <label class="block text-[10px] font-bold uppercase tracking-wider text-zinc-500 mb-1">Uživatelské jméno</label>
448
- <input type="text" id="auth-username" class="chat-input text-xs" placeholder="Zadej login...">
 
 
 
 
449
  </div>
450
  <div>
451
  <label class="block text-[10px] font-bold uppercase tracking-wider text-zinc-500 mb-1">Heslo</label>
452
- <input type="password" id="auth-password" class="chat-input text-xs" placeholder="••••••••">
 
 
 
 
 
 
 
 
 
 
 
453
  </div>
454
  </div>
455
-
456
- <div class="flex flex-col gap-2 pt-1">
457
- <button onclick="handleAuthAction('login')" class="btn-primary w-full py-2.5 font-semibold text-xs">
458
- Přihlásit se do systému ⚡
459
- </button>
460
- <button onclick="handleAuthAction('register')" class="btn-secondary w-full py-2.5 text-xs">
461
- Registrovat nový cloudový účet
462
  </button>
463
-
464
  <div class="relative flex py-2 items-center">
465
  <div class="flex-grow border-t border-zinc-800"></div>
466
  <span class="flex-shrink mx-3 text-[10px] text-zinc-600 font-bold uppercase">Nebo</span>
467
  <div class="flex-grow border-t border-zinc-800"></div>
468
  </div>
469
-
470
- <button onclick="bypassAuthAsGuest()" class="w-full py-2 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-zinc-300 rounded-xl text-xs font-semibold transition-all">
471
- Pokračovat bez registrace (Free Plan) →
472
  </button>
473
  </div>
474
  </div>
475
  </div>
476
 
477
 
478
- <!-- ═══ 2. MAIN APPLICATION WORKSPACE ═══ -->
479
- <div id="app-screen" style="display: none;">
480
- <header style="border-bottom:1px solid var(--border);background:rgba(9,9,11,.92);backdrop-filter:blur(16px); position:sticky; top:0; z-index:40;">
481
- <div style="max-width:1280px;margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  <div style="display:flex;align-items:center;gap:12px;">
483
- <div style="width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;">
484
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 3h12M2 6h8M2 9h10M2 12h6" stroke="white" stroke-width="1.5" stroke-linecap="round"/><circle cx="13" cy="11" r="2.5" stroke="white" stroke-width="1.2"/><path d="M15 13l1.5 1.5" stroke="white" stroke-width="1.2" stroke-linecap="round"/></svg>
485
  </div>
486
- <span style="color:#fff;font-weight:600;font-size:15px;">OmniParse <span class="grad">AI</span></span>
 
487
  </div>
488
-
489
- <div style="display:flex;align-items:center;gap:16px;">
490
- <div style="display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:8px;background:var(--surface2);border:1px solid var(--border);">
491
- <span id="status-dot" style="width:8px;height:8px;border-radius:50%;background:#34d399;"></span>
492
- <span id="status-text" style="font-size:12px;color:var(--text2);font-weight:500;">System Ready</span>
493
  </div>
494
- <span id="user-display" class="text-sm font-medium"></span>
495
- <button onclick="handleLogout()" class="text-xs text-red-400 hover:underline">Zpět na login / Odhlásit</button>
 
496
  </div>
497
  </div>
498
  </header>
499
-
500
- <main style="max-width:1280px;margin:0 auto;padding:24px;">
501
- <!-- Stats Banner -->
502
- <div class="grid grid-cols-4 gap-3 mb-5">
 
503
  <div class="card p-4 flex items-center gap-3">
504
- <div class="text-2xl">📄</div>
505
- <div><p class="text-[10px] text-zinc-500 uppercase font-bold">Zpracované dokumenty</p><p id="st-docs" class="text-lg font-bold text-white">0</p></div>
 
 
 
506
  </div>
507
  <div class="card p-4 flex items-center gap-3">
508
- <div class="text-2xl">⚡</div>
509
- <div><p class="text-[10px] text-zinc-500 uppercase font-bold">Vytěžená pole</p><p id="st-fields" class="text-lg font-bold text-white">0</p></div>
 
 
 
510
  </div>
511
  <div class="card p-4 flex items-center gap-3">
512
- <div class="text-2xl">⏱️</div>
513
- <div><p class="text-[10px] text-zinc-500 uppercase font-bold">Čas operace</p><p id="st-time" class="text-lg font-bold text-white">—</p></div>
 
 
 
514
  </div>
515
  <div class="card p-4 flex items-center gap-3">
516
- <div class="text-2xl">🎯</div>
517
- <div><p class="text-[10px] text-zinc-500 uppercase font-bold">Spolehlivost</p><p id="st-conf" class="text-lg font-bold text-white">—</p></div>
 
 
 
518
  </div>
519
  </div>
520
-
521
- <!-- Workspace Grid -->
 
 
 
 
 
 
 
 
 
 
 
 
522
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
523
- <!-- Left Controls -->
524
- <div class="space-y-4">
525
  <div class="card p-5 space-y-4">
526
- <div class="flex justify-between items-center">
527
- <span class="text-sm font-semibold">Vstupní dokument</span>
528
  <span class="section-label">Stage 01</span>
529
  </div>
530
-
531
- <div id="drop-zone" class="drop-zone p-8 flex flex-col items-center justify-center gap-3 text-center min-h-[160px]"
532
  onclick="document.getElementById('file-input').click()"
533
  ondragover="event.preventDefault();this.classList.add('drag-over')"
534
  ondragleave="this.classList.remove('drag-over')"
535
  ondrop="handleDrop(event)">
536
- <input type="file" id="file-input" style="display:none;" accept="image/*" onchange="handleFiles(this.files)"/>
537
- <div class="text-3xl text-zinc-500">📥</div>
538
- <p class="text-sm text-zinc-300">Hoď soubor sem nebo <span class="text-blue-400 underline">klikni pro vyhledání</span></p>
 
539
  </div>
540
-
541
  <div id="file-preview" style="display:none;" class="border border-zinc-800 p-2 rounded-lg">
542
- <img id="img-preview" style="width:100%; max-height:180px; object-fit:contain; border-radius:6px;"/>
543
  </div>
544
  </div>
545
-
546
  <div class="card p-4">
547
- <button id="exec-btn" class="btn-primary w-full py-4 flex items-center justify-center gap-2" disabled onclick="executeExtraction()">
548
- <span id="btn-idle">Spustit AI extrakci dat z faktury ⚡</span>
 
 
 
549
  <span id="btn-loading" style="display:none;" class="flex items-center gap-2">
550
- <svg class="spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
551
- Extrahuji data z dokumentu...
552
  </span>
553
  </button>
554
  </div>
555
-
 
556
  <div class="card p-5 space-y-3">
557
- <div class="flex justify-between items-center"><span class="text-sm font-semibold">Export dat</span><span class="section-label">Stage 03</span></div>
558
- <div id="export-actions" style="display:none;" class="w-full">
559
- <button onclick="downloadCSV()" class="dl-card w-full p-3 flex justify-between items-center text-sm font-medium text-zinc-200">
560
- <span>📊 Exportovat do datové struktury CSV</span>
561
- <span class="text-xs text-blue-400 bg-blue-950/50 px-2 py-1 rounded">STÁHNOUT</span>
 
 
 
562
  </button>
563
  </div>
564
- <div id="export-empty" class="text-xs text-zinc-500 text-center py-2">Čekám na zpracování dokumentu...</div>
 
 
 
 
565
  </div>
566
  </div>
567
-
568
- <!-- Right Terminal & Terminal Assist -->
569
- <div class="space-y-4">
570
  <div class="card p-5 space-y-3">
571
- <div class="flex justify-between items-center"><span class="text-sm font-semibold">Live Extraction Terminal</span><span class="section-label">Output JSON</span></div>
572
- <div class="code-view p-4 min-h-[160px] max-h-[220px] overflow-y-auto">
573
- <pre id="json-display" class="text-xs text-zinc-400 leading-relaxed font-mono">// Terminál je prázdný.</pre>
 
 
 
574
  </div>
575
  </div>
576
-
577
- <div class="grid grid-cols-2 gap-3">
578
  <div class="card p-4 space-y-1">
579
- <p class="text-[10px] uppercase font-bold tracking-wider text-zinc-500">Validace DPH / Shody</p>
580
- <p id="comp-val" class="text-xs font-semibold text-zinc-300">—</p>
581
  </div>
582
  <div class="card p-4 space-y-1">
583
- <p class="text-[10px] uppercase font-bold tracking-wider text-zinc-500">Detekce duplicity</p>
584
- <p id="comp-dup" class="text-xs font-semibold text-zinc-300">—</p>
585
  </div>
586
  </div>
587
-
588
- <div class="card p-5 flex flex-col h-[280px]">
589
- <p class="text-xs font-bold uppercase tracking-wider text-zinc-400 mb-2">AI Audit Companion</p>
590
- <div id="chat-history" class="flex-1 overflow-y-auto space-y-2 mb-3 pr-1 text-xs">
591
- <div class="p-2 rounded bg-blue-950/20 border border-blue-900/30 text-zinc-300">
592
- Systém je připraven. Zeptej se mě na cokoliv ohledně položek faktury.
 
 
 
 
593
  </div>
594
  </div>
595
- <div class="flex gap-2">
596
- <input type="text" id="chat-input" class="chat-input text-xs" placeholder="Napiš dotaz k faktuře..." onkeydown="if(event.key==='Enter') sendChatMessage()"/>
597
- <button onclick="sendChatMessage()" class="btn-primary px-3 text-xs font-bold">Odeslat</button>
598
  </div>
599
  </div>
600
  </div>
601
  </div>
 
 
 
 
 
 
 
602
  </main>
603
  </div>
604
 
605
- <!-- ═══ 3. GRADIO PIPELINE JAVASCRIPT BRIDGE ═══ -->
 
606
  <script>
 
607
  let currentBase64 = "";
608
  let latestExtractedJson = "";
609
  let csvB64Data = "";
610
- let authPolling = null;
611
- let parsePolling = null;
612
- let chatPolling = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
- // Lokální paměť statistik pro uživatele na Free Planu (neukládá se do DB)
615
- let guestDocsCount = 0;
616
- let guestFieldsCount = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
 
618
- function showToast(msg, type='info') {
619
- const root = document.getElementById("toast-root");
620
- const toast = document.createElement("div");
621
- toast.className = `toast card px-4 py-3 rounded-xl flex items-center gap-3 text-xs shadow-2xl text-white ${type === 'error' ? 'bg-red-950/80 border-red-800' : 'bg-zinc-900/90 border-zinc-700'}`;
622
- toast.innerHTML = `<span>${type === 'error' ? '❌' : '✅'}</span><span>${msg}</span>`;
623
- root.appendChild(toast);
624
- setTimeout(() => {
625
- toast.style.opacity = '0';
626
- setTimeout(() => toast.remove(), 300);
627
- }, 4000);
628
  }
629
 
630
- function checkLoginState() {
631
- const user = sessionStorage.getItem("omniparse_user");
632
- if (user) {
633
- document.getElementById("auth-screen").style.display = "none";
634
- document.getElementById("app-screen").style.display = "block";
635
-
636
- if (user.toLowerCase() === "guest") {
637
- document.getElementById("user-display").className = "text-sm font-medium text-amber-400";
638
- document.getElementById("user-display").innerText = "👤 Free Plan (Neregistrován)";
639
- document.getElementById("st-docs").innerText = guestDocsCount + " (Free)";
640
- document.getElementById("st-fields").innerText = guestFieldsCount + " (Místní)";
641
- } else {
642
- document.getElementById("user-display").className = "text-sm font-medium text-blue-400";
643
- document.getElementById("user-display").innerText = `👤 ${user}`;
644
-
645
- // Refresh cloudu z DB pro registrované
646
- const grUser = document.querySelector("#gr-auth-user textarea");
647
- const grPass = document.querySelector("#gr-auth-pass textarea");
648
- if (grUser && grPass) {
649
- grUser.value = user;
650
- grUser.dispatchEvent(new Event("input", { bubbles: true }));
651
- grPass.value = "SESSION_REFRESH";
652
- grPass.dispatchEvent(new Event("input", { bubbles: true }));
653
- document.querySelector("#gr-login-btn").click();
654
- }
655
- if(authPolling) clearInterval(authPolling);
656
- authPolling = setInterval(checkAuthResponse, 150);
657
- }
658
- } else {
659
- document.getElementById("auth-screen").style.display = "flex";
660
- document.getElementById("app-screen").style.display = "none";
661
- }
662
  }
663
 
664
  function bypassAuthAsGuest() {
665
- sessionStorage.setItem("omniparse_user", "Guest");
666
- showToast("Vstupuješ jako host v bezplatném režimu.");
667
- checkLoginState();
 
668
  }
669
 
670
  function handleAuthAction(action) {
671
- const userVal = document.getElementById("auth-username").value.trim();
672
- const passVal = document.getElementById("auth-password").value;
673
-
674
- if(!userVal || !passVal) {
675
- showToast("Musíš vyplnit obě pole!", "error");
676
- return;
677
- }
678
-
679
- const grUser = document.querySelector("#gr-auth-user textarea");
680
- const grPass = document.querySelector("#gr-auth-pass textarea");
681
-
682
- if(!grUser || !grPass) return;
683
-
684
- grUser.value = userVal;
685
- grUser.dispatchEvent(new Event("input", { bubbles: true }));
686
- grPass.value = passVal;
687
- grPass.dispatchEvent(new Event("input", { bubbles: true }));
688
-
689
- if (action === 'login') {
690
- document.querySelector("#gr-login-btn").click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
691
  } else {
692
- document.querySelector("#gr-register-btn").click();
 
 
 
 
 
 
 
 
 
 
 
 
693
  }
694
-
695
- if(authPolling) clearInterval(authPolling);
696
- authPolling = setInterval(checkAuthResponse, 150);
 
697
  }
698
 
699
  function checkAuthResponse() {
700
- const respEl = document.querySelector("#gr-auth-response textarea");
701
- if (!respEl) return;
702
- const val = respEl.value;
703
- if (val && val.trim() !== "") {
704
- clearInterval(authPolling);
705
- authPolling = null;
706
- respEl.value = "";
707
- respEl.dispatchEvent(new Event("input", { bubbles: true }));
708
- try {
709
- const data = JSON.parse(val);
710
- if (data.status === "SUCCESS") {
711
- if (data.username) {
712
- sessionStorage.setItem("omniparse_user", data.username);
713
- showToast("Přihlášení úspěšné!");
714
- document.getElementById("auth-screen").style.display = "none";
715
- document.getElementById("app-screen").style.display = "block";
716
- document.getElementById("user-display").className = "text-sm font-medium text-blue-400";
717
- document.getElementById("user-display").innerText = `👤 ${data.username}`;
718
- if (data.stats) {
719
- document.getElementById("st-docs").innerText = data.stats.docs_parsed;
720
- document.getElementById("st-fields").innerText = data.stats.fields_extracted;
721
- }
722
- } else {
723
- showToast(data.message);
724
- }
725
- } else {
726
- showToast(data.message, "error");
727
- }
728
- } catch(e) {
729
- console.error("Auth json error", e);
730
  }
731
- }
 
 
 
 
732
  }
733
 
734
- function handleLogout() {
735
- sessionStorage.clear();
736
- location.reload();
737
  }
738
 
739
- function handleFiles(files) {
740
- if (!files.length) return;
741
- const file = files[0];
742
- const reader = new FileReader();
743
- reader.onload = function(e) {
744
- currentBase64 = e.target.result;
745
- document.getElementById("img-preview").src = currentBase64;
746
- document.getElementById("file-preview").style.display = "block";
747
- document.getElementById("exec-btn").disabled = false;
748
- showToast(`Doklad načten do cache.`);
749
- };
750
- reader.readAsDataURL(file);
 
 
 
 
751
  }
752
 
753
- function handleDrop(e) {
754
- e.preventDefault();
755
- handleFiles(e.dataTransfer.files);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  }
757
 
758
- function executeExtraction() {
759
- if (!currentBase64) return;
760
-
761
- document.getElementById("btn-idle").style.display = "none";
762
- document.getElementById("btn-loading").style.display = "flex";
763
- document.getElementById("status-text").innerText = "Těžím data...";
764
-
765
- const user = sessionStorage.getItem("omniparse_user") || "Guest";
766
- const grImg = document.querySelector("#gr-image-input textarea");
767
- const grUser = document.querySelector("#gr-current-user textarea");
768
-
769
- grImg.value = currentBase64;
770
- grImg.dispatchEvent(new Event("input", { bubbles: true }));
771
- grUser.value = user;
772
- grUser.dispatchEvent(new Event("input", { bubbles: true }));
773
-
774
- document.querySelector("#gr-parse-btn").click();
775
-
776
- if(parsePolling) clearInterval(parsePolling);
777
- parsePolling = setInterval(checkParseResponse, 200);
778
  }
779
 
780
- function checkParseResponse() {
781
- const outEl = document.querySelector("#gr-json-output textarea");
782
- if (!outEl) return;
783
- const val = outEl.value;
784
- if (val && val.trim() !== "") {
785
- clearInterval(parsePolling);
786
- parsePolling = null;
787
- outEl.value = "";
788
- outEl.dispatchEvent(new Event("input", { bubbles: true }));
789
-
790
- document.getElementById("btn-idle").style.display = "flex";
791
- document.getElementById("btn-loading").style.display = "none";
792
- document.getElementById("status-text").innerText = "System Idle";
793
-
794
- try {
795
- const res = JSON.parse(val);
796
- if (res.parser_status === "SUCCESS") {
797
- latestExtractedJson = JSON.stringify(res, null, 2);
798
- document.getElementById("json-display").innerText = latestExtractedJson;
799
- document.getElementById("st-time").innerText = `${res.parse_time_seconds}s`;
800
- document.getElementById("st-conf").innerText = `${Math.round(res.security_and_compliance.confidence_score * 100)}%`;
801
- document.getElementById("comp-val").innerText = res.security_and_compliance.cross_field_validation;
802
-
803
- // Zobrazení duplicity
804
- if (sessionStorage.getItem("omniparse_user").toLowerCase() === "guest") {
805
- document.getElementById("comp-dup").innerText = "🔒 Vyžaduje registraci";
806
- } else {
807
- document.getElementById("comp-dup").innerText = res.security_and_compliance.duplicate_detected ? "⚠️ POTENCIÁLNÍ DUPLICITA" : "✅ OK (Unikátní)";
808
- }
809
-
810
- // Počítání a update statistik na panelu
811
- if (sessionStorage.getItem("omniparse_user").toLowerCase() === "guest") {
812
- guestDocsCount++;
813
- guestFieldsCount += Object.keys(res.extracted_data).length;
814
- document.getElementById("st-docs").innerText = guestDocsCount + " (Free)";
815
- document.getElementById("st-fields").innerText = guestFieldsCount + " (Místní)";
816
- } else {
817
- if (res.user_stats) {
818
- document.getElementById("st-docs").innerText = res.user_stats.docs_parsed;
819
- document.getElementById("st-fields").innerText = res.user_stats.fields_extracted;
820
- }
821
- }
822
-
823
- if (res._csv_b64) {
824
- csvB64Data = res._csv_b64;
825
- document.getElementById("export-actions").style.display = "block";
826
- document.getElementById("export-empty").style.display = "none";
827
- }
828
- showToast("Data úspěšně vytěžena!");
829
- } else {
830
- showToast("Chyba: " + res.error_log, "error");
831
- }
832
- } catch(e) {
833
- showToast("Nelze dekódovat výstup.", "error");
 
 
 
 
 
 
 
 
 
 
834
  }
835
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
836
  }
837
 
838
- function downloadCSV() {
839
- if (!csvB64Data) return;
840
- const binStr = atob(csvB64Data);
841
- const bytes = new Uint8Array(binStr.length);
842
- for (let i = 0; i < binStr.length; i++) {
843
- bytes[i] = binStr.charCodeAt(i);
844
- }
845
- const blob = new Blob([bytes], { type: "text/csv" });
846
- const url = URL.createObjectURL(blob);
847
- const a = document.createElement("a");
848
- a.href = url;
849
- a.download = "omniparse_export.csv";
850
- document.body.appendChild(a);
851
- a.click();
852
- document.body.removeChild(a);
853
  }
854
 
855
- function sendChatMessage() {
856
- const inputEl = document.getElementById("chat-input");
857
- const msg = inputEl.value.trim();
858
- if (!msg) return;
859
-
860
- inputEl.value = "";
861
- const hist = document.getElementById("chat-history");
862
- hist.innerHTML += `<div class="p-2 rounded bg-zinc-800 text-zinc-100 text-right font-medium">Vy: ${msg}</div>`;
863
- hist.scrollTop = hist.scrollHeight;
864
-
865
- const grMsg = document.querySelector("#gr-chat-msg textarea");
866
- const grJson = document.querySelector("#gr-chat-json textarea");
867
-
868
- grMsg.value = msg;
869
- grMsg.dispatchEvent(new Event("input", { bubbles: true }));
870
- grJson.value = latestExtractedJson;
871
- grJson.dispatchEvent(new Event("input", { bubbles: true }));
872
-
873
- document.querySelector("#gr-chat-btn").click();
874
-
875
- if (chatPolling) clearInterval(chatPolling);
876
- chatPolling = setInterval(checkChatResponse, 150);
 
 
 
877
  }
878
 
879
- function checkChatResponse() {
880
- const outEl = document.querySelector("#gr-chat-out textarea");
881
- if (!outEl) return;
882
- const val = outEl.value;
883
- if (val && val.trim() !== "") {
884
- clearInterval(chatPolling);
885
- chatPolling = null;
886
- outEl.value = "";
887
- outEl.dispatchEvent(new Event("input", { bubbles: true }));
888
-
889
- const hist = document.getElementById("chat-history");
890
- hist.innerHTML += `<div class="p-2 rounded bg-blue-900/10 border border-blue-900/20 text-zinc-300">AI: ${val}</div>`;
891
- hist.scrollTop = hist.scrollHeight;
892
- }
893
  }
894
 
895
- setTimeout(checkLoginState, 500);
 
 
 
896
  </script>
897
  </body>
898
  </html>
899
  """
900
 
901
- # ─── GRADIO APPLICATION CONTAINER ───────────────────────────────────────────
902
- with gr.Blocks(css=CUSTOM_CSS, title="OmniParse AI Enterprise Terminal") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
  gr.HTML(HTML_UI)
904
 
905
- # Skryté ovládací prvky (Bridge)
906
  with gr.Row(elem_id="hidden-controls", visible=False):
907
  with gr.Column():
908
- gr_image_input = gr.Textbox(label="image_b64", elem_id="gr-image-input", visible=False)
909
- gr_current_user = gr.Textbox(label="current_user", elem_id="gr-current-user", visible=False)
910
- gr_parse_btn = gr.Button("parse", elem_id="gr-parse-btn", visible=False)
911
- gr_json_output = gr.Textbox(label="json_out", elem_id="gr-json-output", visible=False)
912
-
913
- gr_chat_msg = gr.Textbox(label="chat_msg", elem_id="gr-chat-msg", visible=False)
914
- gr_chat_json = gr.Textbox(label="chat_json", elem_id="gr-chat-json", visible=False)
915
- gr_chat_btn = gr.Button("chat", elem_id="gr-chat-btn", visible=False)
916
- gr_chat_out = gr.Textbox(label="chat_out", elem_id="gr-chat-out", visible=False)
917
-
918
- gr_auth_user = gr.Textbox(label="auth_user", elem_id="gr-auth-user", visible=False)
919
- gr_auth_pass = gr.Textbox(label="auth_pass", elem_id="gr-auth-pass", visible=False)
920
- gr_login_btn = gr.Button("login", elem_id="gr-login-btn", visible=False)
921
- gr_register_btn = gr.Button("register", elem_id="gr-register-btn", visible=False)
922
- gr_auth_response = gr.Textbox(label="auth_response", elem_id="gr-auth-response", visible=False)
923
-
924
- gr_parse_btn.click(
925
- fn=gr_parse,
926
- inputs=[gr_image_input, gr_current_user],
927
- outputs=[gr_json_output],
928
- )
929
-
930
- gr_chat_btn.click(
931
- fn=gr_chat,
932
- inputs=[gr_chat_msg, gr_chat_json],
933
- outputs=[gr_chat_out],
934
- )
935
-
936
- gr_login_btn.click(
937
- fn=db_login,
938
- inputs=[gr_auth_user, gr_auth_pass],
939
- outputs=[gr_auth_response]
940
- )
941
-
942
- gr_register_btn.click(
943
- fn=db_register,
944
- inputs=[gr_auth_user, gr_auth_pass],
945
- outputs=[gr_auth_response]
946
- )
947
 
948
  if __name__ == "__main__":
949
  demo.launch()
 
12
  import time
13
  import sqlite3
14
  import hashlib
15
+ import stripe
16
 
17
+ print("Initializing OmniParse AI Enterprise v6.0...")
18
 
19
+ # ─── CONFIG ─────────────────────────────────────────────────────────────────
20
  DB_FILE = "omniparse.db"
21
+ STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY", "")
22
+ STRIPE_WEBHOOK_SECRET = os.environ.get("STRIPE_WEBHOOK_SECRET", "")
23
+ STRIPE_PRICE_ID = os.environ.get("STRIPE_PRICE_ID", "") # Premium monthly price ID
24
+ APP_URL = os.environ.get("APP_URL", "https://your-space.hf.space")
25
+
26
+ stripe.api_key = STRIPE_SECRET_KEY
27
+
28
+ # ─── PLAN LIMITS ────────────────────────────────────────────────────────────
29
+ PLAN_LIMITS = {
30
+ "free": {
31
+ "docs_per_day": 3,
32
+ "chat_enabled": False,
33
+ "export_enabled": False,
34
+ "duplicate_check": False,
35
+ "history_days": 0,
36
+ },
37
+ "registered": {
38
+ "docs_per_day": 20,
39
+ "chat_enabled": True,
40
+ "export_enabled": True,
41
+ "duplicate_check": True,
42
+ "history_days": 30,
43
+ },
44
+ "premium": {
45
+ "docs_per_day": 999,
46
+ "chat_enabled": True,
47
+ "export_enabled": True,
48
+ "duplicate_check": True,
49
+ "history_days": 365,
50
+ },
51
+ }
52
 
53
+ # ─── DATABASE ────────────────────────────────────────────────────────────────
54
  def init_db():
55
  conn = sqlite3.connect(DB_FILE)
56
+ c = conn.cursor()
57
+ c.execute('''CREATE TABLE IF NOT EXISTS users (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ username TEXT UNIQUE NOT NULL,
60
+ email TEXT,
61
+ password_hash TEXT NOT NULL,
62
+ salt TEXT NOT NULL,
63
+ plan TEXT DEFAULT 'registered',
64
+ stripe_customer_id TEXT,
65
+ stripe_subscription_id TEXT,
66
+ subscription_status TEXT DEFAULT 'inactive',
67
+ created_at REAL DEFAULT (strftime('%s','now')),
68
+ tos_accepted INTEGER DEFAULT 0
69
+ )''')
70
+ c.execute('''CREATE TABLE IF NOT EXISTS invoices (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ user_id INTEGER NOT NULL,
73
+ timestamp REAL NOT NULL,
74
+ filename TEXT,
75
+ extracted_json TEXT,
76
+ fields_count INTEGER,
77
+ fingerprint TEXT,
78
+ FOREIGN KEY(user_id) REFERENCES users(id)
79
+ )''')
80
+ c.execute('''CREATE TABLE IF NOT EXISTS usage_log (
81
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ user_id INTEGER NOT NULL,
83
+ action TEXT NOT NULL,
84
+ timestamp REAL DEFAULT (strftime('%s','now')),
85
+ FOREIGN KEY(user_id) REFERENCES users(id)
86
+ )''')
87
  conn.commit()
88
  conn.close()
89
 
90
  init_db()
91
 
92
+ # ─── AUTH HELPERS ────────────────────────────────────────────────────────────
93
  def hash_password(password, salt=None):
94
  if not salt:
95
  salt = os.urandom(16).hex()
96
+ h = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
97
+ return h.hex(), salt
98
+
99
+ def get_user(username):
100
+ conn = sqlite3.connect(DB_FILE)
101
+ c = conn.cursor()
102
+ c.execute("SELECT * FROM users WHERE username=?", (username.strip().lower(),))
103
+ row = c.fetchone()
104
+ conn.close()
105
+ if not row:
106
+ return None
107
+ cols = ["id","username","email","password_hash","salt","plan","stripe_customer_id",
108
+ "stripe_subscription_id","subscription_status","created_at","tos_accepted"]
109
+ return dict(zip(cols, row))
110
+
111
+ def get_user_plan(username):
112
+ u = get_user(username)
113
+ if not u:
114
+ return "free"
115
+ if u["plan"] == "premium" and u["subscription_status"] == "active":
116
+ return "premium"
117
+ return "registered"
118
+
119
+ def get_docs_today(user_id):
120
+ conn = sqlite3.connect(DB_FILE)
121
+ c = conn.cursor()
122
+ day_start = time.time() - 86400
123
+ c.execute("SELECT COUNT(*) FROM invoices WHERE user_id=? AND timestamp>?", (user_id, day_start))
124
+ count = c.fetchone()[0]
125
+ conn.close()
126
+ return count
127
 
128
+ # ─── AUTH ACTIONS ────────────────────────────────────────────────────────────
129
+ def db_register(username, password, email="", tos_accepted=False):
130
  if not username or not password:
131
+ return json.dumps({"status":"ERROR","message":"Vyplň jméno i heslo!"})
 
132
  username = username.strip().lower()
133
  if username == "guest":
134
+ return json.dumps({"status":"ERROR","message":"Jméno 'guest' je rezervované."})
135
+ if not tos_accepted:
136
+ return json.dumps({"status":"ERROR","message":"Musíš přijmout podmínky použití (TOS)."})
137
  try:
138
  conn = sqlite3.connect(DB_FILE)
139
+ c = conn.cursor()
140
+ c.execute("SELECT id FROM users WHERE username=?", (username,))
141
+ if c.fetchone():
142
  conn.close()
143
+ return json.dumps({"status":"ERROR","message":"Uživatel již existuje!"})
 
144
  h, salt = hash_password(password)
145
+ c.execute("INSERT INTO users (username,email,password_hash,salt,plan,tos_accepted) VALUES (?,?,?,?,?,?)",
146
+ (username, email.strip().lower(), h, salt, "registered", 1))
147
  conn.commit()
148
  conn.close()
149
+ return json.dumps({"status":"SUCCESS","message":"Účet vytvořen. Přihlaš se."})
150
  except Exception as e:
151
+ return json.dumps({"status":"ERROR","message":f"DB chyba: {e}"})
152
 
153
  def db_login(username, password):
154
  if not username or not password:
155
+ return json.dumps({"status":"ERROR","message":"Zadej jméno a heslo!"})
 
156
  username = username.strip().lower()
157
  try:
158
  conn = sqlite3.connect(DB_FILE)
159
+ c = conn.cursor()
160
+ c.execute("SELECT id,password_hash,salt,plan,subscription_status FROM users WHERE username=?", (username,))
161
+ row = c.fetchone()
 
162
  if not row:
163
  conn.close()
164
+ return json.dumps({"status":"ERROR","message":"Uživatel neexistuje."})
165
+ user_id, stored_hash, salt, plan, sub_status = row
 
 
 
166
  if password == "SESSION_REFRESH":
167
+ c.execute("SELECT COUNT(*),SUM(fields_count) FROM invoices WHERE user_id=?", (user_id,))
168
+ s = c.fetchone()
 
 
169
  conn.close()
170
+ effective_plan = "premium" if (plan=="premium" and sub_status=="active") else "registered"
171
+ return json.dumps({"status":"SUCCESS","username":username,"plan":effective_plan,
172
+ "stats":{"docs_parsed":s[0]or 0,"fields_extracted":s[1]or 0},
173
+ "limits":PLAN_LIMITS[effective_plan]})
 
 
174
  h, _ = hash_password(password, salt)
175
  if h != stored_hash:
176
  conn.close()
177
+ return json.dumps({"status":"ERROR","message":"Špatné heslo!"})
178
+ c.execute("SELECT COUNT(*),SUM(fields_count) FROM invoices WHERE user_id=?", (user_id,))
179
+ s = c.fetchone()
 
 
 
180
  conn.close()
181
+ effective_plan = "premium" if (plan=="premium" and sub_status=="active") else "registered"
182
+ return json.dumps({"status":"SUCCESS","username":username,"plan":effective_plan,
183
+ "stats":{"docs_parsed":s[0]or 0,"fields_extracted":s[1]or 0},
184
+ "limits":PLAN_LIMITS[effective_plan]})
 
 
185
  except Exception as e:
186
+ return json.dumps({"status":"ERROR","message":f"Chyba: {e}"})
187
+
188
+ # ─── STRIPE ACTIONS ──────────────────────────────────────────────────────────
189
+ def create_checkout_session(username):
190
+ """Returns Stripe Checkout URL for Premium subscription."""
191
+ if not STRIPE_SECRET_KEY or not STRIPE_PRICE_ID:
192
+ return json.dumps({"status":"ERROR","message":"Stripe není nakonfigurován (chybí env proměnné)."})
193
+ u = get_user(username)
194
+ if not u:
195
+ return json.dumps({"status":"ERROR","message":"Uživatel nenalezen."})
196
+ try:
197
+ customer_id = u.get("stripe_customer_id")
198
+ if not customer_id:
199
+ customer = stripe.Customer.create(
200
+ email=u.get("email") or f"{username}@omniparse.ai",
201
+ metadata={"username": username}
202
+ )
203
+ customer_id = customer.id
204
+ conn = sqlite3.connect(DB_FILE)
205
+ conn.execute("UPDATE users SET stripe_customer_id=? WHERE username=?", (customer_id, username))
206
+ conn.commit()
207
+ conn.close()
208
+ session = stripe.checkout.Session.create(
209
+ customer=customer_id,
210
+ payment_method_types=["card"],
211
+ line_items=[{"price": STRIPE_PRICE_ID, "quantity": 1}],
212
+ mode="subscription",
213
+ success_url=f"{APP_URL}?payment=success",
214
+ cancel_url=f"{APP_URL}?payment=cancel",
215
+ metadata={"username": username}
216
+ )
217
+ return json.dumps({"status":"SUCCESS","url":session.url})
218
+ except Exception as e:
219
+ return json.dumps({"status":"ERROR","message":str(e)})
220
 
221
+ def handle_stripe_webhook(payload, sig_header):
222
+ """Process Stripe webhooks to update subscription status."""
223
+ try:
224
+ event = stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_SECRET)
225
+ except Exception as e:
226
+ return json.dumps({"status":"ERROR","message":str(e)})
227
+
228
+ et = event["type"]
229
+ obj = event["data"]["object"]
230
+
231
+ if et in ("customer.subscription.created", "customer.subscription.updated"):
232
+ sub_status = obj["status"]
233
+ customer_id = obj["customer"]
234
+ sub_id = obj["id"]
235
+ plan = "premium" if sub_status == "active" else "registered"
236
+ conn = sqlite3.connect(DB_FILE)
237
+ conn.execute("""UPDATE users SET plan=?,stripe_subscription_id=?,subscription_status=?
238
+ WHERE stripe_customer_id=?""", (plan, sub_id, sub_status, customer_id))
239
+ conn.commit()
240
+ conn.close()
241
+ elif et == "customer.subscription.deleted":
242
+ customer_id = obj["customer"]
243
+ conn = sqlite3.connect(DB_FILE)
244
+ conn.execute("""UPDATE users SET plan='registered',subscription_status='canceled'
245
+ WHERE stripe_customer_id=?""", (customer_id,))
246
+ conn.commit()
247
+ conn.close()
248
+ return json.dumps({"status":"OK"})
249
 
250
+ # ─── MODEL ───────────────────────────────────────────────────────────────────
251
  model_name = "naver-clova-ix/donut-base-finetuned-cord-v2"
252
  try:
253
  processor = DonutProcessor.from_pretrained(model_name)
 
255
  device = "cuda" if torch.cuda.is_available() else "cpu"
256
  model.to(device)
257
  MODEL_LOADED = True
258
+ print(f"Model loaded on {device}")
259
  except Exception as e:
260
  MODEL_LOADED = False
261
+ processor = model = None
 
262
  device = "cpu"
263
+ print(f"Model load failed, using demo fallback: {e}")
264
 
265
+ def clean_and_float(v):
266
  try:
267
+ return float(re.sub(r"[^\d.,]","",str(v)).replace(",","."))
268
+ except:
 
269
  return 0.0
270
 
271
+ # ─── CORE PARSER ─────────────────────────────────────────────────────────────
272
  def parse_invoice(image_b64: str, username: str):
273
  if not image_b64:
274
+ return json.dumps({"parser_status":"ERROR","error_log":"Žádná obrazová data."})
275
+
276
+ # Determine user context
277
+ is_guest = (not username or username.strip().lower() in ("guest",""))
278
+ user_id = None
279
+ effective_plan = "free"
280
+
281
+ if not is_guest:
282
+ u = get_user(username)
283
+ if u:
284
+ user_id = u["id"]
285
+ effective_plan = "premium" if (u["plan"]=="premium" and u["subscription_status"]=="active") else "registered"
286
+ limits = PLAN_LIMITS[effective_plan]
287
+ # Check daily limit
288
+ docs_today = get_docs_today(user_id)
289
+ if docs_today >= limits["docs_per_day"]:
290
+ return json.dumps({
291
+ "parser_status": "LIMIT_REACHED",
292
+ "error_log": f"Denní limit {limits['docs_per_day']} dokumentů vyčerpán. Upgraduj na vyšší plán.",
293
+ "upgrade_required": effective_plan != "premium"
294
+ })
295
+ else:
296
+ is_guest = True
297
+ else:
298
+ limits = PLAN_LIMITS["free"]
299
 
300
  try:
301
  header, _, data = image_b64.partition(",")
302
  img_bytes = base64.b64decode(data if data else image_b64)
303
  img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
304
  except Exception as e:
305
+ return json.dumps({"parser_status":"ERROR","error_log":f"Decode failed: {e}"})
306
 
307
  try:
308
  start_time = time.time()
 
309
  if MODEL_LOADED and processor and model:
310
  task_prompt = "<s_cord-v2>"
311
+ decoder_input_ids = processor.tokenizer(task_prompt, add_special_tokens=False, return_tensors="pt").input_ids
 
 
312
  pixel_values = processor(img, return_tensors="pt").pixel_values
 
313
  outputs = model.generate(
314
  pixel_values.to(device),
315
  decoder_input_ids=decoder_input_ids.to(device),
 
320
  bad_words_ids=[[processor.tokenizer.unk_token_id]],
321
  return_dict_in_generate=True,
322
  )
 
323
  sequence = processor.batch_decode(outputs.sequences)[0]
324
+ sequence = sequence.replace(processor.tokenizer.eos_token,"").replace(processor.tokenizer.pad_token,"")
325
+ sequence = re.sub(r"<[^>]+>"," ",sequence).strip()
 
 
 
326
  extracted_fields = {}
327
+ for line in re.split(r"[;\n]", sequence):
 
328
  if ":" in line:
329
+ k,_,v = line.partition(":")
330
+ k = k.strip().upper().replace(" ","_")
331
+ v = v.strip()
332
+ if k and v:
333
+ extracted_fields[k] = v
 
334
  if not extracted_fields:
335
  extracted_fields["RAW_TEXT"] = sequence.strip()
336
  else:
 
337
  extracted_fields = {
338
  "STORE_NAME": "TutaGarage Stavby s.r.o.",
339
  "TOTAL": "24500.00",
 
345
 
346
  elapsed = round(time.time() - start_time, 2)
347
  confidence_score = round(random.uniform(0.95, 0.99), 2)
348
+ invoice_fp = extracted_fields.get("INVOICE_NUMBER", extracted_fields.get("TOTAL","UNKNOWN"))
349
 
350
+ # Duplicate check (only for registered+)
 
 
351
  is_duplicate = False
352
+ if not is_guest and user_id and PLAN_LIMITS[effective_plan]["duplicate_check"]:
 
 
353
  try:
354
  conn = sqlite3.connect(DB_FILE)
355
+ c = conn.cursor()
356
+ c.execute("SELECT id FROM invoices WHERE user_id=? AND fingerprint=?", (user_id, invoice_fp))
357
+ is_duplicate = bool(c.fetchone())
 
 
 
 
 
358
  conn.close()
359
+ except:
360
  pass
361
 
362
+ subtotal_val = clean_and_float(extracted_fields.get("SUBTOTAL","0"))
363
+ tax_val = clean_and_float(extracted_fields.get("TAX","0"))
364
+ total_val = clean_and_float(extracted_fields.get("TOTAL","0"))
 
365
  validation_status = "PASSED"
366
  if subtotal_val > 0 and total_val > 0:
367
  if abs((subtotal_val + tax_val) - total_val) > 1.0:
368
+ validation_status = "WARNING: Nesouhlasí součet"
369
 
370
  result = {
371
  "parser_status": "SUCCESS",
372
  "parse_time_seconds": elapsed,
373
+ "plan": effective_plan,
374
  "security_and_compliance": {
375
  "confidence_score": confidence_score,
376
+ "human_review_required": confidence_score < 0.96,
377
  "duplicate_detected": is_duplicate,
378
  "cross_field_validation": validation_status,
379
  },
380
  "extracted_data": extracted_fields,
381
  }
382
 
383
+ # Save to DB
384
+ if not is_guest and user_id:
 
385
  try:
386
  conn = sqlite3.connect(DB_FILE)
387
+ c = conn.cursor()
388
+ c.execute(
389
+ "INSERT INTO invoices (user_id,timestamp,filename,extracted_json,fields_count,fingerprint) VALUES (?,?,?,?,?,?)",
390
+ (user_id, time.time(), "uploaded_doc.png", json.dumps(result), len(extracted_fields), invoice_fp)
391
+ )
392
+ c.execute("INSERT INTO usage_log (user_id,action) VALUES (?,?)", (user_id,"parse"))
393
+ conn.commit()
394
+ c.execute("SELECT COUNT(*),SUM(fields_count) FROM invoices WHERE user_id=?", (user_id,))
395
+ s = c.fetchone()
 
 
 
 
 
 
 
 
396
  conn.close()
397
+ result["user_stats"] = {"docs_parsed": s[0]or 0, "fields_extracted": s[1]or 0}
398
+ result["docs_today"] = get_docs_today(user_id)
399
+ result["daily_limit"] = PLAN_LIMITS[effective_plan]["docs_per_day"]
400
+ except Exception as e:
401
+ print(f"DB insert error: {e}")
402
  else:
403
+ result["user_stats"] = {"docs_parsed":"GUEST","fields_extracted":"GUEST"}
404
+
405
+ # Export CSV (registered+ only)
406
+ if not is_guest and PLAN_LIMITS[effective_plan]["export_enabled"]:
407
+ rows = [["Field","Value"]]
408
+ rows.append(["confidence_score", str(confidence_score)])
409
+ rows.append(["duplicate_detected", str(is_duplicate)])
410
+ rows.append(["cross_field_validation", validation_status])
411
+ for k,v in extracted_fields.items():
412
+ rows.append([k,v])
413
+ df = pd.DataFrame(rows[1:], columns=rows[0])
414
+ buf = io.StringIO()
415
+ df.to_csv(buf, index=False)
416
+ result["_csv_b64"] = base64.b64encode(buf.getvalue().encode()).decode()
417
+
418
+ result["feature_flags"] = PLAN_LIMITS[effective_plan]
 
 
 
 
 
 
 
 
419
  return json.dumps(result)
420
 
421
  except Exception as e:
422
+ return json.dumps({"parser_status":"ERROR","error_log":str(e)})
423
 
424
 
425
+ def agent_chat(user_message: str, extracted_json: str, username: str):
426
  if not user_message.strip():
427
  return ""
428
+
429
+ # Check if chat is allowed
430
+ is_guest = (not username or username.strip().lower() in ("guest",""))
431
+ if is_guest:
432
+ return "🔒 AI Audit Companion je dostupný pouze pro registrované uživatele."
433
+
434
+ plan = get_user_plan(username)
435
+ if not PLAN_LIMITS[plan]["chat_enabled"]:
436
+ return "🔒 Chat není dostupný ve vašem plánu."
437
 
438
  try:
439
  data = json.loads(extracted_json) if extracted_json else {}
440
  fields = data.get("extracted_data", {})
441
  compliance = data.get("security_and_compliance", {})
442
+ except:
443
  fields = {}
444
  compliance = {}
445
 
446
  if not fields:
447
+ return "Nejdřív nahraj a vytěž fakturu."
448
 
449
  msg = user_message.lower()
450
  matched = []
451
  for key, val in fields.items():
452
+ clean_key = key.lower().replace("_"," ")
453
+ if clean_key in msg or any(word in clean_key for word in msg.split() if len(word)>3):
454
  matched.append(f"**{key}**: {val}")
 
455
  if matched:
456
+ return "Shody:\n" + "\n".join(f"• {m}" for m in matched[:5])
457
+ if any(w in msg for w in ["total","cena","castka","suma","celkem","amount"]):
458
+ return f"💰 **Celkem**: {fields.get('TOTAL','—')}\n• Základ: {fields.get('SUBTOTAL','—')}\n• DPH: {fields.get('TAX','—')}"
459
+ if any(w in msg for w in ["vendor","firma","kdo","dodavatel","store"]):
460
+ return f"🏢 **Dodavatel**: {fields.get('STORE_NAME',fields.get('VENDOR','—'))}"
461
+ if any(w in msg for w in ["date","datum","kdy"]):
462
+ return f"📅 **Datum**: {fields.get('DATE',fields.get('INVOICE_DATE','—'))}"
463
+ if any(w in msg for w in ["confidence","spolehlivost","presnost"]):
464
+ s = compliance.get("confidence_score","—")
465
+ return f"🎯 **Spolehlivost**: {s} ({float(s)*100:.0f}%)" if s != "—" else "Nedostupné."
466
+ keys = list(fields.keys())[:5]
467
+ return f"Mám {len(fields)} polí. Zeptej se na: {', '.join(keys)}."
468
+
469
+
470
+ # ─── GRADIO WRAPPERS ─────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  def gr_parse(image_b64, username):
472
  return parse_invoice(image_b64, username)
473
 
474
+ def gr_chat(user_msg, extracted_json, username):
475
+ return agent_chat(user_msg, extracted_json, username)
476
 
477
+ def gr_checkout(username):
478
+ return create_checkout_session(username)
479
 
480
+
481
+ # ─── CSS ─────────────────────────────────────────────────────────────────────
482
  CUSTOM_CSS = """
483
+ body{margin:0!important;padding:0!important;background:#09090b!important;}
484
+ .gradio-container{max-width:100%!important;padding:0!important;margin:0!important;background:#09090b!important;}
485
+ footer{display:none!important;}
486
+ #hidden-controls{display:none!important;}
487
  """
488
 
489
+ # ─── HTML UI ─────────────────────────────────────────────────────────────────
490
+ HTML_UI = r"""
491
  <!DOCTYPE html>
492
  <html lang="cs">
493
  <head>
494
  <meta charset="UTF-8"/>
495
  <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
496
  <script src="https://cdn.tailwindcss.com"></script>
497
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
498
  <style>
499
  *{box-sizing:border-box;margin:0;padding:0;}
500
  :root{
501
+ --bg:#09090b;--surface:#111113;--surface2:#18181b;--surface3:#1c1c1f;
502
+ --border:#27272a;--border2:#3f3f46;
503
+ --text:#e4e4e7;--text2:#a1a1aa;--text3:#71717a;--text4:#52525b;
504
+ --blue:#3b82f6;--blue-light:#60a5fa;--blue-dark:#1d4ed8;
505
+ --green:#34d399;--amber:#f59e0b;--red:#ef4444;--purple:#a78bfa;
 
 
 
 
 
 
 
 
506
  }
507
  body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);}
508
  .card{background:var(--surface2);border:1px solid var(--border);border-radius:14px;}
509
+ .card-glow{box-shadow:0 0 0 1px rgba(59,130,246,.15),0 4px 24px rgba(0,0,0,.4);}
510
+ .section-label{font-size:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--text4);}
511
  .drop-zone{border:1.5px dashed var(--border2);border-radius:12px;transition:all .25s ease;cursor:pointer;}
512
+ .drop-zone:hover,.drop-zone.drag-over{border-color:var(--blue);background:rgba(59,130,246,.04);}
513
+ .btn-primary{background:linear-gradient(135deg,var(--blue),var(--blue-dark));color:#fff;border:none;border-radius:10px;font-weight:600;font-size:13px;cursor:pointer;transition:all .2s ease;letter-spacing:.01em;}
514
+ .btn-primary:hover:not(:disabled){filter:brightness(1.12);transform:translateY(-1px);box-shadow:0 4px 16px rgba(59,130,246,.35);}
515
+ .btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none;}
516
+ .btn-secondary{background:var(--surface2);border:1px solid var(--border2);color:var(--text2);border-radius:10px;font-size:13px;font-weight:500;cursor:pointer;transition:all .18s ease;}
517
  .btn-secondary:hover{border-color:var(--blue-light);color:var(--text);}
518
+ .btn-premium{background:linear-gradient(135deg,#7c3aed,#4f46e5);color:#fff;border:none;border-radius:10px;font-weight:600;font-size:13px;cursor:pointer;transition:all .2s ease;}
519
+ .btn-premium:hover{filter:brightness(1.1);transform:translateY(-1px);box-shadow:0 4px 16px rgba(124,58,237,.35);}
520
+ .btn-danger{background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.25);color:#fca5a5;border-radius:8px;font-size:12px;font-weight:500;cursor:pointer;padding:6px 14px;transition:all .18s;}
521
+ .btn-danger:hover{background:rgba(239,68,68,.2);}
522
+ .input-field{background:var(--surface2);border:1px solid var(--border2);color:var(--text);border-radius:10px;padding:10px 14px;font-size:13px;outline:none;width:100%;transition:border .18s;}
523
+ .input-field:focus{border-color:var(--blue);}
524
  .code-view{background:#0d1117;border:1px solid #21262d;border-radius:10px;font-family:'JetBrains Mono',monospace;}
525
+ .dl-card{background:linear-gradient(135deg,rgba(59,130,246,.06),rgba(29,78,216,.03));border:1px solid rgba(59,130,246,.18);border-radius:12px;}
526
  .grad{background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}
527
+ .grad-premium{background:linear-gradient(135deg,#a78bfa,#7c3aed);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}
528
+ .spin{animation:spin .8s linear infinite;}
529
  @keyframes spin{to{transform:rotate(360deg);}}
530
+ .toast{animation:slideIn .3s ease;pointer-events:none;}
531
  @keyframes slideIn{from{transform:translateX(110%);opacity:0;}to{transform:translateX(0);opacity:1;}}
532
+ .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:100px;font-size:10px;font-weight:700;letter-spacing:.05em;}
533
+ .badge-free{background:rgba(245,158,11,.1);color:#fbbf24;border:1px solid rgba(245,158,11,.2);}
534
+ .badge-registered{background:rgba(59,130,246,.1);color:#60a5fa;border:1px solid rgba(59,130,246,.2);}
535
+ .badge-premium{background:rgba(167,139,250,.12);color:#c4b5fd;border:1px solid rgba(167,139,250,.25);}
536
+ .limit-bar-track{background:var(--border);border-radius:100px;height:4px;overflow:hidden;}
537
+ .limit-bar-fill{height:4px;border-radius:100px;background:linear-gradient(90deg,var(--blue),var(--blue-light));transition:width .5s ease;}
538
+ .limit-bar-fill.warn{background:linear-gradient(90deg,var(--amber),#fcd34d);}
539
+ .limit-bar-fill.danger{background:linear-gradient(90deg,var(--red),#fca5a5);}
540
+ .modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(6px);z-index:200;display:flex;align-items:center;justify-content:center;padding:16px;}
541
+ .tab-btn{padding:8px 18px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;border:none;transition:all .18s;}
542
+ .tab-btn.active{background:var(--surface3);color:var(--text);border:1px solid var(--border2);}
543
+ .tab-btn.inactive{background:transparent;color:var(--text3);}
544
+ .tab-btn.inactive:hover{color:var(--text2);}
545
+ .plan-card{border:2px solid var(--border);border-radius:16px;padding:24px;transition:all .2s;cursor:default;}
546
+ .plan-card.current{border-color:var(--blue);}
547
+ .plan-card.premium-card{border-color:rgba(167,139,250,.4);background:linear-gradient(135deg,rgba(124,58,237,.05),rgba(79,70,229,.03));}
548
+ .tos-scroll{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:16px;max-height:200px;overflow-y:auto;font-size:11px;color:var(--text2);line-height:1.6;}
549
  </style>
550
  </head>
551
  <body>
552
 
553
+ <div id="toast-root" style="position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;width:300px;"></div>
554
 
555
+ <!-- ═══ AUTH SCREEN ═══ -->
556
  <div id="auth-screen" class="fixed inset-0 z-50 flex items-center justify-center bg-[#09090b] p-4">
557
+ <div class="card card-glow w-full max-w-sm p-8 space-y-5">
558
  <div class="text-center space-y-2">
559
+ <div class="mx-auto w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center shadow-lg shadow-blue-500/20">
560
  <svg width="22" height="22" viewBox="0 0 16 16" fill="none"><path d="M2 3h12M2 6h8M2 9h10M2 12h6" stroke="white" stroke-width="1.5" stroke-linecap="round"/><circle cx="13" cy="11" r="2.5" stroke="white" stroke-width="1.2"/><path d="M15 13l1.5 1.5" stroke="white" stroke-width="1.2" stroke-linecap="round"/></svg>
561
  </div>
562
+ <h2 class="text-xl font-bold text-white">OmniParse <span class="grad">AI</span></h2>
563
+ <p class="text-[11px] text-zinc-500">Document Intelligence Platform</p>
564
  </div>
565
+
566
+ <!-- Tabs -->
567
+ <div class="flex gap-1 bg-zinc-900 p-1 rounded-lg">
568
+ <button onclick="switchAuthTab('login')" id="tab-login" class="tab-btn active flex-1 text-center">Přihlásit</button>
569
+ <button onclick="switchAuthTab('register')" id="tab-register" class="tab-btn inactive flex-1 text-center">Registrovat</button>
570
+ </div>
571
+
572
  <div class="space-y-3">
573
  <div>
574
  <label class="block text-[10px] font-bold uppercase tracking-wider text-zinc-500 mb-1">Uživatelské jméno</label>
575
+ <input type="text" id="auth-username" class="input-field text-xs" placeholder="login..." onkeydown="if(event.key==='Enter')handleAuthAction(currentAuthTab)"/>
576
+ </div>
577
+ <div id="auth-email-row" style="display:none;">
578
+ <label class="block text-[10px] font-bold uppercase tracking-wider text-zinc-500 mb-1">E-mail (volitelný, pro platby)</label>
579
+ <input type="email" id="auth-email" class="input-field text-xs" placeholder="email@domena.cz"/>
580
  </div>
581
  <div>
582
  <label class="block text-[10px] font-bold uppercase tracking-wider text-zinc-500 mb-1">Heslo</label>
583
+ <input type="password" id="auth-password" class="input-field text-xs" placeholder="••••••••" onkeydown="if(event.key==='Enter')handleAuthAction(currentAuthTab)"/>
584
+ </div>
585
+ <div id="tos-row" style="display:none;" class="space-y-2">
586
+ <div class="tos-scroll">
587
+ <strong>Podmínky použití (ToS) a GDPR</strong><br/><br/>
588
+ Používáním OmniParse AI souhlasíš se zpracováním nahrávaných dokumentů pro účely extrakce dat. Data jsou zpracovávána na serverech Hugging Face Spaces. Neposkytujeme data třetím stranám. Máš právo na výmaz svých dat dle GDPR. Platby jsou zpracovávány přes Stripe. Služba je poskytována "tak jak je" bez záruky přesnosti. Pro podporu: support@omniparse.ai. Minimální věk: 18 let. Zásady ochrany osobních údajů a plné ToS na: <a href="#" style="color:var(--blue)">omniparse.ai/terms</a>.<br/><br/>
589
+ <strong>Disclaimer:</strong> OmniParse AI je nástroj pro asistenci – výstupy nejsou právně závazné. Vždy ověřuj výstupy před použitím v účetnictví či právních procesech.
590
+ </div>
591
+ <label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
592
+ <input type="checkbox" id="tos-checkbox" style="width:14px;height:14px;accent-color:var(--blue)"/>
593
+ <span class="text-[11px] text-zinc-400">Souhlasím s podmínkami použití a zpracováním osobních údajů (GDPR)</span>
594
+ </label>
595
  </div>
596
  </div>
597
+
598
+ <div class="flex flex-col gap-2">
599
+ <button onclick="handleAuthAction(currentAuthTab)" id="auth-main-btn" class="btn-primary w-full py-2.5 font-semibold text-xs">
600
+ Přihlásit se ⚡
 
 
 
601
  </button>
 
602
  <div class="relative flex py-2 items-center">
603
  <div class="flex-grow border-t border-zinc-800"></div>
604
  <span class="flex-shrink mx-3 text-[10px] text-zinc-600 font-bold uppercase">Nebo</span>
605
  <div class="flex-grow border-t border-zinc-800"></div>
606
  </div>
607
+ <button onclick="bypassAuthAsGuest()" class="w-full py-2 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-zinc-400 rounded-xl text-xs font-medium transition-all">
608
+ Pokračovat bez registrace (Free: 3 dok./den, bez exportu) →
 
609
  </button>
610
  </div>
611
  </div>
612
  </div>
613
 
614
 
615
+ <!-- ═══ UPGRADE MODAL ═══ -->
616
+ <div id="upgrade-modal" class="modal-overlay" style="display:none;">
617
+ <div class="card card-glow w-full max-w-lg p-8 space-y-6">
618
+ <div class="flex justify-between items-start">
619
+ <div>
620
+ <h3 class="text-lg font-bold text-white">Upgrade plánu</h3>
621
+ <p class="text-xs text-zinc-500 mt-1">Odemkni plný výkon OmniParse AI</p>
622
+ </div>
623
+ <button onclick="closeUpgradeModal()" class="text-zinc-500 hover:text-zinc-300 text-lg">✕</button>
624
+ </div>
625
+ <div class="grid grid-cols-2 gap-4">
626
+ <!-- Free -->
627
+ <div class="plan-card" id="plan-free-card">
628
+ <div class="text-sm font-bold text-zinc-400 mb-1">Free</div>
629
+ <div class="text-2xl font-black text-white mb-3">0 Kč</div>
630
+ <ul class="space-y-1.5 text-xs text-zinc-400">
631
+ <li>✓ 3 dokumenty / den</li>
632
+ <li class="text-zinc-600">✗ Chat AI</li>
633
+ <li class="text-zinc-600">✗ CSV Export</li>
634
+ <li class="text-zinc-600">✗ Detekce duplicit</li>
635
+ <li class="text-zinc-600">✗ Historie</li>
636
+ </ul>
637
+ </div>
638
+ <!-- Registered -->
639
+ <div class="plan-card" id="plan-reg-card">
640
+ <div class="text-sm font-bold text-blue-400 mb-1">Registrovaný</div>
641
+ <div class="text-2xl font-black text-white mb-3">0 Kč</div>
642
+ <ul class="space-y-1.5 text-xs text-zinc-300">
643
+ <li>✓ 20 dokumentů / den</li>
644
+ <li>✓ Chat AI</li>
645
+ <li>✓ CSV Export</li>
646
+ <li>✓ Detekce duplicit</li>
647
+ <li>✓ 30 dní historie</li>
648
+ </ul>
649
+ </div>
650
+ </div>
651
+ <div class="plan-card premium-card" id="plan-prem-card">
652
+ <div class="flex justify-between items-center mb-2">
653
+ <div class="text-sm font-bold"><span class="grad-premium">Premium</span></div>
654
+ <span class="badge badge-premium">⭐ DOPORUČENO</span>
655
+ </div>
656
+ <div class="text-2xl font-black text-white mb-3">299 Kč<span class="text-sm font-normal text-zinc-500"> / měsíc</span></div>
657
+ <ul class="space-y-1.5 text-xs text-zinc-300 mb-4">
658
+ <li>✓ <strong>Neomezené</strong> dokumenty</li>
659
+ <li>✓ Chat AI</li>
660
+ <li>✓ CSV Export</li>
661
+ <li>✓ Detekce duplicit</li>
662
+ <li>✓ 365 dní historie</li>
663
+ <li>✓ Prioritní zpracování</li>
664
+ </ul>
665
+ <button onclick="startPremiumCheckout()" id="stripe-btn" class="btn-premium w-full py-2.5 text-sm">
666
+ Přejít na platbu (Stripe) →
667
+ </button>
668
+ <p class="text-[10px] text-zinc-600 text-center mt-2">Bezpečná platba · Zrušení kdykoliv · PCI DSS</p>
669
+ </div>
670
+ <p class="text-[10px] text-zinc-600 text-center">Platby zpracovává Stripe Inc. · OmniParse neuchovává čísla karet.</p>
671
+ </div>
672
+ </div>
673
+
674
+
675
+ <!-- ═══ MAIN APP ═══ -->
676
+ <div id="app-screen" style="display:none;">
677
+ <!-- Header -->
678
+ <header style="border-bottom:1px solid var(--border);background:rgba(9,9,11,.95);backdrop-filter:blur(20px);position:sticky;top:0;z-index:40;">
679
+ <div style="max-width:1400px;margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between;">
680
  <div style="display:flex;align-items:center;gap:12px;">
681
+ <div style="width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
682
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 3h12M2 6h8M2 9h10M2 12h6" stroke="white" stroke-width="1.5" stroke-linecap="round"/><circle cx="13" cy="11" r="2.5" stroke="white" stroke-width="1.2"/><path d="M15 13l1.5 1.5" stroke="white" stroke-width="1.2" stroke-linecap="round"/></svg>
683
  </div>
684
+ <span style="color:#fff;font-weight:700;font-size:15px;letter-spacing:-.01em;">OmniParse <span class="grad">AI</span></span>
685
+ <span id="plan-badge" class="badge badge-free">FREE</span>
686
  </div>
687
+ <div style="display:flex;align-items:center;gap:10px;">
688
+ <div style="display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:8px;background:var(--surface2);border:1px solid var(--border);">
689
+ <span id="status-dot" style="width:7px;height:7px;border-radius:50%;background:#34d399;flex-shrink:0;"></span>
690
+ <span id="status-text" style="font-size:11px;color:var(--text2);font-weight:500;">Ready</span>
 
691
  </div>
692
+ <span id="user-display" style="font-size:12px;font-weight:500;color:var(--text2);"></span>
693
+ <button onclick="openUpgradeModal()" id="upgrade-btn" class="btn-premium px-3 py-1.5 text-xs" style="display:none;">⭐ Upgrade</button>
694
+ <button onclick="handleLogout()" class="btn-danger">Odhlásit</button>
695
  </div>
696
  </div>
697
  </header>
698
+
699
+ <main style="max-width:1400px;margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;">
700
+
701
+ <!-- Stats + Limit Bar -->
702
+ <div class="grid grid-cols-4 gap-3">
703
  <div class="card p-4 flex items-center gap-3">
704
+ <div style="font-size:22px;">📄</div>
705
+ <div>
706
+ <p class="section-label mb-0.5">Dokumenty</p>
707
+ <p id="st-docs" style="font-size:20px;font-weight:800;color:var(--text);">0</p>
708
+ </div>
709
  </div>
710
  <div class="card p-4 flex items-center gap-3">
711
+ <div style="font-size:22px;">⚡</div>
712
+ <div>
713
+ <p class="section-label mb-0.5">Vytěžená pole</p>
714
+ <p id="st-fields" style="font-size:20px;font-weight:800;color:var(--text);">0</p>
715
+ </div>
716
  </div>
717
  <div class="card p-4 flex items-center gap-3">
718
+ <div style="font-size:22px;">⏱️</div>
719
+ <div>
720
+ <p class="section-label mb-0.5">Čas operace</p>
721
+ <p id="st-time" style="font-size:20px;font-weight:800;color:var(--text);">—</p>
722
+ </div>
723
  </div>
724
  <div class="card p-4 flex items-center gap-3">
725
+ <div style="font-size:22px;">🎯</div>
726
+ <div>
727
+ <p class="section-label mb-0.5">Spolehlivost</p>
728
+ <p id="st-conf" style="font-size:20px;font-weight:800;color:var(--text);">—</p>
729
+ </div>
730
  </div>
731
  </div>
732
+
733
+ <!-- Daily Limit Indicator -->
734
+ <div id="limit-indicator" class="card p-4 flex items-center gap-4" style="display:none!important;">
735
+ <div style="flex:1;">
736
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
737
+ <span style="font-size:11px;font-weight:600;color:var(--text2);">Denní limit</span>
738
+ <span id="limit-text" style="font-size:11px;color:var(--text3);">0 / 3</span>
739
+ </div>
740
+ <div class="limit-bar-track"><div id="limit-bar" class="limit-bar-fill" style="width:0%;"></div></div>
741
+ </div>
742
+ <button onclick="openUpgradeModal()" style="font-size:11px;color:var(--blue);white-space:nowrap;cursor:pointer;background:none;border:none;">Upgrade →</button>
743
+ </div>
744
+
745
+ <!-- Workspace -->
746
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
747
+ <!-- Left -->
748
+ <div style="display:flex;flex-direction:column;gap:16px;">
749
  <div class="card p-5 space-y-4">
750
+ <div style="display:flex;justify-content:space-between;align-items:center;">
751
+ <span style="font-size:13px;font-weight:600;">Vstupní dokument</span>
752
  <span class="section-label">Stage 01</span>
753
  </div>
754
+ <div id="drop-zone" class="drop-zone p-8 flex flex-col items-center justify-center gap-3 text-center" style="min-height:160px;"
 
755
  onclick="document.getElementById('file-input').click()"
756
  ondragover="event.preventDefault();this.classList.add('drag-over')"
757
  ondragleave="this.classList.remove('drag-over')"
758
  ondrop="handleDrop(event)">
759
+ <input type="file" id="file-input" style="display:none;" accept="image/*,application/pdf" onchange="handleFiles(this.files)"/>
760
+ <div style="font-size:32px;color:var(--text4);">📥</div>
761
+ <p style="font-size:13px;color:var(--text2);">Přetáhni soubor nebo <span style="color:var(--blue);text-decoration:underline;cursor:pointer;">klikni pro výběr</span></p>
762
+ <p style="font-size:11px;color:var(--text4);">JPG, PNG, PDF · Max 10 MB</p>
763
  </div>
 
764
  <div id="file-preview" style="display:none;" class="border border-zinc-800 p-2 rounded-lg">
765
+ <img id="img-preview" style="width:100%;max-height:180px;object-fit:contain;border-radius:6px;"/>
766
  </div>
767
  </div>
768
+
769
  <div class="card p-4">
770
+ <button id="exec-btn" class="btn-primary w-full py-3.5 flex items-center justify-center gap-2" disabled onclick="executeExtraction()">
771
+ <span id="btn-idle" style="display:flex;align-items:center;gap:6px;">
772
+ <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg>
773
+ Spustit AI extrakci ⚡
774
+ </span>
775
  <span id="btn-loading" style="display:none;" class="flex items-center gap-2">
776
+ <svg class="spin h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
777
+ Extrahuji data...
778
  </span>
779
  </button>
780
  </div>
781
+
782
+ <!-- Export -->
783
  <div class="card p-5 space-y-3">
784
+ <div style="display:flex;justify-content:space-between;align-items:center;">
785
+ <span style="font-size:13px;font-weight:600;">Export dat</span>
786
+ <span class="section-label">Stage 03</span>
787
+ </div>
788
+ <div id="export-actions" style="display:none;">
789
+ <button onclick="downloadCSV()" class="dl-card w-full p-3 flex justify-between items-center" style="font-size:13px;font-weight:500;color:var(--text2);cursor:pointer;">
790
+ <span>📊 Exportovat CSV</span>
791
+ <span style="font-size:11px;color:var(--blue);background:rgba(59,130,246,.1);padding:4px 10px;border-radius:6px;">STÁHNOUT</span>
792
  </button>
793
  </div>
794
+ <div id="export-locked" style="display:none;" class="text-center py-3">
795
+ <p style="font-size:11px;color:var(--text4);">🔒 Export dostupný od Registrovaného plánu</p>
796
+ <button onclick="openUpgradeModal()" style="margin-top:6px;font-size:11px;color:var(--blue);cursor:pointer;background:none;border:none;">Registrovat / Upgrade →</button>
797
+ </div>
798
+ <div id="export-empty" style="font-size:11px;color:var(--text4);text-align:center;padding:8px;">Čekám na zpracování...</div>
799
  </div>
800
  </div>
801
+
802
+ <!-- Right -->
803
+ <div style="display:flex;flex-direction:column;gap:16px;">
804
  <div class="card p-5 space-y-3">
805
+ <div style="display:flex;justify-content:space-between;align-items:center;">
806
+ <span style="font-size:13px;font-weight:600;">Live Extraction Terminal</span>
807
+ <span class="section-label">Output JSON</span>
808
+ </div>
809
+ <div class="code-view p-4" style="min-height:160px;max-height:240px;overflow-y:auto;">
810
+ <pre id="json-display" style="font-size:11px;color:var(--text3);line-height:1.55;white-space:pre-wrap;">// Terminál prázdný.</pre>
811
  </div>
812
  </div>
813
+
814
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
815
  <div class="card p-4 space-y-1">
816
+ <p class="section-label">Validace DPH</p>
817
+ <p id="comp-val" style="font-size:12px;font-weight:600;color:var(--text2);">—</p>
818
  </div>
819
  <div class="card p-4 space-y-1">
820
+ <p class="section-label">Detekce duplicity</p>
821
+ <p id="comp-dup" style="font-size:12px;font-weight:600;color:var(--text2);">—</p>
822
  </div>
823
  </div>
824
+
825
+ <!-- AI Chat -->
826
+ <div class="card p-5 flex flex-col" style="height:280px;">
827
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
828
+ <p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text4);">AI Audit Companion</p>
829
+ <span id="chat-plan-badge" style="font-size:10px;color:var(--text4);">Registrovaný+</span>
830
+ </div>
831
+ <div id="chat-history" style="flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:10px;padding-right:2px;font-size:12px;">
832
+ <div style="padding:8px 12px;border-radius:8px;background:rgba(59,130,246,.06);border:1px solid rgba(59,130,246,.12);color:var(--text3);">
833
+ Systém připraven. Zeptej se na položky faktury.
834
  </div>
835
  </div>
836
+ <div style="display:flex;gap:8px;">
837
+ <input type="text" id="chat-input" class="input-field text-xs" placeholder="Napiš dotaz k faktuře..." onkeydown="if(event.key==='Enter') sendChatMessage()" style="font-size:12px;"/>
838
+ <button onclick="sendChatMessage()" class="btn-primary px-3" style="white-space:nowrap;font-size:12px;">Odeslat</button>
839
  </div>
840
  </div>
841
  </div>
842
  </div>
843
+
844
+ <!-- Legal Disclaimer Footer -->
845
+ <div style="border-top:1px solid var(--border);padding-top:16px;margin-top:4px;">
846
+ <p style="font-size:10px;color:var(--text4);text-align:center;line-height:1.6;max-width:700px;margin:0 auto;">
847
+ ⚠️ <strong style="color:var(--text3);">Disclaimer:</strong> OmniParse AI je asistenční nástroj. Výstupy AI extrakce nejsou právně závazné a musí být ověřeny před použitím v účetnictví, daňových přiznáních nebo jiných právních procesech. Zpracování dat probíhá v souladu s GDPR. Nahráváním dokumentů souhlasíš s <a href="#" style="color:var(--blue);">podmínkami použití</a>. Platby zpracovává Stripe. © 2026 OmniParse AI.
848
+ </p>
849
+ </div>
850
  </main>
851
  </div>
852
 
853
+
854
+ <!-- ─── GRADIO BRIDGE (skrytý) ─── -->
855
  <script>
856
+ /* STATE */
857
  let currentBase64 = "";
858
  let latestExtractedJson = "";
859
  let csvB64Data = "";
860
+ let authPolling = null, parsePolling = null, chatPolling = null, checkoutPolling = null;
861
+ let guestDocsCount = 0, guestFieldsCount = 0;
862
+ let currentAuthTab = "login";
863
+ let currentPlan = "free";
864
+ let currentUser = "";
865
+ let dailyLimit = 3, docsToday = 0;
866
+
867
+ /* UTILS */
868
+ function showToast(msg, type='info') {
869
+ const root = document.getElementById("toast-root");
870
+ const t = document.createElement("div");
871
+ t.className = `toast card px-4 py-3 rounded-xl flex items-center gap-3 shadow-2xl text-white ${type==='error'?'bg-red-950/80 border-red-800':type==='warn'?'bg-amber-950/80 border-amber-800':'bg-zinc-900/90 border-zinc-700'}`;
872
+ t.style.cssText="font-size:12px;";
873
+ t.innerHTML = `<span>${type==='error'?'❌':type==='warn'?'⚠️':'✅'}</span><span>${msg}</span>`;
874
+ root.appendChild(t);
875
+ setTimeout(()=>{t.style.opacity='0';setTimeout(()=>t.remove(),300);},4500);
876
+ }
877
 
878
+ function setPlanUI(plan) {
879
+ currentPlan = plan;
880
+ const badge = document.getElementById("plan-badge");
881
+ const upgradeBtn = document.getElementById("upgrade-btn");
882
+ const limInd = document.getElementById("limit-indicator");
883
+ if(plan==='premium'){
884
+ badge.className='badge badge-premium'; badge.textContent='⭐ PREMIUM';
885
+ upgradeBtn.style.display='none';
886
+ limInd.style.display='none';
887
+ } else if(plan==='registered'){
888
+ badge.className='badge badge-registered'; badge.textContent='REGISTERED';
889
+ upgradeBtn.style.display='flex';
890
+ limInd.style.setProperty('display','flex','important');
891
+ dailyLimit=20;
892
+ } else {
893
+ badge.className='badge badge-free'; badge.textContent='FREE';
894
+ upgradeBtn.style.display='flex';
895
+ limInd.style.setProperty('display','flex','important');
896
+ dailyLimit=3;
897
+ }
898
+ // Export visibility
899
+ const ea=document.getElementById("export-actions"),
900
+ el=document.getElementById("export-locked"),
901
+ ee=document.getElementById("export-empty");
902
+ if(plan==='free'){ea.style.display='none';el.style.display='block';ee.style.display='none';}
903
+ else{el.style.display='none';}
904
+ }
905
 
906
+ function updateLimitBar(today, limit) {
907
+ docsToday = today; dailyLimit = limit;
908
+ const pct = Math.min((today/limit)*100, 100);
909
+ document.getElementById("limit-text").textContent = `${today} / ${limit}`;
910
+ const bar = document.getElementById("limit-bar");
911
+ bar.style.width = pct + '%';
912
+ bar.className = 'limit-bar-fill' + (pct>=90?' danger':pct>=70?' warn':'');
 
 
 
913
  }
914
 
915
+ /* AUTH TABS */
916
+ function switchAuthTab(tab) {
917
+ currentAuthTab = tab;
918
+ document.getElementById("tab-login").className = tab==='login'?'tab-btn active flex-1 text-center':'tab-btn inactive flex-1 text-center';
919
+ document.getElementById("tab-register").className = tab==='register'?'tab-btn active flex-1 text-center':'tab-btn inactive flex-1 text-center';
920
+ document.getElementById("auth-email-row").style.display = tab==='register'?'block':'none';
921
+ document.getElementById("tos-row").style.display = tab==='register'?'block':'none';
922
+ document.getElementById("auth-main-btn").textContent = tab==='login'?'Přihlásit se ⚡':'Vytvořit účet ✓';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  }
924
 
925
  function bypassAuthAsGuest() {
926
+ sessionStorage.setItem("omniparse_user","Guest");
927
+ sessionStorage.setItem("omniparse_plan","free");
928
+ showToast("Vstupuješ jako host. Limit: 3 dokumenty/den.");
929
+ checkLoginState();
930
  }
931
 
932
  function handleAuthAction(action) {
933
+ const u = document.getElementById("auth-username").value.trim();
934
+ const p = document.getElementById("auth-password").value;
935
+ const email = document.getElementById("auth-email").value.trim();
936
+ if(!u||!p){showToast("Vyplň jméno a heslo!","error");return;}
937
+
938
+ if(action==='register'){
939
+ const tos = document.getElementById("tos-checkbox").checked;
940
+ if(!tos){showToast("Musíš přijmout podmínky použití!","warn");return;}
941
+ // Pass tos flag via username prefix trick — we encode as JSON
942
+ const payload = JSON.stringify({action:"register",email,tos:true});
943
+ triggerAuth(u, p, payload, 'register');
944
+ } else {
945
+ triggerAuth(u, p, "", 'login');
946
+ }
947
+ }
948
+
949
+ function triggerAuth(u, p, extra, action) {
950
+ const grUser = document.querySelector("#gr-auth-user textarea");
951
+ const grPass = document.querySelector("#gr-auth-pass textarea");
952
+ if(!grUser||!grPass) return;
953
+ // Encode extra info into password field for register
954
+ const passField = action==='register' ? `REGISTER::${p}::${extra}` : p;
955
+ grUser.value=u; grUser.dispatchEvent(new Event("input",{bubbles:true}));
956
+ grPass.value=passField; grPass.dispatchEvent(new Event("input",{bubbles:true}));
957
+ if(action==='login'){
958
+ document.querySelector("#gr-login-btn").click();
959
+ } else {
960
+ document.querySelector("#gr-register-btn").click();
961
+ }
962
+ if(authPolling) clearInterval(authPolling);
963
+ authPolling = setInterval(checkAuthResponse, 150);
964
+ }
965
+
966
+ function checkLoginState() {
967
+ const user = sessionStorage.getItem("omniparse_user");
968
+ if(user){
969
+ document.getElementById("auth-screen").style.display="none";
970
+ document.getElementById("app-screen").style.display="block";
971
+ currentUser=user;
972
+ if(user.toLowerCase()==="guest"){
973
+ document.getElementById("user-display").textContent="👤 Guest";
974
+ setPlanUI("free");
975
+ updateLimitBar(guestDocsCount, 3);
976
  } else {
977
+ document.getElementById("user-display").textContent=`👤 ${user}`;
978
+ const plan = sessionStorage.getItem("omniparse_plan")||"registered";
979
+ setPlanUI(plan);
980
+ // Refresh stats from DB
981
+ const grUser=document.querySelector("#gr-auth-user textarea");
982
+ const grPass=document.querySelector("#gr-auth-pass textarea");
983
+ if(grUser&&grPass){
984
+ grUser.value=user; grUser.dispatchEvent(new Event("input",{bubbles:true}));
985
+ grPass.value="SESSION_REFRESH"; grPass.dispatchEvent(new Event("input",{bubbles:true}));
986
+ document.querySelector("#gr-login-btn").click();
987
+ if(authPolling) clearInterval(authPolling);
988
+ authPolling=setInterval(checkAuthResponse,150);
989
+ }
990
  }
991
+ } else {
992
+ document.getElementById("auth-screen").style.display="flex";
993
+ document.getElementById("app-screen").style.display="none";
994
+ }
995
  }
996
 
997
  function checkAuthResponse() {
998
+ const el = document.querySelector("#gr-auth-response textarea");
999
+ if(!el) return;
1000
+ const val = el.value;
1001
+ if(val&&val.trim()!==""){
1002
+ clearInterval(authPolling); authPolling=null;
1003
+ el.value=""; el.dispatchEvent(new Event("input",{bubbles:true}));
1004
+ try {
1005
+ const d = JSON.parse(val);
1006
+ if(d.status==="SUCCESS"){
1007
+ if(d.username){
1008
+ sessionStorage.setItem("omniparse_user",d.username);
1009
+ sessionStorage.setItem("omniparse_plan",d.plan||"registered");
1010
+ currentUser=d.username;
1011
+ document.getElementById("auth-screen").style.display="none";
1012
+ document.getElementById("app-screen").style.display="block";
1013
+ document.getElementById("user-display").textContent=`👤 ${d.username}`;
1014
+ setPlanUI(d.plan||"registered");
1015
+ if(d.stats){
1016
+ document.getElementById("st-docs").textContent=d.stats.docs_parsed;
1017
+ document.getElementById("st-fields").textContent=d.stats.fields_extracted;
1018
+ }
1019
+ if(d.limits) updateLimitBar(0, d.limits.docs_per_day);
1020
+ showToast("Přihlášení úspěšné!");
1021
+ } else {
1022
+ showToast(d.message||"Hotovo!");
 
 
 
 
 
1023
  }
1024
+ } else {
1025
+ showToast(d.message||"Chyba","error");
1026
+ }
1027
+ } catch(e){console.error("Auth parse error",e);}
1028
+ }
1029
  }
1030
 
1031
+ function handleLogout(){
1032
+ sessionStorage.clear();
1033
+ location.reload();
1034
  }
1035
 
1036
+ /* UPGRADE MODAL */
1037
+ function openUpgradeModal(){document.getElementById("upgrade-modal").style.display="flex";}
1038
+ function closeUpgradeModal(){document.getElementById("upgrade-modal").style.display="none";}
1039
+
1040
+ function startPremiumCheckout(){
1041
+ if(!currentUser||currentUser==="Guest"){
1042
+ showToast("Pro platbu se musíš přihlásit jako registrovaný uživatel.","warn");
1043
+ return;
1044
+ }
1045
+ document.getElementById("stripe-btn").textContent="⏳ Připravuji platbu...";
1046
+ document.getElementById("stripe-btn").disabled=true;
1047
+ const grUser=document.querySelector("#gr-auth-user textarea");
1048
+ grUser.value=currentUser; grUser.dispatchEvent(new Event("input",{bubbles:true}));
1049
+ document.querySelector("#gr-checkout-btn").click();
1050
+ if(checkoutPolling) clearInterval(checkoutPolling);
1051
+ checkoutPolling=setInterval(checkCheckoutResponse,200);
1052
  }
1053
 
1054
+ function checkCheckoutResponse(){
1055
+ const el=document.querySelector("#gr-checkout-out textarea");
1056
+ if(!el) return;
1057
+ const val=el.value;
1058
+ if(val&&val.trim()!==""){
1059
+ clearInterval(checkoutPolling); checkoutPolling=null;
1060
+ el.value=""; el.dispatchEvent(new Event("input",{bubbles:true}));
1061
+ document.getElementById("stripe-btn").textContent="Přejít na platbu (Stripe) →";
1062
+ document.getElementById("stripe-btn").disabled=false;
1063
+ try{
1064
+ const d=JSON.parse(val);
1065
+ if(d.status==="SUCCESS"&&d.url){
1066
+ window.open(d.url,"_blank");
1067
+ showToast("Otevírám Stripe platební stránku...");
1068
+ } else {
1069
+ showToast(d.message||"Chyba při přípravě platby.","error");
1070
+ }
1071
+ } catch(e){showToast("Chyba odpovědi.","error");}
1072
+ }
1073
  }
1074
 
1075
+ /* FILE HANDLING */
1076
+ function handleDrop(e){e.preventDefault();handleFiles(e.dataTransfer.files);}
1077
+ function handleFiles(files){
1078
+ if(!files.length) return;
1079
+ const file=files[0];
1080
+ const reader=new FileReader();
1081
+ reader.onload=function(e){
1082
+ currentBase64=e.target.result;
1083
+ document.getElementById("img-preview").src=currentBase64;
1084
+ document.getElementById("file-preview").style.display="block";
1085
+ document.getElementById("exec-btn").disabled=false;
1086
+ showToast(`Soubor načten: ${file.name}`);
1087
+ };
1088
+ reader.readAsDataURL(file);
 
 
 
 
 
 
1089
  }
1090
 
1091
+ /* EXTRACTION */
1092
+ function executeExtraction(){
1093
+ if(!currentBase64) return;
1094
+
1095
+ // Guest limit check
1096
+ if(currentUser.toLowerCase()==="guest" && guestDocsCount>=3){
1097
+ showToast("Denní limit Free plánu vyčerpán. Zaregistruj se pro více!","warn");
1098
+ openUpgradeModal();
1099
+ return;
1100
+ }
1101
+
1102
+ document.getElementById("btn-idle").style.display="none";
1103
+ document.getElementById("btn-loading").style.display="flex";
1104
+ document.getElementById("status-text").textContent="Extrahuji data...";
1105
+ document.getElementById("status-dot").style.background="#f59e0b";
1106
+
1107
+ const grImg=document.querySelector("#gr-image-input textarea");
1108
+ const grUser=document.querySelector("#gr-current-user textarea");
1109
+ grImg.value=currentBase64; grImg.dispatchEvent(new Event("input",{bubbles:true}));
1110
+ grUser.value=currentUser; grUser.dispatchEvent(new Event("input",{bubbles:true}));
1111
+ document.querySelector("#gr-parse-btn").click();
1112
+ if(parsePolling) clearInterval(parsePolling);
1113
+ parsePolling=setInterval(checkParseResponse,200);
1114
+ }
1115
+
1116
+ function checkParseResponse(){
1117
+ const el=document.querySelector("#gr-json-output textarea");
1118
+ if(!el) return;
1119
+ const val=el.value;
1120
+ if(val&&val.trim()!==""){
1121
+ clearInterval(parsePolling); parsePolling=null;
1122
+ el.value=""; el.dispatchEvent(new Event("input",{bubbles:true}));
1123
+ document.getElementById("btn-idle").style.display="flex";
1124
+ document.getElementById("btn-loading").style.display="none";
1125
+ document.getElementById("status-text").textContent="Ready";
1126
+ document.getElementById("status-dot").style.background="#34d399";
1127
+ try{
1128
+ const res=JSON.parse(val);
1129
+ if(res.parser_status==="LIMIT_REACHED"){
1130
+ showToast(res.error_log,"warn");
1131
+ if(res.upgrade_required) openUpgradeModal();
1132
+ return;
1133
+ }
1134
+ if(res.parser_status==="SUCCESS"){
1135
+ latestExtractedJson=JSON.stringify(res,null,2);
1136
+ document.getElementById("json-display").textContent=latestExtractedJson;
1137
+ document.getElementById("st-time").textContent=`${res.parse_time_seconds}s`;
1138
+ document.getElementById("st-conf").textContent=`${Math.round(res.security_and_compliance.confidence_score*100)}%`;
1139
+ document.getElementById("comp-val").textContent=res.security_and_compliance.cross_field_validation;
1140
+
1141
+ if(currentUser.toLowerCase()==="guest"){
1142
+ document.getElementById("comp-dup").textContent="🔒 Vyžaduje registraci";
1143
+ guestDocsCount++;
1144
+ guestFieldsCount+=Object.keys(res.extracted_data||{}).length;
1145
+ document.getElementById("st-docs").textContent=guestDocsCount+" (Free)";
1146
+ document.getElementById("st-fields").textContent=guestFieldsCount+" (Lokální)";
1147
+ updateLimitBar(guestDocsCount,3);
1148
+ } else {
1149
+ document.getElementById("comp-dup").textContent=res.security_and_compliance.duplicate_detected?"⚠️ DUPLICITA DETEKOVÁNA":"✅ Unikátní";
1150
+ if(res.user_stats){
1151
+ document.getElementById("st-docs").textContent=res.user_stats.docs_parsed;
1152
+ document.getElementById("st-fields").textContent=res.user_stats.fields_extracted;
1153
+ }
1154
+ if(res.docs_today!==undefined) updateLimitBar(res.docs_today, res.daily_limit);
1155
  }
1156
+
1157
+ // Export
1158
+ const ee=document.getElementById("export-empty");
1159
+ const ea=document.getElementById("export-actions");
1160
+ if(res._csv_b64){
1161
+ csvB64Data=res._csv_b64;
1162
+ ea.style.display="block";
1163
+ ee.style.display="none";
1164
+ } else {
1165
+ ea.style.display="none";
1166
+ ee.style.display="block";
1167
+ }
1168
+
1169
+ showToast("Data úspěšně vytěžena!");
1170
+ } else {
1171
+ showToast("Chyba: "+(res.error_log||"Neznámá chyba"),"error");
1172
+ }
1173
+ } catch(e){showToast("Dekódovací chyba výstupu.","error");}
1174
+ }
1175
  }
1176
 
1177
+ /* CSV DOWNLOAD */
1178
+ function downloadCSV(){
1179
+ if(!csvB64Data) return;
1180
+ const bytes=new Uint8Array(atob(csvB64Data).split("").map(c=>c.charCodeAt(0)));
1181
+ const blob=new Blob([bytes],{type:"text/csv"});
1182
+ const a=Object.assign(document.createElement("a"),{href:URL.createObjectURL(blob),download:"omniparse_export.csv"});
1183
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
 
 
 
 
 
 
 
 
1184
  }
1185
 
1186
+ /* CHAT */
1187
+ function sendChatMessage(){
1188
+ const input=document.getElementById("chat-input");
1189
+ const msg=input.value.trim();
1190
+ if(!msg) return;
1191
+ input.value="";
1192
+ const hist=document.getElementById("chat-history");
1193
+ hist.innerHTML+=`<div style="padding:8px 12px;border-radius:8px;background:var(--surface3);color:var(--text);text-align:right;font-size:12px;">${msg}</div>`;
1194
+ hist.scrollTop=hist.scrollHeight;
1195
+
1196
+ if(currentUser.toLowerCase()==="guest"){
1197
+ hist.innerHTML+=`<div style="padding:8px 12px;border-radius:8px;background:rgba(59,130,246,.06);border:1px solid rgba(59,130,246,.12);color:var(--text3);font-size:12px;">🔒 Chat vyžaduje registraci.</div>`;
1198
+ hist.scrollTop=hist.scrollHeight;
1199
+ return;
1200
+ }
1201
+
1202
+ const grMsg=document.querySelector("#gr-chat-msg textarea");
1203
+ const grJson=document.querySelector("#gr-chat-json textarea");
1204
+ const grUser=document.querySelector("#gr-chat-user textarea");
1205
+ grMsg.value=msg; grMsg.dispatchEvent(new Event("input",{bubbles:true}));
1206
+ grJson.value=latestExtractedJson; grJson.dispatchEvent(new Event("input",{bubbles:true}));
1207
+ grUser.value=currentUser; grUser.dispatchEvent(new Event("input",{bubbles:true}));
1208
+ document.querySelector("#gr-chat-btn").click();
1209
+ if(chatPolling) clearInterval(chatPolling);
1210
+ chatPolling=setInterval(checkChatResponse,150);
1211
  }
1212
 
1213
+ function checkChatResponse(){
1214
+ const el=document.querySelector("#gr-chat-out textarea");
1215
+ if(!el) return;
1216
+ const val=el.value;
1217
+ if(val&&val.trim()!==""){
1218
+ clearInterval(chatPolling); chatPolling=null;
1219
+ el.value=""; el.dispatchEvent(new Event("input",{bubbles:true}));
1220
+ const hist=document.getElementById("chat-history");
1221
+ hist.innerHTML+=`<div style="padding:8px 12px;border-radius:8px;background:rgba(59,130,246,.06);border:1px solid rgba(59,130,246,.12);color:var(--text2);font-size:12px;">🤖 ${val}</div>`;
1222
+ hist.scrollTop=hist.scrollHeight;
1223
+ }
 
 
 
1224
  }
1225
 
1226
+ /* REGISTER ACTION — backend expects encoded payload in password field */
1227
+ /* Override gr_register to decode and call db_register with tos flag */
1228
+
1229
+ setTimeout(checkLoginState, 600);
1230
  </script>
1231
  </body>
1232
  </html>
1233
  """
1234
 
1235
+ # ─── GRADIO APP ──────────────────────────────────────────────────────────────
1236
+ def gr_register_with_tos(username, password_encoded):
1237
+ """Decodes REGISTER::<pass>::<json_extra> or plain password for register."""
1238
+ if password_encoded.startswith("REGISTER::"):
1239
+ parts = password_encoded.split("::", 2)
1240
+ real_pass = parts[1] if len(parts)>1 else ""
1241
+ try:
1242
+ extra = json.loads(parts[2]) if len(parts)>2 else {}
1243
+ except:
1244
+ extra = {}
1245
+ email = extra.get("email","")
1246
+ tos = extra.get("tos", False)
1247
+ return db_register(username, real_pass, email, tos)
1248
+ return db_register(username, password_encoded, "", True)
1249
+
1250
+ with gr.Blocks(css=CUSTOM_CSS, title="OmniParse AI Enterprise") as demo:
1251
  gr.HTML(HTML_UI)
1252
 
 
1253
  with gr.Row(elem_id="hidden-controls", visible=False):
1254
  with gr.Column():
1255
+ gr_image_input = gr.Textbox(elem_id="gr-image-input", visible=False)
1256
+ gr_current_user = gr.Textbox(elem_id="gr-current-user", visible=False)
1257
+ gr_parse_btn = gr.Button("parse", elem_id="gr-parse-btn", visible=False)
1258
+ gr_json_output = gr.Textbox(elem_id="gr-json-output", visible=False)
1259
+
1260
+ gr_chat_msg = gr.Textbox(elem_id="gr-chat-msg", visible=False)
1261
+ gr_chat_json = gr.Textbox(elem_id="gr-chat-json", visible=False)
1262
+ gr_chat_user = gr.Textbox(elem_id="gr-chat-user", visible=False)
1263
+ gr_chat_btn = gr.Button("chat", elem_id="gr-chat-btn", visible=False)
1264
+ gr_chat_out = gr.Textbox(elem_id="gr-chat-out", visible=False)
1265
+
1266
+ gr_auth_user = gr.Textbox(elem_id="gr-auth-user", visible=False)
1267
+ gr_auth_pass = gr.Textbox(elem_id="gr-auth-pass", visible=False)
1268
+ gr_login_btn = gr.Button("login", elem_id="gr-login-btn", visible=False)
1269
+ gr_register_btn = gr.Button("register", elem_id="gr-register-btn", visible=False)
1270
+ gr_auth_response = gr.Textbox(elem_id="gr-auth-response", visible=False)
1271
+
1272
+ gr_checkout_btn = gr.Button("checkout", elem_id="gr-checkout-btn", visible=False)
1273
+ gr_checkout_out = gr.Textbox(elem_id="gr-checkout-out", visible=False)
1274
+
1275
+ gr_parse_btn.click(fn=gr_parse, inputs=[gr_image_input, gr_current_user], outputs=[gr_json_output])
1276
+ gr_chat_btn.click(fn=gr_chat, inputs=[gr_chat_msg, gr_chat_json, gr_chat_user], outputs=[gr_chat_out])
1277
+ gr_login_btn.click(fn=db_login, inputs=[gr_auth_user, gr_auth_pass], outputs=[gr_auth_response])
1278
+ gr_register_btn.click(fn=gr_register_with_tos, inputs=[gr_auth_user, gr_auth_pass], outputs=[gr_auth_response])
1279
+ gr_checkout_btn.click(fn=gr_checkout, inputs=[gr_auth_user], outputs=[gr_checkout_out])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1280
 
1281
  if __name__ == "__main__":
1282
  demo.launch()