File size: 7,333 Bytes
92fa272 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | <!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>
|