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

Add markdown rendering (marked.js + highlight.js)

Browse files
Files changed (1) hide show
  1. chat.html +63 -138
chat.html CHANGED
@@ -4,34 +4,55 @@
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>
@@ -42,140 +63,44 @@
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>
 
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
+ <script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github-dark.min.css"/>
9
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/core.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/python.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/javascript.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/bash.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/json.min.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/cpp.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/typescript.min.js"></script>
16
  <style>
17
+ :root { --bg:#0b0f19; --panel:#141a2a; --bubble-u:#2563eb; --bubble-a:#1f2839; --text:#e5e7eb; --muted:#9ca3af; --border:#27304a; --code-bg:#0d1117; }
18
  * { box-sizing:border-box; }
19
  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; }
20
+ header { padding:12px 16px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:10px; flex-shrink:0; }
21
  header h1 { font-size:15px; margin:0; font-weight:600; }
22
+ header .dot { width:9px; height:9px; border-radius:50%; background:#6b7280; flex-shrink:0; }
23
  header .dot.ok { background:#22c55e; }
24
  header .dot.err { background:#ef4444; }
25
  header .spacer { flex:1; }
26
  header a { color:var(--muted); font-size:12px; text-decoration:none; }
27
  header a:hover { color:var(--text); }
28
  #chat { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:12px; }
29
+ .msg { max-width:780px; width:fit-content; padding:10px 14px; border-radius:12px; line-height:1.6; }
30
+ .msg.user { align-self:flex-end; background:var(--bubble-u); color:#fff; white-space:pre-wrap; word-wrap:break-word; }
31
+ .msg.assistant { align-self:flex-start; background:var(--bubble-a); border:1px solid var(--border); min-width:120px; }
32
+ .answer p { margin:0 0 8px; } .answer p:last-child { margin-bottom:0; }
33
+ .answer h1,.answer h2,.answer h3,.answer h4 { margin:12px 0 6px; font-weight:600; }
34
+ .answer h1 { font-size:1.2em; } .answer h2 { font-size:1.1em; } .answer h3 { font-size:1em; }
35
+ .answer ul,.answer ol { margin:4px 0 8px 20px; padding:0; } .answer li { margin-bottom:2px; }
36
+ .answer a { color:#60a5fa; }
37
+ .answer blockquote { border-left:3px solid var(--border); margin:0 0 8px; padding-left:10px; color:var(--muted); }
38
+ .answer table { border-collapse:collapse; margin-bottom:8px; font-size:13px; }
39
+ .answer th,.answer td { border:1px solid var(--border); padding:4px 8px; } .answer th { background:#10182a; }
40
+ .answer code { background:var(--code-bg); border-radius:4px; padding:1px 5px; font-family:"JetBrains Mono",Consolas,"Courier New",monospace; font-size:13px; }
41
+ .answer pre { background:var(--code-bg); border-radius:8px; margin:0 0 8px; overflow-x:auto; position:relative; }
42
+ .answer pre code { background:none; padding:12px; display:block; font-size:13px; }
43
+ .copy-btn { position:absolute; top:6px; right:6px; background:#27304a; border:none; color:var(--muted); font-size:11px; padding:3px 8px; border-radius:4px; cursor:pointer; opacity:0; transition:opacity 0.15s; }
44
+ .answer pre:hover .copy-btn { opacity:1; } .copy-btn:hover { color:var(--text); }
45
  .think-box { margin-bottom:8px; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
46
  .think-box summary { cursor:pointer; padding:6px 10px; font-size:12px; color:var(--muted); background:#10182a; user-select:none; }
47
  .think-box summary::marker { color:var(--muted); }
48
+ .think-body { padding:8px 10px; color:var(--muted); font-style:italic; font-size:13px; white-space:pre-wrap; border-top:1px solid var(--border); max-height:300px; overflow-y:auto; }
 
49
  .empty { color:var(--muted); text-align:center; margin-top:40px; font-size:14px; }
50
+ footer { border-top:1px solid var(--border); padding:12px 16px; flex-shrink:0; }
51
  .row { display:flex; gap:8px; max-width:900px; margin:0 auto; }
52
  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; }
53
  textarea:focus { outline:none; border-color:var(--bubble-u); }
54
+ button#send { background:var(--bubble-u); color:#fff; border:none; border-radius:10px; padding:0 18px; font-size:14px; font-weight:600; cursor:pointer; }
55
+ button#send:disabled { background:#374151; cursor:not-allowed; }
56
  .hint { max-width:900px; margin:6px auto 0; color:var(--muted); font-size:11px; text-align:center; }
57
  </style>
58
  </head>
 
63
  <span class="spacer"></span>
64
  <a href="/v1/models" target="_blank">API · /v1/models</a>
65
  </header>
66
+ <div id="chat"><div class="empty">Type a message to start. Markdown renderscode, tables, lists all work. Reasoning streams into a collapsible box. (CPU — slow but streams live.)</div></div>
67
  <footer>
68
  <div class="row">
69
  <textarea id="input" placeholder="Send a message… (Enter to send, Shift+Enter for newline)"></textarea>
70
  <button id="send">Send</button>
71
  </div>
72
+ <div class="hint">OpenAI-compatible API at <code>/v1/chat/completions</code> · reasoning streams as <code>reasoning_content</code></div>
73
  </footer>
 
74
  <script>
75
+ marked.setOptions({ breaks: true, gfm: true });
76
+ const renderer = new marked.Renderer();
77
+ const origCode = renderer.code.bind(renderer);
78
+ renderer.code = function(code, lang) { return origCode(code,lang).replace('<pre>','<pre><button class="copy-btn" onclick="copyCode(this)">copy</button>'); };
79
+ marked.use({ renderer });
80
+ function copyCode(btn){navigator.clipboard.writeText(btn.parentElement.querySelector('code').innerText).then(()=>{btn.textContent='copied!';setTimeout(()=>btn.textContent='copy',1500);});}
81
+ function renderMd(text){const div=document.createElement('div');div.innerHTML=marked.parse(text||'');div.querySelectorAll('pre code').forEach(b=>hljs.highlightElement(b));return div;}
82
+ const chatEl=document.getElementById('chat'),inputEl=document.getElementById('input'),sendBtn=document.getElementById('send'),statusEl=document.getElementById('status');
83
+ const history=[];let busy=false;
84
+ fetch('/v1/models').then(r=>r.ok?statusEl.classList.add('ok'):statusEl.classList.add('err')).catch(()=>statusEl.classList.add('err'));
85
+ function addUser(t){chatEl.querySelector('.empty')?.remove();const d=document.createElement('div');d.className='msg user';d.textContent=t;chatEl.appendChild(d);chatEl.scrollTop=chatEl.scrollHeight;}
86
+ function addAssistant(){chatEl.querySelector('.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};}
87
+ 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='';
88
+ try{const resp=await fetch('/v1/chat/completions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:history,max_tokens:2048,stream:true})});
89
+ if(!resp.ok||!resp.body)throw new Error('HTTP '+resp.status);
90
+ const reader=resp.body.getReader();const dec=new TextDecoder();let buf='';
91
+ 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();
92
+ for(const line of lines){const s=line.trim();if(!s.startsWith('data:'))continue;const data=s.slice(5).trim();if(data==='[DONE]')continue;
93
+ try{const j=JSON.parse(data);const delta=j.choices?.[0]?.delta||{};
94
+ if(delta.reasoning_content){reasoning+=delta.reasoning_content;ui.details.style.display='';ui.thinkBody.textContent=reasoning;ui.thinkBody.scrollTop=ui.thinkBody.scrollHeight;}
95
+ if(delta.content){answer+=delta.content;ui.answer.innerHTML='';ui.answer.appendChild(renderMd(answer));if(ui.details.open){ui.details.open=false;ui.summary.textContent='Thinking (done) — click to expand';}}
96
+ chatEl.scrollTop=chatEl.scrollHeight;}catch(_){}}}
97
+ if(!reasoning&&answer.includes('</think>')){const m=answer.match(/^([\s\S]*?)<\/think>([\s\S]*)$/);if(m){reasoning=m[1].replace(/^<think>/,'').trim();answer=m[2].trim();ui.details.style.display='';ui.details.open=false;ui.summary.textContent='Thinking (done) — click to expand';ui.thinkBody.textContent=reasoning;ui.answer.innerHTML='';ui.answer.appendChild(renderMd(answer));}}
98
+ if(!answer)ui.answer.textContent='(no answer text returned)';history.push({role:'assistant',content:answer});}
99
+ catch(e){ui.answer.textContent='Error: '+e.message;}
100
+ finally{busy=false;sendBtn.disabled=false;inputEl.focus();}}
101
+ sendBtn.addEventListener('click',send);
102
+ inputEl.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}});
103
+ inputEl.addEventListener('input',()=>{inputEl.style.height='auto';inputEl.style.height=Math.min(inputEl.scrollHeight,160)+'px';});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  </script>
105
  </body>
106
  </html>