Spaces:
Running on Zero
Running on Zero
Download index.html from khtsly/Luau-Coder-Preview: direct link, hf CLI and curl.
- Browser
- Download file 25.3 kB
-
https://huggingface.co/spaces/khtsly/Luau-Coder-Preview/resolve/main/index.html
- Command line
-
hf download hf://spaces/khtsly/Luau-Coder-Preview/index.html
-
curl -L -o index.html https://huggingface.co/spaces/khtsly/Luau-Coder-Preview/resolve/main/index.html
25.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>Luau Coder Preview | khtsly</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/18.0.12/lib/marked.umd.min.js"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.18.7/katex.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.18.7/katex.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.18.7/contrib/auto-render.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #ffffff; | |
| --blue: #00A2FF; | |
| --cyan: #0077C8; | |
| --text: #1e293b; | |
| --text-muted: #64748b; | |
| --glass: rgba(0, 0, 0, 0.02); | |
| --glass-border: #e2e8f0; | |
| --accent: #00A2FF; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| height: 100vh; | |
| margin: 0; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| .chat-scroll-area { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding-bottom: 140px; | |
| -webkit-overflow-scrolling: touch; | |
| scroll-behavior: smooth; | |
| } | |
| .chat-scroll-area::-webkit-scrollbar { width: 4px; } | |
| .chat-scroll-area::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; } | |
| .message-bubble { | |
| max-width: 85%; | |
| padding: 15px; | |
| animation: fadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(12px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .user-message { | |
| background: linear-gradient(135deg, var(--blue), var(--cyan)); | |
| color: #ffffff; | |
| box-shadow: 0 4px 14px rgba(0, 162, 255, 0.28); | |
| border-radius: 20px 20px 4px 20px; | |
| } | |
| .bot-message { | |
| background: #f8fafc; | |
| border: 1px solid var(--glass-border); | |
| border-radius: 20px 20px 20px 4px; | |
| } | |
| .typing-dot { | |
| width: 4px; height: 4px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| animation: bounce 1.4s infinite ease-in-out; | |
| } | |
| @keyframes bounce { | |
| 0%, 80%, 100% { transform: scale(0.3); opacity: 0.4; } | |
| 40% { transform: scale(1); opacity: 1; } | |
| } | |
| .input-pill { | |
| background: #ffffff; | |
| border: 1px solid var(--glass-border); | |
| transition: all 0.3s ease; | |
| } | |
| .input-pill:focus-within { | |
| border-color: rgba(0, 162, 255, 0.45); | |
| box-shadow: 0 0 0 3px rgba(0, 162, 255, 0.12); | |
| } | |
| .logo-glow { | |
| filter: drop-shadow(0 0 8px rgba(0, 162, 255, 0.35)); | |
| } | |
| .send-btn { | |
| background: linear-gradient(135deg, #334155, #0f172a); | |
| box-shadow: 0 4px 12px rgba(15, 23, 42, 0.35); | |
| transition: all 0.3s ease; | |
| } | |
| .send-btn:hover:not(:disabled) { transform: scale(1.05); filter: brightness(1.25); } | |
| .settings-panel { | |
| background: rgba(255, 255, 255, 0.98); | |
| backdrop-filter: blur(20px); | |
| border-left: 1px solid var(--glass-border); | |
| transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); | |
| } | |
| .control-slider { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 4px; | |
| background: #e2e8f0; | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| .control-slider::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 12px; height: 12px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .bot-message pre { | |
| background: #f1f5f9; | |
| border: 1px solid #e2e8f0; | |
| border-radius: 10px; | |
| padding: 12px; | |
| overflow-x: auto; | |
| } | |
| .bot-message code { color: #0284C7; } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="h-16 flex items-center justify-between px-6 md:px-10 shrink-0 z-50 border-b border-slate-200 bg-white"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 logo-glow rounded-lg flex items-center justify-center overflow-hidden" style="background: #0f172a;"> | |
| <img src="https://upload.wikimedia.org/wikipedia/commons/8/8f/Luau_Logo_%28Programming_Language%29.svg" | |
| alt="Luau" class="w-9 h-9"> | |
| </div> | |
| <div> | |
| <h1 class="text-lg font-bold text-slate-800">Luau Coder</h1> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-[0.2em] font-semibold">By khtsly · Preview</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-4"> | |
| <button id="toggle-settings" class="p-2 rounded-xl hover:bg-slate-100 text-slate-500 hover:text-slate-800 transition-all"> | |
| <i data-lucide="sliders-horizontal" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <div id="settings-panel" class="fixed top-0 right-0 h-full w-72 z-[100] translate-x-full settings-panel p-6 flex flex-col gap-6 shadow-[-12px_0_40px_rgba(0,0,0,0.08)] overflow-y-auto"> | |
| <div class="flex items-center justify-between"> | |
| <h2 class="text-base font-bold text-slate-800">Settings</h2> | |
| <button id="close-settings" class="text-slate-400 hover:text-slate-700"> | |
| <i data-lucide="x" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-5"> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Temperature</span> | |
| <span id="temp-val">0.6</span> | |
| </div> | |
| <input type="range" id="temp-slider" min="0" max="1.5" step="0.05" value="0.6" class="control-slider"> | |
| <p class="text-[11px] text-slate-400">0.0 = Greedy</p> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Max Tokens</span> | |
| <span id="tokens-val">32</span> | |
| </div> | |
| <input type="range" id="tokens-slider" min="16" max="256" step="1" value="32" class="control-slider"> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Top-P</span> | |
| <span id="topp-val">0.95</span> | |
| </div> | |
| <input type="range" id="topp-slider" min="0" max="1" step="0.05" value="0.95" class="control-slider"> | |
| <p class="text-[11px] text-slate-400">1.0 = Off</p> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Top-K</span> | |
| <span id="topk-val">40</span> | |
| </div> | |
| <input type="range" id="topk-slider" min="0" max="100" step="1" value="40" class="control-slider"> | |
| <p class="text-[11px] text-slate-400">0 = Off</p> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Min-P</span> | |
| <span id="minp-val">0.05</span> | |
| </div> | |
| <input type="range" id="minp-slider" min="0" max="1" step="0.01" value="0.05" class="control-slider"> | |
| <p class="text-[11px] text-slate-400">0.0 = Off</p> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Repetition Penalty</span> | |
| <span id="rep-val">1.05</span> | |
| </div> | |
| <input type="range" id="rep-slider" min="1" max="2" step="0.05" value="1.05" class="control-slider"> | |
| <p class="text-[11px] text-slate-400">1.0 = Off</p> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="flex justify-between text-xs font-bold text-slate-400 uppercase tracking-widest"> | |
| <span>Presence Penalty</span> | |
| <span id="pres-val">0.0</span> | |
| </div> | |
| <input type="range" id="pres-slider" min="0" max="2" step="0.1" value="0.0" class="control-slider"> | |
| <p class="text-[11px] text-slate-400">0.0 = Off</p> | |
| </div> | |
| <button onclick="clearHistory()" class="w-full py-3 rounded-xl bg-red-50 border border-red-200 text-red-600 text-sm font-bold hover:bg-red-100 transition-all flex items-center justify-center gap-2 mt-4"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| Clear History | |
| </button> | |
| </div> | |
| </div> | |
| <main id="chat-messages" class="chat-scroll-area px-4 flex-1"> | |
| <div class="max-w-3xl mx-auto space-y-6 pt-6 pb-32" id="chat-container"> | |
| <div class="flex gap-3 items-start"> | |
| <div class="bot-message message-bubble shadow-sm"> | |
| <p class="text-slate-700 leading-relaxed text-[15px]"> | |
| Hello! | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <div class="fixed bottom-0 left-0 right-0 p-4 md:p-8 pointer-events-none z-50"> | |
| <div class="max-w-3xl mx-auto pointer-events-auto"> | |
| <div class="input-pill rounded-[1.5rem] p-2 shadow-lg"> | |
| <textarea id="user-input" placeholder="Enter a prompt or code to continue..." rows="1" | |
| class="w-full bg-transparent border-none focus:ring-0 text-slate-800 placeholder-slate-400 py-3 px-3 resize-none max-h-40 leading-relaxed font-medium outline-none"></textarea> | |
| <div class="flex items-center justify-between pl-2 pr-1 pb-1"> | |
| <div class="relative" id="model-picker"> | |
| <button id="model-select-btn" title="Model variant" class="text-xs font-semibold border border-slate-200 rounded-full pl-3 pr-8 py-1.5 bg-slate-50 text-slate-600 hover:bg-slate-100 hover:text-slate-800 transition-all focus:outline-none focus:border-sky-300 focus:ring-2 focus:ring-sky-100 max-w-[11rem] truncate"> | |
| Nalu 1.0 (Base) | |
| </button> | |
| <i data-lucide="chevron-down" class="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i> | |
| <div id="model-menu" class="hidden absolute bottom-full left-0 mb-2 min-w-[10rem] bg-white border border-slate-200 rounded-2xl shadow-xl p-1.5 z-50"></div> | |
| </div> | |
| <button id="send-btn" class="send-btn w-9 h-9 text-white rounded-full flex items-center justify-center shrink-0"> | |
| <i data-lucide="arrow-up" class="w-5 h-5" id="send-icon"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; | |
| lucide.createIcons(); | |
| const chatContainer = document.getElementById('chat-container'); | |
| const chatScrollArea = document.getElementById('chat-messages'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const settingsPanel = document.getElementById('settings-panel'); | |
| const toggleSettings = document.getElementById('toggle-settings'); | |
| const closeSettings = document.getElementById('close-settings'); | |
| const tempSlider = document.getElementById('temp-slider'); | |
| const tokensSlider = document.getElementById('tokens-slider'); | |
| const modelSelectBtn = document.getElementById('model-select-btn'); | |
| const modelMenu = document.getElementById('model-menu'); | |
| const modelPicker = document.getElementById('model-picker'); | |
| const toppSlider = document.getElementById('topp-slider'); | |
| const topkSlider = document.getElementById('topk-slider'); | |
| const minpSlider = document.getElementById('minp-slider'); | |
| const repSlider = document.getElementById('rep-slider'); | |
| const presSlider = document.getElementById('pres-slider'); | |
| let client = null; | |
| let chatHistory = []; | |
| let currentJob = null; | |
| let isSettingsOpen = false; | |
| let currentModel = "Nalu 1.0 (Base)"; | |
| let modelVariants = [ | |
| { id: "Nalu 1.0 (Base)", enabled: true }, | |
| { id: "Moana 1.0 (Base)", enabled: true }, | |
| { id: "Moana 1.5", enabled: false }, | |
| ]; | |
| let pendingModelLoad = false; | |
| async function init() { | |
| try { | |
| client = await Client.connect(window.location.origin, { events: ["data", "status"] }); | |
| } catch (err) { | |
| console.error("Gradio connection error", err); | |
| } | |
| } | |
| init(); | |
| loadModelOptions(); | |
| async function loadModelOptions() { | |
| let fallback = "0.5B-Base"; | |
| try { | |
| const res = await fetch(window.location.origin + "/models"); | |
| if (res.ok) { | |
| const data = await res.json(); | |
| if (Array.isArray(data.variants) && data.variants.length) { | |
| modelVariants = data.variants.map((v) => | |
| typeof v === "string" ? { id: v, enabled: true } : { id: v.id, enabled: v.enabled !== false } | |
| ); | |
| } | |
| if (data.default) fallback = data.default; | |
| } | |
| } catch (err) { | |
| console.error("Model list fetch error", err); | |
| } | |
| const enabledIds = modelVariants.filter((v) => v.enabled).map((v) => v.id); | |
| currentModel = enabledIds.includes(fallback) ? fallback : enabledIds[0]; | |
| renderModelMenu(); | |
| } | |
| function renderModelMenu() { | |
| modelMenu.innerHTML = ""; | |
| for (const v of modelVariants) { | |
| const item = document.createElement("button"); | |
| const isCurrent = v.id === currentModel; | |
| if (v.enabled) { | |
| item.className = `w-full flex items-center justify-between gap-3 px-3 py-2 rounded-xl text-xs font-semibold transition-all ${isCurrent ? "bg-sky-50 text-sky-700" : "text-slate-600 hover:bg-slate-100"}`; | |
| } else { | |
| item.className = "w-full flex items-center justify-between gap-3 px-3 py-2 rounded-xl text-xs font-semibold text-slate-400 opacity-70 cursor-not-allowed"; | |
| item.disabled = true; | |
| item.title = "Coming soon"; | |
| } | |
| const left = document.createElement("span"); | |
| left.className = "flex items-center gap-2 min-w-0"; | |
| const label = document.createElement("span"); | |
| label.className = "truncate"; | |
| label.textContent = v.id; | |
| left.appendChild(label); | |
| if (!v.enabled) { | |
| const badge = document.createElement("span"); | |
| badge.className = "text-[9px] font-bold tracking-widest px-1.5 py-0.5 rounded-md bg-amber-100 text-amber-700 border border-amber-200 shrink-0"; | |
| badge.textContent = "SOON"; | |
| left.appendChild(badge); | |
| } | |
| item.appendChild(left); | |
| if (isCurrent) { | |
| const check = document.createElement("i"); | |
| check.setAttribute("data-lucide", "check"); | |
| check.className = "w-3.5 h-3.5 shrink-0"; | |
| item.appendChild(check); | |
| } | |
| if (v.enabled) { | |
| item.onclick = (e) => { e.stopPropagation(); selectModel(v.id); }; | |
| } | |
| modelMenu.appendChild(item); | |
| } | |
| modelSelectBtn.textContent = currentModel; | |
| if (window.lucide) lucide.createIcons(); | |
| } | |
| function toggleModelMenu(open) { | |
| const willOpen = open === undefined ? modelMenu.classList.contains("hidden") : open; | |
| modelMenu.classList.toggle("hidden", !willOpen); | |
| } | |
| function selectModel(next) { | |
| toggleModelMenu(false); | |
| if (next === currentModel) return; | |
| if (chatHistory.length > 0) { | |
| if (!confirm(`Switch model to ${next}? Conversation history will be cleared.`)) return; | |
| } | |
| currentModel = next; | |
| pendingModelLoad = true; | |
| clearHistory(); | |
| renderModelMenu(); | |
| } | |
| function renderMath(el) { | |
| if (window.renderMathInElement) { | |
| renderMathInElement(el, { | |
| delimiters: [ | |
| {left: '$$', right: '$$', display: true}, | |
| {left: '$', right: '$', display: false}, | |
| ], | |
| throwOnError: false | |
| }); | |
| } | |
| } | |
| function appendMessage(role, text = '') { | |
| const div = document.createElement('div'); | |
| div.className = `flex gap-3 items-start ${role === 'user' ? 'flex-row-reverse' : ''}`; | |
| const bubbleClass = role === 'user' ? 'user-message' : 'bot-message'; | |
| div.innerHTML = ` | |
| <div class="${bubbleClass} message-bubble shadow-sm"> | |
| <div class="content-container leading-relaxed text-[15px]">${role === 'user' ? escapeHtml(text) : marked.parse(text)}</div> | |
| </div> | |
| `; | |
| chatContainer.appendChild(div); | |
| if (role === 'bot') renderMath(div.querySelector('.content-container')); | |
| chatScrollArea.scrollTo({ top: chatScrollArea.scrollHeight, behavior: 'smooth' }); | |
| return div; | |
| } | |
| function escapeHtml(s) { | |
| return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| } | |
| function updateBotMessage(div, fullText) { | |
| const contentContainer = div.querySelector('.content-container'); | |
| if (fullText) { | |
| contentContainer.innerHTML = marked.parse(fullText); | |
| } else { | |
| contentContainer.innerHTML = ''; | |
| } | |
| renderMath(contentContainer); | |
| chatScrollArea.scrollTo({ top: chatScrollArea.scrollHeight, behavior: 'smooth' }); | |
| return fullText; | |
| } | |
| async function sendMessage() { | |
| const text = userInput.value.trim(); | |
| if (!text) return; | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| appendMessage('user', text); | |
| sendBtn.disabled = true; | |
| const botDiv = appendMessage('bot', ''); | |
| const contentContainer = botDiv.querySelector('.content-container'); | |
| if (pendingModelLoad) { | |
| contentContainer.innerHTML = `<p class="text-slate-500">Loading model ${escapeHtml(currentModel)}… first load may take a while.</p>`; | |
| pendingModelLoad = false; | |
| } else { | |
| contentContainer.innerHTML = '<div class="flex gap-1.5 py-2"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>'; | |
| } | |
| let isStopped = false; | |
| sendBtn.onclick = () => { | |
| if (currentJob) { | |
| currentJob.cancel(); | |
| isStopped = true; | |
| resetSendBtn(); | |
| } | |
| }; | |
| try { | |
| currentJob = client.submit("/predict", { | |
| message: text, | |
| history: chatHistory, | |
| max_tokens: parseInt(tokensSlider.value), | |
| temperature: parseFloat(tempSlider.value), | |
| top_p: parseFloat(toppSlider.value), | |
| top_k: parseInt(topkSlider.value), | |
| min_p: parseFloat(minpSlider.value), | |
| repetition_penalty: parseFloat(repSlider.value), | |
| presence_penalty: parseFloat(presSlider.value), | |
| model_id: currentModel, | |
| }); | |
| let finalAnswer = ""; | |
| for await (const msg of currentJob) { | |
| if (isStopped) break; | |
| if (msg.type === "data" && msg.data) { | |
| finalAnswer = updateBotMessage(botDiv, msg.data[0]); | |
| } else if (msg.type === "status" && msg.stage === "complete") { | |
| break; | |
| } else if (msg.type === "status" && msg.stage === "error") { | |
| throw new Error(msg.message || "Generation failed"); | |
| } | |
| } | |
| if (!isStopped && finalAnswer) { | |
| chatHistory.push([text, finalAnswer]); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| if (!isStopped) { | |
| contentContainer.innerHTML = '<p class="text-red-500">Error: please try again.</p>'; | |
| } | |
| } finally { | |
| resetSendBtn(); | |
| currentJob = null; | |
| } | |
| } | |
| function resetSendBtn() { | |
| sendBtn.disabled = false; | |
| sendBtn.onclick = sendMessage; | |
| } | |
| window.clearHistory = function() { | |
| chatHistory = []; | |
| chatContainer.innerHTML = ` | |
| <div class="flex gap-3 items-start"> | |
| <div class="bot-message message-bubble shadow-sm"> | |
| <p class="text-slate-700 leading-relaxed text-[15px]">History cleared. How can I help you?</p> | |
| </div> | |
| </div> | |
| `; | |
| toggleSettingsSidebar(false); | |
| }; | |
| function toggleSettingsSidebar(open) { | |
| isSettingsOpen = open; | |
| settingsPanel.classList.toggle('translate-x-full', !open); | |
| settingsPanel.classList.toggle('translate-x-0', open); | |
| } | |
| toggleSettings.onclick = (e) => { e.stopPropagation(); toggleSettingsSidebar(true); }; | |
| closeSettings.onclick = () => toggleSettingsSidebar(false); | |
| document.addEventListener('click', (e) => { | |
| if (isSettingsOpen && !settingsPanel.contains(e.target) && !toggleSettings.contains(e.target)) { | |
| toggleSettingsSidebar(false); | |
| } | |
| if (!modelMenu.classList.contains('hidden') && !modelPicker.contains(e.target)) { | |
| toggleModelMenu(false); | |
| } | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') toggleModelMenu(false); | |
| }); | |
| modelSelectBtn.onclick = (e) => { e.stopPropagation(); toggleModelMenu(); }; | |
| tempSlider.oninput = () => document.getElementById('temp-val').textContent = tempSlider.value; | |
| tokensSlider.oninput = () => document.getElementById('tokens-val').textContent = tokensSlider.value; | |
| toppSlider.oninput = () => document.getElementById('topp-val').textContent = parseFloat(toppSlider.value).toFixed(2).replace(/0$/, ''); | |
| topkSlider.oninput = () => document.getElementById('topk-val').textContent = topkSlider.value; | |
| minpSlider.oninput = () => document.getElementById('minp-val').textContent = parseFloat(minpSlider.value).toFixed(2).replace(/0$/, ''); | |
| repSlider.oninput = () => document.getElementById('rep-val').textContent = parseFloat(repSlider.value).toFixed(2).replace(/0$/, ''); | |
| presSlider.oninput = () => document.getElementById('pres-val').textContent = parseFloat(presSlider.value).toFixed(1); | |
| sendBtn.onclick = sendMessage; | |
| userInput.onkeydown = (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }; | |
| userInput.oninput = () => { | |
| userInput.style.height = 'auto'; | |
| userInput.style.height = userInput.scrollHeight + 'px'; | |
| }; | |
| </script> | |
| </body> | |
| </html> | |