// State variables
let promptHistory = [];
const HISTORY_KEY = 'gemma_code_llm_history';
const THEME_KEY = 'gemma_code_llm_theme';
// DOM Elements
const statusBadge = document.getElementById('statusBadge');
const statusDot = statusBadge.querySelector('.status-dot');
const statusText = statusBadge.querySelector('.status-text');
const temperatureInput = document.getElementById('temperature');
const tempVal = document.getElementById('tempVal');
const topPInput = document.getElementById('top_p');
const topPVal = document.getElementById('topPVal');
const maxTokensInput = document.getElementById('max_new_tokens');
const maxTokensVal = document.getElementById('maxTokensVal');
const safetyInput = document.getElementById('safety');
const instructionInput = document.getElementById('instructionInput');
const contextInput = document.getElementById('contextInput');
const generateBtn = document.getElementById('generateBtn');
const btnText = generateBtn.querySelector('.btn-text');
const btnArrow = generateBtn.querySelector('.btn-arrow');
const btnSpinner = generateBtn.querySelector('.btn-spinner');
const codeOutput = document.getElementById('codeOutput');
const preOutput = codeOutput.parentElement;
const skeletonLoader = document.getElementById('skeletonLoader');
const copyBtn = document.getElementById('copyBtn');
const latencyTimer = document.getElementById('latencyTimer');
const historyList = document.getElementById('historyList');
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
const themeToggleBtn = document.getElementById('themeToggleBtn');
const toastContainer = document.getElementById('toastContainer');
// Initialize App
document.addEventListener('DOMContentLoaded', () => {
initSliders();
initTheme();
loadHistory();
checkHealth();
initTemplateTags();
// Periodically check server status (every 10 seconds)
setInterval(checkHealth, 10000);
});
// Slider values updating
function initSliders() {
temperatureInput.addEventListener('input', (e) => {
tempVal.textContent = parseFloat(e.target.value).toFixed(1);
});
topPInput.addEventListener('input', (e) => {
topPVal.textContent = parseFloat(e.target.value).toFixed(2);
});
maxTokensInput.addEventListener('input', (e) => {
maxTokensVal.textContent = parseInt(e.target.value);
});
}
// Light / Dark Theme setup
function initTheme() {
const savedTheme = localStorage.getItem(THEME_KEY);
if (savedTheme === 'light') {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
} else {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
}
themeToggleBtn.addEventListener('click', () => {
if (document.body.classList.contains('dark-theme')) {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
localStorage.setItem(THEME_KEY, 'light');
showToast('Switched to light theme', 'info');
} else {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
localStorage.setItem(THEME_KEY, 'dark');
showToast('Switched to dark theme', 'info');
}
});
}
// Check backend server health
async function checkHealth() {
statusDot.className = 'status-dot loading animate-pulse';
statusText.textContent = 'Checking status...';
try {
const response = await fetch('/health');
if (response.ok) {
const data = await response.json();
if (data.status === 'ok') {
statusDot.className = 'status-dot online';
statusText.textContent = 'Connected';
} else {
setOfflineStatus('Unhealthy');
}
} else {
setOfflineStatus('Offline');
}
} catch (err) {
setOfflineStatus('Offline');
}
}
function setOfflineStatus(reason) {
statusDot.className = 'status-dot error';
statusText.textContent = reason;
}
// Templates Handling
function initTemplateTags() {
const tagBtns = document.querySelectorAll('.tag-btn');
tagBtns.forEach(btn => {
btn.addEventListener('click', () => {
instructionInput.value = btn.getAttribute('data-inst');
contextInput.value = btn.getAttribute('data-ctx');
// Highlight inputs briefly
instructionInput.focus();
showToast('Loaded template prompt', 'info');
});
});
}
// History caching and UI rendering
function loadHistory() {
const cached = localStorage.getItem(HISTORY_KEY);
if (cached) {
try {
promptHistory = JSON.parse(cached);
} catch (e) {
promptHistory = [];
}
}
renderHistory();
}
function saveHistory() {
localStorage.setItem(HISTORY_KEY, JSON.stringify(promptHistory));
renderHistory();
}
function renderHistory() {
historyList.innerHTML = '';
if (promptHistory.length === 0) {
historyList.innerHTML = '
No past instructions yet.
';
return;
}
promptHistory.slice().reverse().forEach((item, index) => {
// True index in original array
const realIndex = promptHistory.length - 1 - index;
const historyItem = document.createElement('div');
historyItem.className = 'history-item';
historyItem.innerHTML = `
${escapeHtml(item.instruction)}
t=${item.temperature} • max=${item.max_new_tokens}
`;
// Populate inputs when clicking history item
historyItem.addEventListener('click', (e) => {
// Ignore click if it was on the delete button
if (e.target.closest('.delete-history-item')) return;
loadHistoryItem(item);
});
// Hook up single item delete
const delBtn = historyItem.querySelector('.delete-history-item');
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteHistoryItem(realIndex);
});
historyList.appendChild(historyItem);
});
}
function loadHistoryItem(item) {
instructionInput.value = item.instruction;
contextInput.value = item.context || '';
temperatureInput.value = item.temperature;
tempVal.textContent = parseFloat(item.temperature).toFixed(1);
topPInput.value = item.top_p;
topPVal.textContent = parseFloat(item.top_p).toFixed(2);
maxTokensInput.value = item.max_new_tokens;
maxTokensVal.textContent = parseInt(item.max_new_tokens);
safetyInput.checked = item.safety !== false;
// Render the output immediately
codeOutput.textContent = item.response;
// Auto detect python vs other languages in simple regex
detectLanguageAndHighlight(item.response);
// Display metadata
if (item.time_taken) {
latencyTimer.textContent = `${item.time_taken.toFixed(2)}s`;
latencyTimer.classList.remove('hidden');
} else {
latencyTimer.classList.add('hidden');
}
copyBtn.disabled = false;
showToast('Loaded prompt details from history', 'success');
}
function deleteHistoryItem(index) {
promptHistory.splice(index, 1);
saveHistory();
showToast('Removed item from history', 'info');
}
clearHistoryBtn.addEventListener('click', () => {
if (promptHistory.length === 0) return;
if (confirm('Are you sure you want to clear your entire playground history?')) {
promptHistory = [];
saveHistory();
showToast('Cleared all history', 'info');
}
});
// Prompt execution triggers
generateBtn.addEventListener('click', async () => {
const instruction = instructionInput.value.trim();
const context = contextInput.value.trim();
if (!instruction) {
showToast('Instruction is required', 'error');
instructionInput.focus();
return;
}
// Toggle Loading UI
setGeneratingState(true);
const requestData = {
instruction: instruction,
input: context,
temperature: parseFloat(temperatureInput.value),
top_p: parseFloat(topPInput.value),
max_new_tokens: parseInt(maxTokensInput.value),
safety: safetyInput.checked
};
const startTime = performance.now();
try {
const response = await fetch('/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
if (response.ok) {
const data = await response.json();
const clientTime = (performance.now() - startTime) / 1000;
const serverTime = data.time_taken || clientTime;
// Render Result
codeOutput.textContent = data.completion;
detectLanguageAndHighlight(data.completion);
// UI actions
latencyTimer.textContent = `${serverTime.toFixed(2)}s`;
latencyTimer.classList.remove('hidden');
copyBtn.disabled = false;
// Add to history
addToHistory(instruction, context, requestData, data.completion, serverTime);
showToast('Generation complete', 'success');
} else {
const errData = await response.json();
const errMsg = errData.detail || 'Inference error occurred.';
showToast(`Server Error: ${errMsg}`, 'error');
setPlaceholderOutput(`/* Generation Error: \n${errMsg}\n*/`);
}
} catch (err) {
showToast('Network error: Is the backend server running?', 'error');
setPlaceholderOutput(`/* Network Connection Failed.\nPlease make sure the FastAPI server is running on port 8000.\n*/`);
} finally {
setGeneratingState(false);
}
});
function setGeneratingState(isGenerating) {
if (isGenerating) {
generateBtn.disabled = true;
btnText.textContent = 'Generating...';
btnArrow.classList.add('hidden');
btnSpinner.classList.remove('hidden');
preOutput.classList.add('hidden');
skeletonLoader.classList.remove('hidden');
copyBtn.disabled = true;
latencyTimer.classList.add('hidden');
} else {
generateBtn.disabled = false;
btnText.textContent = 'Generate Code';
btnArrow.classList.remove('hidden');
btnSpinner.classList.add('hidden');
skeletonLoader.classList.add('hidden');
preOutput.classList.remove('hidden');
}
}
function setPlaceholderOutput(text) {
codeOutput.textContent = text;
codeOutput.className = 'language-javascript';
Prism.highlightElement(codeOutput);
}
function detectLanguageAndHighlight(codeText) {
// Basic language detection from output signature
codeOutput.className = 'language-python'; // Default
if (codeText.includes('import ') || codeText.includes('def ')) {
codeOutput.className = 'language-python';
} else if (codeText.includes('const ') || codeText.includes('let ') || codeText.includes('function ')) {
codeOutput.className = 'language-javascript';
} else if (codeText.includes('echo ') || codeText.includes('sudo ') || codeText.startsWith('#!/bin/')) {
codeOutput.className = 'language-bash';
}
Prism.highlightElement(codeOutput);
}
function addToHistory(instruction, context, params, response, timeTaken) {
// Check if duplicate instruction exists, remove it to bubble it to top
promptHistory = promptHistory.filter(item => item.instruction !== instruction);
promptHistory.push({
instruction: instruction,
context: context,
temperature: params.temperature,
top_p: params.top_p,
max_new_tokens: params.max_new_tokens,
safety: params.safety,
response: response,
time_taken: timeTaken,
timestamp: Date.now()
});
// Cap history length at 25 items
if (promptHistory.length > 25) {
promptHistory.shift();
}
saveHistory();
}
// Copy Code Clipboard trigger
copyBtn.addEventListener('click', async () => {
const code = codeOutput.textContent;
if (!code) return;
try {
await navigator.clipboard.writeText(code);
copyBtn.classList.add('copied');
copyBtn.querySelector('span').textContent = 'Copied!';
showToast('Code copied to clipboard', 'success');
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.querySelector('span').textContent = 'Copy';
}, 2000);
} catch (err) {
showToast('Failed to copy code to clipboard', 'error');
}
});
// Toast notification trigger
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
// Select Icon based on Type
let icon = '';
if (type === 'success') {
icon = ``;
} else if (type === 'error') {
icon = ``;
} else {
icon = ``;
}
toast.innerHTML = `
${icon}
${message}
`;
// Close toast button click event
toast.querySelector('.toast-close').addEventListener('click', () => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
});
toastContainer.appendChild(toast);
// Auto-remove toast after 4 seconds
setTimeout(() => {
if (toast.parentElement) {
toast.style.opacity = '0';
toast.style.transform = 'translateY(10px)';
setTimeout(() => toast.remove(), 300);
}
}, 4000);
}
// Helper to escape HTML characters
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}