NanoBotAIAgent commited on
Commit
8948dd0
·
verified ·
1 Parent(s): f0b913c

Add chat UI (mirror 27B space)

Browse files
Files changed (1) hide show
  1. chat.html +181 -0
chat.html ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Gemma-4-E4B Uncensored — Chat</title>
7
+ <style>
8
+ :root { --bg:#0b0f19; --panel:#141a2a; --bubble-u:#2563eb; --bubble-a:#1f2839; --text:#e5e7eb; --muted:#9ca3af; --border:#27304a; }
9
+ * { box-sizing:border-box; }
10
+ 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; }
11
+ header { padding:12px 16px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:10px; }
12
+ header h1 { font-size:15px; margin:0; font-weight:600; }
13
+ header .dot { width:9px; height:9px; border-radius:50%; background:#6b7280; }
14
+ header .dot.ok { background:#22c55e; }
15
+ header .dot.err { background:#ef4444; }
16
+ header .spacer { flex:1; }
17
+ header a { color:var(--muted); font-size:12px; text-decoration:none; }
18
+ header a:hover { color:var(--text); }
19
+ #chat { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:12px; }
20
+ .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; }
21
+ .msg.user { align-self:flex-end; background:var(--bubble-u); color:#fff; }
22
+ .msg.assistant { align-self:flex-start; background:var(--bubble-a); border:1px solid var(--border); }
23
+ .think-box { margin-bottom:8px; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
24
+ .think-box summary { cursor:pointer; padding:6px 10px; font-size:12px; color:var(--muted); background:#10182a; user-select:none; }
25
+ .think-box summary::marker { color:var(--muted); }
26
+ .think-body { padding:8px 10px; color:var(--muted); font-style:italic; font-size:13px; white-space:pre-wrap; border-top:1px solid var(--border); }
27
+ .answer { white-space:pre-wrap; }
28
+ .empty { color:var(--muted); text-align:center; margin-top:40px; font-size:14px; }
29
+ footer { border-top:1px solid var(--border); padding:12px 16px; }
30
+ .row { display:flex; gap:8px; max-width:900px; margin:0 auto; }
31
+ 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; }
32
+ textarea:focus { outline:none; border-color:var(--bubble-u); }
33
+ button { background:var(--bubble-u); color:#fff; border:none; border-radius:10px; padding:0 18px; font-size:14px; font-weight:600; cursor:pointer; }
34
+ button:disabled { background:#374151; cursor:not-allowed; }
35
+ .hint { max-width:900px; margin:6px auto 0; color:var(--muted); font-size:11px; text-align:center; }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <header>
40
+ <span id="status" class="dot"></span>
41
+ <h1>Gemma-4-E4B Uncensored (Q8_K_P)</h1>
42
+ <span class="spacer"></span>
43
+ <a href="/v1/models" target="_blank">API · /v1/models</a>
44
+ </header>
45
+ <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>
46
+ <footer>
47
+ <div class="row">
48
+ <textarea id="input" placeholder="Send a message… (Enter to send, Shift+Enter for newline)"></textarea>
49
+ <button id="send">Send</button>
50
+ </div>
51
+ <div class="hint">OpenAI-compatible API at <code>/v1/chat/completions</code> — reasoning streams as the <code>reasoning_content</code> delta field.</div>
52
+ </footer>
53
+
54
+ <script>
55
+ const chatEl = document.getElementById('chat');
56
+ const inputEl = document.getElementById('input');
57
+ const sendBtn = document.getElementById('send');
58
+ const statusEl = document.getElementById('status');
59
+ const history = [];
60
+ let busy = false;
61
+
62
+ fetch('/v1/models').then(r => r.ok ? statusEl.classList.add('ok') : statusEl.classList.add('err'))
63
+ .catch(() => statusEl.classList.add('err'));
64
+
65
+ function addUser(text) {
66
+ const empty = chatEl.querySelector('.empty');
67
+ if (empty) empty.remove();
68
+ const d = document.createElement('div');
69
+ d.className = 'msg user';
70
+ d.textContent = text;
71
+ chatEl.appendChild(d);
72
+ chatEl.scrollTop = chatEl.scrollHeight;
73
+ }
74
+
75
+ function addAssistant() {
76
+ const empty = chatEl.querySelector('.empty');
77
+ if (empty) empty.remove();
78
+ const wrap = document.createElement('div');
79
+ wrap.className = 'msg assistant';
80
+
81
+ const details = document.createElement('details');
82
+ details.className = 'think-box';
83
+ details.open = true;
84
+ details.style.display = 'none';
85
+ const summary = document.createElement('summary');
86
+ summary.textContent = 'Thinking…';
87
+ const thinkBody = document.createElement('div');
88
+ thinkBody.className = 'think-body';
89
+ details.appendChild(summary);
90
+ details.appendChild(thinkBody);
91
+
92
+ const answer = document.createElement('div');
93
+ answer.className = 'answer';
94
+ answer.textContent = '…';
95
+
96
+ wrap.appendChild(details);
97
+ wrap.appendChild(answer);
98
+ chatEl.appendChild(wrap);
99
+ chatEl.scrollTop = chatEl.scrollHeight;
100
+ return { wrap, details, summary, thinkBody, answer };
101
+ }
102
+
103
+ async function send() {
104
+ if (busy) return;
105
+ const text = inputEl.value.trim();
106
+ if (!text) return;
107
+ inputEl.value = '';
108
+ busy = true; sendBtn.disabled = true;
109
+
110
+ addUser(text);
111
+ history.push({ role: 'user', content: text });
112
+
113
+ const ui = addAssistant();
114
+ let reasoning = '', answer = '';
115
+
116
+ try {
117
+ const resp = await fetch('/v1/chat/completions', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ model: 'gemma', messages: history, max_tokens: 2048, stream: true })
121
+ });
122
+ if (!resp.ok || !resp.body) throw new Error('HTTP ' + resp.status);
123
+
124
+ const reader = resp.body.getReader();
125
+ const dec = new TextDecoder();
126
+ let buf = '';
127
+ while (true) {
128
+ const { value, done } = await reader.read();
129
+ if (done) break;
130
+ buf += dec.decode(value, { stream: true });
131
+ const lines = buf.split('\n');
132
+ buf = lines.pop();
133
+ for (const line of lines) {
134
+ const s = line.trim();
135
+ if (!s.startsWith('data:')) continue;
136
+ const data = s.slice(5).trim();
137
+ if (data === '[DONE]') continue;
138
+ try {
139
+ const j = JSON.parse(data);
140
+ const delta = j.choices?.[0]?.delta || {};
141
+ if (delta.reasoning_content) {
142
+ reasoning += delta.reasoning_content;
143
+ ui.details.style.display = '';
144
+ ui.thinkBody.textContent = reasoning;
145
+ }
146
+ if (delta.content) {
147
+ answer += delta.content;
148
+ ui.answer.textContent = answer;
149
+ if (ui.details.open) { ui.details.open = false; ui.summary.textContent = 'Thinking (done) — click to expand'; }
150
+ }
151
+ chatEl.scrollTop = chatEl.scrollHeight;
152
+ } catch (_) {}
153
+ }
154
+ }
155
+ // Fallback: inline <think> tags
156
+ if (!reasoning && answer.includes('</think>')) {
157
+ const m = answer.match(/^([\s\S]*?)<\/think>([\s\S]*)$/);
158
+ reasoning = m[1].replace(/^<think>/, '').trim();
159
+ answer = m[2].trim();
160
+ ui.details.style.display = ''; ui.details.open = false;
161
+ ui.summary.textContent = 'Thinking (done) — click to expand';
162
+ ui.thinkBody.textContent = reasoning;
163
+ ui.answer.textContent = answer;
164
+ }
165
+ if (!answer) ui.answer.textContent = '(no answer text returned)';
166
+ history.push({ role: 'assistant', content: answer });
167
+ } catch (e) {
168
+ ui.answer.textContent = 'Error: ' + e.message;
169
+ } finally {
170
+ busy = false; sendBtn.disabled = false;
171
+ inputEl.focus();
172
+ }
173
+ }
174
+
175
+ sendBtn.addEventListener('click', send);
176
+ inputEl.addEventListener('keydown', e => {
177
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
178
+ });
179
+ </script>
180
+ </body>
181
+ </html>