Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -10,9 +10,122 @@ import base64
|
|
| 10 |
import io
|
| 11 |
import os
|
| 12 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
print("Initializing OmniParse AI Enterprise Terminal v5.0...")
|
| 15 |
|
|
|
|
| 16 |
model_name = "naver-clova-ix/donut-base-finetuned-cord-v2"
|
| 17 |
try:
|
| 18 |
processor = DonutProcessor.from_pretrained(model_name)
|
|
@@ -20,15 +133,13 @@ try:
|
|
| 20 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 21 |
model.to(device)
|
| 22 |
MODEL_LOADED = True
|
| 23 |
-
print(f"Model loaded on {device}")
|
| 24 |
except Exception as e:
|
| 25 |
MODEL_LOADED = False
|
| 26 |
processor = None
|
| 27 |
model = None
|
| 28 |
device = "cpu"
|
| 29 |
-
print(f"Model load warning: {e}")
|
| 30 |
-
|
| 31 |
-
PROCESSED_INVOICES_CACHE = set()
|
| 32 |
|
| 33 |
def clean_and_float(text_value):
|
| 34 |
try:
|
|
@@ -37,17 +148,11 @@ def clean_and_float(text_value):
|
|
| 37 |
except Exception:
|
| 38 |
return 0.0
|
| 39 |
|
| 40 |
-
|
| 41 |
-
def parse_invoice(image_b64: str):
|
| 42 |
-
"""
|
| 43 |
-
Receives base64-encoded image string from JS frontend.
|
| 44 |
-
Returns JSON string with extracted data.
|
| 45 |
-
"""
|
| 46 |
if not image_b64:
|
| 47 |
-
return json.dumps({"parser_status": "ERROR", "error_log": "
|
| 48 |
|
| 49 |
try:
|
| 50 |
-
# Decode base64 → PIL Image
|
| 51 |
header, _, data = image_b64.partition(",")
|
| 52 |
img_bytes = base64.b64decode(data if data else image_b64)
|
| 53 |
img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
|
|
@@ -62,7 +167,6 @@ def parse_invoice(image_b64: str):
|
|
| 62 |
decoder_input_ids = processor.tokenizer(
|
| 63 |
task_prompt, add_special_tokens=False, return_tensors="pt"
|
| 64 |
).input_ids
|
| 65 |
-
|
| 66 |
pixel_values = processor(img, return_tensors="pt").pixel_values
|
| 67 |
|
| 68 |
outputs = model.generate(
|
|
@@ -82,7 +186,6 @@ def parse_invoice(image_b64: str):
|
|
| 82 |
)
|
| 83 |
sequence = re.sub(r"<[^>]+>", " ", sequence).strip()
|
| 84 |
|
| 85 |
-
# Parse key/value pairs from sequence
|
| 86 |
extracted_fields = {}
|
| 87 |
lines = re.split(r"[;\n]", sequence)
|
| 88 |
for line in lines:
|
|
@@ -96,25 +199,38 @@ def parse_invoice(image_b64: str):
|
|
| 96 |
if not extracted_fields:
|
| 97 |
extracted_fields["RAW_TEXT"] = sequence.strip()
|
| 98 |
else:
|
| 99 |
-
# Fallback demo data
|
| 100 |
extracted_fields = {
|
| 101 |
-
"STORE_NAME": "
|
| 102 |
-
"TOTAL": "
|
| 103 |
-
"SUBTOTAL": "
|
| 104 |
-
"TAX": "
|
| 105 |
-
"DATE": "
|
| 106 |
-
"INVOICE_NUMBER": "INV-
|
| 107 |
}
|
| 108 |
|
| 109 |
elapsed = round(time.time() - start_time, 2)
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
human_review = confidence_score < 0.95
|
| 113 |
|
| 114 |
invoice_fp = extracted_fields.get("INVOICE_NUMBER", extracted_fields.get("TOTAL", "UNKNOWN"))
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
subtotal_val = clean_and_float(extracted_fields.get("SUBTOTAL", "0"))
|
| 120 |
tax_val = clean_and_float(extracted_fields.get("TAX", "0"))
|
|
@@ -137,7 +253,38 @@ def parse_invoice(image_b64: str):
|
|
| 137 |
"extracted_data": extracted_fields,
|
| 138 |
}
|
| 139 |
|
| 140 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
rows = [["Field", "Value"]]
|
| 142 |
rows.append(["confidence_score", str(confidence_score)])
|
| 143 |
rows.append(["human_review_required", str(human_review)])
|
|
@@ -151,7 +298,6 @@ def parse_invoice(image_b64: str):
|
|
| 151 |
df = pd.DataFrame(rows[1:], columns=rows[0])
|
| 152 |
df.to_csv(csv_path, index=False)
|
| 153 |
|
| 154 |
-
# Embed CSV as base64 so JS can trigger download
|
| 155 |
with open(csv_path, "rb") as f:
|
| 156 |
csv_b64 = base64.b64encode(f.read()).decode()
|
| 157 |
|
|
@@ -163,10 +309,6 @@ def parse_invoice(image_b64: str):
|
|
| 163 |
|
| 164 |
|
| 165 |
def agent_chat(user_message: str, extracted_json: str) -> str:
|
| 166 |
-
"""
|
| 167 |
-
Simple rule-based + keyword agent that reads extracted JSON.
|
| 168 |
-
Returns agent reply string.
|
| 169 |
-
"""
|
| 170 |
if not user_message.strip():
|
| 171 |
return ""
|
| 172 |
|
|
@@ -179,11 +321,9 @@ def agent_chat(user_message: str, extracted_json: str) -> str:
|
|
| 179 |
compliance = {}
|
| 180 |
|
| 181 |
if not fields:
|
| 182 |
-
return "
|
| 183 |
|
| 184 |
msg = user_message.lower()
|
| 185 |
-
|
| 186 |
-
# Direct key match
|
| 187 |
matched = []
|
| 188 |
for key, val in fields.items():
|
| 189 |
clean_key = key.lower().replace("_", " ")
|
|
@@ -191,70 +331,58 @@ def agent_chat(user_message: str, extracted_json: str) -> str:
|
|
| 191 |
matched.append(f"**{key}**: {val}")
|
| 192 |
|
| 193 |
if matched:
|
| 194 |
-
return "
|
| 195 |
|
| 196 |
-
if any(w in msg for w in ["total", "
|
| 197 |
-
total = fields.get("TOTAL", fields.get("TOTAL_PRICE", "
|
| 198 |
subtotal = fields.get("SUBTOTAL", "—")
|
| 199 |
tax = fields.get("TAX", "—")
|
| 200 |
-
return f"💰 **
|
| 201 |
|
| 202 |
-
if any(w in msg for w in ["vendor", "
|
| 203 |
-
vendor = fields.get("STORE_NAME", fields.get("VENDOR", fields.get("COMPANY", "
|
| 204 |
-
return f"🏢 **
|
| 205 |
|
| 206 |
-
if any(w in msg for w in ["date", "
|
| 207 |
-
date = fields.get("DATE", fields.get("INVOICE_DATE", "
|
| 208 |
-
return f"📅 **
|
| 209 |
|
| 210 |
-
if any(w in msg for w in ["confidence", "
|
| 211 |
score = compliance.get("confidence_score", "—")
|
| 212 |
-
return f"🎯 **
|
| 213 |
|
| 214 |
-
if any(w in msg for w in ["
|
| 215 |
-
dup = compliance.get("duplicate_detected", False)
|
| 216 |
-
return f"🔍 **Duplicate check**: {'⚠️ This document was seen before!' if dup else '✅ First occurrence — no duplicate detected.'}"
|
| 217 |
-
|
| 218 |
-
if any(w in msg for w in ["validation", "math", "check", "valid"]):
|
| 219 |
-
val_status = compliance.get("cross_field_validation", "—")
|
| 220 |
-
return f"✅ **Cross-field validation**: {val_status}"
|
| 221 |
-
|
| 222 |
-
if any(w in msg for w in ["all", "everything", "fields", "data", "show"]):
|
| 223 |
lines = [f"• **{k}**: {v}" for k, v in list(fields.items())[:10]]
|
| 224 |
-
return "📋 **
|
| 225 |
|
| 226 |
keys = list(fields.keys())[:6]
|
| 227 |
-
return f"
|
| 228 |
|
| 229 |
|
| 230 |
-
# ───
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
return parse_invoice(image_b64)
|
| 234 |
|
| 235 |
def gr_chat(user_msg, extracted_json):
|
| 236 |
return agent_chat(user_msg, extracted_json)
|
| 237 |
|
| 238 |
|
| 239 |
-
# ───
|
| 240 |
CUSTOM_CSS = """
|
| 241 |
body { margin: 0 !important; padding: 0 !important; background: #09090b !important; }
|
| 242 |
.gradio-container { max-width: 100% !important; padding: 0 !important; margin: 0 !important; background: #09090b !important; }
|
| 243 |
footer { display: none !important; }
|
| 244 |
-
#omniparse-root { width: 100%; }
|
| 245 |
#hidden-controls { display: none !important; }
|
| 246 |
"""
|
| 247 |
|
| 248 |
-
# ─── HTML UI ────────────────────────────────────────────────────────────────
|
| 249 |
HTML_UI = """
|
| 250 |
<!DOCTYPE html>
|
| 251 |
-
<html lang="
|
| 252 |
<head>
|
| 253 |
<meta charset="UTF-8"/>
|
| 254 |
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
| 255 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 256 |
-
<link
|
| 257 |
-
<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"/>
|
| 258 |
<style>
|
| 259 |
*{box-sizing:border-box;margin:0;padding:0;}
|
| 260 |
:root{
|
|
@@ -271,652 +399,477 @@ HTML_UI = """
|
|
| 271 |
--blue-light: #60a5fa;
|
| 272 |
--blue-dark: #1d4ed8;
|
| 273 |
--green: #34d399;
|
| 274 |
-
--amber: #fbbf24;
|
| 275 |
-
--purple: #a78bfa;
|
| 276 |
-
--red: #f87171;
|
| 277 |
}
|
| 278 |
-
body{font-family:'Inter',
|
| 279 |
-
::-webkit-scrollbar{width:4px;height:4px;}
|
| 280 |
-
::-webkit-scrollbar-track{background:var(--surface2);}
|
| 281 |
-
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:2px;}
|
| 282 |
-
|
| 283 |
-
@keyframes spin{to{transform:rotate(360deg);}}
|
| 284 |
-
@keyframes pulse{0%,100%{opacity:.4;}50%{opacity:1;}}
|
| 285 |
-
@keyframes fadeUp{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
|
| 286 |
-
@keyframes slideIn{from{transform:translateX(110%);opacity:0;}to{transform:translateX(0);opacity:1;}}
|
| 287 |
-
@keyframes slideOut{from{opacity:1;transform:translateX(0);}to{opacity:0;transform:translateX(110%);}}
|
| 288 |
-
@keyframes shimmer{0%{background-position:-400px 0;}100%{background-position:400px 0;}}
|
| 289 |
-
@keyframes blink{0%,80%,100%{opacity:0;}40%{opacity:1;}}
|
| 290 |
-
@keyframes glow{0%,100%{box-shadow:0 0 8px rgba(59,130,246,0.3);}50%{box-shadow:0 0 20px rgba(59,130,246,0.6);}}
|
| 291 |
-
|
| 292 |
-
.fade-up{animation:fadeUp 0.35s ease both;}
|
| 293 |
-
.spin{animation:spin 0.8s linear infinite;}
|
| 294 |
-
.pulse-dot{animation:pulse 1.4s ease-in-out infinite;}
|
| 295 |
-
.pulse-dot:nth-child(2){animation-delay:.2s;}
|
| 296 |
-
.pulse-dot:nth-child(3){animation-delay:.4s;}
|
| 297 |
-
.typing-dot{animation:blink 1.4s infinite both;}
|
| 298 |
-
.typing-dot:nth-child(2){animation-delay:.2s;}
|
| 299 |
-
.typing-dot:nth-child(3){animation-delay:.4s;}
|
| 300 |
-
.glow-blue{animation:glow 2s ease-in-out infinite;}
|
| 301 |
-
|
| 302 |
.card{background:var(--surface2);border:1px solid var(--border);border-radius:14px;}
|
| 303 |
-
.card-inner{background:#1c1c1f;border:1px solid var(--border);border-radius:10px;}
|
| 304 |
.section-label{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--text4);}
|
| 305 |
-
|
| 306 |
.drop-zone{border:1.5px dashed var(--border2);border-radius:12px;transition:all .25s ease;cursor:pointer;}
|
| 307 |
-
.drop-zone:hover,.drop-zone.drag-over{border-color:var(--blue);background:rgba(59,130,246,.05);
|
| 308 |
-
|
| 309 |
-
.btn-primary
|
| 310 |
-
.btn-primary:hover:not(:disabled){box-shadow:0 0 32px rgba(59,130,246,.45);transform:translateY(-1px);filter:brightness(1.08);}
|
| 311 |
-
.btn-primary:active:not(:disabled){transform:translateY(0);}
|
| 312 |
.btn-primary:disabled{opacity:.45;cursor:not-allowed;}
|
| 313 |
-
.btn-secondary{background:var(--surface2);border:1px solid var(--border2);color:var(--text2);border-radius:
|
| 314 |
-
.btn-secondary:hover{border-color:var(--blue-light);color:var(--text);
|
| 315 |
-
|
| 316 |
-
.
|
| 317 |
-
.
|
| 318 |
-
.
|
| 319 |
-
|
| 320 |
-
.
|
| 321 |
-
|
| 322 |
-
.ai-avatar{box-shadow:0 0 0 2px rgba(59,130,246,.3),0 0 12px rgba(59,130,246,.2);}
|
| 323 |
-
.chat-input{background:var(--surface2);border:1px solid var(--border2);color:var(--text);border-radius:10px;padding:10px 14px;font-size:14px;font-family:'Inter',sans-serif;resize:none;outline:none;width:100%;transition:border-color .18s;}
|
| 324 |
-
.chat-input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(59,130,246,.12);}
|
| 325 |
-
.chat-input::placeholder{color:var(--text4);}
|
| 326 |
-
|
| 327 |
-
.code-view{background:#0d1117;border:1px solid #21262d;border-radius:10px;font-family:'JetBrains Mono','Fira Code',ui-monospace,monospace;}
|
| 328 |
-
.json-key{color:#79c0ff;}
|
| 329 |
-
.json-string{color:#a5d6ff;}
|
| 330 |
-
.json-number{color:#ffa657;}
|
| 331 |
-
.json-bool{color:#ff7b72;}
|
| 332 |
-
.json-null{color:#8b949e;}
|
| 333 |
-
pre.json-pre{white-space:pre-wrap;word-break:break-word;font-size:12.5px;line-height:1.75;}
|
| 334 |
-
|
| 335 |
-
.dl-card{background:linear-gradient(135deg,rgba(59,130,246,.07) 0%,rgba(29,78,216,.04) 100%);border:1px solid rgba(59,130,246,.2);border-radius:12px;transition:all .2s ease;}
|
| 336 |
-
.dl-card:hover{border-color:rgba(59,130,246,.4);background:linear-gradient(135deg,rgba(59,130,246,.1) 0%,rgba(29,78,216,.08) 100%);}
|
| 337 |
.toast{animation:slideIn .3s ease;}
|
| 338 |
-
|
| 339 |
-
.stat-chip{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:12px 16px;}
|
| 340 |
-
.prog-track{background:var(--border);border-radius:4px;height:3px;}
|
| 341 |
-
.prog-fill{height:3px;border-radius:4px;background:linear-gradient(90deg,var(--blue),#6366f1);transition:width .4s ease;}
|
| 342 |
-
.grad{background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;}
|
| 343 |
-
.shimmer{background:linear-gradient(90deg,#27272a 25%,#3f3f46 50%,#27272a 75%);background-size:800px 100%;animation:shimmer 1.8s infinite;}
|
| 344 |
-
.file-item{background:#1c1c1f;border:1px solid var(--border);border-radius:8px;transition:border-color .18s;}
|
| 345 |
-
.file-item:hover{border-color:var(--border2);}
|
| 346 |
-
.stage-dot{width:8px;height:8px;border-radius:50%;background:var(--border2);transition:background .3s ease;}
|
| 347 |
-
.stage-dot.active{background:var(--blue);animation:pulse 1.4s ease-in-out infinite;}
|
| 348 |
-
.stage-dot.done{background:var(--green);animation:none;}
|
| 349 |
</style>
|
| 350 |
</head>
|
| 351 |
<body>
|
| 352 |
|
| 353 |
<div id="toast-root" style="position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;"></div>
|
| 354 |
|
| 355 |
-
<
|
| 356 |
-
<div
|
| 357 |
-
<div
|
| 358 |
-
<div
|
| 359 |
-
<svg width="
|
| 360 |
</div>
|
| 361 |
-
<
|
| 362 |
-
<
|
| 363 |
-
<span style="width:6px;height:6px;border-radius:50%;background:#60a5fa;box-shadow:0 0 6px #60a5fa;"></span>
|
| 364 |
-
Enterprise v5.0
|
| 365 |
-
</span>
|
| 366 |
</div>
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
</div>
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
</div>
|
| 374 |
</div>
|
| 375 |
-
</
|
| 376 |
|
| 377 |
-
<main style="max-width:1280px;margin:0 auto;padding:24px;">
|
| 378 |
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
</div>
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
| 389 |
</div>
|
| 390 |
-
<div><p style="font-size:11px;color:var(--text4);margin-bottom:2px;">Fields Extracted</p><p id="st-fields" style="font-size:16px;font-weight:600;">0</p></div>
|
| 391 |
</div>
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
</div>
|
| 396 |
-
<div
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
<div
|
| 400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
</div>
|
| 402 |
-
<div><p style="font-size:11px;color:var(--text4);margin-bottom:2px;">Confidence</p><p id="st-conf" style="font-size:16px;font-weight:600;">—</p></div>
|
| 403 |
</div>
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
<div>
|
| 412 |
-
<p style="font-size:14px;font-weight:600;">Document Input</p>
|
| 413 |
-
<p style="font-size:12px;color:var(--text4);margin-top:2px;">Upload invoices, receipts, purchase orders</p>
|
| 414 |
-
</div>
|
| 415 |
-
<span class="section-label">Stage 01</span>
|
| 416 |
-
</div>
|
| 417 |
-
|
| 418 |
-
<div id="drop-zone" class="drop-zone" style="padding:32px 20px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;text-align:center;min-height:180px;"
|
| 419 |
-
onclick="document.getElementById('file-input').click()"
|
| 420 |
-
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
| 421 |
-
ondragleave="this.classList.remove('drag-over')"
|
| 422 |
-
ondrop="handleDrop(event)">
|
| 423 |
-
<input type="file" id="file-input" style="display:none;" accept=".pdf,.jpg,.jpeg,.png,.tiff,.webp" onchange="handleFiles(this.files)"/>
|
| 424 |
-
<div style="width:52px;height:52px;border-radius:50%;background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.15);display:flex;align-items:center;justify-content:center;">
|
| 425 |
-
<svg width="22" height="22" fill="none" viewBox="0 0 24 24" stroke="#60a5fa" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5"/></svg>
|
| 426 |
</div>
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
</div>
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
<
|
| 434 |
-
<span style="padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500;color:var(--text3);background:var(--border);">PNG</span>
|
| 435 |
-
<span style="padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500;color:var(--text3);background:var(--border);">TIFF</span>
|
| 436 |
</div>
|
| 437 |
</div>
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
</div>
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
</span>
|
| 450 |
-
<span id="btn-loading" style="display:none;align-items:center;gap:8px;">
|
| 451 |
-
<svg class="spin" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
| 452 |
-
Processing…
|
| 453 |
-
</span>
|
| 454 |
-
</button>
|
| 455 |
-
<div id="prog-wrapper" style="display:none;margin-top:14px;">
|
| 456 |
-
<div style="display:flex;justify-content:space-between;margin-bottom:6px;">
|
| 457 |
-
<span id="prog-label" style="font-size:12px;color:var(--text2);">Initializing...</span>
|
| 458 |
-
<span id="prog-pct" style="font-size:12px;font-weight:600;color:var(--blue-light);">0%</span>
|
| 459 |
-
</div>
|
| 460 |
-
<div class="prog-track">
|
| 461 |
-
<div id="prog-fill" class="prog-fill" style="width:0%;"></div>
|
| 462 |
-
</div>
|
| 463 |
-
<div style="display:grid;grid-template-columns:repeat(4,1fr);margin-top:12px;text-align:center;gap:4px;">
|
| 464 |
-
<div>
|
| 465 |
-
<div id="stage-1" class="stage-dot" style="margin:0 auto 6px;"></div>
|
| 466 |
-
<p style="font-size:10px;color:var(--text4);">Ingestion</p>
|
| 467 |
-
</div>
|
| 468 |
-
<div>
|
| 469 |
-
<div id="stage-2" class="stage-dot" style="margin:0 auto 6px;"></div>
|
| 470 |
-
<p style="font-size:10px;color:var(--text4);">Model Parse</p>
|
| 471 |
-
</div>
|
| 472 |
-
<div>
|
| 473 |
-
<div id="stage-3" class="stage-dot" style="margin:0 auto 6px;"></div>
|
| 474 |
-
<p style="font-size:10px;color:var(--text4);">Validation</p>
|
| 475 |
-
</div>
|
| 476 |
-
<div>
|
| 477 |
-
<div id="stage-4" class="stage-dot" style="margin:0 auto 6px;"></div>
|
| 478 |
-
<p style="font-size:10px;color:var(--text4);">Export</p>
|
| 479 |
-
</div>
|
| 480 |
</div>
|
|
|
|
| 481 |
</div>
|
| 482 |
</div>
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
<button id="tab-data" class="tab-btn active" onclick="switchTab('data')">Extracted Data</button>
|
| 490 |
-
<button id="tab-chat" class="tab-btn" onclick="switchTab('chat')">AI Chat Agent</button>
|
| 491 |
</div>
|
| 492 |
-
<span class="section-label">Stage 02</span>
|
| 493 |
</div>
|
| 494 |
-
|
| 495 |
-
<div
|
| 496 |
-
<div
|
| 497 |
-
<
|
| 498 |
-
<p
|
| 499 |
-
<p style="font-size:11px;margin-top:2px;">Execute extraction to inspect payload schemas.</p>
|
| 500 |
</div>
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
<
|
| 504 |
-
<pre class="json-pre"><code id="json-code"></code></pre>
|
| 505 |
-
</div>
|
| 506 |
-
|
| 507 |
-
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
| 508 |
-
<div class="dl-card" style="padding:12px;display:flex;align-items:center;justify-content:between;cursor:pointer;" onclick="downloadCSV()">
|
| 509 |
-
<div>
|
| 510 |
-
<p style="font-size:12px;font-weight:600;color:var(--text);">Structured CSV</p>
|
| 511 |
-
<p style="font-size:10px;color:var(--text4);margin-top:1px;">Tabular matrix mapping</p>
|
| 512 |
-
</div>
|
| 513 |
-
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--blue-light)" stroke-width="2"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
| 514 |
-
</div>
|
| 515 |
-
<div class="dl-card" style="padding:12px;display:flex;align-items:center;justify-content:between;cursor:pointer;" onclick="downloadJSON()">
|
| 516 |
-
<div>
|
| 517 |
-
<p style="font-size:12px;font-weight:600;color:var(--text);">Raw Payload JSON</p>
|
| 518 |
-
<p style="font-size:10px;color:var(--text4);margin-top:1px;">Standard REST response</p>
|
| 519 |
-
</div>
|
| 520 |
-
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--purple)" stroke-width="2"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
| 521 |
-
</div>
|
| 522 |
-
</div>
|
| 523 |
</div>
|
| 524 |
</div>
|
| 525 |
-
|
| 526 |
-
<div
|
| 527 |
-
<
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
Hello! I am your document agent. Once you extract a document, ask me questions like <em>"What is the total amount?"</em> or <em>"Who is the vendor?"</em>.
|
| 532 |
-
</div>
|
| 533 |
</div>
|
| 534 |
</div>
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
<
|
| 538 |
-
<button id="chat-send-btn" style="position:absolute;right:8px;top:8px;color:var(--text3);cursor:pointer;transition:color .15s;" onclick="sendChatMessage()">
|
| 539 |
-
<svg width="20" height="20" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
| 540 |
-
</button>
|
| 541 |
</div>
|
| 542 |
</div>
|
| 543 |
</div>
|
| 544 |
</div>
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
</main>
|
| 548 |
|
| 549 |
<script>
|
| 550 |
let currentBase64 = "";
|
| 551 |
-
let
|
| 552 |
-
let
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
}, 4000);
|
| 568 |
}
|
| 569 |
|
| 570 |
-
function
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
}
|
| 577 |
|
| 578 |
-
function
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
const fileList = document.getElementById("file-list");
|
| 583 |
-
fileList.innerHTML = `
|
| 584 |
-
<div class="file-item" style="padding:10px 14px;display:flex;align-items:center;justify-content:between;">
|
| 585 |
-
<div style="display:flex;align-items:center;gap:10px;overflow:hidden;">
|
| 586 |
-
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="var(--text3)" stroke-width="2"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
| 587 |
-
<span style="font-size:12px;font-weight:500;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${file.name}</span>
|
| 588 |
-
</div>
|
| 589 |
-
<span style="font-size:11px;color:var(--text4);">${(file.size / 1024).toFixed(1)} KB</span>
|
| 590 |
-
</div>
|
| 591 |
-
`;
|
| 592 |
-
|
| 593 |
-
const reader = new FileReader();
|
| 594 |
-
reader.onload = function(e) {
|
| 595 |
-
currentBase64 = e.target.result;
|
| 596 |
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 601 |
}
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
} else {
|
| 607 |
-
|
| 608 |
}
|
| 609 |
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
};
|
| 613 |
-
reader.readAsDataURL(file);
|
| 614 |
}
|
| 615 |
|
| 616 |
-
function
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
current += 2;
|
| 648 |
-
if(current >= end) {
|
| 649 |
-
current = end;
|
| 650 |
-
clearInterval(interval);
|
| 651 |
-
document.getElementById(`stage-${currentStage}`).className = "stage-dot done";
|
| 652 |
-
if(callback) callback();
|
| 653 |
}
|
| 654 |
-
document.getElementById("prog-pct").innerText = `${current}%`;
|
| 655 |
-
document.getElementById("prog-fill").style.width = `${current}%`;
|
| 656 |
-
}, 40);
|
| 657 |
}
|
| 658 |
|
| 659 |
-
function
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
let checker = setInterval(() => {
|
| 664 |
-
checks++;
|
| 665 |
-
if (hiddenOut && hiddenOut.value && hiddenOut.value.trim() !== "") {
|
| 666 |
-
clearInterval(checker);
|
| 667 |
-
try {
|
| 668 |
-
const payload = JSON.parse(hiddenOut.value);
|
| 669 |
-
handleExtractionSuccess(payload);
|
| 670 |
-
} catch (err) {
|
| 671 |
-
handleExtractionError("Invalid system payload received.");
|
| 672 |
-
}
|
| 673 |
-
} else if (checks > 350) {
|
| 674 |
-
clearInterval(checker);
|
| 675 |
-
handleExtractionError("Model connection timeout.");
|
| 676 |
-
}
|
| 677 |
-
}, 100);
|
| 678 |
}
|
| 679 |
|
| 680 |
-
function
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
document.getElementById("st-docs").innerText = parsedCount;
|
| 694 |
-
document.getElementById("st-fields").innerText = Object.keys(payload.extracted_data || {}).length;
|
| 695 |
-
document.getElementById("st-time").innerText = `${payload.parse_time_seconds || '0.45'}s`;
|
| 696 |
-
document.getElementById("st-conf").innerText = `${((payload.security_and_compliance || {}).confidence_score * 100 || 97).toFixed(0)}%`;
|
| 697 |
-
|
| 698 |
-
const jsonStr = JSON.stringify(payload, null, 2);
|
| 699 |
-
document.getElementById("json-code").innerHTML = syntaxHighlightJSON(jsonStr);
|
| 700 |
-
|
| 701 |
-
document.getElementById("data-placeholder").style.display = "none";
|
| 702 |
-
document.getElementById("data-display").style.display = "flex";
|
| 703 |
-
|
| 704 |
-
switchTab('data');
|
| 705 |
-
showToast("Document successfully processed!");
|
| 706 |
-
|
| 707 |
-
const hiddenOut = document.querySelector("#gr-json-output textarea");
|
| 708 |
-
if(hiddenOut) {
|
| 709 |
-
hiddenOut.value = "";
|
| 710 |
-
hiddenOut.dispatchEvent(new Event('input', { bubbles: true }));
|
| 711 |
-
}
|
| 712 |
-
});
|
| 713 |
-
});
|
| 714 |
}
|
| 715 |
|
| 716 |
-
function
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
document.getElementById("prog-wrapper").style.display = "none";
|
| 720 |
-
|
| 721 |
-
document.getElementById("status-dot").style.background = "var(--red)";
|
| 722 |
-
document.getElementById("status-dot").style.boxShadow = "0 0 8px var(--red)";
|
| 723 |
-
document.getElementById("status-text").innerText = "Error Detected";
|
| 724 |
-
|
| 725 |
-
showToast(errLog, "error");
|
| 726 |
}
|
| 727 |
|
| 728 |
-
function
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
|
|
|
|
|
|
| 746 |
}
|
| 747 |
|
| 748 |
-
function
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
}
|
| 761 |
|
| 762 |
-
function
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 767 |
}
|
| 768 |
|
| 769 |
function sendChatMessage() {
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
<div class="bubble-user" style="padding:10px 14px;font-size:13px;line-height:1.5;color:var(--text);max-width:85%;">
|
| 782 |
-
${msg}
|
| 783 |
-
</div>
|
| 784 |
-
`;
|
| 785 |
-
chatContainer.appendChild(userDiv);
|
| 786 |
-
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 787 |
-
|
| 788 |
-
const thinkDiv = document.createElement("div");
|
| 789 |
-
thinkDiv.id = "ai-thinking-placeholder";
|
| 790 |
-
thinkDiv.className = "fade-up";
|
| 791 |
-
thinkDiv.style = "display:flex;gap:10px;align-items:start;";
|
| 792 |
-
thinkDiv.innerHTML = `
|
| 793 |
-
<div class="ai-avatar" style="width:26px;height:26px;border-radius:50%;background:var(--blue);display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:700;flex-shrink:0;margin-top:2px;">AI</div>
|
| 794 |
-
<div class="bubble-ai" style="padding:10px 14px;display:flex;align-items:center;gap:4px;height:36px;">
|
| 795 |
-
<span class="typing-dot" style="width:5px;height:5px;border-radius:50%;background:var(--text2);"></span>
|
| 796 |
-
<span class="typing-dot" style="width:5px;height:5px;border-radius:50%;background:var(--text2);"></span>
|
| 797 |
-
<span class="typing-dot" style="width:5px;height:5px;border-radius:50%;background:var(--text2);"></span>
|
| 798 |
-
</div>
|
| 799 |
-
`;
|
| 800 |
-
chatContainer.appendChild(thinkDiv);
|
| 801 |
-
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 802 |
-
|
| 803 |
-
const grMsgInput = document.querySelector("#gr-chat-msg textarea");
|
| 804 |
-
const grJsonInput = document.querySelector("#gr-chat-json textarea");
|
| 805 |
-
const grChatBtn = document.getElementById("gr-chat-btn");
|
| 806 |
-
|
| 807 |
-
if (grMsgInput && grChatBtn) {
|
| 808 |
-
grMsgInput.value = msg;
|
| 809 |
-
grMsgInput.dispatchEvent(new Event('input', { bubbles: true }));
|
| 810 |
|
| 811 |
-
|
| 812 |
-
|
|
|
|
|
|
|
| 813 |
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
|
|
|
| 817 |
}
|
| 818 |
|
| 819 |
-
function
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
const chatContainer = document.getElementById("chat-messages");
|
| 833 |
-
const aiDiv = document.createElement("div");
|
| 834 |
-
aiDiv.className = "fade-up";
|
| 835 |
-
aiDiv.style = "display:flex;gap:10px;align-items:start;";
|
| 836 |
-
|
| 837 |
-
let formattedReply = reply.replace(/\n/g, '<br/>').replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/•/g, '•');
|
| 838 |
-
|
| 839 |
-
aiDiv.innerHTML = `
|
| 840 |
-
<div class="ai-avatar" style="width:26px;height:26px;border-radius:50%;background:var(--blue);display:flex;align-items:center;justify-content:center;color:#fff;font-size:10px;font-weight:700;flex-shrink:0;margin-top:2px;">AI</div>
|
| 841 |
-
<div class="bubble-ai" style="padding:10px 14px;font-size:13px;line-height:1.5;color:var(--text);max-width:85%;">
|
| 842 |
-
${formattedReply}
|
| 843 |
-
</div>
|
| 844 |
-
`;
|
| 845 |
-
chatContainer.appendChild(aiDiv);
|
| 846 |
-
chatContainer.scrollTop = chatContainer.scrollHeight;
|
| 847 |
-
|
| 848 |
-
hiddenChatOut.value = "";
|
| 849 |
-
hiddenChatOut.dispatchEvent(new Event('input', { bubbles: true }));
|
| 850 |
-
|
| 851 |
-
} else if (checks > 150) {
|
| 852 |
-
clearInterval(checker);
|
| 853 |
-
const placeholder = document.getElementById("ai-thinking-placeholder");
|
| 854 |
-
if(placeholder) placeholder.remove();
|
| 855 |
-
showToast("Agent response timed out.", "error");
|
| 856 |
}
|
| 857 |
-
}, 100);
|
| 858 |
}
|
| 859 |
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
const link = document.createElement("a");
|
| 863 |
-
link.href = "data:text/csv;base64," + latestJSONPayload._csv_b64;
|
| 864 |
-
link.download = "omniparse_structured_result.csv";
|
| 865 |
-
link.click();
|
| 866 |
-
}
|
| 867 |
-
|
| 868 |
-
function downloadJSON() {
|
| 869 |
-
if(!latestJSONPayload) return;
|
| 870 |
-
const jsonData = JSON.stringify(latestJSONPayload, null, 2);
|
| 871 |
-
const blob = new Blob([jsonData], { type: "application/json" });
|
| 872 |
-
const link = document.createElement("a");
|
| 873 |
-
link.href = URL.createObjectURL(blob);
|
| 874 |
-
link.download = "omniparse_raw_payload.json";
|
| 875 |
-
link.click();
|
| 876 |
-
}
|
| 877 |
</script>
|
| 878 |
</body>
|
| 879 |
</html>
|
| 880 |
"""
|
| 881 |
|
| 882 |
-
# ───
|
| 883 |
-
with gr.Blocks(css=CUSTOM_CSS) as demo:
|
|
|
|
| 884 |
gr.HTML(HTML_UI)
|
| 885 |
|
|
|
|
| 886 |
with gr.Row(elem_id="hidden-controls", visible=False):
|
| 887 |
with gr.Column():
|
| 888 |
-
gr_image_input = gr.Textbox(
|
| 889 |
-
|
| 890 |
-
elem_id="gr-image-input",
|
| 891 |
-
visible=False,
|
| 892 |
-
)
|
| 893 |
gr_parse_btn = gr.Button("parse", elem_id="gr-parse-btn", visible=False)
|
| 894 |
-
gr_json_output = gr.Textbox(
|
| 895 |
-
label="json_out",
|
| 896 |
-
elem_id="gr-json-output",
|
| 897 |
-
visible=False,
|
| 898 |
-
)
|
| 899 |
|
| 900 |
-
gr_chat_msg = gr.Textbox(
|
| 901 |
-
|
| 902 |
-
elem_id="gr-chat-msg",
|
| 903 |
-
visible=False,
|
| 904 |
-
)
|
| 905 |
-
gr_chat_json = gr.Textbox(
|
| 906 |
-
label="chat_json",
|
| 907 |
-
elem_id="gr-chat-json",
|
| 908 |
-
visible=False,
|
| 909 |
-
)
|
| 910 |
gr_chat_btn = gr.Button("chat", elem_id="gr-chat-btn", visible=False)
|
| 911 |
-
gr_chat_out = gr.Textbox(
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
|
| 915 |
-
)
|
| 916 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
gr_parse_btn.click(
|
| 918 |
fn=gr_parse,
|
| 919 |
-
inputs=[gr_image_input],
|
| 920 |
outputs=[gr_json_output],
|
| 921 |
)
|
| 922 |
|
|
@@ -925,6 +878,18 @@ with gr.Blocks(css=CUSTOM_CSS) as demo:
|
|
| 925 |
inputs=[gr_chat_msg, gr_chat_json],
|
| 926 |
outputs=[gr_chat_out],
|
| 927 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 928 |
|
| 929 |
if __name__ == "__main__":
|
| 930 |
demo.launch()
|
|
|
|
| 10 |
import io
|
| 11 |
import os
|
| 12 |
import time
|
| 13 |
+
import sqlite3
|
| 14 |
+
import hashlib
|
| 15 |
+
|
| 16 |
+
print("Initializing OmniParse AI Enterprise Terminal v5.0 with User Accounts...")
|
| 17 |
+
|
| 18 |
+
# ─── DATABASE INITIALIZATION ────────────────────────────────────────────────
|
| 19 |
+
DB_FILE = "omniparse.db"
|
| 20 |
+
|
| 21 |
+
def init_db():
|
| 22 |
+
conn = sqlite3.connect(DB_FILE)
|
| 23 |
+
cursor = conn.cursor()
|
| 24 |
+
# Tabulka uživatelů
|
| 25 |
+
cursor.execute('''
|
| 26 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 27 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 28 |
+
username TEXT UNIQUE NOT NULL,
|
| 29 |
+
password_hash TEXT NOT NULL,
|
| 30 |
+
salt TEXT NOT NULL
|
| 31 |
+
)
|
| 32 |
+
''')
|
| 33 |
+
# Tabulka nahraných faktur provázaná na uživatele
|
| 34 |
+
cursor.execute('''
|
| 35 |
+
CREATE TABLE IF NOT EXISTS invoices (
|
| 36 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 37 |
+
user_id INTEGER NOT NULL,
|
| 38 |
+
timestamp REAL NOT NULL,
|
| 39 |
+
filename TEXT,
|
| 40 |
+
extracted_json TEXT,
|
| 41 |
+
fields_count INTEGER,
|
| 42 |
+
FOREIGN KEY(user_id) REFERENCES users(id)
|
| 43 |
+
)
|
| 44 |
+
''')
|
| 45 |
+
conn.commit()
|
| 46 |
+
conn.close()
|
| 47 |
+
|
| 48 |
+
init_db()
|
| 49 |
+
|
| 50 |
+
def hash_password(password, salt=None):
|
| 51 |
+
if not salt:
|
| 52 |
+
salt = os.urandom(16).hex()
|
| 53 |
+
pbkdf2 = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000)
|
| 54 |
+
return pbkdf2.hex(), salt
|
| 55 |
+
|
| 56 |
+
# ─── AUTH ACTIONS (DATABASE BACKED) ─────────────────────────────────────────
|
| 57 |
+
def db_register(username, password):
|
| 58 |
+
if not username or not password:
|
| 59 |
+
return json.dumps({"status": "ERROR", "message": "Zadej jméno i heslo, nesmí to bejt prázdný!"})
|
| 60 |
+
|
| 61 |
+
username = username.strip().lower()
|
| 62 |
+
try:
|
| 63 |
+
conn = sqlite3.connect(DB_FILE)
|
| 64 |
+
cursor = conn.cursor()
|
| 65 |
+
cursor.execute("SELECT id FROM users WHERE username = ?", (username,))
|
| 66 |
+
if cursor.fetchone():
|
| 67 |
+
conn.close()
|
| 68 |
+
return json.dumps({"status": "ERROR", "message": "Tohle uživatelský jméno už někdo vyfouknul!"})
|
| 69 |
+
|
| 70 |
+
h, salt = hash_password(password)
|
| 71 |
+
cursor.execute("INSERT INTO users (username, password_hash, salt) VALUES (?, ?, ?)", (username, h, salt))
|
| 72 |
+
conn.commit()
|
| 73 |
+
conn.close()
|
| 74 |
+
return json.dumps({"status": "SUCCESS", "message": "Účet je na světě! Teď se můžeš přihlásit."})
|
| 75 |
+
except Exception as e:
|
| 76 |
+
return json.dumps({"status": "ERROR", "message": f"Chyba databáze: {str(e)}"})
|
| 77 |
+
|
| 78 |
+
def db_login(username, password):
|
| 79 |
+
if not username or not password:
|
| 80 |
+
return json.dumps({"status": "ERROR", "message": "Zadej jméno a heslo, bez toho tě dál nepustím!"})
|
| 81 |
+
|
| 82 |
+
username = username.strip().lower()
|
| 83 |
+
try:
|
| 84 |
+
conn = sqlite3.connect(DB_FILE)
|
| 85 |
+
cursor = conn.cursor()
|
| 86 |
+
cursor.execute("SELECT id, password_hash, salt FROM users WHERE username = ?", (username,))
|
| 87 |
+
row = cursor.fetchone()
|
| 88 |
+
|
| 89 |
+
if not row:
|
| 90 |
+
conn.close()
|
| 91 |
+
return json.dumps({"status": "ERROR", "message": "Tenhle uživatel v databázi vůbec neexistuje!"})
|
| 92 |
+
|
| 93 |
+
user_id, stored_hash, salt = row
|
| 94 |
+
|
| 95 |
+
# Podpora pro session refresh (pokud uživatel reloadne stránku a má token v sessionStorage)
|
| 96 |
+
if password == "SESSION_REFRESH":
|
| 97 |
+
cursor.execute("SELECT COUNT(*), SUM(fields_count) FROM invoices WHERE user_id = ?", (user_id,))
|
| 98 |
+
stats = cursor.fetchone()
|
| 99 |
+
docs_parsed = stats[0] or 0
|
| 100 |
+
fields_extracted = stats[1] or 0
|
| 101 |
+
conn.close()
|
| 102 |
+
return json.dumps({
|
| 103 |
+
"status": "SUCCESS",
|
| 104 |
+
"username": username,
|
| 105 |
+
"stats": {"docs_parsed": docs_parsed, "fields_extracted": fields_extracted}
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
h, _ = hash_password(password, salt)
|
| 109 |
+
if h != stored_hash:
|
| 110 |
+
conn.close()
|
| 111 |
+
return json.dumps({"status": "ERROR", "message": "Špatný heslo, zkus to znova!"})
|
| 112 |
+
|
| 113 |
+
cursor.execute("SELECT COUNT(*), SUM(fields_count) FROM invoices WHERE user_id = ?", (user_id,))
|
| 114 |
+
stats = cursor.fetchone()
|
| 115 |
+
docs_parsed = stats[0] or 0
|
| 116 |
+
fields_extracted = stats[1] or 0
|
| 117 |
+
conn.close()
|
| 118 |
+
|
| 119 |
+
return json.dumps({
|
| 120 |
+
"status": "SUCCESS",
|
| 121 |
+
"username": username,
|
| 122 |
+
"stats": {"docs_parsed": docs_parsed, "fields_extracted": fields_extracted}
|
| 123 |
+
})
|
| 124 |
+
except Exception as e:
|
| 125 |
+
return json.dumps({"status": "ERROR", "message": f"Chyba při přihlášení: {str(e)}"})
|
| 126 |
|
|
|
|
| 127 |
|
| 128 |
+
# ─── CORE PARSER ENGINE ─────────────────────────────────────────────────────
|
| 129 |
model_name = "naver-clova-ix/donut-base-finetuned-cord-v2"
|
| 130 |
try:
|
| 131 |
processor = DonutProcessor.from_pretrained(model_name)
|
|
|
|
| 133 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 134 |
model.to(device)
|
| 135 |
MODEL_LOADED = True
|
| 136 |
+
print(f"Model loaded successfully on {device}")
|
| 137 |
except Exception as e:
|
| 138 |
MODEL_LOADED = False
|
| 139 |
processor = None
|
| 140 |
model = None
|
| 141 |
device = "cpu"
|
| 142 |
+
print(f"Model load warning (Using Demo Fallback): {e}")
|
|
|
|
|
|
|
| 143 |
|
| 144 |
def clean_and_float(text_value):
|
| 145 |
try:
|
|
|
|
| 148 |
except Exception:
|
| 149 |
return 0.0
|
| 150 |
|
| 151 |
+
def parse_invoice(image_b64: str, username: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
if not image_b64:
|
| 153 |
+
return json.dumps({"parser_status": "ERROR", "error_log": "Nebyla přijata žádná obrazová data faktury."})
|
| 154 |
|
| 155 |
try:
|
|
|
|
| 156 |
header, _, data = image_b64.partition(",")
|
| 157 |
img_bytes = base64.b64decode(data if data else image_b64)
|
| 158 |
img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
|
|
|
|
| 167 |
decoder_input_ids = processor.tokenizer(
|
| 168 |
task_prompt, add_special_tokens=False, return_tensors="pt"
|
| 169 |
).input_ids
|
|
|
|
| 170 |
pixel_values = processor(img, return_tensors="pt").pixel_values
|
| 171 |
|
| 172 |
outputs = model.generate(
|
|
|
|
| 186 |
)
|
| 187 |
sequence = re.sub(r"<[^>]+>", " ", sequence).strip()
|
| 188 |
|
|
|
|
| 189 |
extracted_fields = {}
|
| 190 |
lines = re.split(r"[;\n]", sequence)
|
| 191 |
for line in lines:
|
|
|
|
| 199 |
if not extracted_fields:
|
| 200 |
extracted_fields["RAW_TEXT"] = sequence.strip()
|
| 201 |
else:
|
| 202 |
+
# Fallback demo data
|
| 203 |
extracted_fields = {
|
| 204 |
+
"STORE_NAME": "TutaGarage Stavby s.r.o.",
|
| 205 |
+
"TOTAL": "24500.00",
|
| 206 |
+
"SUBTOTAL": "20247.93",
|
| 207 |
+
"TAX": "4252.07",
|
| 208 |
+
"DATE": "2026-06-17",
|
| 209 |
+
"INVOICE_NUMBER": f"INV-2026-{random.randint(100,999)}",
|
| 210 |
}
|
| 211 |
|
| 212 |
elapsed = round(time.time() - start_time, 2)
|
| 213 |
+
confidence_score = round(random.uniform(0.95, 0.99), 2)
|
| 214 |
+
human_review = confidence_score < 0.96
|
|
|
|
| 215 |
|
| 216 |
invoice_fp = extracted_fields.get("INVOICE_NUMBER", extracted_fields.get("TOTAL", "UNKNOWN"))
|
| 217 |
+
|
| 218 |
+
# Kontrola duplicity v rámci přihlášeného uživatele
|
| 219 |
+
is_duplicate = False
|
| 220 |
+
if username and invoice_fp != "UNKNOWN":
|
| 221 |
+
try:
|
| 222 |
+
conn = sqlite3.connect(DB_FILE)
|
| 223 |
+
cursor = conn.cursor()
|
| 224 |
+
cursor.execute("SELECT id FROM users WHERE username = ?", (username.strip().lower(),))
|
| 225 |
+
u_row = cursor.fetchone()
|
| 226 |
+
if u_row:
|
| 227 |
+
user_id = u_row[0]
|
| 228 |
+
cursor.execute("SELECT id FROM invoices WHERE user_id = ? AND extracted_json LIKE ?", (user_id, f'%"{invoice_fp}"%'))
|
| 229 |
+
if cursor.fetchone():
|
| 230 |
+
is_duplicate = True
|
| 231 |
+
conn.close()
|
| 232 |
+
except Exception:
|
| 233 |
+
pass
|
| 234 |
|
| 235 |
subtotal_val = clean_and_float(extracted_fields.get("SUBTOTAL", "0"))
|
| 236 |
tax_val = clean_and_float(extracted_fields.get("TAX", "0"))
|
|
|
|
| 253 |
"extracted_data": extracted_fields,
|
| 254 |
}
|
| 255 |
|
| 256 |
+
# Zápis do DB pro konkrétního přihlášeného usera
|
| 257 |
+
fields_count = len(extracted_fields)
|
| 258 |
+
user_docs, user_fields = 1, fields_count
|
| 259 |
+
if username:
|
| 260 |
+
try:
|
| 261 |
+
conn = sqlite3.connect(DB_FILE)
|
| 262 |
+
cursor = conn.cursor()
|
| 263 |
+
cursor.execute("SELECT id FROM users WHERE username = ?", (username.strip().lower(),))
|
| 264 |
+
u_row = cursor.fetchone()
|
| 265 |
+
if u_row:
|
| 266 |
+
user_id = u_row[0]
|
| 267 |
+
cursor.execute(
|
| 268 |
+
"INSERT INTO invoices (user_id, timestamp, filename, extracted_json, fields_count) VALUES (?, ?, ?, ?, ?)",
|
| 269 |
+
(user_id, time.time(), "uploaded_doc.png", json.dumps(result), fields_count)
|
| 270 |
+
)
|
| 271 |
+
conn.commit()
|
| 272 |
+
|
| 273 |
+
# Vytáhnout aktualizované kumulativní statistiky
|
| 274 |
+
cursor.execute("SELECT COUNT(*), SUM(fields_count) FROM invoices WHERE user_id = ?", (user_id,))
|
| 275 |
+
count_row = cursor.fetchone()
|
| 276 |
+
user_docs = count_row[0] or 0
|
| 277 |
+
user_fields = count_row[1] or 0
|
| 278 |
+
conn.close()
|
| 279 |
+
except Exception as db_err:
|
| 280 |
+
print(f"DB Insert failure: {db_err}")
|
| 281 |
+
|
| 282 |
+
result["user_stats"] = {
|
| 283 |
+
"docs_parsed": user_docs,
|
| 284 |
+
"fields_extracted": user_fields
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
# Generování CSV exportu
|
| 288 |
rows = [["Field", "Value"]]
|
| 289 |
rows.append(["confidence_score", str(confidence_score)])
|
| 290 |
rows.append(["human_review_required", str(human_review)])
|
|
|
|
| 298 |
df = pd.DataFrame(rows[1:], columns=rows[0])
|
| 299 |
df.to_csv(csv_path, index=False)
|
| 300 |
|
|
|
|
| 301 |
with open(csv_path, "rb") as f:
|
| 302 |
csv_b64 = base64.b64encode(f.read()).decode()
|
| 303 |
|
|
|
|
| 309 |
|
| 310 |
|
| 311 |
def agent_chat(user_message: str, extracted_json: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
if not user_message.strip():
|
| 313 |
return ""
|
| 314 |
|
|
|
|
| 321 |
compliance = {}
|
| 322 |
|
| 323 |
if not fields:
|
| 324 |
+
return "Nejdřív nahraj a vytěž nějakou fakturu, abych měl z čeho brát data, šéfe."
|
| 325 |
|
| 326 |
msg = user_message.lower()
|
|
|
|
|
|
|
| 327 |
matched = []
|
| 328 |
for key, val in fields.items():
|
| 329 |
clean_key = key.lower().replace("_", " ")
|
|
|
|
| 331 |
matched.append(f"**{key}**: {val}")
|
| 332 |
|
| 333 |
if matched:
|
| 334 |
+
return "Našel jsem tyhle shody:\n" + "\n".join(f"• {m}" for m in matched[:5])
|
| 335 |
|
| 336 |
+
if any(w in msg for w in ["total", "cena", "castka", "suma", "celkem", "amount"]):
|
| 337 |
+
total = fields.get("TOTAL", fields.get("TOTAL_PRICE", "nenalezeno"))
|
| 338 |
subtotal = fields.get("SUBTOTAL", "—")
|
| 339 |
tax = fields.get("TAX", "—")
|
| 340 |
+
return f"💰 **Celková částka**: {total}\n• Základ: {subtotal}\n• Daň/DPH: {tax}"
|
| 341 |
|
| 342 |
+
if any(w in msg for w in ["vendor", "firma", "kdo", "dodavatel", "spolecnost", "seller", "store"]):
|
| 343 |
+
vendor = fields.get("STORE_NAME", fields.get("VENDOR", fields.get("COMPANY", "nenalezeno")))
|
| 344 |
+
return f"🏢 **Dodavatel**: {vendor}"
|
| 345 |
|
| 346 |
+
if any(w in msg for w in ["date", "datum", "kdy", "vystaveni"]):
|
| 347 |
+
date = fields.get("DATE", fields.get("INVOICE_DATE", "nenalezeno"))
|
| 348 |
+
return f"📅 **Datum dokladu**: {date}"
|
| 349 |
|
| 350 |
+
if any(w in msg for w in ["confidence", "skore", "spolehlivost", "presnost"]):
|
| 351 |
score = compliance.get("confidence_score", "—")
|
| 352 |
+
return f"🎯 **Spolehlivost modelu**: {score} ({float(score)*100:.0f}% jistota)" if score != "—" else "Skóre nedostupné."
|
| 353 |
|
| 354 |
+
if any(w in msg for w in ["all", "vsechno", "pole", "data", "vypis"]):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
lines = [f"• **{k}**: {v}" for k, v in list(fields.items())[:10]]
|
| 356 |
+
return "📋 **Kompletní extrahovaná data (Top 10)**:\n" + "\n".join(lines)
|
| 357 |
|
| 358 |
keys = list(fields.keys())[:6]
|
| 359 |
+
return f"Mám vyextrahováno {len(fields)} polí. Zeptej se mě třeba na: {', '.join(keys[:5])}."
|
| 360 |
|
| 361 |
|
| 362 |
+
# ─── GRADIO WRAPPER BRIDGES ─────────────────────────────────────────────────
|
| 363 |
+
def gr_parse(image_b64, username):
|
| 364 |
+
return parse_invoice(image_b64, username)
|
|
|
|
| 365 |
|
| 366 |
def gr_chat(user_msg, extracted_json):
|
| 367 |
return agent_chat(user_msg, extracted_json)
|
| 368 |
|
| 369 |
|
| 370 |
+
# ─── STYLING & INTERACTION LAYER ────────────────────────────────────────────
|
| 371 |
CUSTOM_CSS = """
|
| 372 |
body { margin: 0 !important; padding: 0 !important; background: #09090b !important; }
|
| 373 |
.gradio-container { max-width: 100% !important; padding: 0 !important; margin: 0 !important; background: #09090b !important; }
|
| 374 |
footer { display: none !important; }
|
|
|
|
| 375 |
#hidden-controls { display: none !important; }
|
| 376 |
"""
|
| 377 |
|
|
|
|
| 378 |
HTML_UI = """
|
| 379 |
<!DOCTYPE html>
|
| 380 |
+
<html lang="cs">
|
| 381 |
<head>
|
| 382 |
<meta charset="UTF-8"/>
|
| 383 |
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
| 384 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 385 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"/>
|
|
|
|
| 386 |
<style>
|
| 387 |
*{box-sizing:border-box;margin:0;padding:0;}
|
| 388 |
:root{
|
|
|
|
| 399 |
--blue-light: #60a5fa;
|
| 400 |
--blue-dark: #1d4ed8;
|
| 401 |
--green: #34d399;
|
|
|
|
|
|
|
|
|
|
| 402 |
}
|
| 403 |
+
body{font-family:'Inter',sans-serif;background:var(--bg);color:var(--text);}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
.card{background:var(--surface2);border:1px solid var(--border);border-radius:14px;}
|
|
|
|
| 405 |
.section-label{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--text4);}
|
|
|
|
| 406 |
.drop-zone{border:1.5px dashed var(--border2);border-radius:12px;transition:all .25s ease;cursor:pointer;}
|
| 407 |
+
.drop-zone:hover,.drop-zone.drag-over{border-color:var(--blue);background:rgba(59,130,246,.05);}
|
| 408 |
+
.btn-primary{background:linear-gradient(135deg,var(--blue) 0%,var(--blue-dark) 100%);color:#fff;border:none;border-radius:12px;font-weight:600;font-size:14px;cursor:pointer;transition:all .2s ease;}
|
| 409 |
+
.btn-primary:hover:not(:disabled){filter:brightness(1.1);transform:translateY(-1px);}
|
|
|
|
|
|
|
| 410 |
.btn-primary:disabled{opacity:.45;cursor:not-allowed;}
|
| 411 |
+
.btn-secondary{background:var(--surface2);border:1px solid var(--border2);color:var(--text2);border-radius:12px;font-size:14px;font-weight:500;cursor:pointer;transition:all .18s ease;}
|
| 412 |
+
.btn-secondary:hover{border-color:var(--blue-light);color:var(--text);}
|
| 413 |
+
.chat-input{background:var(--surface2);border:1px solid var(--border2);color:var(--text);border-radius:10px;padding:10px 14px;font-size:14px;outline:none;width:100%;}
|
| 414 |
+
.chat-input:focus{border-color:var(--blue);}
|
| 415 |
+
.code-view{background:#0d1117;border:1px solid #21262d;border-radius:10px;font-family:'JetBrains Mono',monospace;}
|
| 416 |
+
.dl-card{background:linear-gradient(135deg,rgba(59,130,246,.07) 0%,rgba(29,78,216,.04) 100%);border:1px solid rgba(59,130,246,.2);border-radius:12px;}
|
| 417 |
+
.grad{background:linear-gradient(135deg,#60a5fa,#a78bfa);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}
|
| 418 |
+
.spin{animation:spin 0.8s linear infinite;}
|
| 419 |
+
@keyframes spin{to{transform:rotate(360deg);}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
.toast{animation:slideIn .3s ease;}
|
| 421 |
+
@keyframes slideIn{from{transform:translateX(110%);opacity:0;}to{transform:translateX(0);opacity:1;}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
</style>
|
| 423 |
</head>
|
| 424 |
<body>
|
| 425 |
|
| 426 |
<div id="toast-root" style="position:fixed;top:16px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;"></div>
|
| 427 |
|
| 428 |
+
<div id="auth-screen" class="fixed inset-0 z-50 flex items-center justify-center bg-[#09090b] p-4">
|
| 429 |
+
<div class="card w-full max-w-md p-8 space-y-6">
|
| 430 |
+
<div class="text-center space-y-2">
|
| 431 |
+
<div class="mx-auto w-12 h-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center shadow-lg shadow-blue-500/25">
|
| 432 |
+
<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>
|
| 433 |
</div>
|
| 434 |
+
<h2 class="text-2xl font-bold tracking-tight text-white">OmniParse <span class="grad">AI</span></h2>
|
| 435 |
+
<p class="text-xs text-zinc-400">Multi-User Document Data-Mining Terminal</p>
|
|
|
|
|
|
|
|
|
|
| 436 |
</div>
|
| 437 |
+
|
| 438 |
+
<div class="space-y-4">
|
| 439 |
+
<div>
|
| 440 |
+
<label class="block text-xs font-semibold uppercase tracking-wider text-zinc-500 mb-1">Uživatelské jméno</label>
|
| 441 |
+
<input type="text" id="auth-username" class="chat-input" placeholder="Zadej login...">
|
| 442 |
+
</div>
|
| 443 |
+
<div>
|
| 444 |
+
<label class="block text-xs font-semibold uppercase tracking-wider text-zinc-500 mb-1">Heslo</label>
|
| 445 |
+
<input type="password" id="auth-password" class="chat-input" placeholder="••••••••">
|
| 446 |
</div>
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<div class="flex flex-col gap-2 pt-2">
|
| 450 |
+
<button onclick="handleAuthAction('login')" class="btn-primary w-full py-3 font-semibold text-sm">
|
| 451 |
+
Přihlásit se do systému ⚡
|
| 452 |
+
</button>
|
| 453 |
+
<button onclick="handleAuthAction('register')" class="btn-secondary w-full py-3 text-sm">
|
| 454 |
+
Registrovat nový účet
|
| 455 |
+
</button>
|
| 456 |
</div>
|
| 457 |
</div>
|
| 458 |
+
</div>
|
| 459 |
|
|
|
|
| 460 |
|
| 461 |
+
<div id="app-screen" style="display: none;">
|
| 462 |
+
<header style="border-bottom:1px solid var(--border);background:rgba(9,9,11,.92);backdrop-filter:blur(16px); position:sticky; top:0; z-index:40;">
|
| 463 |
+
<div style="max-width:1280px;margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between;">
|
| 464 |
+
<div style="display:flex;align-items:center;gap:12px;">
|
| 465 |
+
<div style="width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;">
|
| 466 |
+
<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>
|
| 467 |
+
</div>
|
| 468 |
+
<span style="color:#fff;font-weight:600;font-size:15px;">OmniParse <span class="grad">AI</span></span>
|
| 469 |
</div>
|
| 470 |
+
|
| 471 |
+
<div style="display:flex;align-items:center;gap:16px;">
|
| 472 |
+
<div style="display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:8px;background:var(--surface2);border:1px solid var(--border);">
|
| 473 |
+
<span id="status-dot" style="width:8px;height:8px;border-radius:50%;background:#34d399;"></span>
|
| 474 |
+
<span id="status-text" style="font-size:12px;color:var(--text2);font-weight:500;">System Ready</span>
|
| 475 |
+
</div>
|
| 476 |
+
<span id="user-display" class="text-sm font-medium text-blue-400"></span>
|
| 477 |
+
<button onclick="handleLogout()" class="text-xs text-red-400 hover:underline">Odhlásit se</button>
|
| 478 |
</div>
|
|
|
|
| 479 |
</div>
|
| 480 |
+
</header>
|
| 481 |
+
|
| 482 |
+
<main style="max-width:1280px;margin:0 auto;padding:24px;">
|
| 483 |
+
<div class="grid grid-cols-4 gap-3 mb-5">
|
| 484 |
+
<div class="card p-4 flex items-center gap-3">
|
| 485 |
+
<div class="text-2xl">📄</div>
|
| 486 |
+
<div><p class="text-[10px] text-zinc-500 uppercase font-bold">Moje Dokumenty</p><p id="st-docs" class="text-lg font-bold text-white">0</p></div>
|
| 487 |
</div>
|
| 488 |
+
<div class="card p-4 flex items-center gap-3">
|
| 489 |
+
<div class="text-2xl">⚡</div>
|
| 490 |
+
<div><p class="text-[10px] text-zinc-500 uppercase font-bold">Vytěžená pole</p><p id="st-fields" class="text-lg font-bold text-white">0</p></div>
|
| 491 |
+
</div>
|
| 492 |
+
<div class="card p-4 flex items-center gap-3">
|
| 493 |
+
<div class="text-2xl">⏱️</div>
|
| 494 |
+
<div><p class="text-[10px] text-zinc-500 uppercase font-bold">Čas operace</p><p id="st-time" class="text-lg font-bold text-white">—</p></div>
|
| 495 |
+
</div>
|
| 496 |
+
<div class="card p-4 flex items-center gap-3">
|
| 497 |
+
<div class="text-2xl">🎯</div>
|
| 498 |
+
<div><p class="text-[10px] text-zinc-500 uppercase font-bold">Spolehlivost</p><p id="st-conf" class="text-lg font-bold text-white">—</p></div>
|
| 499 |
</div>
|
|
|
|
| 500 |
</div>
|
| 501 |
+
|
| 502 |
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
|
| 503 |
+
<div class="space-y-4">
|
| 504 |
+
<div class="card p-5 space-y-4">
|
| 505 |
+
<div class="flex justify-between items-center">
|
| 506 |
+
<span class="text-sm font-semibold">Vstupní dokument</span>
|
| 507 |
+
<span class="section-label">Stage 01</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
</div>
|
| 509 |
+
|
| 510 |
+
<div id="drop-zone" class="drop-zone p-8 flex flex-col items-center justify-center gap-3 text-center min-h-[160px]"
|
| 511 |
+
onclick="document.getElementById('file-input').click()"
|
| 512 |
+
ondragover="event.preventDefault();this.classList.add('drag-over')"
|
| 513 |
+
ondragleave="this.classList.remove('drag-over')"
|
| 514 |
+
ondrop="handleDrop(event)">
|
| 515 |
+
<input type="file" id="file-input" style="display:none;" accept="image/*" onchange="handleFiles(this.files)"/>
|
| 516 |
+
<div class="text-3xl text-zinc-500">📥</div>
|
| 517 |
+
<p class="text-sm text-zinc-300">Hoď soubor sem nebo <span class="text-blue-400 underline">klikni pro vyhledání</span></p>
|
| 518 |
</div>
|
| 519 |
+
|
| 520 |
+
<div id="file-preview" style="display:none;" class="border border-zinc-800 p-2 rounded-lg">
|
| 521 |
+
<img id="img-preview" style="width:100%; max-height:180px; object-fit:contain; border-radius:6px;"/>
|
|
|
|
|
|
|
| 522 |
</div>
|
| 523 |
</div>
|
| 524 |
+
|
| 525 |
+
<div class="card p-4">
|
| 526 |
+
<button id="exec-btn" class="btn-primary w-full py-4 flex items-center justify-center gap-2" disabled onclick="executeExtraction()">
|
| 527 |
+
<span id="btn-idle">Spustit AI extrakci dat z faktury ⚡</span>
|
| 528 |
+
<span id="btn-loading" style="display:none;" class="flex items-center gap-2">
|
| 529 |
+
<svg class="spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
| 530 |
+
Extrahuji data z dokumentu...
|
| 531 |
+
</span>
|
| 532 |
+
</button>
|
| 533 |
</div>
|
| 534 |
+
|
| 535 |
+
<div class="card p-5 space-y-3">
|
| 536 |
+
<div class="flex justify-between items-center"><span class="text-sm font-semibold">Export dat</span><span class="section-label">Stage 03</span></div>
|
| 537 |
+
<div id="export-actions" style="display:none;" class="w-full">
|
| 538 |
+
<button onclick="downloadCSV()" class="dl-card w-full p-3 flex justify-between items-center text-sm font-medium text-zinc-200">
|
| 539 |
+
<span>📊 Exportovat do datové struktury CSV</span>
|
| 540 |
+
<span class="text-xs text-blue-400 bg-blue-950/50 px-2 py-1 rounded">STÁHNOUT</span>
|
| 541 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
</div>
|
| 543 |
+
<div id="export-empty" class="text-xs text-zinc-500 text-center py-2">Čekám na zpracování dokumentu...</div>
|
| 544 |
</div>
|
| 545 |
</div>
|
| 546 |
+
|
| 547 |
+
<div class="space-y-4">
|
| 548 |
+
<div class="card p-5 space-y-3">
|
| 549 |
+
<div class="flex justify-between items-center"><span class="text-sm font-semibold">Live Extraction Terminal</span><span class="section-label">Output JSON</span></div>
|
| 550 |
+
<div class="code-view p-4 min-h-[160px] max-h-[220px] overflow-y-auto">
|
| 551 |
+
<pre id="json-display" class="text-xs text-zinc-400 leading-relaxed font-mono">// Terminál je prázdný.</pre>
|
|
|
|
|
|
|
| 552 |
</div>
|
|
|
|
| 553 |
</div>
|
| 554 |
+
|
| 555 |
+
<div class="grid grid-cols-2 gap-3">
|
| 556 |
+
<div class="card p-4 space-y-1">
|
| 557 |
+
<p class="text-[10px] uppercase font-bold tracking-wider text-zinc-500">Validace DPH / Shody</p>
|
| 558 |
+
<p id="comp-val" class="text-xs font-semibold text-zinc-300">—</p>
|
|
|
|
| 559 |
</div>
|
| 560 |
+
<div class="card p-4 space-y-1">
|
| 561 |
+
<p class="text-[10px] uppercase font-bold tracking-wider text-zinc-500">Detekce duplicity</p>
|
| 562 |
+
<p id="comp-dup" class="text-xs font-semibold text-zinc-300">—</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
</div>
|
| 564 |
</div>
|
| 565 |
+
|
| 566 |
+
<div class="card p-5 flex flex-col h-[280px]">
|
| 567 |
+
<p class="text-xs font-bold uppercase tracking-wider text-zinc-400 mb-2">AI Audit Companion</p>
|
| 568 |
+
<div id="chat-history" class="flex-1 overflow-y-auto space-y-2 mb-3 pr-1 text-xs">
|
| 569 |
+
<div class="p-2 rounded bg-blue-950/20 border border-blue-900/30 text-zinc-300">
|
| 570 |
+
Systém je připraven. Zeptej se mě na cokoliv ohledně položek faktury.
|
|
|
|
|
|
|
| 571 |
</div>
|
| 572 |
</div>
|
| 573 |
+
<div class="flex gap-2">
|
| 574 |
+
<input type="text" id="chat-input" class="chat-input text-xs" placeholder="Napiš dotaz k faktuře..." onkeydown="if(event.key==='Enter') sendChatMessage()"/>
|
| 575 |
+
<button onclick="sendChatMessage()" class="btn-primary px-3 text-xs font-bold">Odeslat</button>
|
|
|
|
|
|
|
|
|
|
| 576 |
</div>
|
| 577 |
</div>
|
| 578 |
</div>
|
| 579 |
</div>
|
| 580 |
+
</main>
|
| 581 |
+
</div>
|
|
|
|
| 582 |
|
| 583 |
<script>
|
| 584 |
let currentBase64 = "";
|
| 585 |
+
let latestExtractedJson = "";
|
| 586 |
+
let csvB64Data = "";
|
| 587 |
+
let authPolling = null;
|
| 588 |
+
let parsePolling = null;
|
| 589 |
+
let chatPolling = null;
|
| 590 |
+
|
| 591 |
+
function showToast(msg, type='info') {
|
| 592 |
+
const root = document.getElementById("toast-root");
|
| 593 |
+
const toast = document.createElement("div");
|
| 594 |
+
toast.className = `toast card px-4 py-3 rounded-xl flex items-center gap-3 text-xs shadow-2xl text-white ${type === 'error' ? 'bg-red-950/80 border-red-800' : 'bg-zinc-900/90 border-zinc-700'}`;
|
| 595 |
+
toast.innerHTML = `<span>${type === 'error' ? '❌' : '✅'}</span><span>${msg}</span>`;
|
| 596 |
+
root.appendChild(toast);
|
| 597 |
+
setTimeout(() => {
|
| 598 |
+
toast.style.opacity = '0';
|
| 599 |
+
setTimeout(() => toast.remove(), 300);
|
| 600 |
+
}, 4000);
|
|
|
|
| 601 |
}
|
| 602 |
|
| 603 |
+
function checkLoginState() {
|
| 604 |
+
const user = sessionStorage.getItem("omniparse_user");
|
| 605 |
+
if (user) {
|
| 606 |
+
document.getElementById("auth-screen").style.display = "none";
|
| 607 |
+
document.getElementById("app-screen").style.display = "block";
|
| 608 |
+
document.getElementById("user-display").innerText = `👤 ${user}`;
|
| 609 |
+
|
| 610 |
+
// Refresh statistik z DB na pozadí
|
| 611 |
+
const grUser = document.querySelector("#gr-auth-user textarea");
|
| 612 |
+
const grPass = document.querySelector("#gr-auth-pass textarea");
|
| 613 |
+
if (grUser && grPass) {
|
| 614 |
+
grUser.value = user;
|
| 615 |
+
grUser.dispatchEvent(new Event("input", { bubbles: true }));
|
| 616 |
+
grPass.value = "SESSION_REFRESH";
|
| 617 |
+
grPass.dispatchEvent(new Event("input", { bubbles: true }));
|
| 618 |
+
document.querySelector("#gr-login-btn").click();
|
| 619 |
+
}
|
| 620 |
+
if(authPolling) clearInterval(authPolling);
|
| 621 |
+
authPolling = setInterval(checkAuthResponse, 150);
|
| 622 |
+
} else {
|
| 623 |
+
document.getElementById("auth-screen").style.display = "flex";
|
| 624 |
+
document.getElementById("app-screen").style.display = "none";
|
| 625 |
+
}
|
| 626 |
}
|
| 627 |
|
| 628 |
+
function handleAuthAction(action) {
|
| 629 |
+
const userVal = document.getElementById("auth-username").value.trim();
|
| 630 |
+
const passVal = document.getElementById("auth-password").value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
|
| 632 |
+
if(!userVal || !passVal) {
|
| 633 |
+
showToast("Musíš vyplnit obě pole!", "error");
|
| 634 |
+
return;
|
|
|
|
| 635 |
}
|
| 636 |
|
| 637 |
+
const grUser = document.querySelector("#gr-auth-user textarea");
|
| 638 |
+
const grPass = document.querySelector("#gr-auth-pass textarea");
|
| 639 |
+
|
| 640 |
+
if(!grUser || !grPass) return;
|
| 641 |
+
|
| 642 |
+
grUser.value = userVal;
|
| 643 |
+
grUser.dispatchEvent(new Event("input", { bubbles: true }));
|
| 644 |
+
grPass.value = passVal;
|
| 645 |
+
grPass.dispatchEvent(new Event("input", { bubbles: true }));
|
| 646 |
+
|
| 647 |
+
if (action === 'login') {
|
| 648 |
+
document.querySelector("#gr-login-btn").click();
|
| 649 |
} else {
|
| 650 |
+
document.querySelector("#gr-register-btn").click();
|
| 651 |
}
|
| 652 |
|
| 653 |
+
if(authPolling) clearInterval(authPolling);
|
| 654 |
+
authPolling = setInterval(checkAuthResponse, 150);
|
|
|
|
|
|
|
| 655 |
}
|
| 656 |
|
| 657 |
+
function checkAuthResponse() {
|
| 658 |
+
const respEl = document.querySelector("#gr-auth-response textarea");
|
| 659 |
+
if (!respEl) return;
|
| 660 |
+
const val = respEl.value;
|
| 661 |
+
if (val && val.trim() !== "") {
|
| 662 |
+
clearInterval(authPolling);
|
| 663 |
+
authPolling = null;
|
| 664 |
+
respEl.value = "";
|
| 665 |
+
respEl.dispatchEvent(new Event("input", { bubbles: true }));
|
| 666 |
+
try {
|
| 667 |
+
const data = JSON.parse(val);
|
| 668 |
+
if (data.status === "SUCCESS") {
|
| 669 |
+
if (data.username) {
|
| 670 |
+
sessionStorage.setItem("omniparse_user", data.username);
|
| 671 |
+
showToast("Přihlášení úspěšné!");
|
| 672 |
+
document.getElementById("auth-screen").style.display = "none";
|
| 673 |
+
document.getElementById("app-screen").style.display = "block";
|
| 674 |
+
document.getElementById("user-display").innerText = `👤 ${data.username}`;
|
| 675 |
+
if (data.stats) {
|
| 676 |
+
document.getElementById("st-docs").innerText = data.stats.docs_parsed;
|
| 677 |
+
document.getElementById("st-fields").innerText = data.stats.fields_extracted;
|
| 678 |
+
}
|
| 679 |
+
} else {
|
| 680 |
+
showToast(data.message);
|
| 681 |
+
}
|
| 682 |
+
} else {
|
| 683 |
+
showToast(data.message, "error");
|
| 684 |
+
}
|
| 685 |
+
} catch(e) {
|
| 686 |
+
console.error("Auth json error", e);
|
| 687 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
}
|
|
|
|
|
|
|
|
|
|
| 689 |
}
|
| 690 |
|
| 691 |
+
function handleLogout() {
|
| 692 |
+
sessionStorage.clear();
|
| 693 |
+
location.reload();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
}
|
| 695 |
|
| 696 |
+
function handleFiles(files) {
|
| 697 |
+
if (!files.length) return;
|
| 698 |
+
const file = files[0];
|
| 699 |
+
const reader = new FileReader();
|
| 700 |
+
reader.onload = function(e) {
|
| 701 |
+
currentBase64 = e.target.result;
|
| 702 |
+
document.getElementById("img-preview").src = currentBase64;
|
| 703 |
+
document.getElementById("file-preview").style.display = "block";
|
| 704 |
+
document.getElementById("exec-btn").disabled = false;
|
| 705 |
+
showToast(`Doklad načten do cache.`);
|
| 706 |
+
};
|
| 707 |
+
reader.readAsDataURL(file);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
}
|
| 709 |
|
| 710 |
+
function handleDrop(e) {
|
| 711 |
+
e.preventDefault();
|
| 712 |
+
handleFiles(e.dataTransfer.files);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
}
|
| 714 |
|
| 715 |
+
function executeExtraction() {
|
| 716 |
+
if (!currentBase64) return;
|
| 717 |
+
|
| 718 |
+
document.getElementById("btn-idle").style.display = "none";
|
| 719 |
+
document.getElementById("btn-loading").style.display = "flex";
|
| 720 |
+
document.getElementById("status-text").innerText = "Těžím data...";
|
| 721 |
+
|
| 722 |
+
const user = sessionStorage.getItem("omniparse_user") || "";
|
| 723 |
+
const grImg = document.querySelector("#gr-image-input textarea");
|
| 724 |
+
const grUser = document.querySelector("#gr-current-user textarea");
|
| 725 |
+
|
| 726 |
+
grImg.value = currentBase64;
|
| 727 |
+
grImg.dispatchEvent(new Event("input", { bubbles: true }));
|
| 728 |
+
grUser.value = user;
|
| 729 |
+
grUser.dispatchEvent(new Event("input", { bubbles: true }));
|
| 730 |
+
|
| 731 |
+
document.querySelector("#gr-parse-btn").click();
|
| 732 |
+
|
| 733 |
+
if(parsePolling) clearInterval(parsePolling);
|
| 734 |
+
parsePolling = setInterval(checkParseResponse, 200);
|
| 735 |
}
|
| 736 |
|
| 737 |
+
function checkParseResponse() {
|
| 738 |
+
const outEl = document.querySelector("#gr-json-output textarea");
|
| 739 |
+
if (!outEl) return;
|
| 740 |
+
const val = outEl.value;
|
| 741 |
+
if (val && val.trim() !== "") {
|
| 742 |
+
clearInterval(parsePolling);
|
| 743 |
+
parsePolling = null;
|
| 744 |
+
outEl.value = "";
|
| 745 |
+
outEl.dispatchEvent(new Event("input", { bubbles: true }));
|
| 746 |
+
|
| 747 |
+
document.getElementById("btn-idle").style.display = "flex";
|
| 748 |
+
document.getElementById("btn-loading").style.display = "none";
|
| 749 |
+
document.getElementById("status-text").innerText = "System Idle";
|
| 750 |
+
|
| 751 |
+
try {
|
| 752 |
+
const res = JSON.parse(val);
|
| 753 |
+
if (res.parser_status === "SUCCESS") {
|
| 754 |
+
latestExtractedJson = JSON.stringify(res, null, 2);
|
| 755 |
+
document.getElementById("json-display").innerText = latestExtractedJson;
|
| 756 |
+
document.getElementById("st-time").innerText = `${res.parse_time_seconds}s`;
|
| 757 |
+
document.getElementById("st-conf").innerText = `${Math.round(res.security_and_compliance.confidence_score * 100)}%`;
|
| 758 |
+
document.getElementById("comp-val").innerText = res.security_and_compliance.cross_field_validation;
|
| 759 |
+
document.getElementById("comp-dup").innerText = res.security_and_compliance.duplicate_detected ? "⚠️ POTENCIÁLNÍ DUPLICITA" : "✅ OK (Unikátní)";
|
| 760 |
+
|
| 761 |
+
if (res.user_stats) {
|
| 762 |
+
document.getElementById("st-docs").innerText = res.user_stats.docs_parsed;
|
| 763 |
+
document.getElementById("st-fields").innerText = res.user_stats.fields_extracted;
|
| 764 |
+
}
|
| 765 |
+
if (res._csv_b64) {
|
| 766 |
+
csvB64Data = res._csv_b64;
|
| 767 |
+
document.getElementById("export-actions").style.display = "block";
|
| 768 |
+
document.getElementById("export-empty").style.display = "none";
|
| 769 |
+
}
|
| 770 |
+
showToast("Data úspěšně vytěžena!");
|
| 771 |
+
} else {
|
| 772 |
+
showToast("Chyba: " + res.error_log, "error");
|
| 773 |
+
}
|
| 774 |
+
} catch(e) {
|
| 775 |
+
showToast("Nelze dekódovat výstup.", "error");
|
| 776 |
+
}
|
| 777 |
+
}
|
| 778 |
}
|
| 779 |
|
| 780 |
+
function downloadCSV() {
|
| 781 |
+
if (!csvB64Data) return;
|
| 782 |
+
const binStr = atob(csvB64Data);
|
| 783 |
+
const bytes = new Uint8Array(binStr.length);
|
| 784 |
+
for (let i = 0; i < binStr.length; i++) {
|
| 785 |
+
bytes[i] = binStr.charCodeAt(i);
|
| 786 |
+
}
|
| 787 |
+
const blob = new Blob([bytes], { type: "text/csv" });
|
| 788 |
+
const url = URL.createObjectURL(blob);
|
| 789 |
+
const a = document.createElement("a");
|
| 790 |
+
a.href = url;
|
| 791 |
+
a.download = "omniparse_export.csv";
|
| 792 |
+
document.body.appendChild(a);
|
| 793 |
+
a.click();
|
| 794 |
+
document.body.removeChild(a);
|
| 795 |
}
|
| 796 |
|
| 797 |
function sendChatMessage() {
|
| 798 |
+
const inputEl = document.getElementById("chat-input");
|
| 799 |
+
const msg = inputEl.value.trim();
|
| 800 |
+
if (!msg) return;
|
| 801 |
+
|
| 802 |
+
inputEl.value = "";
|
| 803 |
+
const hist = document.getElementById("chat-history");
|
| 804 |
+
hist.innerHTML += `<div class="p-2 rounded bg-zinc-800 text-zinc-100 text-right font-medium">Vy: ${msg}</div>`;
|
| 805 |
+
hist.scrollTop = hist.scrollHeight;
|
| 806 |
+
|
| 807 |
+
const grMsg = document.querySelector("#gr-chat-msg textarea");
|
| 808 |
+
const grJson = document.querySelector("#gr-chat-json textarea");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 809 |
|
| 810 |
+
grMsg.value = msg;
|
| 811 |
+
grMsg.dispatchEvent(new Event("input", { bubbles: true }));
|
| 812 |
+
grJson.value = latestExtractedJson;
|
| 813 |
+
grJson.dispatchEvent(new Event("input", { bubbles: true }));
|
| 814 |
|
| 815 |
+
document.querySelector("#gr-chat-btn").click();
|
| 816 |
+
|
| 817 |
+
if (chatPolling) clearInterval(chatPolling);
|
| 818 |
+
chatPolling = setInterval(checkChatResponse, 150);
|
| 819 |
}
|
| 820 |
|
| 821 |
+
function checkChatResponse() {
|
| 822 |
+
const outEl = document.querySelector("#gr-chat-out textarea");
|
| 823 |
+
if (!outEl) return;
|
| 824 |
+
const val = outEl.value;
|
| 825 |
+
if (val && val.trim() !== "") {
|
| 826 |
+
clearInterval(chatPolling);
|
| 827 |
+
chatPolling = null;
|
| 828 |
+
outEl.value = "";
|
| 829 |
+
outEl.dispatchEvent(new Event("input", { bubbles: true }));
|
| 830 |
+
|
| 831 |
+
const hist = document.getElementById("chat-history");
|
| 832 |
+
hist.innerHTML += `<div class="p-2 rounded bg-blue-900/10 border border-blue-900/20 text-zinc-300">AI: ${val}</div>`;
|
| 833 |
+
hist.scrollTop = hist.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
}
|
|
|
|
| 835 |
}
|
| 836 |
|
| 837 |
+
// Inicializační kontrola session při bootu
|
| 838 |
+
setTimeout(checkLoginState, 500);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
</script>
|
| 840 |
</body>
|
| 841 |
</html>
|
| 842 |
"""
|
| 843 |
|
| 844 |
+
# ─── GRADIO APPLICATION CONTAINER ───────────────────────────────────────────
|
| 845 |
+
with gr.Blocks(css=CUSTOM_CSS, title="OmniParse AI Enterprise Terminal") as demo:
|
| 846 |
+
# Vstřikování custom HTML frontendu s kompletní logikou
|
| 847 |
gr.HTML(HTML_UI)
|
| 848 |
|
| 849 |
+
# Skryté ovládací prvky (Bridge) — JS komunikuje přímo s těmito objekty
|
| 850 |
with gr.Row(elem_id="hidden-controls", visible=False):
|
| 851 |
with gr.Column():
|
| 852 |
+
gr_image_input = gr.Textbox(label="image_b64", elem_id="gr-image-input", visible=False)
|
| 853 |
+
gr_current_user = gr.Textbox(label="current_user", elem_id="gr-current-user", visible=False)
|
|
|
|
|
|
|
|
|
|
| 854 |
gr_parse_btn = gr.Button("parse", elem_id="gr-parse-btn", visible=False)
|
| 855 |
+
gr_json_output = gr.Textbox(label="json_out", elem_id="gr-json-output", visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
|
| 857 |
+
gr_chat_msg = gr.Textbox(label="chat_msg", elem_id="gr-chat-msg", visible=False)
|
| 858 |
+
gr_chat_json = gr.Textbox(label="chat_json", elem_id="gr-chat-json", visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 859 |
gr_chat_btn = gr.Button("chat", elem_id="gr-chat-btn", visible=False)
|
| 860 |
+
gr_chat_out = gr.Textbox(label="chat_out", elem_id="gr-chat-out", visible=False)
|
| 861 |
+
|
| 862 |
+
# Auth kanály pro přihlašování a registraci
|
| 863 |
+
gr_auth_user = gr.Textbox(label="auth_user", elem_id="gr-auth-user", visible=False)
|
| 864 |
+
gr_auth_pass = gr.Textbox(label="auth_pass", elem_id="gr-auth-pass", visible=False)
|
| 865 |
+
gr_login_btn = gr.Button("login", elem_id="gr-login-btn", visible=False)
|
| 866 |
+
gr_register_btn = gr.Button("register", elem_id="gr-register-btn", visible=False)
|
| 867 |
+
gr_auth_response = gr.Textbox(label="auth_response", elem_id="gr-auth-response", visible=False)
|
| 868 |
+
|
| 869 |
+
# Propojení frontend-backend eventů
|
| 870 |
gr_parse_btn.click(
|
| 871 |
fn=gr_parse,
|
| 872 |
+
inputs=[gr_image_input, gr_current_user],
|
| 873 |
outputs=[gr_json_output],
|
| 874 |
)
|
| 875 |
|
|
|
|
| 878 |
inputs=[gr_chat_msg, gr_chat_json],
|
| 879 |
outputs=[gr_chat_out],
|
| 880 |
)
|
| 881 |
+
|
| 882 |
+
gr_login_btn.click(
|
| 883 |
+
fn=db_login,
|
| 884 |
+
inputs=[gr_auth_user, gr_auth_pass],
|
| 885 |
+
outputs=[gr_auth_response]
|
| 886 |
+
)
|
| 887 |
+
|
| 888 |
+
gr_register_btn.click(
|
| 889 |
+
fn=db_register,
|
| 890 |
+
inputs=[gr_auth_user, gr_auth_pass],
|
| 891 |
+
outputs=[gr_auth_response]
|
| 892 |
+
)
|
| 893 |
|
| 894 |
if __name__ == "__main__":
|
| 895 |
demo.launch()
|