Laguna-XS.2 / index.html
akhaliq's picture
akhaliq HF Staff
refactor: overhaul UI with a glassmorphism theme, custom scrollbars, and animated gradient background
d30fd20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Laguna XS.2 — Poolside AI</title>
<meta name="description" content="Chat with Poolside's Laguna-XS.2 reasoning model">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--purple-50:#faf5ff;--purple-100:#f3e8ff;--purple-200:#e9d5ff;
--purple-300:#d8b4fe;--purple-400:#c084fc;--purple-500:#a855f7;
--purple-600:#9333ea;--purple-700:#7e22ce;--purple-800:#6b21a8;
--purple-900:#581c87;--purple-950:#3b0764;
--bg:#0c0612;
--surface:rgba(20,12,30,0.65);
--surface-solid:#140c1e;
--surface2:rgba(30,18,45,0.55);
--glass:rgba(255,255,255,0.04);
--glass-border:rgba(255,255,255,0.08);
--glass-hover:rgba(255,255,255,0.07);
--text:#f0eaf8;--text-secondary:#a89bc2;--text-dim:#6b5f80;
--accent:#a855f7;--accent-light:#c084fc;--accent-glow:rgba(168,85,247,0.15);
--accent-hover:#b66dfa;
--think-bg:rgba(88,28,135,0.12);--think-border:rgba(168,85,247,0.15);
--success:#34d399;--error:#f87171;
--radius-lg:24px;--radius-xl:32px;--radius-full:9999px;
}
html,body{
height:100%;font-family:'Inter',system-ui,sans-serif;
background:var(--bg);color:var(--text);overflow:hidden;
}
/* Animated gradient background */
body::before{
content:'';position:fixed;inset:0;z-index:0;
background:
radial-gradient(ellipse 80% 60% at 10% 90%, rgba(147,51,234,0.18) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 85% 20%, rgba(168,85,247,0.12) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 50% 50%, rgba(192,132,252,0.06) 0%, transparent 50%);
animation:bgShift 20s ease-in-out infinite alternate;
}
@keyframes bgShift{
0%{filter:hue-rotate(0deg)}
50%{filter:hue-rotate(-8deg)}
100%{filter:hue-rotate(5deg)}
}
/* App container */
.app{
position:relative;z-index:1;display:flex;flex-direction:column;
height:100vh;max-width:900px;margin:0 auto;
}
/* Header */
.header{
padding:16px 28px;display:flex;align-items:center;justify-content:space-between;
flex-shrink:0;
border-bottom:1px solid var(--glass-border);
background:var(--surface);
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
}
.header-brand{display:flex;align-items:center;gap:12px}
.header-logo{
width:36px;height:36px;border-radius:10px;
object-fit:cover;
box-shadow:0 0 20px rgba(168,85,247,0.25);
}
.header-text h1{
font-size:1.05rem;font-weight:600;letter-spacing:-.01em;
color:var(--text);
}
.header-text span{
font-size:.7rem;color:var(--text-dim);font-weight:400;letter-spacing:.02em;
}
.header-actions{display:flex;gap:8px;align-items:center}
/* Pill buttons */
.pill-btn{
display:inline-flex;align-items:center;gap:6px;
padding:6px 14px;border-radius:var(--radius-full);
border:1px solid var(--glass-border);background:var(--glass);
color:var(--text-secondary);font-size:.75rem;font-weight:500;
cursor:pointer;transition:all .2s ease;font-family:inherit;
backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
}
.pill-btn:hover{background:var(--glass-hover);border-color:rgba(255,255,255,0.15);color:var(--text)}
.pill-btn.active{background:var(--accent-glow);border-color:rgba(168,85,247,0.3);color:var(--accent-light)}
.pill-btn svg{width:14px;height:14px;opacity:.7}
.pill-btn.danger:hover{border-color:rgba(248,113,113,0.4);color:var(--error);background:rgba(248,113,113,0.08)}
/* Controls bar */
.controls{
display:flex;gap:8px;padding:10px 28px;flex-shrink:0;align-items:center;flex-wrap:wrap;
border-bottom:1px solid var(--glass-border);
background:rgba(20,12,30,0.35);
backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
}
.control-chip{
display:flex;align-items:center;gap:6px;
padding:4px 12px;border-radius:var(--radius-full);
background:var(--glass);border:1px solid var(--glass-border);
font-size:.72rem;color:var(--text-secondary);
}
.control-chip label{cursor:pointer;white-space:nowrap}
.control-chip input[type=range]{
width:70px;height:4px;accent-color:var(--accent);
-webkit-appearance:none;appearance:none;background:rgba(255,255,255,0.1);
border-radius:2px;outline:none;
}
.control-chip input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;width:12px;height:12px;border-radius:50%;
background:var(--accent);cursor:pointer;border:2px solid var(--bg);
}
.control-chip span{color:var(--accent-light);font-weight:600;min-width:28px;text-align:right}
/* Toggle switch */
.switch{position:relative;width:32px;height:18px;cursor:pointer;display:inline-block;vertical-align:middle}
.switch input{opacity:0;width:0;height:0}
.switch .track{
position:absolute;inset:0;background:rgba(255,255,255,0.1);
border-radius:var(--radius-full);transition:.3s ease;
}
.switch .track::before{
content:'';position:absolute;left:2px;top:2px;
width:14px;height:14px;background:var(--text-dim);
border-radius:50%;transition:.3s ease;
}
.switch input:checked+.track{background:var(--accent)}
.switch input:checked+.track::before{transform:translateX(14px);background:#fff}
/* Messages area */
.messages{
flex:1;overflow-y:auto;padding:28px;display:flex;flex-direction:column;
gap:4px;scroll-behavior:smooth;
}
.messages::-webkit-scrollbar{width:5px}
.messages::-webkit-scrollbar-track{background:transparent}
.messages::-webkit-scrollbar-thumb{background:rgba(168,85,247,0.2);border-radius:3px}
.messages::-webkit-scrollbar-thumb:hover{background:rgba(168,85,247,0.35)}
/* Message rows */
.msg{animation:msgIn .35s cubic-bezier(.16,1,.3,1)}
@keyframes msgIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
.msg-row{display:flex;gap:12px;padding:6px 0}
.msg-row.user{justify-content:flex-end}
.msg-row.assistant{justify-content:flex-start}
.msg-avatar{
width:30px;height:30px;border-radius:10px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:.75rem;font-weight:700;margin-top:2px;
}
.msg-row.user .msg-avatar{
background:linear-gradient(135deg,var(--purple-600),var(--purple-400));
color:#fff;order:2;
}
.msg-row.assistant .msg-avatar{
background:var(--glass);border:1px solid var(--glass-border);
overflow:hidden;
}
.msg-row.assistant .msg-avatar img{width:100%;height:100%;object-fit:cover}
.msg-bubble{
max-width:72%;padding:14px 18px;font-size:.88rem;line-height:1.7;
word-wrap:break-word;overflow-wrap:break-word;
}
.msg-row.user .msg-bubble{
background:linear-gradient(135deg,var(--purple-700),var(--purple-600));
color:#fff;border-radius:var(--radius-lg) var(--radius-lg) 6px var(--radius-lg);
box-shadow:0 4px 24px rgba(168,85,247,0.15);
}
.msg-row.assistant .msg-bubble{
background:var(--surface);
border:1px solid var(--glass-border);
border-radius:var(--radius-lg) var(--radius-lg) var(--radius-lg) 6px;
backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
}
/* Thinking block */
.think-block{
background:var(--think-bg);border:1px solid var(--think-border);
border-radius:14px;padding:10px 14px;margin-bottom:10px;
}
.think-toggle{
cursor:pointer;display:flex;align-items:center;gap:8px;
font-size:.72rem;color:var(--text-dim);user-select:none;
font-weight:500;letter-spacing:.02em;text-transform:uppercase;
}
.think-toggle .think-icon{
width:18px;height:18px;border-radius:50%;
background:rgba(168,85,247,0.15);
display:flex;align-items:center;justify-content:center;
font-size:.6rem;
}
.think-content{
margin-top:8px;white-space:pre-wrap;display:none;
max-height:280px;overflow-y:auto;font-size:.8rem;
color:var(--text-secondary);line-height:1.6;
padding-right:8px;
}
.think-content::-webkit-scrollbar{width:3px}
.think-content::-webkit-scrollbar-thumb{background:var(--think-border);border-radius:2px}
.think-content.open{display:block}
/* Code formatting */
.msg-bubble pre{
background:rgba(0,0,0,0.35);border:1px solid var(--glass-border);
border-radius:14px;padding:14px 16px;margin:10px 0;overflow-x:auto;
font-family:'JetBrains Mono',monospace;font-size:.8rem;line-height:1.6;
}
.msg-bubble code{font-family:'JetBrains Mono',monospace;font-size:.83em;
background:rgba(168,85,247,0.1);padding:1px 5px;border-radius:5px;}
.msg-bubble pre code{background:none;padding:0;border-radius:0}
.msg-bubble p{margin:5px 0}
.msg-bubble ul,.msg-bubble ol{margin:6px 0;padding-left:20px}
.msg-bubble li{margin:3px 0}
/* Typing indicator */
.typing-dots{display:flex;gap:5px;padding:6px 0}
.typing-dots span{
width:7px;height:7px;background:var(--accent);opacity:.4;
border-radius:50%;animation:dotPulse 1.4s infinite;
}
.typing-dots span:nth-child(2){animation-delay:.2s}
.typing-dots span:nth-child(3){animation-delay:.4s}
@keyframes dotPulse{0%,80%,100%{transform:scale(.5);opacity:.3}40%{transform:scale(1);opacity:.8}}
/* Input area */
.input-area{
padding:16px 28px 28px;flex-shrink:0;
}
.input-container{
background:var(--surface);
border:1px solid var(--glass-border);
border-radius:var(--radius-xl);
padding:8px 8px 8px 22px;
display:flex;align-items:flex-end;gap:10px;
transition:all .3s ease;
backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);
box-shadow:0 8px 40px rgba(0,0,0,0.25);
}
.input-container:focus-within{
border-color:rgba(168,85,247,0.35);
box-shadow:0 8px 40px rgba(0,0,0,0.25),0 0 0 3px var(--accent-glow);
}
#userInput{
flex:1;background:none;border:none;color:var(--text);
font-size:.9rem;font-family:'Inter',system-ui,sans-serif;
resize:none;outline:none;max-height:150px;min-height:26px;
line-height:1.55;padding:8px 0;
}
#userInput::placeholder{color:var(--text-dim)}
#sendBtn{
width:42px;height:42px;border-radius:var(--radius-full);
border:none;background:linear-gradient(135deg,var(--purple-600),var(--purple-500));
color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;
transition:all .25s ease;flex-shrink:0;
box-shadow:0 4px 16px rgba(168,85,247,0.3);
}
#sendBtn:hover{transform:scale(1.06);box-shadow:0 6px 24px rgba(168,85,247,0.4)}
#sendBtn:active{transform:scale(0.96)}
#sendBtn:disabled{opacity:.35;cursor:not-allowed;transform:none;box-shadow:none}
#sendBtn svg{width:18px;height:18px}
/* Welcome / Empty state */
.welcome{
display:flex;flex-direction:column;align-items:center;justify-content:center;
flex:1;gap:16px;padding:60px 40px;text-align:center;
}
.welcome-logo{
width:64px;height:64px;border-radius:18px;
box-shadow:0 0 40px rgba(168,85,247,0.25);
animation:logoPulse 3s ease-in-out infinite;
}
@keyframes logoPulse{
0%,100%{box-shadow:0 0 30px rgba(168,85,247,0.2)}
50%{box-shadow:0 0 50px rgba(168,85,247,0.35)}
}
.welcome h2{
font-size:1.3rem;font-weight:600;
background:linear-gradient(135deg,var(--purple-300),var(--purple-500));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;
}
.welcome p{
font-size:.85rem;color:var(--text-dim);max-width:420px;line-height:1.7;
}
.welcome-suggestions{
display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:8px;max-width:520px;
}
.suggestion{
padding:8px 16px;border-radius:var(--radius-full);
background:var(--glass);border:1px solid var(--glass-border);
color:var(--text-secondary);font-size:.78rem;cursor:pointer;
transition:all .2s ease;font-family:inherit;
}
.suggestion:hover{
background:var(--accent-glow);border-color:rgba(168,85,247,0.3);
color:var(--accent-light);transform:translateY(-1px);
}
/* Footer hint */
.input-hint{
text-align:center;padding-top:8px;
font-size:.68rem;color:var(--text-dim);
}
/* Responsive */
@media(max-width:640px){
.app{max-width:100%}
.msg-bubble{max-width:88%}
.controls{padding:8px 16px}
.messages{padding:20px 16px}
.input-area{padding:12px 16px 20px}
.header{padding:12px 16px}
.welcome-suggestions{flex-direction:column;align-items:center}
}
</style>
</head>
<body>
<div class="app">
<!-- Header -->
<div class="header">
<div class="header-brand">
<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/699484cbe85a4b61cbc5ee0f/GpYWuz-CovEFgbPOW21dZ.png" alt="Poolside" class="header-logo">
<div class="header-text">
<h1>Laguna XS.2</h1>
<span>by Poolside AI</span>
</div>
</div>
<div class="header-actions">
<button class="pill-btn danger" id="clearBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg>
Clear
</button>
</div>
</div>
<!-- Controls -->
<div class="controls">
<div class="control-chip">
<label class="switch"><input type="checkbox" id="thinkToggle" checked><span class="track"></span></label>
<label for="thinkToggle">Thinking</label>
</div>
<div class="control-chip">
<label>Temp</label>
<input type="range" id="tempSlider" min="0.1" max="1.5" step="0.1" value="0.7">
<span id="tempVal">0.7</span>
</div>
<div class="control-chip">
<label>Tokens</label>
<input type="range" id="tokenSlider" min="256" max="4096" step="256" value="1024">
<span id="tokenVal">1024</span>
</div>
</div>
<!-- Messages -->
<div class="messages" id="messages">
<div class="welcome">
<img src="https://cdn-avatars.huggingface.co/v1/production/uploads/699484cbe85a4b61cbc5ee0f/GpYWuz-CovEFgbPOW21dZ.png" alt="Poolside" class="welcome-logo">
<h2>What can I help you build?</h2>
<p>Laguna XS.2 is a reasoning-capable code model by Poolside AI. Ask me to write code, debug problems, or explain complex concepts.</p>
<div class="welcome-suggestions">
<button class="suggestion" data-prompt="Write a Python FastAPI server with CRUD endpoints">Build a FastAPI server</button>
<button class="suggestion" data-prompt="Explain how transformers work in machine learning">Explain transformers</button>
<button class="suggestion" data-prompt="Write a React component for a dark mode toggle with animation">React dark mode toggle</button>
<button class="suggestion" data-prompt="Debug this: why does my async function return a pending promise?">Debug async/await</button>
</div>
</div>
</div>
<!-- Input -->
<div class="input-area">
<div class="input-container">
<textarea id="userInput" rows="1" placeholder="Ask Laguna anything..." autofocus></textarea>
<button id="sendBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"/>
<path d="M12 5l7 7-7 7"/>
</svg>
</button>
</div>
<div class="input-hint">Laguna XS.2 can make mistakes. Verify important information.</div>
</div>
</div>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
const messagesEl = document.getElementById('messages');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const thinkToggle = document.getElementById('thinkToggle');
const tempSlider = document.getElementById('tempSlider');
const tempVal = document.getElementById('tempVal');
const tokenSlider = document.getElementById('tokenSlider');
const tokenVal = document.getElementById('tokenVal');
const clearBtn = document.getElementById('clearBtn');
let history = [];
let client = null;
let isGenerating = false;
const LOGO_URL = 'https://cdn-avatars.huggingface.co/v1/production/uploads/699484cbe85a4b61cbc5ee0f/GpYWuz-CovEFgbPOW21dZ.png';
const WELCOME_HTML = `<div class="welcome"><img src="${LOGO_URL}" alt="Poolside" class="welcome-logo"><h2>What can I help you build?</h2><p>Laguna XS.2 is a reasoning-capable code model by Poolside AI. Ask me to write code, debug problems, or explain complex concepts.</p><div class="welcome-suggestions"><button class="suggestion" data-prompt="Write a Python FastAPI server with CRUD endpoints">Build a FastAPI server</button><button class="suggestion" data-prompt="Explain how transformers work in machine learning">Explain transformers</button><button class="suggestion" data-prompt="Write a React component for a dark mode toggle with animation">React dark mode toggle</button><button class="suggestion" data-prompt="Debug this: why does my async function return a pending promise?">Debug async/await</button></div></div>`;
async function initClient() {
client = await Client.connect(window.location.origin);
}
initClient();
tempSlider.oninput = () => tempVal.textContent = tempSlider.value;
tokenSlider.oninput = () => tokenVal.textContent = tokenSlider.value;
clearBtn.onclick = () => {
history = [];
messagesEl.innerHTML = WELCOME_HTML;
bindSuggestions();
};
// Suggestion chips
function bindSuggestions() {
document.querySelectorAll('.suggestion').forEach(btn => {
btn.onclick = () => {
userInput.value = btn.dataset.prompt;
sendMessage();
};
});
}
bindSuggestions();
userInput.addEventListener('input', () => {
userInput.style.height = 'auto';
userInput.style.height = Math.min(userInput.scrollHeight, 150) + 'px';
});
userInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
sendBtn.onclick = sendMessage;
function addMessage(role, content, reasoning) {
const welcome = messagesEl.querySelector('.welcome');
if (welcome) welcome.remove();
const wrapper = document.createElement('div');
wrapper.className = 'msg';
const div = document.createElement('div');
div.className = `msg-row ${role}`;
let avatarHTML;
if (role === 'user') {
avatarHTML = `<div class="msg-avatar">U</div>`;
} else {
avatarHTML = `<div class="msg-avatar"><img src="${LOGO_URL}" alt="AI"></div>`;
}
div.innerHTML = `${avatarHTML}<div class="msg-bubble">${formatContent(content, role, reasoning)}</div>`;
wrapper.appendChild(div);
messagesEl.appendChild(wrapper);
messagesEl.scrollTop = messagesEl.scrollHeight;
return wrapper;
}
function addTypingIndicator() {
const wrapper = document.createElement('div');
wrapper.className = 'msg';
wrapper.id = 'typing';
const div = document.createElement('div');
div.className = 'msg-row assistant';
div.innerHTML = `<div class="msg-avatar"><img src="${LOGO_URL}" alt="AI"></div><div class="msg-bubble"><div class="typing-dots"><span></span><span></span><span></span></div></div>`;
wrapper.appendChild(div);
messagesEl.appendChild(wrapper);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function removeTypingIndicator() {
const el = document.getElementById('typing');
if (el) el.remove();
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
function formatContent(text, role, reasoning) {
if (role === 'user') return escapeHtml(text);
let html = '';
if (reasoning) {
html += `<div class="think-block"><div class="think-toggle" onclick="this.nextElementSibling.classList.toggle('open');this.querySelector('span').textContent=this.nextElementSibling.classList.contains('open')?'Hide reasoning':'Show reasoning'"><div class="think-icon">💭</div><span>Show reasoning</span></div><div class="think-content">${escapeHtml(reasoning)}</div></div>`;
}
let formatted = escapeHtml(text);
formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
formatted = formatted.replace(/\n/g, '<br>');
html += formatted;
return html;
}
async function sendMessage() {
const text = userInput.value.trim();
if (!text || isGenerating || !client) return;
isGenerating = true;
sendBtn.disabled = true;
userInput.value = '';
userInput.style.height = 'auto';
addMessage('user', text);
addTypingIndicator();
try {
const result = await client.predict("/chat", {
message: text,
history: history,
enable_thinking: thinkToggle.checked,
max_tokens: parseInt(tokenSlider.value),
temperature: parseFloat(tempSlider.value),
});
removeTypingIndicator();
const response = result.data[0];
let reasoningText = '';
if (response.reasoning_details && Array.isArray(response.reasoning_details)) {
reasoningText = response.reasoning_details
.filter(d => d.type === 'text')
.map(d => d.text)
.join('\n');
}
addMessage('assistant', response.content || '', reasoningText);
history.push({ role: "user", content: text });
history.push({
role: "assistant",
content: response.content || '',
reasoning_details: response.reasoning_details || null,
});
} catch (err) {
removeTypingIndicator();
addMessage('assistant', `Error: ${err.message}`);
}
isGenerating = false;
sendBtn.disabled = false;
userInput.focus();
}
</script>
</body>
</html>