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>