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:
148
translator/static/app.js
Normal file
148
translator/static/app.js
Normal file
@@ -0,0 +1,148 @@
|
||||
// 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user