File size: 10,032 Bytes
8948dd0
 
 
 
 
 
d6e8ac2
 
 
 
 
 
 
 
 
8948dd0
d6e8ac2
8948dd0
 
d6e8ac2
8948dd0
d6e8ac2
8948dd0
 
 
 
 
 
d6e8ac2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8948dd0
 
 
d6e8ac2
8948dd0
d6e8ac2
8948dd0
 
 
d6e8ac2
 
8948dd0
 
 
 
 
 
 
 
 
 
d6e8ac2
8948dd0
 
 
 
 
d6e8ac2
8948dd0
 
d6e8ac2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8948dd0
 
 
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Gemma-4-E4B Uncensored — Chat</title>
<script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github-dark.min.css"/>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/javascript.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/json.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/cpp.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/typescript.min.js"></script>
<style>
  :root { --bg:#0b0f19; --panel:#141a2a; --bubble-u:#2563eb; --bubble-a:#1f2839; --text:#e5e7eb; --muted:#9ca3af; --border:#27304a; --code-bg:#0d1117; }
  * { 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; flex-shrink:0; }
  header h1 { font-size:15px; margin:0; font-weight:600; }
  header .dot { width:9px; height:9px; border-radius:50%; background:#6b7280; flex-shrink:0; }
  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:780px; width:fit-content; padding:10px 14px; border-radius:12px; line-height:1.6; }
  .msg.user { align-self:flex-end; background:var(--bubble-u); color:#fff; white-space:pre-wrap; word-wrap:break-word; }
  .msg.assistant { align-self:flex-start; background:var(--bubble-a); border:1px solid var(--border); min-width:120px; }
  .answer p { margin:0 0 8px; } .answer p:last-child { margin-bottom:0; }
  .answer h1,.answer h2,.answer h3,.answer h4 { margin:12px 0 6px; font-weight:600; }
  .answer h1 { font-size:1.2em; } .answer h2 { font-size:1.1em; } .answer h3 { font-size:1em; }
  .answer ul,.answer ol { margin:4px 0 8px 20px; padding:0; } .answer li { margin-bottom:2px; }
  .answer a { color:#60a5fa; }
  .answer blockquote { border-left:3px solid var(--border); margin:0 0 8px; padding-left:10px; color:var(--muted); }
  .answer table { border-collapse:collapse; margin-bottom:8px; font-size:13px; }
  .answer th,.answer td { border:1px solid var(--border); padding:4px 8px; } .answer th { background:#10182a; }
  .answer code { background:var(--code-bg); border-radius:4px; padding:1px 5px; font-family:"JetBrains Mono",Consolas,"Courier New",monospace; font-size:13px; }
  .answer pre { background:var(--code-bg); border-radius:8px; margin:0 0 8px; overflow-x:auto; position:relative; }
  .answer pre code { background:none; padding:12px; display:block; font-size:13px; }
  .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; }
  .answer pre:hover .copy-btn { opacity:1; } .copy-btn:hover { color:var(--text); }
  .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); max-height:300px; overflow-y:auto; }
  .empty { color:var(--muted); text-align:center; margin-top:40px; font-size:14px; }
  footer { border-top:1px solid var(--border); padding:12px 16px; flex-shrink:0; }
  .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#send { background:var(--bubble-u); color:#fff; border:none; border-radius:10px; padding:0 18px; font-size:14px; font-weight:600; cursor:pointer; }
  button#send: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-E4B Uncensored (Q8_K_P)</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. Markdown renders — code, tables, lists all work. Reasoning streams into a collapsible box. (CPU — 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 <code>reasoning_content</code></div>
</footer>
<script>
marked.setOptions({ breaks: true, gfm: true });
const renderer = new marked.Renderer();
const origCode = renderer.code.bind(renderer);
renderer.code = function(code, lang) { return origCode(code,lang).replace('<pre>','<pre><button class="copy-btn" onclick="copyCode(this)">copy</button>'); };
marked.use({ renderer });
function copyCode(btn){navigator.clipboard.writeText(btn.parentElement.querySelector('code').innerText).then(()=>{btn.textContent='copied!';setTimeout(()=>btn.textContent='copy',1500);});}
function renderMd(text){const div=document.createElement('div');div.innerHTML=marked.parse(text||'');div.querySelectorAll('pre code').forEach(b=>hljs.highlightElement(b));return div;}
const chatEl=document.getElementById('chat'),inputEl=document.getElementById('input'),sendBtn=document.getElementById('send'),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(t){chatEl.querySelector('.empty')?.remove();const d=document.createElement('div');d.className='msg user';d.textContent=t;chatEl.appendChild(d);chatEl.scrollTop=chatEl.scrollHeight;}
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};}
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({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;ui.thinkBody.scrollTop=ui.thinkBody.scrollHeight;}
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';}}
chatEl.scrollTop=chatEl.scrollHeight;}catch(_){}}}
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));}}
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();}});
inputEl.addEventListener('input',()=>{inputEl.style.height='auto';inputEl.style.height=Math.min(inputEl.scrollHeight,160)+'px';});
</script>
</body>
</html>