Files
AI_portal/email-drafter/static/app.js
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

121 lines
3.9 KiB
JavaScript

// 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);
});
})();