simikkk commited on
Commit
2ffe341
·
verified ·
1 Parent(s): d44e84f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +585 -620
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": "No image data received."})
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 when model not loaded
100
  extracted_fields = {
101
- "STORE_NAME": "Demo Vendor Ltd.",
102
- "TOTAL": "142.50",
103
- "SUBTOTAL": "130.00",
104
- "TAX": "12.50",
105
- "DATE": "2025-06-14",
106
- "INVOICE_NUMBER": "INV-DEMO-001",
107
  }
108
 
109
  elapsed = round(time.time() - start_time, 2)
110
-
111
- confidence_score = round(random.uniform(0.94, 0.99), 2)
112
- human_review = confidence_score < 0.95
113
 
114
  invoice_fp = extracted_fields.get("INVOICE_NUMBER", extracted_fields.get("TOTAL", "UNKNOWN"))
115
- is_duplicate = invoice_fp in PROCESSED_INVOICES_CACHE
116
- if not is_duplicate and invoice_fp != "UNKNOWN":
117
- PROCESSED_INVOICES_CACHE.add(invoice_fp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Build CSV
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "Please upload and process a document first, then I can answer questions about the extracted data."
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 "Here is what I found:\n" + "\n".join(f"• {m}" for m in matched[:5])
195
 
196
- if any(w in msg for w in ["total", "amount", "sum", "price", "cost"]):
197
- total = fields.get("TOTAL", fields.get("TOTAL_PRICE", "not found"))
198
  subtotal = fields.get("SUBTOTAL", "—")
199
  tax = fields.get("TAX", "—")
200
- return f"💰 **Total due**: {total}\n• Subtotal: {subtotal}\n• Tax: {tax}"
201
 
202
- if any(w in msg for w in ["vendor", "company", "who", "seller", "store", "from"]):
203
- vendor = fields.get("STORE_NAME", fields.get("VENDOR", fields.get("COMPANY", "not found")))
204
- return f"🏢 **Vendor**: {vendor}"
205
 
206
- if any(w in msg for w in ["date", "when", "issued", "day"]):
207
- date = fields.get("DATE", fields.get("INVOICE_DATE", "not found"))
208
- return f"📅 **Document date**: {date}"
209
 
210
- if any(w in msg for w in ["confidence", "score", "accuracy", "reliable"]):
211
  score = compliance.get("confidence_score", "—")
212
- return f"🎯 **Confidence score**: {score} ({float(score)*100:.0f}% accuracy)" if score != "—" else "Confidence data not available."
213
 
214
- if any(w in msg for w in ["duplicate", "seen", "processed before"]):
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 "📋 **Extracted fields**:\n" + "\n".join(lines)
225
 
226
  keys = list(fields.keys())[:6]
227
- return f"I have {len(fields)} fields extracted from this document. You can ask about: {', '.join(keys[:5])}{'...' if len(keys) > 5 else ''}."
228
 
229
 
230
- # ─── Gradio wrapper functions ───────────────────────────────────────────────
231
-
232
- def gr_parse(image_b64):
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
- # ─── Custom CSS ─────────────────────────────────────────────────────────────
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="en">
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 rel="preconnect" href="https://fonts.googleapis.com"/>
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',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;}
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);box-shadow:0 0 0 4px rgba(59,130,246,.08),inset 0 0 24px rgba(59,130,246,.04);}
308
-
309
- .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;box-shadow:0 0 20px rgba(59,130,246,.25);}
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:8px;font-size:12px;font-weight:500;cursor:pointer;transition:all .18s ease;padding:6px 12px;}
314
- .btn-secondary:hover{border-color:var(--blue-light);color:var(--text);background:#1c1c1f;}
315
-
316
- .tab-btn{font-size:12px;font-weight:500;padding:6px 12px;border-radius:6px;border:none;cursor:pointer;transition:all .18s ease;color:var(--text3);background:transparent;}
317
- .tab-btn.active{color:var(--text);background:var(--border);}
318
- .tab-btn:hover:not(.active){color:var(--text2);background:rgba(39,39,42,.5);}
319
-
320
- .bubble-user{background:var(--border);border:1px solid var(--border2);border-radius:14px 14px 4px 14px;}
321
- .bubble-ai{background:rgba(59,130,246,.07);border:1px solid rgba(59,130,246,.18);border-radius:14px 14px 14px 4px;}
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
- .toast.hide{animation:slideOut .3s ease forwards;}
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
- <header style="position:sticky;top:0;z-index:40;border-bottom:1px solid var(--border);background:rgba(9,9,11,.92);backdrop-filter:blur(16px);">
356
- <div style="max-width:1280px;margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between;">
357
- <div style="display:flex;align-items:center;gap:12px;">
358
- <div style="width:32px;height:32px;border-radius:8px;background:linear-gradient(135deg,#3b82f6,#1d4ed8);display:flex;align-items:center;justify-content:center;">
359
- <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>
360
  </div>
361
- <span style="color:#fff;font-weight:600;font-size:15px;letter-spacing:-.015em;">OmniParse <span class="grad">AI</span></span>
362
- <span style="display:flex;align-items:center;gap:6px;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);color:#60a5fa;">
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
- <div style="display:flex;align-items:center;gap:10px;">
368
- <div style="display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:8px;background:var(--surface2);border:1px solid var(--border);">
369
- <span id="status-dot" style="width:8px;height:8px;border-radius:50%;background:#34d399;box-shadow:0 0 6px #34d399;"></span>
370
- <span id="status-text" style="font-size:12px;color:var(--text2);font-weight:500;">System Idle</span>
 
 
 
 
 
371
  </div>
372
- <div style="width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,rgba(59,130,246,.2),rgba(29,78,216,.2));border:1px solid rgba(59,130,246,.3);display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;color:#93c5fd;">OP</div>
 
 
 
 
 
 
 
 
373
  </div>
374
  </div>
375
- </header>
376
 
377
- <main style="max-width:1280px;margin:0 auto;padding:24px;">
378
 
379
- <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;" id="stats-bar">
380
- <div class="stat-chip" style="display:flex;align-items:center;gap:12px;">
381
- <div style="width:32px;height:32px;border-radius:8px;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
382
- <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="#60a5fa" stroke-width="2"><path d="M9 12h6m-6 4h6m2 5H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5.586a1 1 0 0 1 .707.293l5.414 5.414a1 1 0 0 1 .293.707V19a2 2 0 0 1-2 2z"/></svg>
 
 
 
 
383
  </div>
384
- <div><p style="font-size:11px;color:var(--text4);margin-bottom:2px;">Docs Parsed</p><p id="st-docs" style="font-size:16px;font-weight:600;">0</p></div>
385
- </div>
386
- <div class="stat-chip" style="display:flex;align-items:center;gap:12px;">
387
- <div style="width:32px;height:32px;border-radius:8px;background:rgba(52,211,153,.1);border:1px solid rgba(52,211,153,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
388
- <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="#34d399" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
 
 
 
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
- <div class="stat-chip" style="display:flex;align-items:center;gap:12px;">
393
- <div style="width:32px;height:32px;border-radius:8px;background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.15);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
394
- <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="#fbbf24" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
 
 
 
 
395
  </div>
396
- <div><p style="font-size:11px;color:var(--text4);margin-bottom:2px;">Parse Time</p><p id="st-time" style="font-size:16px;font-weight:600;">—</p></div>
397
- </div>
398
- <div class="stat-chip" style="display:flex;align-items:center;gap:12px;">
399
- <div style="width:32px;height:32px;border-radius:8px;background:rgba(167,139,250,.1);border:1px solid rgba(167,139,250,.2);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
400
- <svg width="14" height="14" fill="none" viewBox="0 0 24 24" stroke="#a78bfa" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
 
 
 
 
 
 
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
- </div>
405
-
406
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
407
-
408
- <div style="display:flex;flex-direction:column;gap:16px;">
409
- <div class="card" style="padding:20px;">
410
- <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
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
- <div>
428
- <p style="font-size:14px;font-weight:500;color:var(--text);">Drop files here or <span style="color:var(--blue-light);">browse</span></p>
429
- <p style="font-size:12px;color:var(--text4);margin-top:4px;">PDF, JPG, PNG, TIFF, WEBP — up to 50 MB</p>
 
 
 
 
 
 
430
  </div>
431
- <div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;">
432
- <span style="padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500;color:var(--text3);background:var(--border);">PDF</span>
433
- <span style="padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500;color:var(--text3);background:var(--border);">JPG</span>
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
- <div id="file-preview" style="margin-top:12px;display:none;">
439
- <img id="img-preview" style="width:100%;max-height:200px;object-fit:contain;border-radius:8px;border:1px solid var(--border);"/>
 
 
 
 
 
 
 
440
  </div>
441
- <div id="file-list" style="margin-top:10px;display:flex;flex-direction:column;gap:8px;"></div>
442
- </div>
443
-
444
- <div class="card" style="padding:16px;">
445
- <button id="exec-btn" class="btn-primary" style="width:100%;padding:14px;display:flex;align-items:center;justify-content:center;gap:10px;" disabled onclick="executeExtraction()">
446
- <span id="btn-idle" style="display:flex;align-items:center;gap:8px;">
447
- <svg width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
448
- Execute Document Extraction ⚡
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
- </div>
484
-
485
- <div style="display:flex;flex-direction:column;gap:16px;">
486
- <div class="card" style="padding:16px;display:flex;flex-direction:column;height:100%;min-height:480px;">
487
- <div style="display:flex;align-items:center;justify-content:between;border-bottom:1px solid var(--border);padding-bottom:12px;margin-bottom:12px;">
488
- <div style="display:flex;gap:6px;background:var(--surface);padding:4px;border-radius:8px;border:1px solid var(--border);">
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 id="view-data" style="display:flex;flex-direction:column;gap:12px;flex-grow:1;">
496
- <div id="data-placeholder" style="display:flex;flex-direction:column;align-items:center;justify-content:center;flex-grow:1;color:var(--text4);text-align:center;padding:4px;">
497
- <svg width="32" height="32" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5" style="margin-bottom:8px;"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376A8.965 8.965 0 0 0 12 12.75c-.131 0-.262.007-.392.021m3.416 4.479a8.978 8.978 0 0 4-4.479m-4 4.479a9.15 9.15 0 0 1-3.084-2.435m0 0A8.958 8.958 0 0 1 12 12.75c1.24 0 2.391.251 3.434.703m-6.434-.703a9.036 9.036 0 0 1 1.566-3.472m0 0a9.043 9.043 0 0 1 3.468-1.564"/></svg>
498
- <p style="font-size:13px;font-weight:500;">Awaiting Extraction Results</p>
499
- <p style="font-size:11px;margin-top:2px;">Execute extraction to inspect payload schemas.</p>
500
  </div>
501
-
502
- <div id="data-display" style="display:none;flex-direction:column;gap:12px;flex-grow:1;">
503
- <div class="code-view" style="flex-grow:1;padding:14px;overflow-y:auto;max-height:320px;">
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 id="view-chat" style="display:none;flex-direction:column;height:100%;flex-grow:1;">
527
- <div id="chat-messages" style="flex-grow:1;overflow-y:auto;max-height:300px;display:flex;flex-direction:column;gap:12px;padding-right:4px;margin-bottom:12px;">
528
- <div class="fade-up" style="display:flex;gap:10px;align-items:start;">
529
- <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>
530
- <div class="bubble-ai" style="padding:10px 14px;font-size:13px;line-height:1.5;color:var(--text);">
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
- <div style="display:flex;gap:8px;position:relative;">
537
- <textarea id="chat-textarea" class="chat-input" placeholder="Query document contents..." rows="1" onkeydown="handleChatKey(event)"></textarea>
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
- </div>
547
- </main>
548
 
549
  <script>
550
  let currentBase64 = "";
551
- let latestJSONPayload = null;
552
- let parsedCount = 0;
553
-
554
- function showToast(message, type = "success") {
555
- const container = document.getElementById("toast-root");
556
- const toast = document.createElement("div");
557
- toast.className = "toast card";
558
- toast.style.cssText = `padding:12px 16px;display:flex;align-items:center;gap:10px;min-width:260px;background:var(--surface2);border-left:4px solid ${type === 'success' ? 'var(--green)' : 'var(--red)'};`;
559
- toast.innerHTML = `
560
- <span style="font-size:13px;font-weight:500;color:var(--text);flex-grow:1;">${message}</span>
561
- <button onclick="this.parentElement.remove()" style="color:var(--text4);font-size:12px;cursor:pointer;">✕</button>
562
- `;
563
- container.appendChild(toast);
564
- setTimeout(() => {
565
- toast.classList.add("hide");
566
- setTimeout(() => toast.remove(), 300);
567
- }, 4000);
568
  }
569
 
570
- function handleDrop(e) {
571
- e.preventDefault();
572
- document.getElementById("drop-zone").classList.remove("drag-over");
573
- if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
574
- handleFiles(e.dataTransfer.files);
575
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  }
577
 
578
- function handleFiles(files) {
579
- if (files.length === 0) return;
580
- const file = files[0];
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
- const hiddenInput = document.querySelector("#gr-image-input textarea");
598
- if(hiddenInput) {
599
- hiddenInput.value = currentBase64;
600
- hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
601
  }
602
 
603
- if (file.type.startsWith("image/")) {
604
- document.getElementById("img-preview").src = currentBase64;
605
- document.getElementById("file-preview").style.display = "block";
 
 
 
 
 
 
 
 
 
606
  } else {
607
- document.getElementById("file-preview").style.display = "none";
608
  }
609
 
610
- document.getElementById("exec-btn").removeAttribute("disabled");
611
- showToast("Document staged for extraction.");
612
- };
613
- reader.readAsDataURL(file);
614
  }
615
 
616
- function executeExtraction() {
617
- if (!currentBase64) return;
618
-
619
- document.getElementById("exec-btn").setAttribute("disabled", "true");
620
- document.getElementById("btn-idle").style.display = "none";
621
- document.getElementById("btn-loading").style.display = "flex";
622
- document.getElementById("prog-wrapper").style.display = "block";
623
-
624
- document.getElementById("status-dot").style.background = "var(--amber)";
625
- document.getElementById("status-dot").style.boxShadow = "0 0 8px var(--amber)";
626
- document.getElementById("status-text").innerText = "Analyzing Schema...";
627
-
628
- for(let i=1; i<=4; i++) {
629
- document.getElementById(`stage-${i}`).className = "stage-dot";
630
- }
631
-
632
- animateProgress(0, 25, "Ingesting Document...", 1, () => {
633
- animateProgress(25, 65, "Running Vision Transformer Model...", 2, () => {
634
- const grBtn = document.getElementById("gr-parse-btn");
635
- if(grBtn) grBtn.click();
636
- waitForExtractionResponse();
637
- });
638
- });
639
- }
640
-
641
- function animateProgress(start, end, label, currentStage, callback) {
642
- document.getElementById("prog-label").innerText = label;
643
- document.getElementById(`stage-${currentStage}`).className = "stage-dot active";
644
-
645
- let current = start;
646
- let interval = setInterval(() => {
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 waitForExtractionResponse() {
660
- const hiddenOut = document.querySelector("#gr-json-output textarea");
661
- let checks = 0;
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 handleExtractionSuccess(payload) {
681
- latestJSONPayload = payload;
682
- parsedCount++;
683
-
684
- animateProgress(65, 90, "Cross-Field Validation...", 3, () => {
685
- animateProgress(90, 100, "Compiling Result Schemes...", 4, () => {
686
- document.getElementById("btn-idle").style.display = "flex";
687
- document.getElementById("btn-loading").style.display = "none";
688
-
689
- document.getElementById("status-dot").style.background = "#34d399";
690
- document.getElementById("status-dot").style.boxShadow = "0 0 6px #34d399";
691
- document.getElementById("status-text").innerText = "System Idle";
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 handleExtractionError(errLog) {
717
- document.getElementById("btn-idle").style.display = "flex";
718
- document.getElementById("btn-loading").style.display = "none";
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 syntaxHighlightJSON(json) {
729
- json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
730
- return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, function (match) {
731
- let cls = 'json-number';
732
- if (/^"/.test(match)) {
733
- if (/:$/.test(match)) {
734
- cls = 'json-key';
735
- return `<span class="${cls}">${match.replace(':', '')}</span>:`;
736
- } else {
737
- cls = 'json-string';
738
- }
739
- } else if (/true|false/.test(match)) {
740
- cls = 'json-bool';
741
- } else if (/null/.test(match)) {
742
- cls = 'json-null';
743
- }
744
- return `<span class="${cls}">${match}</span>`;
745
- });
 
 
746
  }
747
 
748
- function switchTab(tab) {
749
- if(tab === 'data') {
750
- document.getElementById("tab-data").classList.add("active");
751
- document.getElementById("tab-chat").classList.remove("active");
752
- document.getElementById("view-data").style.display = "flex";
753
- document.getElementById("view-chat").style.display = "none";
754
- } else {
755
- document.getElementById("tab-data").classList.remove("active");
756
- document.getElementById("tab-chat").classList.add("active");
757
- document.getElementById("view-data").style.display = "none";
758
- document.getElementById("view-chat").style.display = "flex";
759
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  }
761
 
762
- function handleChatKey(e) {
763
- if(e.key === 'Enter' && !e.shiftKey) {
764
- e.preventDefault();
765
- sendChatMessage();
766
- }
 
 
 
 
 
 
 
 
 
 
767
  }
768
 
769
  function sendChatMessage() {
770
- const txtArea = document.getElementById("chat-textarea");
771
- const msg = txtArea.value.trim();
772
- if(!msg) return;
773
-
774
- txtArea.value = "";
775
-
776
- const chatContainer = document.getElementById("chat-messages");
777
- const userDiv = document.createElement("div");
778
- userDiv.className = "fade-up";
779
- userDiv.style = "display:flex;gap:10px;align-items:start;justify-content:flex-end;";
780
- userDiv.innerHTML = `
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
- grJsonInput.value = latestJSONPayload ? JSON.stringify(latestJSONPayload) : "";
812
- grJsonInput.dispatchEvent(new Event('input', { bubbles: true }));
 
 
813
 
814
- grChatBtn.click();
815
- waitForChatResponse();
816
- }
 
817
  }
818
 
819
- function waitForChatResponse() {
820
- const hiddenChatOut = document.querySelector("#gr-chat-out textarea");
821
- let checks = 0;
822
-
823
- let checker = setInterval(() => {
824
- checks++;
825
- if (hiddenChatOut && hiddenChatOut.value && hiddenChatOut.value.trim() !== "") {
826
- clearInterval(checker);
827
- const reply = hiddenChatOut.value;
828
-
829
- const placeholder = document.getElementById("ai-thinking-placeholder");
830
- if(placeholder) placeholder.remove();
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, '&bull;');
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
- function downloadCSV() {
861
- if(!latestJSONPayload || !latestJSONPayload._csv_b64) return;
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
- # ─── Gradio Block Core Setup ────────────────────────────────────────────────
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
- label="image_b64",
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
- label="chat_msg",
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
- label="chat_out",
913
- elem_id="gr-chat-out",
914
- visible=False,
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()