Spaces:
Runtime error
Runtime error
Update app.py
Browse files
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
|
| 17 |
|
| 18 |
-
# ───
|
| 19 |
DB_FILE = "omniparse.db"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
| 21 |
def init_db():
|
| 22 |
conn = sqlite3.connect(DB_FILE)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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 |
-
|
| 54 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
# ─── AUTH ACTIONS
|
| 57 |
-
def db_register(username, password):
|
| 58 |
if not username or not password:
|
| 59 |
-
return json.dumps({"status":
|
| 60 |
-
|
| 61 |
username = username.strip().lower()
|
| 62 |
if username == "guest":
|
| 63 |
-
return json.dumps({"status":
|
| 64 |
-
|
|
|
|
| 65 |
try:
|
| 66 |
conn = sqlite3.connect(DB_FILE)
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
if
|
| 70 |
conn.close()
|
| 71 |
-
return json.dumps({"status":
|
| 72 |
-
|
| 73 |
h, salt = hash_password(password)
|
| 74 |
-
|
|
|
|
| 75 |
conn.commit()
|
| 76 |
conn.close()
|
| 77 |
-
return json.dumps({"status":
|
| 78 |
except Exception as e:
|
| 79 |
-
return json.dumps({"status":
|
| 80 |
|
| 81 |
def db_login(username, password):
|
| 82 |
if not username or not password:
|
| 83 |
-
return json.dumps({"status":
|
| 84 |
-
|
| 85 |
username = username.strip().lower()
|
| 86 |
try:
|
| 87 |
conn = sqlite3.connect(DB_FILE)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
row =
|
| 91 |
-
|
| 92 |
if not row:
|
| 93 |
conn.close()
|
| 94 |
-
return json.dumps({"status":
|
| 95 |
-
|
| 96 |
-
user_id, stored_hash, salt = row
|
| 97 |
-
|
| 98 |
-
# Podpora pro session refresh
|
| 99 |
if password == "SESSION_REFRESH":
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
docs_parsed = stats[0] or 0
|
| 103 |
-
fields_extracted = stats[1] or 0
|
| 104 |
conn.close()
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
})
|
| 110 |
-
|
| 111 |
h, _ = hash_password(password, salt)
|
| 112 |
if h != stored_hash:
|
| 113 |
conn.close()
|
| 114 |
-
return json.dumps({"status":
|
| 115 |
-
|
| 116 |
-
|
| 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 |
-
|
| 124 |
-
|
| 125 |
-
"stats": {"docs_parsed": docs_parsed, "fields_extracted": fields_extracted}
|
| 126 |
-
})
|
| 127 |
except Exception as e:
|
| 128 |
-
return json.dumps({"status":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
# ───
|
| 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
|
| 140 |
except Exception as e:
|
| 141 |
MODEL_LOADED = False
|
| 142 |
-
processor = None
|
| 143 |
-
model = None
|
| 144 |
device = "cpu"
|
| 145 |
-
print(f"Model load
|
| 146 |
|
| 147 |
-
def clean_and_float(
|
| 148 |
try:
|
| 149 |
-
|
| 150 |
-
|
| 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":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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,
|
| 188 |
-
|
| 189 |
-
)
|
| 190 |
-
sequence = re.sub(r"<[^>]+>", " ", sequence).strip()
|
| 191 |
-
|
| 192 |
extracted_fields = {}
|
| 193 |
-
|
| 194 |
-
for line in lines:
|
| 195 |
if ":" in line:
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
if
|
| 200 |
-
extracted_fields[
|
| 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 |
-
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
# Kontrola duplicity v rámci přihlášeného uživatele (pokud to není host)
|
| 222 |
is_duplicate = False
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
if not is_guest and invoice_fp != "UNKNOWN":
|
| 226 |
try:
|
| 227 |
conn = sqlite3.connect(DB_FILE)
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 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
|
| 238 |
pass
|
| 239 |
|
| 240 |
-
subtotal_val = clean_and_float(extracted_fields.get("SUBTOTAL",
|
| 241 |
-
tax_val = clean_and_float(extracted_fields.get("TAX",
|
| 242 |
-
total_val = clean_and_float(extracted_fields.get("TOTAL",
|
| 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:
|
| 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":
|
| 255 |
"duplicate_detected": is_duplicate,
|
| 256 |
"cross_field_validation": validation_status,
|
| 257 |
},
|
| 258 |
"extracted_data": extracted_fields,
|
| 259 |
}
|
| 260 |
|
| 261 |
-
#
|
| 262 |
-
if not is_guest:
|
| 263 |
-
fields_count = len(extracted_fields)
|
| 264 |
try:
|
| 265 |
conn = sqlite3.connect(DB_FILE)
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 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 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
| 286 |
else:
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 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":
|
| 315 |
|
| 316 |
|
| 317 |
-
def agent_chat(user_message: str, extracted_json: 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
|
| 326 |
fields = {}
|
| 327 |
compliance = {}
|
| 328 |
|
| 329 |
if not fields:
|
| 330 |
-
return "Nejdřív nahraj a vytěž
|
| 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)
|
| 337 |
matched.append(f"**{key}**: {val}")
|
| 338 |
-
|
| 339 |
if matched:
|
| 340 |
-
return "
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
return f"
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 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 |
-
|
|
|
|
| 377 |
CUSTOM_CSS = """
|
| 378 |
-
body
|
| 379 |
-
.gradio-container
|
| 380 |
-
footer
|
| 381 |
-
#hidden-controls
|
| 382 |
"""
|
| 383 |
|
| 384 |
-
|
|
|
|
| 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:
|
| 396 |
-
--
|
| 397 |
-
--
|
| 398 |
-
--
|
| 399 |
-
--
|
| 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 |
-
.
|
|
|
|
| 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,.
|
| 414 |
-
.btn-primary{background:linear-gradient(135deg,var(--blue)
|
| 415 |
-
.btn-primary:hover:not(:disabled){filter:brightness(1.
|
| 416 |
-
.btn-primary:disabled{opacity:.
|
| 417 |
-
.btn-secondary{background:var(--surface2);border:1px solid var(--border2);color:var(--text2);border-radius:
|
| 418 |
.btn-secondary:hover{border-color:var(--blue-light);color:var(--text);}
|
| 419 |
-
.
|
| 420 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,.
|
| 423 |
.grad{background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}
|
| 424 |
-
.
|
|
|
|
| 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 |
-
<!-- ═══
|
| 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-
|
| 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/
|
| 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-
|
| 442 |
-
<p class="text-
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
</div>
|
| 454 |
</div>
|
| 455 |
-
|
| 456 |
-
<div class="flex flex-col gap-2
|
| 457 |
-
<button onclick="handleAuthAction(
|
| 458 |
-
Přihlásit se
|
| 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 |
-
|
| 471 |
-
Pokračovat bez registrace (Free Plan) →
|
| 472 |
</button>
|
| 473 |
</div>
|
| 474 |
</div>
|
| 475 |
</div>
|
| 476 |
|
| 477 |
|
| 478 |
-
<!-- ═══
|
| 479 |
-
<div id="
|
| 480 |
-
<
|
| 481 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
|
|
|
| 487 |
</div>
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
<span id="status-
|
| 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"
|
| 495 |
-
<button onclick="
|
|
|
|
| 496 |
</div>
|
| 497 |
</div>
|
| 498 |
</header>
|
| 499 |
-
|
| 500 |
-
<main style="max-width:
|
| 501 |
-
|
| 502 |
-
<
|
|
|
|
| 503 |
<div class="card p-4 flex items-center gap-3">
|
| 504 |
-
<div
|
| 505 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 506 |
</div>
|
| 507 |
<div class="card p-4 flex items-center gap-3">
|
| 508 |
-
<div
|
| 509 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 510 |
</div>
|
| 511 |
<div class="card p-4 flex items-center gap-3">
|
| 512 |
-
<div
|
| 513 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 514 |
</div>
|
| 515 |
<div class="card p-4 flex items-center gap-3">
|
| 516 |
-
<div
|
| 517 |
-
<div>
|
|
|
|
|
|
|
|
|
|
| 518 |
</div>
|
| 519 |
</div>
|
| 520 |
-
|
| 521 |
-
<!--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
|
| 523 |
-
<!-- Left
|
| 524 |
-
<div
|
| 525 |
<div class="card p-5 space-y-4">
|
| 526 |
-
<div
|
| 527 |
-
<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
|
| 538 |
-
<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%;
|
| 543 |
</div>
|
| 544 |
</div>
|
| 545 |
-
|
| 546 |
<div class="card p-4">
|
| 547 |
-
<button id="exec-btn" class="btn-primary w-full py-
|
| 548 |
-
<span id="btn-idle"
|
|
|
|
|
|
|
|
|
|
| 549 |
<span id="btn-loading" style="display:none;" class="flex items-center gap-2">
|
| 550 |
-
<svg class="spin h-4 w-4
|
| 551 |
-
Extrahuji data
|
| 552 |
</span>
|
| 553 |
</button>
|
| 554 |
</div>
|
| 555 |
-
|
|
|
|
| 556 |
<div class="card p-5 space-y-3">
|
| 557 |
-
<div
|
| 558 |
-
|
| 559 |
-
<
|
| 560 |
-
|
| 561 |
-
|
|
|
|
|
|
|
|
|
|
| 562 |
</button>
|
| 563 |
</div>
|
| 564 |
-
<div id="export-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
</div>
|
| 566 |
</div>
|
| 567 |
-
|
| 568 |
-
<!-- Right
|
| 569 |
-
<div
|
| 570 |
<div class="card p-5 space-y-3">
|
| 571 |
-
<div
|
| 572 |
-
|
| 573 |
-
<
|
|
|
|
|
|
|
|
|
|
| 574 |
</div>
|
| 575 |
</div>
|
| 576 |
-
|
| 577 |
-
<div
|
| 578 |
<div class="card p-4 space-y-1">
|
| 579 |
-
<p class="
|
| 580 |
-
<p id="comp-val"
|
| 581 |
</div>
|
| 582 |
<div class="card p-4 space-y-1">
|
| 583 |
-
<p class="
|
| 584 |
-
<p id="comp-dup"
|
| 585 |
</div>
|
| 586 |
</div>
|
| 587 |
-
|
| 588 |
-
<
|
| 589 |
-
|
| 590 |
-
<div
|
| 591 |
-
<
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
</div>
|
| 594 |
</div>
|
| 595 |
-
<div
|
| 596 |
-
<input type="text" id="chat-input" class="
|
| 597 |
-
<button onclick="sendChatMessage()" class="btn-primary px-3
|
| 598 |
</div>
|
| 599 |
</div>
|
| 600 |
</div>
|
| 601 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
</main>
|
| 603 |
</div>
|
| 604 |
|
| 605 |
-
|
|
|
|
| 606 |
<script>
|
|
|
|
| 607 |
let currentBase64 = "";
|
| 608 |
let latestExtractedJson = "";
|
| 609 |
let csvB64Data = "";
|
| 610 |
-
let authPolling = null;
|
| 611 |
-
let
|
| 612 |
-
let
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
|
| 618 |
-
function
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
toast.style.opacity = '0';
|
| 626 |
-
setTimeout(() => toast.remove(), 300);
|
| 627 |
-
}, 4000);
|
| 628 |
}
|
| 629 |
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 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 |
-
|
| 666 |
-
|
| 667 |
-
|
|
|
|
| 668 |
}
|
| 669 |
|
| 670 |
function handleAuthAction(action) {
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
const
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
} else {
|
| 692 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
}
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
|
|
|
| 697 |
}
|
| 698 |
|
| 699 |
function checkAuthResponse() {
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 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 |
-
|
| 736 |
-
|
| 737 |
}
|
| 738 |
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
}
|
| 752 |
|
| 753 |
-
function
|
| 754 |
-
|
| 755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
}
|
| 757 |
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 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 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
}
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
}
|
| 837 |
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 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 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
|
|
|
|
|
|
|
|
|
| 877 |
}
|
| 878 |
|
| 879 |
-
function checkChatResponse()
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 896 |
</script>
|
| 897 |
</body>
|
| 898 |
</html>
|
| 899 |
"""
|
| 900 |
|
| 901 |
-
# ─── GRADIO
|
| 902 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 909 |
-
gr_current_user = gr.Textbox(
|
| 910 |
-
gr_parse_btn
|
| 911 |
-
gr_json_output
|
| 912 |
-
|
| 913 |
-
gr_chat_msg
|
| 914 |
-
gr_chat_json = gr.Textbox(
|
| 915 |
-
|
| 916 |
-
|
| 917 |
-
|
| 918 |
-
|
| 919 |
-
|
| 920 |
-
|
| 921 |
-
|
| 922 |
-
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
)
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 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()
|