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:
95
feature-request/static/app.js
Normal file
95
feature-request/static/app.js
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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]));
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user