nova-1-chat-serve / index.html
Bc-AI's picture
Upload index.html
788c774 verified
Raw
History Blame Contribute Delete
31.6 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>
:root{
--bg:#1a1a2e; --bg2:#16213e; --bg-input:#1e1e3f; --sidebar:#0f0f23;
--bubble-u:#0f3460; --text:#e6e6f0; --text2:#8888aa; --text3:#555577;
--accent:#e94560; --accent2:#7b6cff; --border:#222244; --green:#22c55e;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;height:100vh;overflow:hidden;background:var(--bg);
font-family:'Inter',-apple-system,'Segoe UI',Roboto,sans-serif;color:var(--text)}
#gradio-client{display:none}
/* ── Layout ── */
.app{display:flex;height:100vh;overflow:hidden}
#left-sidebar{width:260px;min-width:260px;background:var(--sidebar);
border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
#chat-main{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden;background:var(--bg)}
#right-sidebar{width:280px;min-width:280px;background:var(--sidebar);
border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
/* ── Left sidebar ── */
.ls-header{padding:16px 18px 12px;display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid var(--border);flex-shrink:0}
.ls-header h1{margin:0;font-size:1.15rem;font-weight:700;
background:linear-gradient(135deg,var(--accent),var(--accent2));
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.ls-sub{font-size:.62rem;color:var(--text3);margin-top:2px}
.ls-logo{width:34px;height:34px;border-radius:10px;
background:linear-gradient(135deg,var(--accent),var(--accent2));
display:flex;align-items:center;justify-content:center;font-size:1.1rem;flex-shrink:0}
.new-chat{margin:14px 10px;padding:10px 14px;border:none;border-radius:14px;
background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;
font-weight:600;font-size:.82rem;cursor:pointer;transition:all .2s;flex-shrink:0}
.new-chat:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(233,69,96,.35)}
.ls-chats-label{padding:6px 18px;font-size:.65rem;font-weight:600;color:var(--text3);
text-transform:uppercase;letter-spacing:1px;flex-shrink:0}
.ls-chat-item{display:flex;align-items:center;gap:8px;padding:9px 12px;border-radius:8px;
cursor:pointer;color:var(--text2);font-size:.82rem;margin:0 8px 2px;transition:background .15s}
.ls-chat-item:hover{background:rgba(255,255,255,.05);color:var(--text)}
.ls-chat-item.active{background:rgba(233,69,96,.12);color:var(--text)}
.ls-spacer{flex:1}
.ls-footer{padding:10px 18px 14px;border-top:1px solid var(--border);
font-size:.68rem;color:var(--text3);text-align:center;flex-shrink:0}
/* ── Chat main ── */
.chat-header{padding:14px 28px;border-bottom:1px solid var(--border);
display:flex;align-items:center;justify-content:space-between;flex-shrink:0;
background:rgba(26,26,46,.85);backdrop-filter:blur(8px)}
.chat-header h2{margin:0;font-size:1rem;font-weight:600}
.chat-status{font-size:.72rem;color:var(--text2);display:flex;align-items:center;gap:6px}
.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 6px var(--green)}
/* ── Messages ── */
#messages{flex:1;overflow-y:auto;padding:24px 28px;scroll-behavior:smooth}
#messages::-webkit-scrollbar{width:6px}
#messages::-webkit-scrollbar-thumb{background:#333366;border-radius:4px}
#messages::-webkit-scrollbar-track{background:transparent}
.msg{max-width:760px;margin:0 auto 16px;display:flex;flex-direction:column;animation:msgIn .35s cubic-bezier(.22,1,.36,1)}
@keyframes msgIn{from{opacity:0;transform:translateY(14px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}
.msg.user{align-items:flex-end}
.msg.bot{align-items:flex-start}
.msg-bubble{max-width:75%;padding:12px 18px;line-height:1.6;font-size:.95rem}
.msg.user .msg-bubble{background:var(--bubble-u);border-radius:20px 20px 6px 20px;color:var(--text)}
.msg.bot .msg-bubble{background:transparent;padding:4px 0;color:var(--text)}
.msg.bot .msg-bubble p{margin:.4em 0}
.msg.bot .msg-bubble pre{background:#0d0d1d;border:1px solid var(--border);border-radius:8px;
padding:12px 14px;overflow-x:auto;font-size:.82em;margin:8px 0}
.msg.bot .msg-bubble code{background:#0d0d1d;padding:2px 5px;border-radius:4px;font-size:.82em;
font-family:'JetBrains Mono','Fira Code',monospace}
.msg.bot .msg-bubble pre code{padding:0;background:transparent}
.msg.bot .msg-bubble ul,.msg.bot .msg-bubble ol{margin:.4em 0;padding-left:1.4em}
.msg.bot .msg-bubble strong{color:#fff}
.msg.bot .msg-bubble a{color:var(--accent2)}
.typing-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--text2);
margin:0 2px;animation:blink 1s infinite}
.typing-dot:nth-child(2){animation-delay:.2s}
.typing-dot:nth-child(3){animation-delay:.4s}
@keyframes blink{0%,60%,100%{opacity:.3}30%{opacity:1}}
/* ── Action bar ── */
#action-bar{max-width:760px;margin:0 auto;padding:6px 28px;display:flex;
justify-content:center;gap:8px;flex-shrink:0}
#action-bar button{background:transparent;border:1px solid var(--border);color:var(--text2);
border-radius:20px;padding:5px 14px;font-size:.75rem;cursor:pointer;transition:all .18s}
#action-bar button:hover{background:rgba(255,255,255,.06);color:var(--text);border-color:#444466}
/* ── Input area (oval pill) ── */
#input-area{max-width:760px;margin:0 auto 20px;padding:0 28px;flex-shrink:0}
#input-row{display:flex;align-items:flex-end;gap:8px;background:var(--bg-input);
border:1px solid var(--border);border-radius:28px;padding:6px 6px 6px 20px;
transition:border-color .2s,box-shadow .2s}
#input-row:focus-within{border-color:var(--accent);box-shadow:0 0 0 2px rgba(233,69,96,.12)}
#input-row textarea{flex:1;background:transparent;border:none;color:var(--text);
padding:8px 0;resize:none;font-size:.95rem;line-height:1.5;max-height:150px;outline:none;
font-family:inherit}
#input-row textarea::placeholder{color:var(--text3)}
#send-btn{background:linear-gradient(135deg,var(--accent),var(--accent2));min-width:38px;
width:38px;height:38px;border-radius:50%;border:none;padding:0;cursor:pointer;
display:flex;align-items:center;justify-content:center;font-size:1.05rem;color:#fff;
transition:all .2s}
#send-btn:hover{transform:scale(1.08);box-shadow:0 0 12px rgba(233,69,96,.4)}
#send-btn:disabled{opacity:.4;transform:none}
#input-hint{text-align:center;font-size:.68rem;color:var(--text3);margin-top:6px}
/* ── Right sidebar ── */
.rs-header{padding:16px 18px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
.rs-header h2{margin:0;font-size:.8rem;font-weight:600;color:var(--text2);
text-transform:uppercase;letter-spacing:1px}
.right-body{flex:1;overflow-y:auto;padding:14px 18px}
.right-body::-webkit-scrollbar{width:5px}
.right-body::-webkit-scrollbar-thumb{background:#333366;border-radius:4px}
.rs-section{margin-bottom:20px}
.rs-title{font-size:.68rem;font-weight:600;color:var(--text3);text-transform:uppercase;
letter-spacing:1px;margin-bottom:8px}
.rs-label{font-size:.75rem;color:var(--text2);margin-bottom:4px}
.rs-section textarea,.rs-section input[type=text]{
width:100%;background:var(--bg-input);border:1px solid var(--border);color:var(--text);
border-radius:8px;padding:8px 10px;font-size:.78rem;font-family:inherit;outline:none;resize:vertical}
.rs-section input[type=range]{width:100%;accent-color:var(--accent);margin-top:4px}
.rs-slider-row{display:flex;justify-content:space-between;font-size:.75rem;color:var(--text2)}
.rs-slider-val{color:var(--accent);font-weight:600}
/* ── YaRN button ── */
#yarn-btn{background:transparent;border:1px solid var(--accent);color:var(--accent);
border-radius:14px;padding:10px;font-weight:600;font-size:.8rem;cursor:pointer;
width:100%;transition:all .2s}
#yarn-btn:hover{background:rgba(233,69,96,.1);box-shadow:0 0 12px rgba(233,69,96,.2);transform:translateY(-1px)}
.yarn-status{margin-top:8px;font-size:.72rem}
.yarn-badge{display:inline-block;background:rgba(233,69,96,.15);color:var(--accent);
padding:3px 10px;border-radius:20px;font-size:.7rem;font-weight:600}
/* ── Mode toggle ── */
.mode-group{display:flex;gap:6px}
.mode-group label{flex:1;text-align:center;border:1px solid var(--border);border-radius:8px;
padding:6px 12px;font-size:.72rem;color:var(--text2);cursor:pointer;transition:all .15s}
.mode-group label.active{background:rgba(233,69,96,.12);border-color:var(--accent);color:var(--text)}
/* ── 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,.6);backdrop-filter:blur(4px)}
#yarn-modal.show{display:flex;animation:modalIn .25s ease}
@keyframes modalIn{from{opacity:0}to{opacity:1}}
.modal-card{background:var(--bg2);border:1px solid var(--border);border-radius:20px;
width:440px;max-width:92vw;padding:28px;box-shadow:0 20px 60px rgba(0,0,0,.5);
animation:cardIn .3s cubic-bezier(.22,1,.36,1)}
@keyframes cardIn{from{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:translateY(0) scale(1)}}
.modal-card h3{margin:0 0 4px;font-size:1.1rem;font-weight:700}
.modal-desc{font-size:.78rem;color:var(--text2);margin-bottom:18px;line-height:1.5}
.modal-warning{background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.3);color:#fbbf24;
padding:10px 12px;border-radius:10px;font-size:.72rem;margin-bottom:16px;line-height:1.5}
.yarn-options{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
.yarn-option{background:var(--bg-input);border:1.5px solid var(--border);border-radius:14px;
padding:14px;text-align:center;cursor:pointer;transition:all .2s;position:relative}
.yarn-option:hover{border-color:#555577;transform:translateY(-2px)}
.yarn-option.selected{border-color:var(--accent);background:rgba(233,69,96,.08);
box-shadow:0 0 12px rgba(233,69,96,.15)}
.yarn-option .y-label{font-size:.9rem;font-weight:700;color:var(--text)}
.yarn-option .y-factor{font-size:.68rem;color:var(--text2);margin-top:3px}
.yarn-option .y-native{font-size:.6rem;color:var(--green);margin-top:2px;font-weight:600}
.yarn-option.selected .y-label{color:var(--accent)}
#yarn-anim{display:none;margin:0 auto 16px;text-align:center}
#yarn-anim.show{display:block}
.stretch-wrapper{background:var(--bg-input);border-radius:20px;height:50px;width:100%;
overflow:hidden;position:relative;margin-bottom:12px}
.stretch-bar{height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));
border-radius:20px;width:0%;transition:width .3s ease;display:flex;align-items:center;
justify-content:center;color:#fff;font-size:.7rem;font-weight:600}
.stretch-label{font-size:.78rem;color:var(--text2)}
.stretch-check{font-size:1.2rem;color:var(--green);margin-top:6px;display:none}
.stretch-check.show{display:block;animation:checkPop .4s ease}
@keyframes checkPop{from{opacity:0;transform:scale(.5)}to{opacity:1;transform:scale(1)}}
.modal-btns{display:flex;gap:10px;justify-content:flex-end}
.modal-btns button{border:none;border-radius:12px;padding:9px 18px;font-size:.82rem;
font-weight:600;cursor:pointer;transition:all .2s}
.btn-cancel{background:#2a2a4a;color:var(--text2)}
.btn-cancel:hover{background:#333366;color:var(--text)}
.btn-apply{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff}
.btn-apply:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(233,69,96,.3)}
.btn-apply:disabled{opacity:.5;cursor:not-allowed;transform:none}
.responsive-only{display:none}
@media(max-width:1000px){
#right-sidebar{display:none}
.responsive-only{display:block}
}
@media(max-width:700px){
#left-sidebar{display:none}
}
</style>
</head>
<body>
<!-- ── App Layout ── -->
<div class="app">
<!-- LEFT SIDEBAR -->
<div id="left-sidebar">
<div class="ls-header">
<div>
<h1>Nova-1</h1>
<div class="ls-sub">Standard Preview</div>
</div>
<div class="ls-logo">✦</div>
</div>
<button class="new-chat" onclick="newChat()">οΌ‹ New Chat</button>
<div class="ls-chats-label">Chat History</div>
<div class="ls-chat-item active">πŸ’¬ New conversation</div>
<div class="ls-spacer"></div>
<div class="ls-footer">ZeroGPU ⚑ · Powered by MoD</div>
</div>
<!-- CHAT MAIN -->
<div id="chat-main">
<div class="chat-header">
<h2>Nova-1 Chat</h2>
<div class="chat-status">
<div class="status-dot"></div>
<span id="status-text">Online Β· ZeroGPU</span>
</div>
</div>
<div id="messages"></div>
<div id="action-bar">
<button onclick="clearChat()">πŸ—‘οΈ Clear</button>
<button onclick="retryLast()">πŸ”„ Retry</button>
<button onclick="undoLast()">↩️ Undo</button>
</div>
<div id="input-area">
<div id="input-row">
<textarea id="msg" placeholder="Send a message…" rows="1"
onkeydown="onKeyDown(event)" oninput="autoGrow(this)"></textarea>
<button id="send-btn" onclick="sendMessage()">➀</button>
</div>
<div id="input-hint">Press Enter to send Β· Shift+Enter for new line</div>
</div>
</div>
<!-- RIGHT SIDEBAR -->
<div id="right-sidebar">
<div class="rs-header"><h2>Parameters</h2></div>
<div class="right-body">
<!-- YaRN -->
<div class="rs-section">
<div class="rs-title">Context Window (YaRN)</div>
<button id="yarn-btn" onclick="openYarnModal()">🧡 YaRN Stretch
(<span id="yarn-current">2K</span>)
</button>
<div class="yarn-status" id="yarn-status-display">
<span class="yarn-badge">βœ… Native 2K active</span>
</div>
</div>
<!-- Generation Params -->
<div class="rs-section">
<div class="rs-title">Generation</div>
<div class="rs-slider-row"><span class="rs-label">🎯 Max Tokens</span>
<span class="rs-slider-val" id="val-maxtok">512</span></div>
<input type="range" min="128" max="2048" value="512" step="64"
oninput="updateSlider('maxtok',this.value)"
onchange="updateSlider('maxtok',this.value)"><br>
<div class="rs-slider-row" style="margin-top:10px"><span class="rs-label">🌑️ Temperature</span>
<span class="rs-slider-val" id="val-temp">0.6</span></div>
<input type="range" min="0.1" max="2.0" value="0.6" step="0.1"
oninput="updateSlider('temp',this.value)"
onchange="updateSlider('temp',this.value)"><br>
<div class="rs-slider-row" style="margin-top:10px"><span class="rs-label">🎲 Top-P</span>
<span class="rs-slider-val" id="val-topp">0.9</span></div>
<input type="range" min="0.1" max="1.0" value="0.9" step="0.05"
oninput="updateSlider('topp',this.value)"
onchange="updateSlider('topp',this.value)">
</div>
<!-- System Prompt -->
<div class="rs-section">
<div class="rs-title">System Prompt</div>
<textarea id="sys-prompt" rows="4"
placeholder="You are Nova, a helpful assistant…">You are Nova, a helpful, honest AI assistant. Answer questions clearly and concisely.</textarea>
</div>
<!-- Mode -->
<div class="rs-section">
<div class="rs-title">Mode</div>
<div class="mode-group">
<label id="mode-chat" class="active" onclick="setMode('Chat')">Chat</label>
<label id="mode-base" onclick="setMode('Base')">Base</label>
</div>
</div>
<div class="rs-section responsive-only">
<div class="rs-title">Parameters</div>
<p style="font-size:.72rem;color:var(--text3)">Use desktop view to access all settings.</p>
</div>
</div>
</div>
</div>
<!-- ── YaRN Modal ── -->
<div id="yarn-modal" onclick="onModalBackdropClick(event)">
<div class="modal-card" onclick="event.stopPropagation()">
<h3>🧡 YaRN Context Extrapolation</h3>
<div class="modal-desc">
YaRN stretches the model's context window beyond its native 2K limit.
Choose a target context length below.
</div>
<div class="modal-warning">
⚠️ Stretching context lets the AI read longer documents, but can make it slightly
less precise on small details. If unsure, leave at 2K!
</div>
<div class="yarn-options" id="yarn-options">
<div class="yarn-option selected" data-ctx="2K (Native)" data-k="2" data-factor="1"
onclick="yarnSelect(this)">
<div class="y-label">2K</div>
<div class="y-factor">Factor 1.0Γ—</div>
<div class="y-native">● Native</div>
</div>
<div class="yarn-option" data-ctx="4K (YaRN x2)" data-k="4" data-factor="2"
onclick="yarnSelect(this)">
<div class="y-label">4K</div>
<div class="y-factor">Factor 2.0Γ—</div>
</div>
<div class="yarn-option" data-ctx="8K (YaRN x4)" data-k="8" data-factor="4"
onclick="yarnSelect(this)">
<div class="y-label">8K</div>
<div class="y-factor">Factor 4.0Γ—</div>
</div>
<div class="yarn-option" data-ctx="16K (YaRN x8)" data-k="16" data-factor="8"
onclick="yarnSelect(this)">
<div class="y-label">16K</div>
<div class="y-factor">Factor 8.0Γ—</div>
</div>
</div>
<div id="yarn-anim">
<div class="stretch-wrapper">
<div class="stretch-bar" id="yarn-bar">0%</div>
</div>
<div class="stretch-label" id="yarn-anim-label">Initializing…</div>
<div class="stretch-check" id="yarn-check">βœ“ Context stretched successfully!</div>
</div>
<div class="modal-btns">
<button class="btn-cancel" onclick="closeYarnModal()">Cancel</button>
<button class="btn-apply" id="yarn-apply" onclick="yarnApply()">Apply</button>
</div>
</div>
</div>
<!-- ── Gradio JS Client (for SSE streaming through the queue) ── -->
<script type="module">
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
window.gradioClient = await Client.connect(window.location.origin);
window.dispatchEvent(new Event("gradio-ready"));
</script>
<script>
// ═══ State ═════════════════════════════════════════════════════════
let chatHistory = []; // [{role, content}]
let isGenerating = false;
let currentMode = "Chat";
const $ = id => document.getElementById(id);
const messagesEl = $("messages");
const msgInput = $("msg");
const sendBtn = $("send-btn");
// ═══ Gradio Client ══════════════════════════════════════════════════
let client = null;
window.addEventListener("gradio-ready", () => {
client = window.gradioClient;
$("status-text").textContent = "Online Β· ZeroGPU";
});
// Wait up to 5s for client to be ready
setTimeout(() => {
if (window.gradioClient) client = window.gradioClient;
}, 3000);
// ═══ Markdown renderer (minimal) ════════════════════════════════════
function renderMarkdown(text) {
// code blocks
let html = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (_, lang, code) =>
`<pre><code>${code.trim()}</code></pre>`);
// inline code
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// bold/italic
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
// links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// unordered lists
html = html.replace(/^ - (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>.*<\/li>\n?)+/g, m => `<ul>${m}</ul>`);
// line breaks
html = html.split('\n').map(l => {
if (l.match(/^<(pre|ul)>/)) return l;
if (l.trim() === '') return '';
return `<p>${l}</p>`;
}).join('\n');
return html;
}
// ═══ UI Helpers ═════════════════════════════════════════════════════
function scrollToBottom() {
messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: "smooth" });
}
function autoGrow(el) {
el.style.height = "auto";
el.style.height = Math.min(el.scrollHeight, 150) + "px";
}
function onKeyDown(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function updateSlider(name, val) {
if (name === "maxtok") {
$("val-maxtok").textContent = val;
} else if (name === "temp") {
$("val-temp").textContent = Number(val).toFixed(1);
} else if (name === "topp") {
$("val-topp").textContent = Number(val).toFixed(2);
}
}
function setMode(m) {
currentMode = m;
$("mode-chat").classList.toggle("active", m === "Chat");
$("mode-base").classList.toggle("active", m === "Base");
}
// ═══ Chat rendering ═════════════════════════════════════════════════
function appendMessage(role, content) {
const div = document.createElement("div");
div.className = `msg ${role === "user" ? "user" : "bot"}`;
const bubble = document.createElement("div");
bubble.className = "msg-bubble";
if (role === "user") {
bubble.textContent = content;
} else {
bubble.innerHTML = renderMarkdown(content);
}
div.appendChild(bubble);
messagesEl.appendChild(div);
scrollToBottom();
// For bot messages, return the bubble so we append to it during streaming
if (role === "assistant") {
return { div, bubble, raw: content };
}
return null;
}
function appendTypingIndicator() {
const div = document.createElement("div");
div.className = "msg bot";
div.id = "typing-indicator";
div.innerHTML = `<div class="msg-bubble">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>`;
messagesEl.appendChild(div);
setTimeout(scrollToBottom, 50);
return div;
}
function removeTypingIndicator() {
const el = $("typing-indicator");
if (el) el.remove();
}
// ═══ Send Message ══════════════════════════════════════════════════
async function sendMessage() {
const text = msgInput.value.trim();
if (!text || isGenerating) return;
// Need client ready
if (!client) {
if (window.gradioClient) {
client = window.gradioClient;
} else {
// Fallback retry
client = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Client not ready"));
}, 5000);
window.addEventListener("gradio-ready", () => {
clearTimeout(timeout);
resolve(window.gradioClient);
});
}).catch(() => null);
}
if (!client) return;
}
isGenerating = true;
sendBtn.disabled = true;
// Add user message to UI
chatHistory.push({ role: "user", content: text });
appendMessage("user", text);
// Clear input
msgInput.value = "";
msgInput.style.height = "auto";
// Typing indicator
appendTypingIndicator();
// Create the bot message bubble (initially empty)
const priorHistory = chatHistory.slice(0, -1);
try {
let fullResponse = "";
let botBubble = null;
const handle = await client.predict("/generate", {
message: text,
history: priorHistory,
system_prompt: $("sys-prompt").value,
max_tokens: parseInt($("val-maxtok").textContent),
temperature: parseFloat($("val-temp").textContent),
top_p: parseFloat($("val-topp").textContent),
mode: currentMode,
});
// The Gradio client with streaming returns an async iterator
// For SSE streaming via @app.api with stream_every, the client
// supports .stream() and yields intermediate results.
// However predict() may also work β€” we handle both cases.
} catch (e) {
console.error("predict error:", e);
}
// Try streaming approach
try {
removeTypingIndicator();
// Start with empty bot bubble
const botEl = appendMessage("assistant", "");
botBubble = botEl.bubble;
let fullText = "";
const stream = client.stream("/generate", {
message: text,
history: priorHistory,
system_prompt: $("sys-prompt").value,
max_tokens: parseInt($("val-maxtok").textContent),
temperature: parseFloat($("val-temp").textContent),
top_p: parseFloat($("val-topp").textContent),
mode: currentMode,
});
if (stream) {
for await (const chunk of stream) {
if (chunk && chunk.data) {
const piece = typeof chunk.data === "string"
? chunk.data
: (chunk.data[0] || "");
fullText += piece;
botBubble.innerHTML = renderMarkdown(fullText);
scrollToBottom();
}
}
chatHistory.push({ role: "assistant", content: fullText });
}
} catch (streamErr) {
console.error("_stream error:", streamErr);
// Fallback: try predict (non-streaming)
removeTypingIndicator();
try {
const result = await client.predict("/generate", {
message: text,
history: priorHistory,
system_prompt: $("sys-prompt").value,
max_tokens: parseInt($("val-maxtok").textContent),
temperature: parseFloat($("val-temp").textContent),
top_p: parseFloat($("val-topp").textContent),
mode: currentMode,
});
const fullText = (result && result.data)
? (result.data[0] || "")
: (result || "");
const botEl = appendMessage("assistant", fullText);
chatHistory.push({ role: "assistant", content: fullText });
} catch (predictErr) {
if (!botBubble) {
removeTypingIndicator();
const errEl = appendMessage("assistant", "⚠️ Failed to get response. " + (predictErr.message || ""));
chatHistory.push({ role: "assistant", content: "⚠️ Failed to get response." });
}
}
}
isGenerating = false;
sendBtn.disabled = false;
}
// ═══ Chat Controls ═════════════════════════════════════════════════
function newChat() {
chatHistory = [];
messagesEl.innerHTML = "";
msgInput.value = "";
}
function clearChat() {
chatHistory = [];
messagesEl.innerHTML = "";
}
function retryLast() {
if (isGenerating || chatHistory.length === 0) return;
// Remove last assistant message
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === "assistant") {
chatHistory.pop();
const msgs = messagesEl.querySelectorAll(".msg");
if (msgs.length > 0) msgs[msgs.length - 1].remove();
}
// Check if last is user message
if (chatHistory.length === 0 || chatHistory[chatHistory.length - 1].role !== "user") {
return;
}
// Get the user message
const lastUserMsg = chatHistory[chatHistory.length - 1].content;
// Remove the user message from UI (it'll be re-added by sendMessage logic)
chatHistory.pop();
const msgs = messagesEl.querySelectorAll(".msg");
if (msgs.length > 0) msgs[msgs.length - 1].remove();
// Re-send
msgInput.value = lastUserMsg;
sendMessage();
}
function undoLast() {
if (isGenerating || chatHistory.length === 0) return;
// Remove last assistant
if (chatHistory[chatHistory.length - 1].role === "assistant") {
chatHistory.pop();
const msgs = messagesEl.querySelectorAll(".msg");
if (msgs.length > 0) msgs[msgs.length - 1].remove();
}
// Remove last user
if (chatHistory.length > 0 && chatHistory[chatHistory.length - 1].role === "user") {
chatHistory.pop();
const msgs = messagesEl.querySelectorAll(".msg");
if (msgs.length > 0) msgs[msgs.length - 1].remove();
}
}
// ═══ YaRN Modal ════════════════════════════════════════════════════
function openYarnModal() {
const m = $("yarn-modal");
m.classList.add("show");
// reset
$("yarn-anim").classList.remove("show");
$("yarn-bar").style.width = "0%";
$("yarn-bar").textContent = "0%";
$("yarn-anim-label").textContent = "Initializing…";
$("yarn-check").classList.remove("show");
$("yarn-apply").disabled = false;
// reset selection
document.querySelectorAll("#yarn-options .yarn-option").forEach(o => o.classList.remove("selected"));
document.querySelector('#yarn-options .yarn-option[data-k="2"]').classList.add("selected");
}
function closeYarnModal() {
$("yarn-modal").classList.remove("show");
}
function onModalBackdropClick(e) {
if (e.target.id === "yarn-modal") closeYarnModal();
}
function yarnSelect(el) {
document.querySelectorAll("#yarn-options .yarn-option").forEach(o => o.classList.remove("selected"));
el.classList.add("selected");
}
async function yarnApply() {
const sel = document.querySelector("#yarn-options .yarn-option.selected");
if (!sel) return;
const ctx = sel.getAttribute("data-ctx");
const k = parseInt(sel.getAttribute("data-k"));
const factor = parseFloat(sel.getAttribute("data-factor"));
const applyBtn = $("yarn-apply");
applyBtn.disabled = true;
const anim = $("yarn-anim");
anim.classList.add("show");
const check = $("yarn-check");
check.classList.remove("show");
if (factor === 1.0) {
// No animation needed
const bar = $("yarn-bar");
bar.style.width = "100%";
bar.textContent = "100%";
$("yarn-anim-label").textContent = "Native context β€” no stretch needed.";
check.classList.add("show");
// Call backend
try {
const result = await client.predict("/yarn", { ctx });
updateYarnStatus(ctx, k);
} catch (e) { console.error("Yarn API error:", e); }
setTimeout(() => closeYarnModal(), 800);
return;
}
// Animate stretch bar
let step = 0;
const total = 10;
const interval = setInterval(() => {
step++;
const pct = Math.round((step / total) * 100);
const bar = $("yarn-bar");
bar.style.width = pct + "%";
bar.textContent = pct + "%";
$("yarn-anim-label").textContent = `Stretching context window to ${k}K…`;
if (step >= total) {
clearInterval(interval);
// Call backend (this runs YaRN config on the server)
if (client) {
client.predict("/yarn", { ctx }).then(() => {
updateYarnStatus(ctx, k);
}).catch(e => console.error("Yarn API error:", e));
}
$("yarn-anim-label").textContent = `βœ… Context stretched to ${k}K (Factor ${factor.toFixed(1)}Γ—)`;
check.classList.add("show");
setTimeout(() => closeYarnModal(), 1300);
}
}, 130);
}
function updateYarnStatus(ctx, k) {
$("yarn-current").textContent = k + "K";
$("yarn-status-display").innerHTML = k === 2
? '<span class="yarn-badge">βœ… Native 2K active</span>'
: `<span class="yarn-badge">βœ… ${k}K (YaRN) active</span>`;
}
// ═══ Init ══════════════════════════════════════════════════════════
autoGrow(msgInput);
</script>
</body>
</html>