Files
AI_portal/translator/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

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