NMT / scripts /hf_space_ui.html
marconolimits's picture
deploy: clean orphan branch for HF Spaces - CPU threading optimisation
c7b4419
Raw
History Blame Contribute Delete
10.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>NMT MenKan — Bidirectional Translator</title>
<style>
:root {
--bg: #0f1419;
--surface: #1a2332;
--border: #2d3a4d;
--text: #e7ecf3;
--muted: #8b9cb3;
--accent: #3b82f6;
--accent-hover: #2563eb;
--success: #34d399;
--err: #f87171;
--radius: 12px;
--font: ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(1200px 600px at 20% -10%, #1e3a5f 0%, transparent 55%),
radial-gradient(800px 400px at 90% 100%, #1a2744 0%, transparent 50%),
var(--bg);
color: var(--text);
font-family: var(--font);
line-height: 1.5;
}
.wrap {
max-width: 52rem;
margin: 0 auto;
padding: 2rem 1.25rem 3rem;
}
header {
margin-bottom: 1.75rem;
}
header h1 {
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.02em;
margin: 0 0 0.35rem;
}
header p {
margin: 0;
color: var(--muted);
font-size: 0.95rem;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.25rem 1.35rem;
margin-bottom: 1rem;
}
label {
display: block;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 0.45rem;
}
.lang-selector-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
select {
background: #0d1218;
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.4rem 0.6rem;
font-family: inherit;
font-size: 0.9rem;
cursor: pointer;
}
.swap-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
cursor: pointer;
padding: 0.4rem;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
}
.swap-btn:hover {
color: var(--text);
border-color: var(--muted);
}
textarea {
width: 100%;
min-height: 7rem;
padding: 0.75rem 0.85rem;
border-radius: 8px;
border: 1px solid var(--border);
background: #0d1218;
color: var(--text);
font-size: 1rem;
resize: vertical;
font-family: inherit;
}
textarea:focus {
outline: 2px solid rgba(59, 130, 246, 0.45);
border-color: var(--accent);
}
.row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
margin-top: 1rem;
}
button.primary {
appearance: none;
border: none;
border-radius: 8px;
padding: 0.65rem 1.25rem;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
background: var(--accent);
color: white;
font-family: inherit;
}
button.primary:hover { background: var(--accent-hover); }
button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
button.secondary {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
padding: 0.65rem 1.25rem;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
font-family: inherit;
}
button.secondary:hover {
background: rgba(255,255,255,0.06);
}
.out {
min-height: 5.5rem;
white-space: pre-wrap;
word-break: break-word;
font-size: 1.05rem;
line-height: 1.55;
}
.meta {
font-size: 0.8rem;
color: var(--muted);
margin-top: 0.75rem;
}
.meta span { margin-right: 1rem; }
.error {
color: var(--err);
font-size: 0.9rem;
margin-top: 0.5rem;
}
details {
margin-top: 1.5rem;
color: var(--muted);
font-size: 0.85rem;
}
details summary { cursor: pointer; user-select: none; }
details code {
font-size: 0.8rem;
background: #0d1218;
padding: 0.15rem 0.35rem;
border-radius: 4px;
}
#apiKeyRow.hidden { display: none; }
#apiKey {
width: 100%;
padding: 0.55rem 0.65rem;
border-radius: 8px;
border: 1px solid var(--border);
background: #0d1218;
color: var(--text);
font-family: ui-monospace, monospace;
font-size: 0.85rem;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>NMT MenKan</h1>
<p>Bidirectional NMT · CTranslate2 · Optimized for Casual Speech</p>
</header>
<div class="card" id="apiKeyRow">
<label for="apiKey">API key</label>
<input type="password" id="apiKey" name="apiKey" autocomplete="off" placeholder="Paste X-API-Key if your Space requires it" />
</div>
<div class="card">
<div class="lang-selector-row">
<select id="sourceLang">
<option value="eng_Latn" selected>English</option>
<option value="ita_Latn">Italian</option>
</select>
<button type="button" class="swap-btn" id="swapLangs" title="Swap languages">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M7 16V4M7 4L3 8M7 4L11 8M17 8v12M17 20l4-4M17 20l-4-4"/></svg>
</button>
<select id="targetLang">
<option value="ita_Latn" selected>Italian</option>
<option value="eng_Latn">English</option>
</select>
</div>
<label for="src" id="srcLabel">English</label>
<textarea id="src" placeholder="Type or paste text here…"></textarea>
<div class="row">
<button type="button" class="primary" id="go">Translate</button>
<button type="button" class="secondary" id="clear">Clear</button>
<span class="meta" id="hint"></span>
</div>
</div>
<div class="card">
<label id="tgtLabel">Italian</label>
<div class="out" id="out">Translation appears here.</div>
<div class="meta" id="meta"></div>
<div class="error" id="err" hidden></div>
</div>
<details>
<summary>API reference</summary>
<p><code>POST /translate</code> with <code>{"text":"...", "source_lang":"eng_Latn", "target_lang":"ita_Latn"}</code></p>
</details>
</div>
<script>
(function () {
var REQUIRE = __INJECT_REQUIRE_API_KEY__;
var MAX = __INJECT_MAX_CHARS__;
var src = document.getElementById("src");
var out = document.getElementById("out");
var meta = document.getElementById("meta");
var err = document.getElementById("err");
var go = document.getElementById("go");
var clear = document.getElementById("clear");
var apiKeyRow = document.getElementById("apiKeyRow");
var apiKey = document.getElementById("apiKey");
var hint = document.getElementById("hint");
var sourceLang = document.getElementById("sourceLang");
var targetLang = document.getElementById("targetLang");
var swapLangs = document.getElementById("swapLangs");
var srcLabel = document.getElementById("srcLabel");
var tgtLabel = document.getElementById("tgtLabel");
hint.textContent = "Max ~" + MAX + " characters · Ctrl+Enter to translate";
if (!REQUIRE) {
apiKeyRow.classList.add("hidden");
}
function updateLabels() {
srcLabel.textContent = sourceLang.options[sourceLang.selectedIndex].text;
tgtLabel.textContent = targetLang.options[targetLang.selectedIndex].text;
}
swapLangs.addEventListener("click", function() {
var s = sourceLang.value;
sourceLang.value = targetLang.value;
targetLang.value = s;
updateLabels();
var tmp = src.value;
src.value = out.textContent === "Translation appears here." ? "" : out.textContent;
out.textContent = tmp || "Translation appears here.";
});
sourceLang.addEventListener("change", updateLabels);
targetLang.addEventListener("change", updateLabels);
function showError(msg) {
err.hidden = false;
err.textContent = msg;
}
function hideError() {
err.hidden = true;
err.textContent = "";
}
async function translate() {
hideError();
var text = (src.value || "").trim();
if (!text) {
showError("Enter some text first.");
return;
}
if (text.length > MAX) {
showError("Text is longer than the server limit (" + MAX + " characters).");
return;
}
go.disabled = true;
out.textContent = "Translating…";
meta.textContent = "";
var headers = { "Content-Type": "application/json", Accept: "application/json" };
var key = (apiKey.value || "").trim();
if (key) headers["X-API-Key"] = key;
try {
var res = await fetch("/translate", {
method: "POST",
headers: headers,
body: JSON.stringify({
text: text,
source_lang: sourceLang.value,
target_lang: targetLang.value
}),
});
var data = await res.json();
if (!res.ok) {
showError(data.detail || "Translation failed.");
out.textContent = "";
return;
}
out.textContent = data.translation || "";
meta.innerHTML = '<span>Latency: ' + (data.latency_ms || "—") + ' ms</span>' +
'<span>Model: ' + (data.model_variant || "—") + '</span>';
} catch (e) {
showError(e.message || "Request error.");
out.textContent = "";
} finally {
go.disabled = false;
}
}
go.addEventListener("click", translate);
clear.addEventListener("click", function () {
src.value = "";
out.textContent = "Translation appears here.";
meta.textContent = "";
hideError();
src.focus();
});
src.addEventListener("keydown", function (e) {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
translate();
}
});
})();
</script>
</body>
</html>