@threadcrumbs/common 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/totals.mjs ADDED
@@ -0,0 +1,94 @@
1
+ import { compactNumber } from './format.mjs'
2
+ import { esc, instAttrs, termButton } from './html.mjs'
3
+ import { FOOT_DRAW, FOOT_SIZE, binTimes } from './pictures.mjs'
4
+ import { linesHTML } from './rows.mjs'
5
+ import { URGENCY_ORDER, lampHTML, statusOf } from './status.mjs'
6
+
7
+ export function statusCounts(list) {
8
+ const counts = Object.fromEntries(URGENCY_ORDER.map((k) => [k, 0]))
9
+ for (const s of list) if (s.status in counts) counts[s.status]++
10
+ return counts
11
+ }
12
+
13
+ export function lampsHTML(counts, total, pressed = null) {
14
+ const lamps = URGENCY_ORDER.map((k) => {
15
+ const name = statusOf(k).name
16
+ const n = counts[k] || 0
17
+ return `<button type="button" class="lamp-btn" data-st="${k}" data-act="status" data-key="${k}" aria-pressed="${pressed === k}"${n ? '' : ' data-zero'} data-tip="${esc(`${name}: ${n}. Show only these.`)}" aria-label="${esc(`${name}: ${n}`)}">${lampHTML(k)}<b>${n}</b><span class="word">${esc(name)}</span></button>`
18
+ }).join('')
19
+ return `${lamps}<button type="button" class="lamp-btn total" data-act="status" data-key="" aria-pressed="false" data-tip="${esc(`${total} threads in total. Show them all.`)}" aria-label="${esc(`${total} threads in total`)}"><b>${total}</b><span class="word">${total === 1 ? 'Thread' : 'Threads'}</span></button>`
20
+ }
21
+
22
+ export function footerTotals(list) {
23
+ let out = 0
24
+ let outKnown = false
25
+ let added = 0
26
+ let removed = 0
27
+ let linesKnown = false
28
+ for (const s of list) {
29
+ if (s.tokens && Number.isFinite(s.tokens.output)) {
30
+ out += s.tokens.output
31
+ outKnown = true
32
+ }
33
+ if (s.lines) {
34
+ added += s.lines.added || 0
35
+ removed += s.lines.removed || 0
36
+ linesKnown = true
37
+ }
38
+ }
39
+ const counts = statusCounts(list)
40
+ return { live: list.filter((s) => s.live).length, total: list.length, sts: URGENCY_ORDER.map((k) => counts[k]), out, outKnown, added, removed, linesKnown }
41
+ }
42
+
43
+ function totalHTML(k, term, spec, val, shown, label) {
44
+ const svg = spec ? FOOT_DRAW[spec.k](spec, FOOT_SIZE) : ''
45
+ const attrs = svg ? instAttrs(spec, val) : ''
46
+ return `<span class="tot" data-k="${k}">${svg ? `<span class="i-wrap" data-term="${term}" ${attrs}>${svg}</span>` : ''}<span class="tot-t"><b>${shown}</b>${termButton(term, label, attrs)}</span></span>`
47
+ }
48
+
49
+ export function footerHTML(t, { short = false, bars = null } = {}) {
50
+ const out = compactNumber(t.out)
51
+ return [
52
+ totalHTML('live', 'liveAgents', { k: 'waffle', run: t.live, total: t.total, sts: t.sts }, `${t.live} of ${t.total} threads`, t.live, short ? 'Live' : 'Live agents'),
53
+ totalHTML('output', 'outputTotal', t.outKnown && { k: 'cols', v: t.out, base: 1000, ...(bars ? { bars } : {}) }, out, t.outKnown ? esc(out) : '—', short ? 'Output' : 'Output tokens'),
54
+ totalHTML('lines', 'linesTotal', t.linesKnown && { k: 'pie', a: t.added, r: t.removed }, `+${compactNumber(t.added)} −${compactNumber(t.removed)}`, t.linesKnown ? linesHTML({ added: t.added, removed: t.removed }) : '—', short ? 'Lines' : 'Lines changed'),
55
+ ].join('')
56
+ }
57
+
58
+ export const OUTPUT_WINDOW_MS = 60 * 60 * 1000
59
+ export const OUTPUT_MIN_SAMPLES = 3
60
+ export const OUTPUT_BINS = 14
61
+
62
+ export const emptyOutputLog = () => ({ since: null, last: new Map(), samples: [] })
63
+
64
+ export function recordOutput(log, sessions, now) {
65
+ const last = new Map()
66
+ const grew = []
67
+ for (const s of sessions) {
68
+ const out = s.tokens?.output
69
+ if (!Number.isFinite(out)) continue
70
+ last.set(s.id, out)
71
+ const before = log.last.get(s.id)
72
+ if (before != null && out > before) grew.push([s.id, out - before])
73
+ }
74
+ const samples = log.samples.filter((x) => now - x.at <= OUTPUT_WINDOW_MS)
75
+ if (grew.length) samples.push({ at: now, out: new Map(grew) })
76
+ return { since: log.since ?? now, last, samples }
77
+ }
78
+
79
+ export function outputBars(log, ids, now, n = OUTPUT_BINS) {
80
+ if (log.since == null) return null
81
+ const shown = new Set(ids)
82
+ const times = []
83
+ const amounts = []
84
+ for (const x of log.samples) {
85
+ let v = 0
86
+ for (const [id, d] of x.out) if (shown.has(id)) v += d
87
+ if (!v) continue
88
+ times.push(x.at)
89
+ amounts.push(v)
90
+ }
91
+ if (times.length < OUTPUT_MIN_SAMPLES) return null
92
+ const from = Math.max(log.since, now - OUTPUT_WINDOW_MS)
93
+ return binTimes(times, from, Math.max(now, from + 1), amounts, n)
94
+ }