Files
Ondřej Glaser 48cef99257 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
2026-05-13 15:25:04 +02:00

96 lines
3.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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) =>
({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
}
})();