Ling-mini-2.0-Megakernel / multichat.html
Yougori's picture
Add files using upload-large-folder tool
e7654c2 verified
Raw History Blame Contribute Delete
6.33 kB
<!doctype html>
<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>