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
149 lines
4.8 KiB
JavaScript
149 lines
4.8 KiB
JavaScript
// Translator frontend
|
|
(() => {
|
|
const $ = (id) => document.getElementById(id);
|
|
|
|
// ── Populate language pickers ──
|
|
async function loadLanguages() {
|
|
try {
|
|
const r = await fetch("/api/languages");
|
|
const data = await r.json();
|
|
const srcSel = $("source-lang");
|
|
const tgtSel = $("target-lang");
|
|
for (const lang of data.languages) {
|
|
const o1 = document.createElement("option");
|
|
o1.value = lang.code;
|
|
o1.textContent = lang.cs;
|
|
srcSel.appendChild(o1);
|
|
if (lang.code === "auto") continue;
|
|
const o2 = document.createElement("option");
|
|
o2.value = lang.code;
|
|
o2.textContent = lang.cs;
|
|
tgtSel.appendChild(o2);
|
|
}
|
|
// Persisted prefs
|
|
srcSel.value = localStorage.getItem("trans_source") || "auto";
|
|
tgtSel.value = localStorage.getItem("trans_target") || "en";
|
|
$("tone").value = localStorage.getItem("trans_tone") || "formal";
|
|
updateSwapEnabled();
|
|
} catch (err) {
|
|
console.error("Failed to load languages", err);
|
|
}
|
|
}
|
|
|
|
function updateSwapEnabled() {
|
|
// Can only swap when source is a specific language (not auto)
|
|
$("swap-btn").disabled = $("source-lang").value === "auto";
|
|
}
|
|
|
|
// Persist on change
|
|
$("source-lang").addEventListener("change", () => {
|
|
localStorage.setItem("trans_source", $("source-lang").value);
|
|
updateSwapEnabled();
|
|
});
|
|
$("target-lang").addEventListener("change", () =>
|
|
localStorage.setItem("trans_target", $("target-lang").value));
|
|
$("tone").addEventListener("change", () =>
|
|
localStorage.setItem("trans_tone", $("tone").value));
|
|
|
|
$("swap-btn").addEventListener("click", () => {
|
|
const src = $("source-lang").value;
|
|
const tgt = $("target-lang").value;
|
|
if (src === "auto") return;
|
|
$("source-lang").value = tgt;
|
|
$("target-lang").value = src;
|
|
// Also swap text content if both panels have content
|
|
const sourceText = $("source-text").value;
|
|
const outputText = $("output-text").value;
|
|
if (outputText.trim()) {
|
|
$("source-text").value = outputText;
|
|
$("output-text").value = sourceText;
|
|
updateCount();
|
|
}
|
|
localStorage.setItem("trans_source", $("source-lang").value);
|
|
localStorage.setItem("trans_target", $("target-lang").value);
|
|
});
|
|
|
|
// ── Source text handling ──
|
|
const sourceTextEl = $("source-text");
|
|
const outputTextEl = $("output-text");
|
|
|
|
function updateCount() {
|
|
const n = sourceTextEl.value.length;
|
|
$("source-count").textContent = `${n.toLocaleString("cs-CZ")} znaků`;
|
|
updateRunButton();
|
|
}
|
|
function updateRunButton() {
|
|
const hasText = sourceTextEl.value.trim().length > 0;
|
|
$("translate-btn").disabled = !hasText;
|
|
$("translate-hint").textContent = hasText
|
|
? "Připraveno k překladu."
|
|
: "Vložte text výše.";
|
|
}
|
|
sourceTextEl.addEventListener("input", updateCount);
|
|
updateCount();
|
|
|
|
$("clear-btn").addEventListener("click", () => {
|
|
sourceTextEl.value = "";
|
|
outputTextEl.value = "";
|
|
$("copy-btn").disabled = true;
|
|
updateCount();
|
|
});
|
|
|
|
// ── Translate ──
|
|
$("translate-btn").addEventListener("click", async () => {
|
|
const text = sourceTextEl.value.trim();
|
|
if (!text) return;
|
|
$("translate-btn").disabled = true;
|
|
$("translate-loading").classList.remove("hidden");
|
|
outputTextEl.value = "";
|
|
try {
|
|
const r = await fetch("/api/translate", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({
|
|
text,
|
|
source_lang: $("source-lang").value,
|
|
target_lang: $("target-lang").value,
|
|
tone: $("tone").value,
|
|
}),
|
|
});
|
|
if (!r.ok) {
|
|
const errBody = await r.json().catch(() => ({detail: r.statusText}));
|
|
throw new Error(errBody.detail || r.statusText);
|
|
}
|
|
const data = await r.json();
|
|
outputTextEl.value = data.translated || "";
|
|
$("copy-btn").disabled = !data.translated;
|
|
} catch (err) {
|
|
alert("Chyba: " + err.message);
|
|
} finally {
|
|
$("translate-loading").classList.add("hidden");
|
|
$("translate-btn").disabled = false;
|
|
}
|
|
});
|
|
|
|
// ── Copy ──
|
|
$("copy-btn").addEventListener("click", (e) => {
|
|
const btn = e.target;
|
|
navigator.clipboard.writeText(outputTextEl.value).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."));
|
|
});
|
|
|
|
// Keyboard shortcut: Ctrl/Cmd+Enter in source area to translate
|
|
sourceTextEl.addEventListener("keydown", (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
|
e.preventDefault();
|
|
if (!$("translate-btn").disabled) $("translate-btn").click();
|
|
}
|
|
});
|
|
|
|
loadLanguages();
|
|
})();
|