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
121 lines
3.9 KiB
JavaScript
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);
|
|
});
|
|
})();
|