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>
|