/*
app.js — GPT-2 Playground
==========================
Handles all client-side behaviour:
1. Slider sync — live-update value badges next to each range input
2. Char counter — updates as the user types in the prompt textarea
3. Generation flow — POSTs to /generate, manages loading state on the button,
renders prompt + generated text in the output pane
4. Copy button — copies the full output text and shows brief confirmation
5. Error display — surfaces API or network errors in the error banner
No build step or framework required — plain ES2020, runs in any modern browser.
*/
const $ = id => document.getElementById(id);
// ── Slider → value badge sync ──────────────────────────────────────────────
const sliders = [
{ input: $("maxTokens"), display: $("maxTokensVal"), decimals: 0 },
{ input: $("temperature"), display: $("temperatureVal"), decimals: 2 },
{ input: $("topP"), display: $("topPVal"), decimals: 2 },
{ input: $("repPenalty"), display: $("repPenaltyVal"), decimals: 2 },
];
function syncSlider({ input, display, decimals }) {
display.textContent = parseFloat(input.value).toFixed(decimals);
}
sliders.forEach(s => {
syncSlider(s);
s.input.addEventListener("input", () => syncSlider(s));
});
// ── Character counter ──────────────────────────────────────────────────────
const promptInput = $("promptInput");
const charCount = $("charCount");
promptInput.addEventListener("input", () => {
charCount.textContent = `${promptInput.value.length} chars`;
});
// ── Generation ─────────────────────────────────────────────────────────────
const generateBtn = $("generateBtn");
const outputPrompt = $("outputPrompt");
const outputGenerated = $("outputGenerated");
const errorBanner = $("errorBanner");
const copyBtn = $("copyBtn");
function setLoading(isLoading) {
generateBtn.disabled = isLoading;
generateBtn.classList.toggle("is-loading", isLoading);
// Apply the wider gradient background only during load so shimmer looks right
if (isLoading) {
generateBtn.style.backgroundImage =
"linear-gradient(90deg,#7c3aed,#6d28d9,#2563eb,#4f46e5,#7c3aed)";
generateBtn.style.backgroundSize = "300% 100%";
generateBtn.querySelector(".btn-label").textContent = "Generating…";
} else {
generateBtn.style.backgroundImage = "";
generateBtn.style.backgroundSize = "";
generateBtn.querySelector(".btn-label").textContent = "Generate";
}
}
function showError(message) {
errorBanner.textContent = message;
errorBanner.hidden = false;
}
function clearError() {
errorBanner.hidden = true;
errorBanner.textContent = "";
}
function renderOutput(prompt, generatedText) {
outputPrompt.textContent = prompt;
outputGenerated.textContent = generatedText;
outputGenerated.innerHTML = ""; // clear placeholder span
outputGenerated.textContent = generatedText;
copyBtn.disabled = false;
}
generateBtn.addEventListener("click", async () => {
const prompt = promptInput.value.trim();
if (!prompt) {
showError("Please enter a prompt before generating.");
return;
}
clearError();
setLoading(true);
copyBtn.disabled = true;
outputPrompt.textContent = "";
outputGenerated.innerHTML = 'Generating…';
const payload = {
prompt,
max_new_tokens: parseInt($("maxTokens").value, 10),
temperature: parseFloat($("temperature").value),
top_p: parseFloat($("topP").value),
repetition_penalty: parseFloat($("repPenalty").value),
};
try {
const response = await fetch("/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `Server error ${response.status}`);
}
const data = await response.json();
renderOutput(data.prompt, data.generated_text);
} catch (err) {
showError(`Generation failed: ${err.message}`);
outputGenerated.innerHTML = 'Generated text will appear here…';
} finally {
setLoading(false);
}
});
// ── Copy button ─────────────────────────────────────────────────────────────
copyBtn.addEventListener("click", () => {
const fullText = (outputPrompt.textContent + outputGenerated.textContent).trim();
if (!fullText) return;
navigator.clipboard.writeText(fullText).then(() => {
copyBtn.textContent = "✓ Copied!";
copyBtn.classList.add("copied");
setTimeout(() => {
copyBtn.innerHTML = `
Copy`;
copyBtn.classList.remove("copied");
}, 2000);
});
});