Apps: - dwg-rooms: extract room numbers from DWG/DXF - dwg-counting: count symbols in PDF drawings (OpenCV template matching) - contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback) - email-drafter: bullet notes → polished Czech/English business emails - invoice-extractor: PDF/image invoice → structured data → Excel - translator: Czech-first translator across 19 languages with tone control - vv-check: find inconsistent unit prices across VV sheets in one workbook - vv-compare: diff original vs new VV files (changes / added / removed) - feature-request: portal users submit ideas + sample files Infrastructure: - LiteLLM gateway with per-app virtual keys + budgets - Langfuse observability - Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL - Caddy reverse proxy on *.klas.chat
210 lines
7.4 KiB
JavaScript
210 lines
7.4 KiB
JavaScript
"use strict";
|
|
|
|
const $ = id => document.getElementById(id);
|
|
|
|
const sUpload = $("s-upload");
|
|
const sProcessing = $("s-processing");
|
|
const sResults = $("s-results");
|
|
const dropZone = $("drop-zone");
|
|
const fileInput = $("file-input");
|
|
const browseBtn = $("browse-btn");
|
|
const stepsList = $("steps-list");
|
|
const roomsTbody = $("rooms-tbody");
|
|
const resultsMeta = $("results-meta");
|
|
const exportBtn = $("export-btn");
|
|
const resetBtn = $("reset-btn");
|
|
const examplesList = $("examples-list");
|
|
const examplesForm = $("examples-form");
|
|
const exampleInput = $("example-input");
|
|
|
|
let jobId = null;
|
|
let rooms = [];
|
|
let examples = [];
|
|
|
|
// ── Examples management ───────────────────────────
|
|
async function loadDefaults() {
|
|
try {
|
|
const resp = await fetch("/api/defaults");
|
|
const data = await resp.json();
|
|
examples = Array.isArray(data.examples) ? data.examples : [];
|
|
renderExamples();
|
|
} catch (e) {
|
|
console.warn("Could not load defaults:", e);
|
|
}
|
|
}
|
|
|
|
function renderExamples() {
|
|
examplesList.innerHTML = "";
|
|
examples.forEach((ex, i) => {
|
|
const chip = document.createElement("span");
|
|
chip.className = "example-chip";
|
|
chip.innerHTML = `
|
|
${esc(ex)}
|
|
<button class="example-chip-remove" type="button" data-i="${i}" aria-label="Odebrat">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
|
</svg>
|
|
</button>
|
|
`;
|
|
examplesList.appendChild(chip);
|
|
});
|
|
examplesList.querySelectorAll(".example-chip-remove").forEach(btn => {
|
|
btn.addEventListener("click", () => {
|
|
examples.splice(parseInt(btn.dataset.i, 10), 1);
|
|
renderExamples();
|
|
});
|
|
});
|
|
}
|
|
|
|
examplesForm.addEventListener("submit", e => {
|
|
e.preventDefault();
|
|
const v = exampleInput.value.trim();
|
|
if (!v) return;
|
|
if (!/\d/.test(v)) {
|
|
exampleInput.focus();
|
|
exampleInput.select();
|
|
return;
|
|
}
|
|
if (!examples.includes(v)) examples.push(v);
|
|
exampleInput.value = "";
|
|
renderExamples();
|
|
exampleInput.focus();
|
|
});
|
|
|
|
// ── Drag & drop ───────────────────────────────────
|
|
dropZone.addEventListener("dragover", e => { e.preventDefault(); dropZone.classList.add("drag-over"); });
|
|
dropZone.addEventListener("dragleave", () => dropZone.classList.remove("drag-over"));
|
|
dropZone.addEventListener("drop", e => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove("drag-over");
|
|
const f = e.dataTransfer.files[0];
|
|
if (f) handleFile(f);
|
|
});
|
|
dropZone.addEventListener("click", () => fileInput.click());
|
|
browseBtn.addEventListener("click", e => { e.stopPropagation(); fileInput.click(); });
|
|
fileInput.addEventListener("change", e => { if (e.target.files[0]) handleFile(e.target.files[0]); });
|
|
|
|
// ── Upload & process ──────────────────────────────
|
|
async function handleFile(file) {
|
|
const ext = file.name.split(".").pop().toLowerCase();
|
|
if (!["dwg", "dxf"].includes(ext)) {
|
|
alert("Podporované formáty jsou .dwg a .dxf");
|
|
return;
|
|
}
|
|
|
|
show(sProcessing); hide(sUpload); hide(sResults);
|
|
stepsList.innerHTML = "";
|
|
addStep("Nahrávání souboru…", "active");
|
|
|
|
const fd = new FormData();
|
|
fd.append("file", file);
|
|
fd.append("examples", JSON.stringify(examples));
|
|
|
|
try {
|
|
const resp = await fetch("/api/upload", { method: "POST", body: fd });
|
|
if (!resp.ok) {
|
|
const err = await resp.json().catch(() => ({}));
|
|
throw new Error(err.detail || `HTTP ${resp.status}`);
|
|
}
|
|
const data = await resp.json();
|
|
jobId = data.job_id;
|
|
|
|
stepsList.innerHTML = "";
|
|
for (const step of data.steps) addStep(step, "done");
|
|
|
|
const rResp = await fetch(`/api/rooms/${jobId}`);
|
|
rooms = await rResp.json();
|
|
renderResults();
|
|
} catch (err) {
|
|
stepsList.innerHTML = "";
|
|
addStep(`Chyba: ${err.message}`, "error");
|
|
setTimeout(() => { show(sUpload); hide(sProcessing); }, 4000);
|
|
}
|
|
}
|
|
|
|
function addStep(label, state) {
|
|
const el = document.createElement("div");
|
|
el.className = `step-item ${state}`;
|
|
el.innerHTML = `<span class="step-dot"></span>${esc(label)}`;
|
|
stepsList.appendChild(el);
|
|
}
|
|
|
|
// ── Render table ──────────────────────────────────
|
|
function renderResults() {
|
|
hide(sProcessing); show(sResults);
|
|
|
|
const total = rooms.length;
|
|
const llmCount = rooms.filter(r => r.source === "llm").length;
|
|
resultsMeta.textContent = `${total} místností nalezeno${llmCount ? ` (${llmCount} z AI)` : ""}`;
|
|
|
|
roomsTbody.innerHTML = "";
|
|
const sorted = [...rooms].sort((a, b) => String(a.room).localeCompare(String(b.room)));
|
|
|
|
sorted.forEach((room, i) => {
|
|
const tr = document.createElement("tr");
|
|
const pct = Math.round((room.confidence ?? 1) * 100);
|
|
const badgeCls = room.source === "llm" ? "badge-llm" : "badge-rule";
|
|
const badgeTxt = room.source === "llm" ? "AI" : "Pravidla";
|
|
|
|
tr.innerHTML = `
|
|
<td contenteditable="true" data-field="room" data-i="${i}">${esc(room.room)}</td>
|
|
<td contenteditable="true" data-field="description" data-i="${i}">${esc(room.description)}</td>
|
|
<td><span class="badge ${badgeCls}">${badgeTxt}</span></td>
|
|
<td>${pct} %</td>
|
|
`;
|
|
|
|
tr.querySelectorAll("[contenteditable]").forEach(cell => {
|
|
cell.addEventListener("blur", () => {
|
|
const idx = parseInt(cell.dataset.i, 10);
|
|
const field = cell.dataset.field;
|
|
rooms[idx][field] = cell.textContent.trim();
|
|
});
|
|
cell.addEventListener("keydown", e => {
|
|
if (e.key === "Enter") { e.preventDefault(); cell.blur(); }
|
|
});
|
|
});
|
|
|
|
roomsTbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
// ── Export ────────────────────────────────────────
|
|
exportBtn.addEventListener("click", async () => {
|
|
exportBtn.disabled = true;
|
|
exportBtn.textContent = "Ukládám…";
|
|
try {
|
|
await fetch(`/api/rooms/${jobId}`, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(rooms),
|
|
});
|
|
window.location.href = `/api/export/${jobId}`;
|
|
} finally {
|
|
setTimeout(() => {
|
|
exportBtn.disabled = false;
|
|
exportBtn.innerHTML = `<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v12m0 0-3.75-3.75M12 16.5l3.75-3.75M4.5 19.5h15"/>
|
|
</svg>Exportovat Excel`;
|
|
}, 1500);
|
|
}
|
|
});
|
|
|
|
// ── Reset ─────────────────────────────────────────
|
|
resetBtn.addEventListener("click", () => {
|
|
jobId = null; rooms = [];
|
|
fileInput.value = "";
|
|
stepsList.innerHTML = ""; roomsTbody.innerHTML = "";
|
|
show(sUpload); hide(sResults); hide(sProcessing);
|
|
});
|
|
|
|
// ── Helpers ───────────────────────────────────────
|
|
function show(el) { el.classList.remove("hidden"); }
|
|
function hide(el) { el.classList.add("hidden"); }
|
|
function esc(s) {
|
|
return String(s ?? "")
|
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
|
|
// ── Init ──────────────────────────────────────────
|
|
loadDefaults();
|