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
96 lines
3.4 KiB
JavaScript
96 lines
3.4 KiB
JavaScript
// Feature request form
|
||
(() => {
|
||
const $ = (id) => document.getElementById(id);
|
||
const sections = {
|
||
form: $("s-form"),
|
||
processing: $("s-processing"),
|
||
thanks: $("s-thanks"),
|
||
};
|
||
const show = (name) => {
|
||
for (const [k, el] of Object.entries(sections)) el.classList.toggle("hidden", k !== name);
|
||
};
|
||
|
||
// Persist name + email for next submission
|
||
for (const k of ["name", "email"]) {
|
||
const v = localStorage.getItem("req_" + k);
|
||
if (v) $(k).value = v;
|
||
$(k).addEventListener("input", () => localStorage.setItem("req_" + k, $(k).value));
|
||
}
|
||
|
||
// ── File handling ──
|
||
const fileInput = $("file-input");
|
||
const dropZone = $("file-drop");
|
||
const dropText = $("file-drop-text");
|
||
let attachedFile = null;
|
||
|
||
function setFile(f) {
|
||
if (!f) return;
|
||
attachedFile = f;
|
||
const size = f.size > 1024 * 1024
|
||
? `${(f.size / 1024 / 1024).toFixed(1)} MB`
|
||
: `${(f.size / 1024).toFixed(0)} kB`;
|
||
dropZone.classList.add("has-file");
|
||
dropText.innerHTML = `
|
||
<span class="file-pill">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||
<polyline points="14 2 14 8 20 8"/>
|
||
</svg>
|
||
${escapeHtml(f.name)} <span style="color:var(--text-tertiary)">(${size})</span>
|
||
<button type="button" class="file-pill-clear" id="file-clear">×</button>
|
||
</span>
|
||
`;
|
||
$("file-clear").addEventListener("click", clearFile);
|
||
}
|
||
function clearFile() {
|
||
attachedFile = null;
|
||
fileInput.value = "";
|
||
dropZone.classList.remove("has-file");
|
||
dropText.innerHTML = `Přetáhněte soubor sem nebo
|
||
<button type="button" class="btn-link" id="file-pick-btn">vyberte z disku</button>`;
|
||
$("file-pick-btn").addEventListener("click", () => fileInput.click());
|
||
}
|
||
|
||
$("file-pick-btn").addEventListener("click", () => fileInput.click());
|
||
fileInput.addEventListener("change", (e) => 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.preventDefault(); setFile(e.dataTransfer.files[0]); });
|
||
|
||
// ── Submit ──
|
||
$("request-form").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
show("processing");
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append("title", $("title").value);
|
||
fd.append("description", $("description").value);
|
||
fd.append("name", $("name").value);
|
||
fd.append("email", $("email").value);
|
||
if (attachedFile) fd.append("file", attachedFile);
|
||
|
||
const r = await fetch("/api/submit", { method: "POST", body: fd });
|
||
if (!r.ok) throw new Error((await r.json()).detail || r.statusText);
|
||
show("thanks");
|
||
} catch (err) {
|
||
alert("Chyba: " + err.message);
|
||
show("form");
|
||
}
|
||
});
|
||
|
||
$("another-btn").addEventListener("click", () => {
|
||
$("title").value = "";
|
||
$("description").value = "";
|
||
clearFile();
|
||
show("form");
|
||
});
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, (c) =>
|
||
({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
||
}
|
||
})();
|