NanoBotAIAgent's picture
Chat UI with reasoning box
92fa272 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Gemma-4 E2B Uncensored — Chat</title>
<style>
:root { --bg:#0b0f19; --panel:#141a2a; --bubble-u:#2563eb; --bubble-a:#1f2839; --text:#e5e7eb; --muted:#9ca3af; --border:#27304a; }
* { box-sizing:border-box; }
body { margin:0; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; }
header { padding:12px 16px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:10px; }
header h1 { font-size:15px; margin:0; font-weight:600; }
header .dot { width:9px; height:9px; border-radius:50%; background:#6b7280; }
header .dot.ok { background:#22c55e; }
header .dot.err { background:#ef4444; }
header .spacer { flex:1; }
header a { color:var(--muted); font-size:12px; text-decoration:none; }
header a:hover { color:var(--text); }
#chat { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:12px; }
.msg { max-width:760px; width:fit-content; padding:10px 14px; border-radius:12px; white-space:pre-wrap; word-wrap:break-word; line-height:1.5; }
.msg.user { align-self:flex-end; background:var(--bubble-u); color:#fff; }
.msg.assistant { align-self:flex-start; background:var(--bubble-a); border:1px solid var(--border); }
.think-box { margin-bottom:8px; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
.think-box summary { cursor:pointer; padding:6px 10px; font-size:12px; color:var(--muted); background:#10182a; user-select:none; }
.think-box summary::marker { color:var(--muted); }
.think-body { padding:8px 10px; color:var(--muted); font-style:italic; font-size:13px; white-space:pre-wrap; border-top:1px solid var(--border); }
.answer { white-space:pre-wrap; }
.empty { color:var(--muted); text-align:center; margin-top:40px; font-size:14px; }
footer { border-top:1px solid var(--border); padding:12px 16px; }
.row { display:flex; gap:8px; max-width:900px; margin:0 auto; }
textarea { flex:1; resize:none; background:var(--panel); color:var(--text); border:1px solid var(--border); border-radius:10px; padding:10px 12px; font-size:14px; font-family:inherit; min-height:44px; max-height:160px; }
textarea:focus { outline:none; border-color:var(--bubble-u); }
button { background:var(--bubble-u); color:#fff; border:none; border-radius:10px; padding:0 18px; font-size:14px; font-weight:600; cursor:pointer; }
button:disabled { background:#374151; cursor:not-allowed; }
.hint { max-width:900px; margin:6px auto 0; color:var(--muted); font-size:11px; text-align:center; }
</style>
</head>
<body>
<header>
<span id="status" class="dot"></span>
<h1>Gemma-4 E2B Uncensored</h1>
<span class="spacer"></span>
<a href="/v1/models" target="_blank">API · /v1/models</a>
</header>
<div id="chat"><div class="empty">Type a message to start. Reasoning is on — the model's thinking streams into a collapsible box, then the answer. (CPU model — slow but streams live.)</div></div>
<footer>
<div class="row">
<textarea id="input" placeholder="Send a message… (Enter to send, Shift+Enter for newline)"></textarea>
<button id="send">Send</button>
</div>
<div class="hint">OpenAI-compatible API at <code>/v1/chat/completions</code> — reasoning streams as the <code>reasoning_content</code> delta field.</div>
</footer>
<script>
const chatEl = document.getElementById('chat');
const inputEl = document.getElementById('input');
const sendBtn = document.getElementById('send');
const statusEl = document.getElementById('status');
const history = [];
let busy = false;
fetch('/v1/models').then(r => r.ok ? statusEl.classList.add('ok') : statusEl.classList.add('err'))
.catch(() => statusEl.classList.add('err'));
function addUser(text) {
const empty = chatEl.querySelector('.empty');
if (empty) empty.remove();
const d = document.createElement('div');
d.className = 'msg user';
d.textContent = text;
chatEl.appendChild(d);
chatEl.scrollTop = chatEl.scrollHeight;
}
function addAssistant() {
const empty = chatEl.querySelector('.empty');
if (empty) empty.remove();
const wrap = document.createElement('div');
wrap.className = 'msg assistant';
const details = document.createElement('details');
details.className = 'think-box';
details.open = true;
details.style.display = 'none';
const summary = document.createElement('summary');
summary.textContent = 'Thinking…';
const thinkBody = document.createElement('div');
thinkBody.className = 'think-body';
details.appendChild(summary);
details.appendChild(thinkBody);
const answer = document.createElement('div');
answer.className = 'answer';
answer.textContent = '…';
wrap.appendChild(details);
wrap.appendChild(answer);
chatEl.appendChild(wrap);
chatEl.scrollTop = chatEl.scrollHeight;
return { wrap, details, summary, thinkBody, answer };
}
async function send() {
if (busy) return;
const text = inputEl.value.trim();
if (!text) return;
inputEl.value = '';
busy = true; sendBtn.disabled = true;
addUser(text);
history.push({ role: 'user', content: text });
const ui = addAssistant();
let reasoning = '', answer = '';
try {
const resp = await fetch('/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'gemma', messages: history, max_tokens: 2048, stream: true })
});
if (!resp.ok || !resp.body) throw new Error('HTTP ' + resp.status);
const reader = resp.body.getReader();
const dec = new TextDecoder();
let buf = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
const s = line.trim();
if (!s.startsWith('data:')) continue;
const data = s.slice(5).trim();
if (data === '[DONE]') continue;
try {
const j = JSON.parse(data);
const delta = j.choices?.[0]?.delta || {};
if (delta.reasoning_content) {
reasoning += delta.reasoning_content;
ui.details.style.display = '';
ui.thinkBody.textContent = reasoning;
}
if (delta.content) {
answer += delta.content;
ui.answer.textContent = answer;
if (ui.details.open) {
ui.details.open = false;
ui.summary.textContent = 'Thinking (done) — click to expand';
}
}
chatEl.scrollTop = chatEl.scrollHeight;
} catch (_) {}
}
}
if (!answer) ui.answer.textContent = '(no answer text returned)';
history.push({ role: 'assistant', content: answer });
} catch (e) {
ui.answer.textContent = 'Error: ' + e.message;
} finally {
busy = false; sendBtn.disabled = false;
inputEl.focus();
}
}
sendBtn.addEventListener('click', send);
inputEl.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
</script>
</body>
</html>