568 lines
22 KiB
JavaScript
568 lines
22 KiB
JavaScript
// 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 = `
|
||
<input type="checkbox" ${item.checked ? "checked" : ""}>
|
||
<div class="checklist-item-body">
|
||
<div class="checklist-item-label">${escapeHtml(item.label)}</div>
|
||
${item.hint ? `<div class="checklist-item-hint">${escapeHtml(item.hint)}</div>` : ""}
|
||
</div>
|
||
${item.custom ? `<button class="checklist-remove" type="button" title="Odstranit">×</button>` : ""}
|
||
`;
|
||
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
|
||
? `<div class="ocr-notice">⚙ 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í).</div>`
|
||
: "";
|
||
overall.innerHTML = `
|
||
<div class="risk-badge ${r[1]}">${r[0]}</div>
|
||
<div class="overall-text">
|
||
${escapeHtml(a.overall_summary || "")}
|
||
${ocrNotice}
|
||
</div>
|
||
`;
|
||
|
||
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) => `
|
||
<div class="excerpt">
|
||
<div class="excerpt-text">
|
||
${ex.page ? `<span class="excerpt-page">str. ${ex.page}</span>` : ""}
|
||
<span>„${escapeHtml(ex.text || "")}"</span>
|
||
</div>
|
||
${ex.comment ? `<div class="excerpt-comment">${escapeHtml(ex.comment)}</div>` : ""}
|
||
</div>
|
||
`).join("");
|
||
const canAdvise = it.status === "warning" || it.status === "problem";
|
||
const adviceBtn = canAdvise
|
||
? `<button class="btn btn-secondary btn-sm advice-btn" data-id="${escapeHtmlAttr(it.id)}" type="button">Navrhnout úpravu</button>`
|
||
: "";
|
||
div.innerHTML = `
|
||
<div class="finding-header">
|
||
<span class="finding-status">${labelMap[it.status] || it.status || ""}</span>
|
||
<span class="finding-title">${escapeHtml(it.title || it.label || it.id)}</span>
|
||
</div>
|
||
${it.summary ? `<p class="finding-summary">${escapeHtml(it.summary)}</p>` : ""}
|
||
${excerptsHtml ? `<div class="finding-excerpts">${excerptsHtml}</div>` : ""}
|
||
${adviceBtn ? `<div class="finding-advice-row">${adviceBtn}</div>
|
||
<div class="finding-advice hidden" data-advice="${escapeHtmlAttr(it.id)}"></div>` : ""}
|
||
`;
|
||
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 `<div class="advice-label">Návrh úprav</div>${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 = `<div class="supplier-loading">Ověřuji protistranu v rejstřících…</div>`;
|
||
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]) =>
|
||
`<span class="rep-flag rep-${lvl}">${escapeHtml(txt)}</span>`).join("");
|
||
const row = (k, val) => val ? `<div class="rep-row"><span>${k}</span><span>${escapeHtml(String(val))}</span></div>` : "";
|
||
const srcMap = { contract: "z textu smlouvy", ares_name: "podle názvu (ARES)", manual: "zadáno ručně" };
|
||
return `
|
||
<div class="supplier-head">
|
||
<span class="supplier-title">Protistrana — ověření v rejstřících</span>
|
||
<button class="btn-link" id="supplier-edit" type="button">Změnit IČO</button>
|
||
</div>
|
||
<div class="rep-flags">${flags}</div>
|
||
${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) : ""}
|
||
<div class="rep-row"><span>Aktivní v rejstříku</span><span>${a.active ? "ano" : "ne"}</span></div>
|
||
<div class="rep-row"><span>Plátce DPH</span><span>${a.vat_registered ? "ano" : "ne"}</span></div>
|
||
<div class="rep-row"><span>Insolvence</span><span>${a.in_insolvency ? "ANO — pozor" : "ne"}</span></div>
|
||
<div class="rep-row"><span>Nespolehlivý plátce DPH</span><span>${v.unreliable === true ? "ANO — pozor" : v.unreliable === false ? "ne" : "neověřeno"}</span></div>
|
||
${v.accounts && v.accounts.length ? `<div class="rep-row"><span>Zveřejněné účty</span><span>${v.accounts.slice(0,4).map(escapeHtml).join(", ")}${v.accounts.length>4?" …":""}</span></div>` : ""}
|
||
<div class="rep-source">Zdroj: ${srcMap[d.source] || d.source} · ARES + MFČR</div>`;
|
||
}
|
||
|
||
function supplierManualHtml(d) {
|
||
const cands = (d.candidates || []).map((c) =>
|
||
`<button class="btn btn-secondary btn-sm sup-cand" data-ico="${escapeHtmlAttr(c.ico)}" type="button">${escapeHtml(c.name || c.ico)} (${escapeHtml(c.ico || "?")})</button>`).join(" ");
|
||
return `
|
||
<div class="supplier-head"><span class="supplier-title">Protistrana — ověření v rejstřících</span></div>
|
||
<p class="supplier-reason">${escapeHtml(d.reason || "IČO se nepodařilo zjistit.")}</p>
|
||
${d.name_guess ? `<p class="supplier-reason">Odhad názvu: <strong>${escapeHtml(d.name_guess)}</strong></p>` : ""}
|
||
${cands ? `<div class="sup-cands">${cands}</div>` : ""}
|
||
<div class="sup-manual-row">
|
||
<input type="text" id="sup-ico" class="add-item-input" placeholder="Zadejte IČO (8 číslic)">
|
||
<button class="btn btn-primary btn-sm" id="sup-check" type="button">Ověřit</button>
|
||
</div>`;
|
||
}
|
||
|
||
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, "<h4>$1</h4>")
|
||
.replace(/^##\s+(.*)$/gm, "<h3>$1</h3>")
|
||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
||
s = s.replace(/^\s*[-•]\s+(.*)$/gm, "<li>$1</li>");
|
||
s = s.replace(/(<li>[\s\S]*?<\/li>)/g, (m) => "<ul>" + m + "</ul>");
|
||
return s.split(/\n{2,}/).map((b) =>
|
||
/^<(ul|h3|h4)/.test(b.trim()) ? b : "<p>" + b.replace(/\n/g, "<br>") + "</p>"
|
||
).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"
|
||
? '<span class="msg-avatar you">Vy</span>'
|
||
: '<span class="msg-avatar">AI</span>';
|
||
const body = role === "user" ? escapeHtml(text) : mdToHtml(text);
|
||
wrap.innerHTML = `<div class="msg-author">${ava}${author}</div><div class="msg-body">${body}</div>`;
|
||
$("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 =
|
||
'<div class="msg-author"><span class="msg-avatar">AI</span>Asistent</div>' +
|
||
'<div class="msg-body"><span class="dot"></span><span class="dot"></span><span class="dot"></span></div>';
|
||
$("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 = `<span class="chat-err">Chyba: ${escapeHtml(evt.error || "")}</span>`;
|
||
}
|
||
}
|
||
}
|
||
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 = `<span class="chat-err">Spojení selhalo: ${escapeHtml(String(err))}</span>`;
|
||
} 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();
|
||
})();
|