Download multichat.html from Yougori/Ling-mini-2.0-Megakernel: direct link, hf CLI and curl.
- Browser
- Download file 6.33 kB
-
https://huggingface.co/Yougori/Ling-mini-2.0-Megakernel/resolve/main/multichat.html
- Command line
-
hf download hf://Yougori/Ling-mini-2.0-Megakernel/multichat.html
-
curl -L -o multichat.html https://huggingface.co/Yougori/Ling-mini-2.0-Megakernel/resolve/main/multichat.html
6.33 kB
| <html lang="en"> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Ling-mini-2.0 路 several chats on one GPU</title> | |
| <style> | |
| :root { --line: #d9dde3; --ink: #1d2330; --muted: #667085; --bg: #f6f7f9; --you: #e8eefc; } | |
| * { box-sizing: border-box; } | |
| body { margin: 0; font: 15px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--ink); background: var(--bg); } | |
| header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; padding: 12px 18px; border-bottom: 1px solid var(--line); background: #fff; } | |
| header h1 { margin: 0; font-size: 16px; font-weight: 600; } | |
| #total { font-variant-numeric: tabular-nums; color: var(--muted); } | |
| #total b { color: var(--ink); } | |
| .all { display: flex; gap: 8px; flex: 1 1 420px; } | |
| .all input { flex: 1; } | |
| input, textarea { font: inherit; padding: 7px 10px; border: 1px solid var(--line); border-radius: 6px; background: #fff; color: var(--ink); } | |
| button { font: inherit; padding: 7px 12px; border: 1px solid var(--line); border-radius: 6px; background: #fff; color: var(--ink); cursor: pointer; } | |
| button:hover { border-color: #b8c0cc; } | |
| button:disabled { color: #a0a7b4; cursor: default; } | |
| main { display: grid; grid-template-columns: repeat(auto-fit, minmax(330px, 1fr)); gap: 12px; padding: 12px; height: calc(100vh - 64px); } | |
| .pane { display: flex; flex-direction: column; min-height: 0; background: #fff; border: 1px solid var(--line); border-radius: 8px; } | |
| .top { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--line); } | |
| .top .name { font-weight: 600; } | |
| .top .state { color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; } | |
| .log { flex: 1; overflow-y: auto; padding: 10px 12px; } | |
| .msg { margin: 0 0 10px; white-space: pre-wrap; word-wrap: break-word; } | |
| .msg.you { background: var(--you); padding: 6px 10px; border-radius: 6px; } | |
| .msg.err { color: #b42318; } | |
| .foot { display: flex; gap: 8px; padding: 8px; border-top: 1px solid var(--line); } | |
| .foot textarea { flex: 1; resize: none; height: 40px; } | |
| </style> | |
| <header> | |
| <h1>Ling-mini-2.0 路 megakernel 路 several chats on one GPU</h1> | |
| <span id="total">total <b>0</b> tok/s 路 <span id="busy">0</span> writing</span> | |
| <div class="all"><input id="allText" placeholder="Ask every open chat at once (Enter)"><button id="allSend">Ask all</button> | |
| <button id="add" title="Open another chat">+ chat</button></div> | |
| </header> | |
| <main id="panes"></main> | |
| <script> | |
| const MAX = 8 | |
| const panes = [] | |
| const $ = (s, el = document) => el.querySelector(s) | |
| function addPane() { | |
| if (panes.length >= MAX) return | |
| const id = panes.length | |
| const el = document.createElement('section') | |
| el.className = 'pane' | |
| el.innerHTML = `<div class="top"><span class="name">Chat ${id + 1}</span><span class="state">ready</span></div> | |
| <div class="log"></div> | |
| <div class="foot"><textarea placeholder="Message (Enter sends, Shift+Enter new line)"></textarea> | |
| <button class="send">Send</button><button class="clear">Clear</button></div>` | |
| $('#panes').appendChild(el) | |
| const p = { id, el, log: $('.log', el), state: $('.state', el), box: $('textarea', el), current: null, busy: false } | |
| panes.push(p) | |
| $('.send', el).onclick = () => send(p, p.box.value) | |
| $('.clear', el).onclick = () => clear(p) | |
| p.box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(p, p.box.value) } }) | |
| fetch(`/history?chat=${id}`).then((r) => r.json()).then((h) => { // what this chat already said | |
| for (const m of h.messages) p.current = line(p, m.role === 'user' ? 'you' : 'bot', m.content) | |
| if (h.writing) { p.busy = true; p.state.textContent = 'writing' } else p.current = null | |
| const ev = new EventSource(`/events?chat=${id}`) | |
| ev.onmessage = (m) => onEvent(p, JSON.parse(m.data)) | |
| }) | |
| $('#add').disabled = panes.length >= MAX | |
| } | |
| function line(p, cls, text) { | |
| const d = document.createElement('p') | |
| d.className = 'msg ' + cls | |
| d.textContent = text | |
| p.log.appendChild(d) | |
| p.log.scrollTop = p.log.scrollHeight | |
| return d | |
| } | |
| async function send(p, text) { | |
| text = text.trim() | |
| if (!text || p.busy) return | |
| p.box.value = '' | |
| const r = await fetch('/send', { method: 'POST', body: JSON.stringify({ chat: p.id, text }) }) | |
| if (!r.ok) { | |
| const e = await r.json().catch(() => ({ error: r.statusText })) | |
| line(p, 'err', e.error || 'error') | |
| } | |
| } | |
| async function clear(p) { | |
| if (p.busy) return | |
| await fetch('/reset', { method: 'POST', body: JSON.stringify({ chat: p.id }) }) | |
| } | |
| function onEvent(p, e) { | |
| if (e.type === 'user') { // a message from any open page | |
| p.busy = true | |
| line(p, 'you', e.text) | |
| p.current = line(p, 'bot', '') | |
| p.state.textContent = 'queued' | |
| } else if (e.type === 'cleared') { p.log.innerHTML = ''; p.current = null; p.state.textContent = 'ready' } | |
| else if (e.type === 'start') p.state.textContent = e.prompt_tokens > 1 ? `reading ${e.prompt_tokens} tokens` : 'writing' | |
| else if (e.type === 'delta' && p.current) { | |
| p.state.textContent = 'writing' | |
| p.current.textContent += e.text | |
| p.log.scrollTop = p.log.scrollHeight | |
| } else if (e.type === 'replace' && p.current) p.current.textContent = e.text | |
| else if (e.type === 'done') { | |
| p.busy = false | |
| p.state.textContent = `${e.tokens} tokens 路 ${Math.round(e.rate)} tok/s` + (e.note ? ` 路 ${e.note}` : '') | |
| } else if (e.type === 'error') { | |
| p.busy = false | |
| line(p, 'err', e.text) | |
| p.state.textContent = 'ready' | |
| } | |
| } | |
| async function poll() { | |
| try { | |
| const s = await (await fetch('/status')).json() | |
| $('#total b').textContent = Math.round(s.total_rate).toLocaleString('en-US') | |
| $('#busy').textContent = s.busy.length | |
| } catch (_) {} | |
| setTimeout(poll, 500) | |
| } | |
| $('#allSend').onclick = () => { const t = $('#allText').value; $('#allText').value = ''; panes.forEach((p) => send(p, t)) } | |
| $('#allText').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('#allSend').click() }) | |
| $('#add').onclick = addPane | |
| for (let i = 0; i < 3; i++) addPane() | |
| poll() | |
| </script> | |
| </html> | |