File size: 6,330 Bytes
e7654c2 | 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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | <!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>
|