// 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, '''); }