Spaces:
Running
Running
| // 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 = '<div class="no-history">No past instructions yet.</div>'; | |
| 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 = ` | |
| <div class="history-text" title="${escapeHtml(item.instruction)}">${escapeHtml(item.instruction)}</div> | |
| <div class="history-meta"> | |
| <span>t=${item.temperature} • max=${item.max_new_tokens}</span> | |
| <button class="delete-history-item" data-index="${realIndex}" title="Delete Item"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="3 6 5 6 21 6"></polyline> | |
| <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> | |
| </svg> | |
| </button> | |
| </div> | |
| `; | |
| // 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 = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`; | |
| } else if (type === 'error') { | |
| icon = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`; | |
| } else { | |
| icon = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`; | |
| } | |
| toast.innerHTML = ` | |
| ${icon} | |
| <span class="toast-message">${message}</span> | |
| <button class="toast-close">×</button> | |
| `; | |
| // 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, '"') | |
| .replace(/'/g, '''); | |
| } | |