// Contract check frontend (() => { const $ = (id) => document.getElementById(id); const sections = { setup: $("s-setup"), processing: $("s-processing"), results: $("s-results"), }; const show = (name) => { for (const [k, el] of Object.entries(sections)) el.classList.toggle("hidden", k !== name); }; let state = { jobId: null, checklist: [], analysis: null, pendingFile: null, // File chosen by user but not yet uploaded }; let nextCustomId = 1; // ── Load default checklist ── async function loadChecklist() { try { const r = await fetch("api/checklist"); const data = await r.json(); state.checklist = (data.items || []).map((it) => ({ ...it, checked: it.default !== false, })); renderChecklist(); } catch (err) { alert("Nepodařilo se načíst kontrolní seznam: " + err.message); } } function renderChecklist() { const c = $("checklist"); c.innerHTML = ""; for (const item of state.checklist) { const div = document.createElement("label"); div.className = "checklist-item" + (item.custom ? " custom" : ""); div.innerHTML = `
${escapeHtml(item.label)}
${item.hint ? `
${escapeHtml(item.hint)}
` : ""}
${item.custom ? `` : ""} `; const cb = div.querySelector("input"); cb.addEventListener("change", () => { item.checked = cb.checked; updateRunButton(); }); const rm = div.querySelector(".checklist-remove"); if (rm) { rm.addEventListener("click", (e) => { e.preventDefault(); state.checklist = state.checklist.filter((x) => x.id !== item.id); renderChecklist(); updateRunButton(); }); } c.appendChild(div); } updateRunButton(); } $("check-all-btn").addEventListener("click", () => { for (const it of state.checklist) it.checked = true; renderChecklist(); }); $("uncheck-all-btn").addEventListener("click", () => { for (const it of state.checklist) it.checked = false; renderChecklist(); }); $("add-item-form").addEventListener("submit", (e) => { e.preventDefault(); const input = $("add-item-input"); const label = input.value.trim(); if (!label) return; state.checklist.push({ id: `custom_${nextCustomId++}`, label, hint: "", checked: true, custom: true, }); input.value = ""; renderChecklist(); }); // ── File selection (just stores the file, doesn't upload yet) ── const fileInput = $("file-input"); const dropZone = $("drop-zone"); $("browse-btn").addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", (e) => e.target.files[0] && setFile(e.target.files[0])); ["dragenter", "dragover"].forEach((ev) => dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.add("drag-over"); })); ["dragleave", "drop"].forEach((ev) => dropZone.addEventListener(ev, (e) => { e.preventDefault(); dropZone.classList.remove("drag-over"); })); dropZone.addEventListener("drop", (e) => e.dataTransfer.files[0] && setFile(e.dataTransfer.files[0])); function setFile(file) { state.pendingFile = file; $("file-info-name").textContent = file.name; $("file-info").classList.remove("hidden"); dropZone.classList.add("compact"); updateRunButton(); } $("file-info-clear").addEventListener("click", () => { state.pendingFile = null; fileInput.value = ""; $("file-info").classList.add("hidden"); dropZone.classList.remove("compact"); updateRunButton(); }); function updateRunButton() { const hasFile = !!state.pendingFile; const selectedCount = state.checklist.filter((it) => it.checked).length; const btn = $("run-btn"); const hint = $("run-hint"); // run-hint only exists after a file is selected (lives in the file-info strip) if (!hasFile) { btn.disabled = true; return; } if (selectedCount === 0) { btn.disabled = true; if (hint) hint.textContent = "Vyberte alespoň jednu položku ke kontrole níže."; } else { btn.disabled = false; if (hint) hint.textContent = `Připraveno spustit kontrolu ${selectedCount} bodů.`; } } $("run-btn").addEventListener("click", async () => { if (!state.pendingFile) return; const selected = state.checklist.filter((it) => it.checked); if (!selected.length) return; show("processing"); $("processing-title").textContent = "Nahrávám smlouvu…"; try { const fd = new FormData(); fd.append("file", state.pendingFile); const ur = await fetch("api/upload", { method: "POST", body: fd }); if (!ur.ok) throw new Error((await ur.json()).detail || ur.statusText); const upJson = await ur.json(); state.jobId = upJson.job_id; $("processing-title").textContent = `Analyzuji smlouvu (${selected.length} bodů)…`; const ar = await fetch(`api/analyze/${state.jobId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: selected.map((it) => ({ id: it.id, label: it.label, hint: it.hint || "", })), }), }); if (!ar.ok) throw new Error((await ar.json()).detail || ar.statusText); state.analysis = await ar.json(); renderResults(); show("results"); } catch (err) { alert("Chyba: " + err.message); show("setup"); } }); function renderResults() { const a = state.analysis; const items = a.items || []; const usedOcr = !!a._used_ocr; const counts = items.reduce((acc, it) => { acc[it.status || "warning"] = (acc[it.status || "warning"] || 0) + 1; return acc; }, {}); $("results-meta").textContent = `Vyhodnoceno ${items.length} bodů. ` + `OK: ${counts.ok || 0}, Pozor: ${counts.warning || 0}, ` + `Problém: ${counts.problem || 0}, Chybí: ${counts.missing || 0}.`; const riskMap = { low: ["Nízké riziko", "risk-low"], medium: ["Střední riziko", "risk-medium"], high: ["Vysoké riziko", "risk-high"], }; const r = riskMap[a.risk_level] || ["—", ""]; const overall = $("overall-card"); const ocrNotice = usedOcr ? `
⚙ Smlouva neměla textovou vrstvu — použito OCR (rozpoznávání textu z obrazu). Stažené PDF bude obsahovat jen souhrnnou stránku, bez zvýraznění v původním textu (kvalita OCR neumožňuje spolehlivé vyhledání citací).
` : ""; overall.innerHTML = `
${r[0]}
${escapeHtml(a.overall_summary || "")} ${ocrNotice}
`; const findings = $("findings"); findings.innerHTML = ""; const labelMap = { ok: "OK", warning: "POZOR", problem: "PROBLÉM", missing: "CHYBÍ" }; // Sort: problem > warning > missing > ok const order = { problem: 0, warning: 1, missing: 2, ok: 3 }; const sorted = [...items].sort((a, b) => (order[a.status] ?? 9) - (order[b.status] ?? 9) ); for (const it of sorted) { const div = document.createElement("div"); div.className = `finding status-${it.status || "warning"}`; const excerptsHtml = (it.excerpts || []).map((ex) => `
${ex.page ? `str. ${ex.page}` : ""} „${escapeHtml(ex.text || "")}"
${ex.comment ? `
${escapeHtml(ex.comment)}
` : ""}
`).join(""); const canAdvise = it.status === "warning" || it.status === "problem"; const adviceBtn = canAdvise ? `` : ""; div.innerHTML = `
${labelMap[it.status] || it.status || ""} ${escapeHtml(it.title || it.label || it.id)}
${it.summary ? `

${escapeHtml(it.summary)}

` : ""} ${excerptsHtml ? `
${excerptsHtml}
` : ""} ${adviceBtn ? `
${adviceBtn}
` : ""} `; findings.appendChild(div); } findings.onclick = onFindingClick; state.supplierDone = false; state.supplierStatus = "checking"; updateDownloadGate(); renderSupplier(); resetChat(); } // ── Per-clause improvement advice ── async function onFindingClick(e) { const btn = e.target.closest(".advice-btn"); if (!btn) return; const id = btn.dataset.id; const box = document.querySelector(`[data-advice="${CSS.escape(id)}"]`); if (box && !box.classList.contains("hidden") && box.dataset.loaded) { box.classList.toggle("hidden"); // collapse if already loaded return; } btn.disabled = true; const old = btn.textContent; btn.textContent = "Generuji…"; try { const r = await fetch(`api/advice/${state.jobId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item_id: id }), }); if (!r.ok) throw new Error((await r.json()).detail || r.statusText); const d = await r.json(); box.innerHTML = renderAdvice(d.advice || ""); box.dataset.loaded = "1"; box.classList.remove("hidden"); btn.textContent = "Skrýt návrh"; } catch (err) { alert("Chyba: " + err.message); btn.textContent = old; } finally { btn.disabled = false; } } function renderAdvice(text) { return `
Návrh úprav
${mdToHtml(text)}`; } function updateDownloadGate() { const btn = $("download-pdf-btn"); const note = $("download-gate-note"); btn.disabled = !state.supplierDone; if (state.supplierDone) { btn.title = ""; note.classList.add("hidden"); note.textContent = ""; } else { const msg = state.supplierStatus === "checking" ? "⏳ PDF bude k dispozici po dokončení ověření protistrany v rejstřících (probíhá…). Ověření je součástí PDF." : "⚠ Stažení PDF je zatím nedostupné: nejprve dokončete ověření protistrany (zadejte IČO v kartě výše). Výsledek ověření je povinnou součástí PDF."; btn.title = "Nejprve dokončete ověření protistrany v rejstřících"; note.textContent = msg; note.classList.remove("hidden"); } } // ── Supplier reputation (ARES + MFČR) ── async function renderSupplier(ico) { const card = $("supplier-card"); card.innerHTML = `
Ověřuji protistranu v rejstřících…
`; state.supplierDone = false; state.supplierStatus = "checking"; updateDownloadGate(); try { const r = await fetch(`api/supplier/${state.jobId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(ico ? { ico } : {}), }); const d = await r.json(); if (d.found) { card.innerHTML = supplierReportHtml(d); state.supplierDone = true; } else { card.innerHTML = supplierManualHtml(d); bindSupplierManual(); state.supplierStatus = "manual"; } } catch (err) { card.innerHTML = supplierManualHtml({ reason: "Ověření selhalo: " + err.message }); bindSupplierManual(); state.supplierStatus = "manual"; } updateDownloadGate(); } function supplierReportHtml(d) { const a = d.report.ares, v = d.report.vat; const flags = (d.report.flags || []).map(([lvl, txt]) => `${escapeHtml(txt)}`).join(""); const row = (k, val) => val ? `
${k}${escapeHtml(String(val))}
` : ""; const srcMap = { contract: "z textu smlouvy", ares_name: "podle názvu (ARES)", manual: "zadáno ručně" }; return `
Protistrana — ověření v rejstřících
${flags}
${row("Název", a.name)} ${row("IČO / DIČ", [a.ico, a.dic].filter(Boolean).join(" / "))} ${row("Adresa", a.address)} ${row("Vznik", a.founded)} ${a.dissolved ? row("Zánik", a.dissolved) : ""}
Aktivní v rejstříku${a.active ? "ano" : "ne"}
Plátce DPH${a.vat_registered ? "ano" : "ne"}
Insolvence${a.in_insolvency ? "ANO — pozor" : "ne"}
Nespolehlivý plátce DPH${v.unreliable === true ? "ANO — pozor" : v.unreliable === false ? "ne" : "neověřeno"}
${v.accounts && v.accounts.length ? `
Zveřejněné účty${v.accounts.slice(0,4).map(escapeHtml).join(", ")}${v.accounts.length>4?" …":""}
` : ""}
Zdroj: ${srcMap[d.source] || d.source} · ARES + MFČR
`; } function supplierManualHtml(d) { const cands = (d.candidates || []).map((c) => ``).join(" "); return `
Protistrana — ověření v rejstřících

${escapeHtml(d.reason || "IČO se nepodařilo zjistit.")}

${d.name_guess ? `

Odhad názvu: ${escapeHtml(d.name_guess)}

` : ""} ${cands ? `
${cands}
` : ""}
`; } function bindSupplierManual() { const chk = $("sup-check"); if (chk) chk.addEventListener("click", () => { const ico = ($("sup-ico").value || "").replace(/\D/g, ""); if (ico.length === 8) renderSupplier(ico); else alert("IČO musí mít 8 číslic."); }); document.querySelectorAll(".sup-cand").forEach((b) => b.addEventListener("click", () => renderSupplier(b.dataset.ico))); const card = $("supplier-card"); card.querySelector("#supplier-edit") && card.querySelector("#supplier-edit").addEventListener("click", () => { card.innerHTML = supplierManualHtml({ reason: "Zadejte IČO protistrany ručně." }); bindSupplierManual(); }); } // delegate "Změnit IČO" (present in the report view) $("supplier-card").addEventListener("click", (e) => { if (e.target.id === "supplier-edit") { $("supplier-card").innerHTML = supplierManualHtml({ reason: "Zadejte IČO protistrany ručně." }); bindSupplierManual(); } }); // ── Chat with the contract ── let chatMessages = []; function resetChat() { chatMessages = []; $("chat-log").innerHTML = ""; } // Minimal markdown → HTML for assistant replies (bold, headings, bullets). function mdToHtml(md) { let s = escapeHtml(md); s = s.replace(/^#{3,}\s+(.*)$/gm, "

$1

") .replace(/^##\s+(.*)$/gm, "

$1

") .replace(/\*\*(.+?)\*\*/g, "$1"); s = s.replace(/^\s*[-•]\s+(.*)$/gm, "
  • $1
  • "); s = s.replace(/(
  • [\s\S]*?<\/li>)/g, (m) => ""); return s.split(/\n{2,}/).map((b) => /^<(ul|h3|h4)/.test(b.trim()) ? b : "

    " + b.replace(/\n/g, "
    ") + "

    " ).join(""); } function appendChat(role, text) { const wrap = document.createElement("div"); wrap.className = `msg msg-${role}`; const author = role === "user" ? "Vy" : "Asistent"; const ava = role === "user" ? 'Vy' : 'AI'; const body = role === "user" ? escapeHtml(text) : mdToHtml(text); wrap.innerHTML = `
    ${ava}${author}
    ${body}
    `; $("chat-log").appendChild(wrap); $("chat-log").scrollTop = $("chat-log").scrollHeight; return wrap.querySelector(".msg-body"); } // Assistant placeholder with animated typing dots (same as Firemní asistent). function appendTyping() { const wrap = document.createElement("div"); wrap.className = "msg msg-assistant msg-typing"; wrap.innerHTML = '
    AIAsistent
    ' + '
    '; $("chat-log").appendChild(wrap); $("chat-log").scrollTop = $("chat-log").scrollHeight; return wrap; } $("chat-form").addEventListener("submit", async (e) => { e.preventDefault(); const inp = $("chat-input"); const q = inp.value.trim(); if (!q || !state.jobId) return; inp.value = ""; inp.style.height = "auto"; appendChat("user", q); chatMessages.push({ role: "user", content: q }); const msgEl = appendTyping(); const body = msgEl.querySelector(".msg-body"); $("chat-send").disabled = true; let acc = "", first = false; try { const r = await fetch(`api/chat/${state.jobId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: chatMessages }), }); if (!r.ok || !r.body) throw new Error("HTTP " + r.status); const reader = r.body.getReader(); const dec = new TextDecoder(); let buf = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); let idx; while ((idx = buf.indexOf("\n\n")) !== -1) { const raw = buf.slice(0, idx); buf = buf.slice(idx + 2); if (!raw.startsWith("data:")) continue; let evt; try { evt = JSON.parse(raw.slice(5).trim()); } catch { continue; } if (evt.type === "textResponseChunk") { if (!first) { msgEl.classList.remove("msg-typing"); body.innerHTML = ""; first = true; } acc += evt.textResponse || ""; body.innerHTML = mdToHtml(acc); $("chat-log").scrollTop = $("chat-log").scrollHeight; } else if (evt.type === "finalizeResponseStream") { msgEl.classList.remove("msg-typing"); acc = evt.textResponse || acc; body.innerHTML = mdToHtml(acc); } else if (evt.type === "error") { msgEl.classList.remove("msg-typing"); body.innerHTML = `Chyba: ${escapeHtml(evt.error || "")}`; } } } if (acc) chatMessages.push({ role: "assistant", content: acc }); else { msgEl.classList.remove("msg-typing"); body.textContent = "Bez odpovědi."; } } catch (err) { msgEl.classList.remove("msg-typing"); body.innerHTML = `Spojení selhalo: ${escapeHtml(String(err))}`; } finally { $("chat-send").disabled = false; } }); // Enter sends, Shift+Enter = newline; textarea auto-grows. const chatInput = $("chat-input"); chatInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); $("chat-form").requestSubmit(); } }); chatInput.addEventListener("input", () => { chatInput.style.height = "auto"; chatInput.style.height = Math.min(chatInput.scrollHeight, 160) + "px"; }); // Download is gated on the reliability check; opens the include-modal. $("download-pdf-btn").addEventListener("click", () => { if (!state.jobId) return; if (!state.supplierDone) { alert("Nejprve dokončete ověření protistrany — bude součástí PDF."); return; } $("inc-chat").checked = chatMessages.length > 0; $("export-modal").classList.remove("hidden"); }); $("export-cancel").addEventListener("click", () => $("export-modal").classList.add("hidden")); $("export-go").addEventListener("click", async () => { const btn = $("export-go"); const old = btn.textContent; btn.disabled = true; btn.textContent = "Generuji…"; try { const r = await fetch(`api/annotated/${state.jobId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ include_supplier: true, include_advice: $("inc-advice").checked, include_chat: $("inc-chat").checked, chat: chatMessages, }), }); if (!r.ok) throw new Error((await r.json()).detail || r.statusText); const cd = r.headers.get("content-disposition") || ""; const m = cd.match(/filename="?([^"]+)"?/); const blob = await r.blob(); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = m ? m[1] : "kontrola.pdf"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 60000); $("export-modal").classList.add("hidden"); } catch (err) { alert("Chyba: " + err.message); } finally { btn.disabled = false; btn.textContent = old; } }); $("restart-btn").addEventListener("click", () => { state = { jobId: null, checklist: state.checklist, analysis: null, pendingFile: null, }; fileInput.value = ""; $("file-info").classList.add("hidden"); dropZone.classList.remove("compact"); show("setup"); updateRunButton(); }); function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); } function escapeHtmlAttr(s) { return escapeHtml(s); } loadChecklist(); })();