Luau-Coder-Preview / index.html
khtsly's picture
Update index.html
97d21a1 verified
Raw History Blame Contribute Delete
25.3 kB
<!DOCTYPE html>
<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 &middot; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
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>