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:
120
email-drafter/static/app.js
Normal file
120
email-drafter/static/app.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// Email drafter frontend
|
||||
(() => {
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const sections = {
|
||||
compose: $("s-compose"),
|
||||
processing: $("s-processing"),
|
||||
result: $("s-result"),
|
||||
};
|
||||
const show = (name) => {
|
||||
for (const [k, el] of Object.entries(sections)) el.classList.toggle("hidden", k !== name);
|
||||
};
|
||||
|
||||
// ── Persist signature & last-used tone/language in localStorage ──
|
||||
const PERSIST_KEYS = ["signature", "tone", "language"];
|
||||
for (const k of PERSIST_KEYS) {
|
||||
const v = localStorage.getItem("email_" + k);
|
||||
if (v !== null) $(k).value = v;
|
||||
}
|
||||
for (const k of PERSIST_KEYS) {
|
||||
$(k).addEventListener("change", () =>
|
||||
localStorage.setItem("email_" + k, $(k).value));
|
||||
$(k).addEventListener("input", () =>
|
||||
localStorage.setItem("email_" + k, $(k).value));
|
||||
}
|
||||
|
||||
// ── Reply toggle ──
|
||||
$("reply-toggle").addEventListener("click", () => {
|
||||
const t = $("reply-toggle");
|
||||
const p = $("reply-panel");
|
||||
const expanded = t.getAttribute("aria-expanded") === "true";
|
||||
t.setAttribute("aria-expanded", String(!expanded));
|
||||
p.classList.toggle("hidden", expanded);
|
||||
});
|
||||
|
||||
// ── Hint + button state ──
|
||||
function updateHint() {
|
||||
const notes = $("notes").value.trim();
|
||||
const btn = $("generate-btn");
|
||||
const hint = $("generate-hint");
|
||||
if (notes.length < 5) {
|
||||
btn.disabled = true;
|
||||
hint.textContent = "Zadejte alespoň krátké poznámky.";
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
hint.textContent = `${notes.length} znaků zadáno. Klikněte pro vygenerování.`;
|
||||
}
|
||||
}
|
||||
$("notes").addEventListener("input", updateHint);
|
||||
updateHint();
|
||||
|
||||
// ── Generate ──
|
||||
async function generate() {
|
||||
const payload = {
|
||||
notes: $("notes").value.trim(),
|
||||
recipient: $("recipient").value.trim(),
|
||||
signature: $("signature").value.trim(),
|
||||
tone: $("tone").value,
|
||||
language: $("language").value,
|
||||
reply_to: $("reply-to").value.trim(),
|
||||
};
|
||||
if (payload.notes.length < 5) {
|
||||
alert("Zadejte alespoň krátké poznámky.");
|
||||
return;
|
||||
}
|
||||
show("processing");
|
||||
try {
|
||||
const r = await fetch("/api/generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!r.ok) {
|
||||
const errBody = await r.json().catch(() => ({ detail: r.statusText }));
|
||||
throw new Error(errBody.detail || r.statusText);
|
||||
}
|
||||
const data = await r.json();
|
||||
$("out-subject").value = data.subject || "";
|
||||
$("out-body").value = data.body || "";
|
||||
// Auto-resize body textarea to fit content
|
||||
const ta = $("out-body");
|
||||
ta.style.height = "auto";
|
||||
ta.style.height = Math.max(280, ta.scrollHeight + 4) + "px";
|
||||
show("result");
|
||||
} catch (err) {
|
||||
alert("Chyba: " + err.message);
|
||||
show("compose");
|
||||
}
|
||||
}
|
||||
|
||||
$("generate-btn").addEventListener("click", generate);
|
||||
$("regenerate-btn").addEventListener("click", generate);
|
||||
$("back-btn").addEventListener("click", () => show("compose"));
|
||||
|
||||
// ── Copy buttons ──
|
||||
function copyText(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "Zkopírováno";
|
||||
btn.classList.add("copied");
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
btn.classList.remove("copied");
|
||||
}, 1500);
|
||||
}).catch(() => alert("Kopírování selhalo. Vyberte text a stiskněte Ctrl+C."));
|
||||
}
|
||||
|
||||
document.querySelectorAll(".btn-copy").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const target = $(btn.dataset.target);
|
||||
copyText(target.value, btn);
|
||||
});
|
||||
});
|
||||
|
||||
$("copy-all-btn").addEventListener("click", (e) => {
|
||||
const subject = $("out-subject").value;
|
||||
const body = $("out-body").value;
|
||||
const combined = `Předmět: ${subject}\n\n${body}`;
|
||||
copyText(combined, e.target);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user