nova-chat-ui / index.html
Bc-AI's picture
Fix centered layout overlap, move actions to per-message hover buttons, proper spacing
81fda96 verified
Raw
History Blame Contribute Delete
35 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nova-1 Chat</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--surface: #212121;
--surface2: #171717;
--surface3: #2f2f2f;
--sidebar-bg: #171717;
--text: #ececec;
--text2: #b4b4b4;
--text3: #7e7e7e;
--accent: #ffffff;
--accent-button: #676767;
--border: #3a3a3a;
--hover: #2f2f2f;
--user-bubble: #2f2f2f;
--shadow-sm: 0 1px 2px rgba(0,0,0,.3);
--radius-lg: 18px;
--radius-md: 10px;
--radius-sm: 6px;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100vh;overflow:hidden;background:var(--surface);font-family:var(--font-sans);color:var(--text);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}
body{display:flex}
/* ── SIDEBARS ── */
.sidebar{background:var(--sidebar-bg);height:100vh;display:flex;flex-direction:column;overflow:hidden;transition:width .35s cubic-bezier(.22,1,.36,1),min-width .35s cubic-bezier(.22,1,.36,1),opacity .35s ease;position:relative;z-index:10}
#left-sidebar{width:260px;min-width:260px;border-right:1px solid var(--border)}
#left-sidebar.collapsed{width:0;min-width:0;opacity:0;pointer-events:none;border-right:none}
#right-sidebar{width:280px;min-width:280px;border-left:1px solid var(--border)}
#right-sidebar.collapsed{width:0;min-width:0;opacity:0;pointer-events:none;border-left:none}
.ls-header{padding:18px 16px 14px;border-bottom:1px solid var(--border)}
.ls-header h1{font-size:1.1rem;font-weight:700;letter-spacing:-.01em;color:var(--text)}
.ls-header .ls-sub{font-size:.68rem;color:var(--text3);margin-top:2px}
.new-chat-btn{margin:12px 14px;background:transparent;border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 14px;font-weight:500;font-size:.8rem;color:var(--text);cursor:pointer;width:calc(100% - 28px);transition:background .15s;font-family:inherit;display:flex;align-items:center;gap:8px}
.new-chat-btn svg{width:16px;height:16px;opacity:.7}
.new-chat-btn:hover{background:var(--hover)}
.ls-chats-label{padding:20px 16px 8px;font-size:.65rem;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.06em}
.ls-chat-item{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);font-size:.82rem;margin:0 8px 2px;transition:background .12s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ls-chat-item:hover{background:var(--hover);color:var(--text)}
.ls-chat-item.active{background:var(--hover);color:var(--text)}
.ls-footer{margin-top:auto;padding:14px 16px;border-top:1px solid var(--border);font-size:.68rem;color:var(--text3);text-align:center}
.rs-header{padding:18px 16px 12px;border-bottom:1px solid var(--border)}
.rs-header h2{font-size:.75rem;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.06em}
.right-body{flex:1;overflow-y:auto;padding:14px 16px}
.right-body::-webkit-scrollbar{width:4px}
.right-body::-webkit-scrollbar-thumb{background:#444;border-radius:3px}
.rs-section{margin-bottom:22px}
.rs-section-title{font-size:.65rem;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px}
.param-group{margin-bottom:10px}
.param-group label{display:block;font-size:.75rem;color:var(--text2);margin-bottom:4px}
.param-group input[type="range"]{width:100%;accent-color:var(--accent);cursor:pointer;height:4px}
.param-val{float:right;color:var(--text2);font-weight:500;font-size:.72rem}
#system-prompt{background:var(--surface3);border:1px solid var(--border);color:var(--text);border-radius:var(--radius-sm);padding:8px 10px;font-size:.76rem;width:100%;font-family:inherit;resize:vertical;min-height:60px}
#system-prompt:focus{outline:none;border-color:#888}
.mode-group{display:flex;gap:6px}
.mode-btn{flex:1;background:var(--surface3);border:1px solid var(--border);color:var(--text2);border-radius:var(--radius-sm);padding:7px 0;font-size:.72rem;cursor:pointer;text-align:center;transition:all .12s;font-family:inherit}
.mode-btn.active{background:var(--hover);border-color:#777;color:var(--text);font-weight:500}
#yarn-btn{background:transparent;border:1px solid var(--border);color:var(--text);border-radius:var(--radius-md);padding:10px 14px;width:100%;cursor:pointer;font-size:.78rem;font-weight:500;transition:all .15s;font-family:inherit;display:flex;align-items:center;gap:8px}
#yarn-btn svg{width:18px;height:18px;opacity:.7}
#yarn-btn:hover{background:var(--hover);border-color:#666}
.yarn-current{display:inline-block;background:var(--hover);color:var(--text2);padding:4px 10px;border-radius:20px;font-size:.7rem;font-weight:500;margin-top:8px}
/* ── MAIN CHAT ── */
#chat-main{flex:1;display:flex;flex-direction:column;height:100vh;background:var(--surface);min-width:0;position:relative}
.toggle-btn{position:absolute;top:18px;z-index:20;width:32px;height:32px;border-radius:50%;background:transparent;border:1px solid var(--border);color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;font-size:1rem}
.toggle-btn:hover{background:var(--hover);color:var(--text)}
#toggle-left{left:12px}
#toggle-right{right:12px}
/* ── MESSAGES AREA ── */
#chat-messages{flex:1;overflow-y:auto;overflow-x:hidden}
#chat-messages::-webkit-scrollbar{width:5px}
#chat-messages::-webkit-scrollbar-thumb{background:#444;border-radius:4px}
#messages-inner{max-width:720px;width:100%;margin:0 auto;padding:20px 20px 0;display:flex;flex-direction:column;min-height:100%}
/* when empty, center welcome */
#messages-inner.empty{justify-content:center;align-items:center;padding-bottom:20px}
.welcome-block{text-align:center;margin-bottom:0;animation:fadeUp .6s cubic-bezier(.22,1,.36,1)}
.welcome-block h2{font-size:1.5rem;font-weight:600;letter-spacing:-.01em;margin-bottom:6px}
.welcome-block p{font-size:.85rem;color:var(--text3);max-width:400px;line-height:1.6}
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
/* ── MESSAGE ROWS ── */
.msg-row{display:flex;margin-bottom:28px;animation:msgIn .4s cubic-bezier(.22,1,.36,1);position:relative}
.msg-row.user{justify-content:flex-end}
.msg-row.bot{justify-content:flex-start}
@keyframes msgIn{from{opacity:0;transform:translateY(16px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
.bubble-user{background:var(--user-bubble);color:var(--text);border-radius:var(--radius-lg) var(--radius-lg) 4px var(--radius-lg);padding:11px 17px;max-width:75%;font-size:.92rem;line-height:1.65;white-space:pre-wrap;word-break:break-word}
.bubble-bot{color:var(--text);max-width:100%;font-size:.92rem;line-height:1.65;white-space:pre-wrap;word-break:break-word;padding:0}
.bubble-bot pre{background:#1a1a1a;border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px 16px;overflow-x:auto;font-size:.78rem;margin:10px 0;font-family:var(--font-mono)}
.bubble-bot code{background:#1a1a1a;padding:2px 5px;border-radius:4px;font-size:.85em;font-family:var(--font-mono)}
.bubble-bot pre code{padding:0;background:transparent}
/* ── ACTIONS UNDER BOT MESSAGES ── */
.msg-actions{display:flex;gap:2px;margin-top:6px;opacity:0;transform:translateY(-4px);transition:opacity .2s,transform .2s;pointer-events:none}
.msg-row.bot:hover .msg-actions,
.msg-row.bot:last-child .msg-actions{opacity:1;transform:translateY(0);pointer-events:auto}
.msg-actions button{background:transparent;border:none;color:var(--text3);border-radius:6px;padding:4px 8px;font-size:.72rem;cursor:pointer;transition:all .12s;font-family:inherit;display:flex;align-items:center;gap:4px}
.msg-actions button:hover{color:var(--text);background:var(--hover)}
.msg-actions button svg{width:13px;height:13px;opacity:.7}
/* typing */
.typing-indicator{display:inline-flex;gap:5px;padding:4px 0}
.typing-indicator span{width:6px;height:6px;border-radius:50%;background:var(--text3);animation:bounce 1.2s infinite}
.typing-indicator span:nth-child(2){animation-delay:.15s}
.typing-indicator span:nth-child(3){animation-delay:.3s}
@keyframes bounce{0%,60%,100%{transform:translateY(0);opacity:.3}30%{transform:translateY(-7px);opacity:1}}
/* ── INPUT AREA (always at bottom) ── */
#input-area{flex-shrink:0;padding:0 20px 16px}
#input-inner{max-width:720px;width:100%;margin:0 auto}
#input-row{display:flex;align-items:flex-end;background:var(--surface3);border:1px solid var(--border);border-radius:28px;padding:5px 5px 5px 18px;transition:border-color .2s,box-shadow .2s}
#input-row:focus-within{border-color:#777;box-shadow:0 0 0 1px rgba(255,255,255,.05)}
#msg-input{flex:1;background:transparent;border:none;color:var(--text);padding:9px 0;font-size:.88rem;resize:none;outline:none;font-family:inherit;line-height:1.5;max-height:160px}
#msg-input::placeholder{color:var(--text3)}
#send-btn{width:36px;height:36px;min-width:36px;border-radius:50%;background:var(--text);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0;color:var(--surface)}
#send-btn svg{width:16px;height:16px}
#send-btn:hover{background:#ddd}
#send-btn:disabled{opacity:.3;cursor:default;background:var(--text3)}
.input-hint{text-align:center;font-size:.68rem;color:var(--text3);margin-top:8px}
/* ── YaRN MODAL ── */
#yarn-modal{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.65);backdrop-filter:blur(6px)}
#yarn-modal.show{display:flex;animation:modalFade .2s ease}
@keyframes modalFade{from{opacity:0}to{opacity:1}}
.modal-card{background:var(--surface2);border:1px solid var(--border);border-radius:18px;width:440px;max-width:92vw;padding:28px;box-shadow:0 24px 64px rgba(0,0,0,.6);animation:cardPop .35s cubic-bezier(.22,1,.36,1)}
@keyframes cardPop{from{opacity:0;transform:translateY(24px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}
.modal-card h3{font-size:1.05rem;font-weight:600;margin-bottom:4px;letter-spacing:-.01em}
.modal-desc{font-size:.78rem;color:var(--text2);margin-bottom:16px;line-height:1.55}
.modal-warning{background:rgba(251,191,36,.06);border:1px solid rgba(251,191,36,.2);color:#d4a81a;padding:10px 14px;border-radius:var(--radius-sm);font-size:.72rem;margin-bottom:18px;line-height:1.55}
.yarn-options{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:18px}
.yarn-option{background:var(--surface3);border:1.5px solid var(--border);border-radius:12px;padding:14px;text-align:center;cursor:pointer;transition:all .18s;position:relative}
.yarn-option:hover{border-color:#555;transform:translateY(-1px)}
.yarn-option.selected{border-color:#999;background:rgba(255,255,255,.04);box-shadow:0 0 12px rgba(255,255,255,.04)}
.yarn-option .y-label{font-size:.92rem;font-weight:700;color:var(--text)}
.yarn-option.selected .y-label{color:var(--text)}
.yarn-option .y-factor{font-size:.68rem;color:var(--text2);margin-top:3px}
.yarn-option .y-native{font-size:.6rem;color:#22c55e;margin-top:2px;font-weight:600}
/* YaRN animation */
#yarn-stretch-anim{display:none;margin:0 0 16px;text-align:center}
#yarn-stretch-anim.show{display:block;padding:12px 4px 0}
.stretch-visual{display:flex;align-items:center;justify-content:center;margin:0 auto 20px;position:relative;height:80px;width:100%}
.helix-svg{width:100%;height:80px;overflow:visible}
.helix-path{stroke:var(--text3);stroke-width:2.5;fill:none;stroke-dasharray:0 400;transition:stroke-dasharray .15s ease;stroke-linecap:round}
.helix-path.active{stroke:#c4c4c4}
.helix-path.done{stroke:#22c55e}
.stretch-info{font-size:.78rem;color:var(--text2);margin-bottom:14px;text-align:center}
.stretch-bar-wrap{background:var(--surface3);border-radius:12px;height:8px;width:100%;overflow:hidden;margin-bottom:14px}
.stretch-bar{height:100%;background:var(--text3);border-radius:12px;width:0%;transition:width .15s ease}
.stretch-bar.active{background:var(--text)}
.stretch-bar.done{background:#22c55e}
.stretch-bar-label{display:flex;justify-content:space-between;font-size:.68rem;color:var(--text3);margin-bottom:6px;padding:0 2px}
.stretch-check{font-size:.82rem;color:#22c55e;margin-top:12px;display:none;font-weight:500;text-align:center}
.stretch-check.show{display:block;animation:checkPop .35s cubic-bezier(.22,1,.36,1)}
@keyframes checkPop{from{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}}
.modal-buttons{display:flex;gap:10px;justify-content:flex-end}
.modal-buttons button{border:none;border-radius:var(--radius-sm);padding:9px 20px;font-size:.8rem;font-weight:500;cursor:pointer;transition:all .15s;font-family:inherit}
.modal-cancel{background:#333;color:var(--text2)}
.modal-cancel:hover{background:#444;color:var(--text)}
.modal-apply{background:var(--text);color:#111;font-weight:600}
.modal-apply:hover{background:#ddd;transform:translateY(-1px)}
.modal-apply:disabled{opacity:.4;cursor:not-allowed;transform:none}
@media (max-width:768px){
#left-sidebar,#right-sidebar{display:none}
#toggle-left,#toggle-right{display:none}
#chat-messages{padding:0}
#messages-inner{padding:16px 12px 0}
#input-area{padding:0 12px 12px}
}
</style>
</head>
<body>
<!-- LEFT SIDEBAR -->
<div class="sidebar" id="left-sidebar">
<div class="ls-header">
<h1>Nova-1</h1>
<div class="ls-sub">Standard Preview</div>
</div>
<button class="new-chat-btn" id="new-chat-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New Chat
</button>
<div class="ls-chats-label">Recent</div>
<div class="ls-chat-item active" id="chat-item-0">New conversation</div>
<div class="ls-footer">ZeroGPU ⚡ · Mixture of Depth</div>
</div>
<!-- MAIN CHAT -->
<div id="chat-main">
<button class="toggle-btn" id="toggle-left" title="Toggle sidebar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
</button>
<button class="toggle-btn" id="toggle-right" title="Toggle parameters">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 1v4m0 14v4M4.22 4.22l2.83 2.83m9.9 9.9l2.83 2.83M1 12h4m14 0h4M4.22 19.78l2.83-2.83m9.9-9.9l2.83-2.83"/></svg>
</button>
<div id="chat-messages">
<div id="messages-inner" class="empty">
<div class="welcome-block">
<h2>How can I help you today?</h2>
<p>Ask me anything — I'm running on ZeroGPU with Mixture of Depth for fast, intelligent responses.</p>
</div>
</div>
</div>
<div id="input-area">
<div id="input-inner">
<div id="input-row">
<textarea id="msg-input" placeholder="Message Nova…" rows="1"></textarea>
<button id="send-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
</button>
</div>
<div class="input-hint">Nova can make mistakes. Verify important information.</div>
</div>
</div>
</div>
<!-- RIGHT SIDEBAR -->
<div class="sidebar" id="right-sidebar">
<div class="rs-header"><h2>Settings</h2></div>
<div class="right-body">
<div class="rs-section">
<div class="rs-section-title">Context Window</div>
<button id="yarn-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 0 1-9 9m9-9a9 9 0 0 0-9-9m9 9H3m9 9a9 9 0 0 1-9-9m9 9c2.5 0 4.5-4 4.5-9S14.5 3 12 3m0 18c-2.5 0-4.5-4-4.5-9S9.5 3 12 3"/></svg>
YaRN Stretch <span id="yarn-current" style="margin-left:auto">2K</span>
</button>
<div class="yarn-current" id="yarn-status">Native 2K</div>
</div>
<div class="rs-section">
<div class="rs-section-title">Generation</div>
<div class="param-group"><label>Max Tokens <span class="param-val" id="val-max-tokens">512</span></label><input type="range" id="max-tokens" min="128" max="2048" step="64" value="512"></div>
<div class="param-group"><label>Temperature <span class="param-val" id="val-temperature">0.6</span></label><input type="range" id="temperature" min="0.1" max="2.0" step="0.1" value="0.6"></div>
<div class="param-group"><label>Top-P <span class="param-val" id="val-top-p">0.90</span></label><input type="range" id="top-p" min="0.1" max="1.0" step="0.05" value="0.9"></div>
</div>
<div class="rs-section">
<div class="rs-section-title">System Prompt</div>
<textarea id="system-prompt" rows="4">You are Nova, a helpful, honest AI assistant. Answer questions clearly and concisely.</textarea>
</div>
<div class="rs-section">
<div class="rs-section-title">Mode</div>
<div class="mode-group">
<button class="mode-btn active" id="mode-chat" data-mode="Chat">Chat</button>
<button class="mode-btn" id="mode-base" data-mode="Base">Base</button>
</div>
</div>
</div>
</div>
<!-- YaRN MODAL -->
<div id="yarn-modal">
<div class="modal-card">
<h3>Context Window Stretching</h3>
<div class="modal-desc">YaRN extrapolation extends the model's native 2K context to handle longer documents and conversations.</div>
<div class="modal-warning">Stretching may slightly reduce precision on fine details. For most use cases, native 2K works best.</div>
<div class="yarn-options" id="yarn-options">
<div class="yarn-option selected" data-ctx="2K (Native)" data-factor="1.0"><div class="y-label">2K</div><div class="y-factor">×1.0</div><div class="y-native">Native</div></div>
<div class="yarn-option" data-ctx="4K (YaRN x2)" data-factor="2.0"><div class="y-label">4K</div><div class="y-factor">×2.0</div></div>
<div class="yarn-option" data-ctx="8K (YaRN x4)" data-factor="4.0"><div class="y-label">8K</div><div class="y-factor">×4.0</div></div>
<div class="yarn-option" data-ctx="16K (YaRN x8)" data-factor="8.0"><div class="y-label">16K</div><div class="y-factor">×8.0</div></div>
</div>
<div id="yarn-stretch-anim">
<div class="stretch-visual">
<svg class="helix-svg" viewBox="0 0 360 70">
<path class="helix-path" id="helix1" d="M20,15 C70,35 60,5 110,25 C160,45 150,15 200,35 C250,55 240,25 290,45 C320,57 340,35 340,35" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
<path class="helix-path" id="helix2" d="M20,55 C70,35 60,65 110,45 C160,25 150,55 200,35 C250,15 240,45 290,25 C320,13 340,35 340,35" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
<circle cx="20" cy="15" r="3" fill="currentColor" opacity="0.3"/>
<circle cx="20" cy="55" r="3" fill="currentColor" opacity="0.3"/>
<circle cx="340" cy="35" r="4" fill="currentColor" opacity="0.5" id="helix-dot"/>
</svg>
</div>
<div class="stretch-bar-label"><span>2K</span><span id="stretch-target-label">2K</span></div>
<div class="stretch-bar-wrap"><div class="stretch-bar" id="yarn-bar" style="width:0%"></div></div>
<div class="stretch-info" id="yarn-anim-label">Ready</div>
<div class="stretch-check" id="yarn-check">Context stretched successfully</div>
</div>
<div class="modal-buttons">
<button class="modal-cancel" id="yarn-cancel">Cancel</button>
<button class="modal-apply" id="yarn-apply">Apply</button>
</div>
</div>
</div>
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
// ── State ──
let history = [];
let isGenerating = false;
let mode = "Chat";
let client = null;
async function initClient() {
try {
client = await Client.connect(window.location.origin);
console.log("✅ Connected to Gradio backend");
} catch (e) {
console.error("❌ Failed to connect:", e);
}
}
initClient();
// ── DOM refs ──
const chatMessages = document.getElementById("chat-messages");
const messagesInner = document.getElementById("messages-inner");
const msgInput = document.getElementById("msg-input");
const sendBtn = document.getElementById("send-btn");
const newChatBtn = document.getElementById("new-chat-btn");
const inputArea = document.getElementById("input-area");
const maxTokensSlider = document.getElementById("max-tokens");
const temperatureSlider = document.getElementById("temperature");
const topPSlider = document.getElementById("top-p");
const systemPromptEl = document.getElementById("system-prompt");
const leftSidebar = document.getElementById("left-sidebar");
const rightSidebar = document.getElementById("right-sidebar");
const toggleLeft = document.getElementById("toggle-left");
const toggleRight = document.getElementById("toggle-right");
let leftOpen = true, rightOpen = true;
toggleLeft.addEventListener("click", () => {
leftOpen = !leftOpen;
leftSidebar.classList.toggle("collapsed", !leftOpen);
});
toggleRight.addEventListener("click", () => {
rightOpen = !rightOpen;
rightSidebar.classList.toggle("collapsed", !rightOpen);
});
maxTokensSlider.addEventListener("input", () => { document.getElementById("val-max-tokens").textContent = maxTokensSlider.value; });
temperatureSlider.addEventListener("input", () => { document.getElementById("val-temperature").textContent = parseFloat(temperatureSlider.value).toFixed(1); });
topPSlider.addEventListener("input", () => { document.getElementById("val-top-p").textContent = parseFloat(topPSlider.value).toFixed(2); });
document.querySelectorAll(".mode-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".mode-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
mode = btn.dataset.mode;
});
});
msgInput.addEventListener("input", () => {
msgInput.style.height = "auto";
msgInput.style.height = Math.min(msgInput.scrollHeight, 160) + "px";
});
function scrollDown() {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// ── Layout helpers ──
function enterChatMode() {
messagesInner.classList.remove("empty");
const wb = messagesInner.querySelector(".welcome-block");
if (wb) wb.remove();
}
function resetToEmpty() {
messagesInner.classList.add("empty");
if (!messagesInner.querySelector(".welcome-block")) {
const wb = document.createElement("div");
wb.className = "welcome-block";
wb.innerHTML = `<h2>How can I help you today?</h2><p>Ask me anything — I'm running on ZeroGPU with Mixture of Depth for fast, intelligent responses.</p>`;
messagesInner.prepend(wb);
}
}
// ── Escape HTML ──
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// ── Action buttons (per message) ──
function createMsgActions(msgIdx) {
const actions = document.createElement("div");
actions.className = "msg-actions";
// Copy button
const copyBtn = document.createElement("button");
copyBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy`;
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(history[msgIdx]?.content || "");
});
// Regenerate button
const reBtn = document.createElement("button");
reBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.5 17.5A9 9 0 1 0 2 12"/></svg> Regenerate`;
reBtn.addEventListener("click", () => regenerate(msgIdx));
actions.appendChild(copyBtn);
actions.appendChild(reBtn);
return actions;
}
// ── Render messages ──
function addUserMessage(text) {
const div = document.createElement("div");
div.className = "msg-row user";
div.innerHTML = `<div class="bubble-user">${escapeHtml(text)}</div>`;
messagesInner.appendChild(div);
scrollDown();
}
function addBotMessage(msgIdx) {
const div = document.createElement("div");
div.className = "msg-row bot";
div.innerHTML = `<div class="bubble-bot"><div class="typing-indicator"><span></span><span></span><span></span></div></div>`;
div.appendChild(createMsgActions(msgIdx));
messagesInner.appendChild(div);
scrollDown();
return div;
}
function updateBotMessage(div, text) {
const bubble = div.querySelector(".bubble-bot");
if (text) {
bubble.innerHTML = escapeHtml(text).replace(/\n/g, "<br>");
} else {
bubble.innerHTML = `<div class="typing-indicator"><span></span><span></span><span></span></div>`;
}
scrollDown();
}
// ── Send message ──
async function sendMessage() {
if (isGenerating) return;
const message = msgInput.value.trim();
if (!message) return;
if (!client) { console.error("Client not connected"); return; }
isGenerating = true;
sendBtn.disabled = true;
msgInput.value = "";
msgInput.style.height = "auto";
enterChatMode();
addUserMessage(message);
history.push({ role: "user", content: message });
const assistantIdx = history.length;
history.push({ role: "assistant", content: "" });
const botDiv = addBotMessage(assistantIdx);
try {
const priorHistory = history.slice(0, -2);
const submission = client.submit("/generate", {
message: message,
history: priorHistory,
system_prompt: systemPromptEl.value,
max_tokens: parseInt(maxTokensSlider.value),
temperature: parseFloat(temperatureSlider.value),
top_p: parseFloat(topPSlider.value),
mode: mode,
});
let fullResponse = "";
for await (const msg of submission) {
if (msg.type === "data") {
let chunk = "";
if (Array.isArray(msg.data)) chunk = msg.data[0] ?? "";
else if (typeof msg.data === "string") chunk = msg.data;
else if (msg.data && typeof msg.data === "object") chunk = msg.data[0] ?? msg.data ?? "";
if (chunk) {
fullResponse += chunk;
history[assistantIdx].content = fullResponse;
updateBotMessage(botDiv, fullResponse);
}
} else if (msg.type === "status" && msg.status === "error") {
fullResponse = "An error occurred during generation. Please try again.";
history[assistantIdx].content = fullResponse;
updateBotMessage(botDiv, fullResponse);
break;
}
}
if (!fullResponse) {
fullResponse = "No response received.";
history[assistantIdx].content = fullResponse;
updateBotMessage(botDiv, fullResponse);
}
} catch (e) {
console.error("Stream error:", e);
const errText = "Connection error: " + e.message;
history[assistantIdx].content = errText;
updateBotMessage(botDiv, errText);
}
isGenerating = false;
sendBtn.disabled = false;
}
// ── Regenerate ──
async function regenerate(botIdx) {
if (isGenerating) return;
if (botIdx < 1 || history[botIdx - 1]?.role !== "user") return;
const userMsg = history[botIdx - 1].content;
// Remove the old bot message from DOM and history
const botRow = messagesInner.querySelectorAll(".msg-row.bot")[
Array.from(messagesInner.querySelectorAll(".msg-row.bot")).findIndex(r =>
r.querySelector(".msg-actions") && r === [...messagesInner.querySelectorAll(".msg-row.bot")].find((_,i) => {
// find by index match
let botCount = 0;
for (const child of messagesInner.children) {
if (child.classList.contains("msg-row") && child.classList.contains("bot")) {
if (botCount === (() => { let c = 0; for (const ch of messagesInner.children) { if (ch.classList.contains("msg-row") && ch.classList.contains("bot")) { if (ch === r) return c; c++; } } return -1; })()) return true;
botCount++;
}
}
return false;
})
)
];
// Simpler approach: just pop last assistant, find its row
// Actually let's just remove the last bot row
const allBotRows = messagesInner.querySelectorAll(".msg-row.bot");
const targetRow = allBotRows[allBotRows.length - 1];
history.pop(); // remove assistant
if (targetRow) targetRow.remove();
const assistantIdx = history.length;
history.push({ role: "assistant", content: "" });
const botDiv = addBotMessage(assistantIdx);
isGenerating = true;
sendBtn.disabled = true;
try {
const priorHistory = history.slice(0, -2);
const submission = client.submit("/generate", {
message: userMsg,
history: priorHistory,
system_prompt: systemPromptEl.value,
max_tokens: parseInt(maxTokensSlider.value),
temperature: parseFloat(temperatureSlider.value),
top_p: parseFloat(topPSlider.value),
mode: mode,
});
let fullResponse = "";
for await (const msg of submission) {
if (msg.type === "data") {
let chunk = Array.isArray(msg.data) ? msg.data[0] : (typeof msg.data === "string" ? msg.data : "");
if (chunk) { fullResponse += chunk; history[assistantIdx].content = fullResponse; updateBotMessage(botDiv, fullResponse); }
}
}
if (!fullResponse) updateBotMessage(botDiv, "No response received.");
} catch (e) { updateBotMessage(botDiv, "Error: " + e.message); }
isGenerating = false;
sendBtn.disabled = false;
}
// ── Events ──
sendBtn.addEventListener("click", sendMessage);
msgInput.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
newChatBtn.addEventListener("click", () => {
history = [];
messagesInner.innerHTML = "";
resetToEmpty();
});
// ── YaRN Modal ──
const yarnModal = document.getElementById("yarn-modal");
const yarnBtn = document.getElementById("yarn-btn");
const yarnCancel = document.getElementById("yarn-cancel");
const yarnApply = document.getElementById("yarn-apply");
const yarnStretchAnim = document.getElementById("yarn-stretch-anim");
const yarnBar = document.getElementById("yarn-bar");
const yarnAnimLabel = document.getElementById("yarn-anim-label");
const yarnCheck = document.getElementById("yarn-check");
const yarnStatus = document.getElementById("yarn-status");
const yarnCurrentSpan = document.getElementById("yarn-current");
const stretchTargetLabel = document.getElementById("stretch-target-label");
const helix1 = document.getElementById("helix1");
const helix2 = document.getElementById("helix2");
let yarnSelectedCtx = "2K (Native)";
let yarnSelectedFactor = 1.0;
let yarnSelectedK = "2K";
function resetYarnModal() {
yarnStretchAnim.classList.remove("show");
yarnBar.style.width = "0%";
yarnBar.className = "stretch-bar";
yarnAnimLabel.textContent = "Ready";
yarnCheck.classList.remove("show");
yarnApply.disabled = false;
helix1.classList.remove("active", "done");
helix2.classList.remove("active", "done");
document.querySelectorAll(".yarn-option").forEach(o => o.classList.remove("selected"));
const nativeOpt = document.querySelector('.yarn-option[data-ctx="2K (Native)"]');
if (nativeOpt) nativeOpt.classList.add("selected");
yarnSelectedCtx = "2K (Native)";
yarnSelectedFactor = 1.0;
yarnSelectedK = "2K";
stretchTargetLabel.textContent = "2K";
}
yarnBtn.addEventListener("click", () => { resetYarnModal(); yarnModal.classList.add("show"); });
yarnCancel.addEventListener("click", () => { yarnModal.classList.remove("show"); });
yarnModal.addEventListener("click", (e) => { if (e.target === yarnModal) yarnModal.classList.remove("show"); });
document.querySelectorAll(".yarn-option").forEach(opt => {
opt.addEventListener("click", () => {
document.querySelectorAll(".yarn-option").forEach(o => o.classList.remove("selected"));
opt.classList.add("selected");
yarnSelectedCtx = opt.dataset.ctx;
yarnSelectedFactor = parseFloat(opt.dataset.factor);
yarnSelectedK = opt.querySelector(".y-label").textContent;
stretchTargetLabel.textContent = yarnSelectedK;
});
});
yarnApply.addEventListener("click", async () => {
yarnApply.disabled = true;
yarnStretchAnim.classList.add("show");
yarnCheck.classList.remove("show");
helix1.classList.add("active");
helix2.classList.add("active");
if (yarnSelectedFactor === 1.0) {
yarnBar.style.width = "100%";
yarnBar.className = "stretch-bar done";
helix1.classList.add("done"); helix1.classList.remove("active");
helix2.classList.add("done"); helix2.classList.remove("active");
yarnAnimLabel.textContent = "Native context — no stretching needed";
yarnCheck.classList.add("show");
try {
const result = await client.predict("/apply_yarn", { target_context: yarnSelectedCtx });
const status = Array.isArray(result.data) ? result.data[0] : result.data;
yarnStatus.textContent = "Native 2K";
yarnCurrentSpan.textContent = "2K";
} catch (e) { console.error("YaRN apply error:", e); }
setTimeout(() => yarnModal.classList.remove("show"), 900);
return;
}
const totalFrames = 60;
const duration = 1800;
const frameTime = duration / totalFrames;
let frame = 0;
const animInterval = setInterval(() => {
frame++;
const progress = frame / totalFrames;
const eased = 1 - Math.pow(1 - progress, 3);
const pct = Math.round(eased * 100);
yarnBar.style.width = pct + "%";
yarnBar.className = "stretch-bar active";
yarnAnimLabel.textContent = `Stretching to ${yarnSelectedK}${pct}%`;
helix1.style.strokeDasharray = (eased * 300) + " 300";
helix2.style.strokeDasharray = (eased * 300) + " 300";
if (frame >= totalFrames) {
clearInterval(animInterval);
yarnBar.className = "stretch-bar done";
helix1.classList.add("done"); helix1.classList.remove("active");
helix2.classList.add("done"); helix2.classList.remove("active");
helix1.style.strokeDasharray = "300 0";
helix2.style.strokeDasharray = "300 0";
(async () => {
try {
const result = await client.predict("/apply_yarn", { target_context: yarnSelectedCtx });
const status = Array.isArray(result.data) ? result.data[0] : result.data;
yarnAnimLabel.textContent = status || `Context: ${yarnSelectedK}`;
yarnStatus.textContent = status || `${yarnSelectedK} active`;
yarnCurrentSpan.textContent = yarnSelectedK;
} catch (e) { yarnAnimLabel.textContent = "Error applying"; console.error("YaRN error:", e); }
yarnCheck.classList.add("show");
setTimeout(() => yarnModal.classList.remove("show"), 1200);
})();
}
}, frameTime);
});
</script>
</body>
</html>