Initial portal commit: landing + 9 AI-powered apps
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
This commit is contained in:
209
dwg-rooms/static/app.js
Normal file
209
dwg-rooms/static/app.js
Normal file
@@ -0,0 +1,209 @@
|
||||
"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();
|
||||
Reference in New Issue
Block a user