bee-ladybug 0.2.0 → 0.4.0
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/{README.md → README.MD} +77 -14
- package/package.json +3 -3
- package/src/adapters/AntAdapter.js +144 -0
- package/src/adapters/CanvasAdapter.js +63 -0
- package/src/adapters/SpiderAdapter.js +160 -0
- package/src/core/BeeLadybugCore.js +196 -27
- package/src/core/UIOverlay.js +225 -3
- package/src/index.d.ts +66 -2
- package/src/index.js +2 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { UIOverlay } from './UIOverlay.js';
|
|
10
10
|
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
11
11
|
|
|
12
|
-
export const BEE_LADYBUG_VERSION = '0.
|
|
12
|
+
export const BEE_LADYBUG_VERSION = '0.4.0';
|
|
13
13
|
|
|
14
14
|
export const CORE_DEFAULTS = Object.freeze({
|
|
15
15
|
toggleKey: 'F2',
|
|
@@ -35,7 +35,8 @@ export const WELL_KNOWN_TYPES = Object.freeze([
|
|
|
35
35
|
'error',
|
|
36
36
|
'state',
|
|
37
37
|
'sys',
|
|
38
|
-
'clear'
|
|
38
|
+
'clear',
|
|
39
|
+
'telemetry'
|
|
39
40
|
]);
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -93,8 +94,10 @@ export class BeeLadybugCore {
|
|
|
93
94
|
this.#onKeyDown = (event) => this.#handleKey(event);
|
|
94
95
|
this.#onTick = (now) => this.#tick(now);
|
|
95
96
|
this.#started = false;
|
|
96
|
-
this.#
|
|
97
|
+
this.#assistants = new Map();
|
|
98
|
+
this.#activeAssistant = null;
|
|
97
99
|
this.#askBusy = false;
|
|
100
|
+
this.#adapters = new Map();
|
|
98
101
|
|
|
99
102
|
this.ui = null;
|
|
100
103
|
if (cfg.ui !== false && typeof document !== 'undefined') {
|
|
@@ -124,8 +127,13 @@ export class BeeLadybugCore {
|
|
|
124
127
|
#onKeyDown;
|
|
125
128
|
#onTick;
|
|
126
129
|
#started;
|
|
127
|
-
|
|
130
|
+
/** @type {Map<string, { name: string, complete: Function }>} */
|
|
131
|
+
#assistants;
|
|
132
|
+
/** @type {string | null} */
|
|
133
|
+
#activeAssistant;
|
|
128
134
|
#askBusy;
|
|
135
|
+
/** @type {Map<string, { name: string, label: string, enabled: boolean, enable: Function, disable: Function }>} */
|
|
136
|
+
#adapters;
|
|
129
137
|
|
|
130
138
|
/**
|
|
131
139
|
* Public adapter contract. `type` is a channel name; `payload` is raw data.
|
|
@@ -159,51 +167,86 @@ export class BeeLadybugCore {
|
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
get hasAssistant() {
|
|
162
|
-
return Boolean(this.#
|
|
170
|
+
return Boolean(this.#activeProvider());
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
get assistantBusy() {
|
|
166
174
|
return this.#askBusy;
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
/** Registered provider names, insertion order. */
|
|
178
|
+
getAssistants() {
|
|
179
|
+
return [...this.#assistants.keys()];
|
|
180
|
+
}
|
|
181
|
+
|
|
169
182
|
/**
|
|
170
|
-
*
|
|
183
|
+
* Add a model without dropping the others. Same `name` updates that slot.
|
|
184
|
+
* The first registration becomes active; later ones do not steal focus.
|
|
171
185
|
* @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
|
|
172
186
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
registerAssistant(provider) {
|
|
188
|
+
return this.#putAssistant(provider, { activate: !this.#activeAssistant, via: 'registerAssistant' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {string} name
|
|
193
|
+
*/
|
|
194
|
+
setActiveAssistant(name) {
|
|
195
|
+
const key = String(name || '');
|
|
196
|
+
if (!this.#assistants.has(key)) {
|
|
176
197
|
this.sendData('warn', {
|
|
177
198
|
source: 'ai',
|
|
178
|
-
message:
|
|
199
|
+
message: `No assistant named "${key}". bee.registerAssistant({ name, complete }) first.`
|
|
179
200
|
});
|
|
180
201
|
return this;
|
|
181
202
|
}
|
|
182
|
-
this.#
|
|
183
|
-
this.sendData('state', { source: 'ai', key: 'ai.assist', value:
|
|
184
|
-
this.sendData('log', {
|
|
185
|
-
source: 'ai',
|
|
186
|
-
message: `assistant ready (${next.name})`
|
|
187
|
-
});
|
|
203
|
+
this.#activeAssistant = key;
|
|
204
|
+
this.sendData('state', { source: 'ai', key: 'ai.assist', value: key });
|
|
205
|
+
this.sendData('log', { source: 'ai', message: `assistant active (${key})` });
|
|
188
206
|
return this;
|
|
189
207
|
}
|
|
190
208
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Register and activate one provider. Does not remove the others.
|
|
211
|
+
* @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
|
|
212
|
+
*/
|
|
213
|
+
setAssistant(provider) {
|
|
214
|
+
return this.#putAssistant(provider, { activate: true, via: 'setAssistant' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {string} [name] omit to drop every provider
|
|
219
|
+
*/
|
|
220
|
+
clearAssistant(name) {
|
|
221
|
+
if (name == null || name === '') {
|
|
222
|
+
this.#assistants.clear();
|
|
223
|
+
this.#activeAssistant = null;
|
|
224
|
+
} else {
|
|
225
|
+
const key = String(name);
|
|
226
|
+
this.#assistants.delete(key);
|
|
227
|
+
if (this.#activeAssistant === key) {
|
|
228
|
+
this.#activeAssistant = this.#assistants.keys().next().value ?? null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
this.sendData('state', {
|
|
232
|
+
source: 'ai',
|
|
233
|
+
key: 'ai.assist',
|
|
234
|
+
value: this.#activeAssistant ?? 'off'
|
|
235
|
+
});
|
|
194
236
|
return this;
|
|
195
237
|
}
|
|
196
238
|
|
|
197
239
|
/**
|
|
198
|
-
* Send recent telemetry to the
|
|
240
|
+
* Send recent telemetry to the active assistant and print the answer.
|
|
199
241
|
* @param {string} [question]
|
|
200
242
|
* @returns {Promise<string|null>}
|
|
201
243
|
*/
|
|
202
244
|
async ask(question = 'Diagnose the current telemetry.') {
|
|
203
|
-
|
|
245
|
+
const assistant = this.#activeProvider();
|
|
246
|
+
if (!assistant) {
|
|
204
247
|
this.sendData('warn', {
|
|
205
248
|
source: 'ai',
|
|
206
|
-
message: 'No assistant. bee.
|
|
249
|
+
message: 'No assistant. bee.registerAssistant({ name, complete }) then ASK AI.'
|
|
207
250
|
});
|
|
208
251
|
return null;
|
|
209
252
|
}
|
|
@@ -217,10 +260,10 @@ export class BeeLadybugCore {
|
|
|
217
260
|
this.sendData('log', { source: 'ai', message: `ask: ${q}` });
|
|
218
261
|
try {
|
|
219
262
|
const snapshot = buildAssistantSnapshot(this, q);
|
|
220
|
-
const raw = await
|
|
263
|
+
const raw = await assistant.complete({ question: q, snapshot });
|
|
221
264
|
const text = String(raw ?? '').trim() || '(empty answer)';
|
|
222
265
|
this.sendData('log', { source: 'ai', message: text });
|
|
223
|
-
this.sendData('state', { source: 'ai', key: 'ai.assist', value:
|
|
266
|
+
this.sendData('state', { source: 'ai', key: 'ai.assist', value: assistant.name });
|
|
224
267
|
return text;
|
|
225
268
|
} catch (err) {
|
|
226
269
|
this.sendData('error', {
|
|
@@ -230,7 +273,7 @@ export class BeeLadybugCore {
|
|
|
230
273
|
this.sendData('state', {
|
|
231
274
|
source: 'ai',
|
|
232
275
|
key: 'ai.assist',
|
|
233
|
-
value:
|
|
276
|
+
value: assistant?.name ?? 'error'
|
|
234
277
|
});
|
|
235
278
|
return null;
|
|
236
279
|
} finally {
|
|
@@ -238,6 +281,117 @@ export class BeeLadybugCore {
|
|
|
238
281
|
}
|
|
239
282
|
}
|
|
240
283
|
|
|
284
|
+
#activeProvider() {
|
|
285
|
+
return this.#activeAssistant ? this.#assistants.get(this.#activeAssistant) ?? null : null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Register a toggleable diagnostic adapter. The core stores hooks only —
|
|
290
|
+
* it does not import or special-case adapter modules.
|
|
291
|
+
* @param {string} name
|
|
292
|
+
* @param {{ enable: () => void, disable: () => void, label?: string }} hooks
|
|
293
|
+
*/
|
|
294
|
+
registerAdapter(name, hooks = {}) {
|
|
295
|
+
const key = String(name || '').trim();
|
|
296
|
+
if (!key || typeof hooks.enable !== 'function' || typeof hooks.disable !== 'function') {
|
|
297
|
+
this.sendData('warn', {
|
|
298
|
+
source: 'core',
|
|
299
|
+
message: 'registerAdapter() needs a name and { enable, disable }.'
|
|
300
|
+
});
|
|
301
|
+
return this;
|
|
302
|
+
}
|
|
303
|
+
const label = String(hooks.label || key).trim() || key;
|
|
304
|
+
this.#adapters.set(key, {
|
|
305
|
+
name: key,
|
|
306
|
+
label,
|
|
307
|
+
enabled: true,
|
|
308
|
+
enable: hooks.enable,
|
|
309
|
+
disable: hooks.disable
|
|
310
|
+
});
|
|
311
|
+
this.sendData('sys', { op: 'adapter', name: key, registered: true, label, enabled: true });
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @param {string} name
|
|
317
|
+
*/
|
|
318
|
+
unregisterAdapter(name) {
|
|
319
|
+
const key = String(name || '').trim();
|
|
320
|
+
if (!key || !this.#adapters.has(key)) return this;
|
|
321
|
+
this.#adapters.delete(key);
|
|
322
|
+
this.sendData('sys', { op: 'adapter', name: key, registered: false });
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Registered diagnostic adapters, insertion order. */
|
|
327
|
+
getAdapters() {
|
|
328
|
+
return [...this.#adapters.values()].map((entry) => ({
|
|
329
|
+
name: entry.name,
|
|
330
|
+
label: entry.label,
|
|
331
|
+
enabled: entry.enabled
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {string} name
|
|
337
|
+
*/
|
|
338
|
+
toggleAdapter(name) {
|
|
339
|
+
const entry = this.#adapters.get(String(name || '').trim());
|
|
340
|
+
if (!entry) return this;
|
|
341
|
+
return this.setAdapterEnabled(entry.name, !entry.enabled);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {string} name
|
|
346
|
+
* @param {boolean} enabled
|
|
347
|
+
*/
|
|
348
|
+
setAdapterEnabled(name, enabled) {
|
|
349
|
+
const entry = this.#adapters.get(String(name || '').trim());
|
|
350
|
+
if (!entry) return this;
|
|
351
|
+
const next = Boolean(enabled);
|
|
352
|
+
if (entry.enabled === next) return this;
|
|
353
|
+
try {
|
|
354
|
+
if (next) entry.enable();
|
|
355
|
+
else entry.disable();
|
|
356
|
+
entry.enabled = next;
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.warn('[BeeLadybug] adapter toggle error', err);
|
|
359
|
+
}
|
|
360
|
+
this.sendData('sys', {
|
|
361
|
+
op: 'adapter',
|
|
362
|
+
name: entry.name,
|
|
363
|
+
enabled: entry.enabled
|
|
364
|
+
});
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @param {{ name?: string, complete: Function }} provider
|
|
370
|
+
* @param {{ activate: boolean, via: string }} opts
|
|
371
|
+
*/
|
|
372
|
+
#putAssistant(provider, opts) {
|
|
373
|
+
const next = normalizeAssistant(provider);
|
|
374
|
+
if (!next) {
|
|
375
|
+
this.sendData('warn', {
|
|
376
|
+
source: 'ai',
|
|
377
|
+
message: `${opts.via}() needs { complete(ctx) }. No vendor is bundled.`
|
|
378
|
+
});
|
|
379
|
+
return this;
|
|
380
|
+
}
|
|
381
|
+
this.#assistants.set(next.name, next);
|
|
382
|
+
if (opts.activate) this.#activeAssistant = next.name;
|
|
383
|
+
this.sendData('state', {
|
|
384
|
+
source: 'ai',
|
|
385
|
+
key: 'ai.assist',
|
|
386
|
+
value: this.#activeAssistant ?? next.name
|
|
387
|
+
});
|
|
388
|
+
this.sendData('log', {
|
|
389
|
+
source: 'ai',
|
|
390
|
+
message: `assistant ready (${next.name})`
|
|
391
|
+
});
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
241
395
|
|
|
242
396
|
start() {
|
|
243
397
|
if (this.#started) return this;
|
|
@@ -280,8 +434,10 @@ export class BeeLadybugCore {
|
|
|
280
434
|
this.#history.clear();
|
|
281
435
|
this.#hud.clear();
|
|
282
436
|
this.#latest.clear();
|
|
283
|
-
this.#
|
|
437
|
+
this.#assistants.clear();
|
|
438
|
+
this.#activeAssistant = null;
|
|
284
439
|
this.#askBusy = false;
|
|
440
|
+
this.#adapters.clear();
|
|
285
441
|
this.ui?.destroy();
|
|
286
442
|
this.ui = null;
|
|
287
443
|
return this;
|
|
@@ -307,6 +463,10 @@ export class BeeLadybugCore {
|
|
|
307
463
|
return this.visible ? this.hide() : this.show();
|
|
308
464
|
}
|
|
309
465
|
|
|
466
|
+
/**
|
|
467
|
+
* Stops the simulated clock only. Overlay RAF keeps running.
|
|
468
|
+
* Adapters must listen for `sys` packets with `op: 'freeze'`.
|
|
469
|
+
*/
|
|
310
470
|
freeze() {
|
|
311
471
|
if (this.frozen) return this;
|
|
312
472
|
this.frozen = true;
|
|
@@ -365,7 +525,9 @@ export class BeeLadybugCore {
|
|
|
365
525
|
logCount: this.#logs.length,
|
|
366
526
|
hud: this.getHud(),
|
|
367
527
|
mode: this.#modeLabel(),
|
|
368
|
-
assistant: this.#
|
|
528
|
+
assistant: this.#activeAssistant,
|
|
529
|
+
assistants: this.getAssistants(),
|
|
530
|
+
adapters: this.getAdapters()
|
|
369
531
|
};
|
|
370
532
|
}
|
|
371
533
|
|
|
@@ -477,6 +639,13 @@ export class BeeLadybugCore {
|
|
|
477
639
|
return;
|
|
478
640
|
}
|
|
479
641
|
|
|
642
|
+
if (type === 'telemetry') {
|
|
643
|
+
const name = String(payload.kind ?? payload.name ?? 'telemetry');
|
|
644
|
+
const value = numeric(payload.duration ?? payload.value);
|
|
645
|
+
if (value != null) this.#pushHistory(name, value);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
480
649
|
if (isConsoleType(type) || packet.level) {
|
|
481
650
|
this.#pushLog(packet);
|
|
482
651
|
}
|
package/src/core/UIOverlay.js
CHANGED
|
@@ -99,8 +99,14 @@ const STYLES = `
|
|
|
99
99
|
border: 1px solid rgba(110, 231, 255, 0.35);
|
|
100
100
|
padding: 1px 5px;
|
|
101
101
|
}
|
|
102
|
-
.bl-mode {
|
|
102
|
+
.bl-mode-wrap {
|
|
103
103
|
margin-left: auto;
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 5px;
|
|
107
|
+
position: relative;
|
|
108
|
+
}
|
|
109
|
+
.bl-mode {
|
|
104
110
|
font-size: 10px;
|
|
105
111
|
font-weight: 700;
|
|
106
112
|
letter-spacing: 0.08em;
|
|
@@ -108,6 +114,50 @@ const STYLES = `
|
|
|
108
114
|
}
|
|
109
115
|
.bl-mode[data-mode="FREEZE"] { color: #ff3b4e; }
|
|
110
116
|
.bl-mode[data-mode^="SLOW"] { color: #ffd36a; }
|
|
117
|
+
.bl-info {
|
|
118
|
+
appearance: none;
|
|
119
|
+
position: relative;
|
|
120
|
+
flex: 0 0 auto;
|
|
121
|
+
width: 15px;
|
|
122
|
+
height: 15px;
|
|
123
|
+
padding: 0;
|
|
124
|
+
border: 1px solid rgba(139, 145, 163, 0.7);
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
background: transparent;
|
|
127
|
+
color: #8b91a3;
|
|
128
|
+
font: 700 9px ui-monospace, Consolas, monospace;
|
|
129
|
+
line-height: 1;
|
|
130
|
+
cursor: help;
|
|
131
|
+
}
|
|
132
|
+
.bl-info:hover,
|
|
133
|
+
.bl-info:focus-visible {
|
|
134
|
+
color: #ffd36a;
|
|
135
|
+
border-color: #ffd36a;
|
|
136
|
+
outline: none;
|
|
137
|
+
}
|
|
138
|
+
.bl-info-tip {
|
|
139
|
+
display: none;
|
|
140
|
+
position: absolute;
|
|
141
|
+
top: calc(100% + 6px);
|
|
142
|
+
right: 0;
|
|
143
|
+
width: 228px;
|
|
144
|
+
padding: 8px 9px;
|
|
145
|
+
background: #0c0e14;
|
|
146
|
+
border: 1px solid rgba(255, 211, 106, 0.45);
|
|
147
|
+
color: #e9edf5;
|
|
148
|
+
font: 400 10px/1.45 ui-monospace, Consolas, monospace;
|
|
149
|
+
letter-spacing: 0;
|
|
150
|
+
text-transform: none;
|
|
151
|
+
z-index: 3;
|
|
152
|
+
pointer-events: none;
|
|
153
|
+
white-space: normal;
|
|
154
|
+
text-align: left;
|
|
155
|
+
}
|
|
156
|
+
.bl-info:hover .bl-info-tip,
|
|
157
|
+
.bl-info:focus .bl-info-tip,
|
|
158
|
+
.bl-info:focus-visible .bl-info-tip {
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
111
161
|
.bl-stats {
|
|
112
162
|
display: grid;
|
|
113
163
|
grid-template-columns: 1fr 1fr;
|
|
@@ -189,6 +239,66 @@ const STYLES = `
|
|
|
189
239
|
border-color: #ff3b4e;
|
|
190
240
|
color: #fff;
|
|
191
241
|
}
|
|
242
|
+
.bl-providers {
|
|
243
|
+
display: inline-flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
gap: 3px;
|
|
246
|
+
}
|
|
247
|
+
.bl-providers:empty {
|
|
248
|
+
display: none;
|
|
249
|
+
}
|
|
250
|
+
.bl-adapters {
|
|
251
|
+
display: inline-flex;
|
|
252
|
+
align-items: center;
|
|
253
|
+
gap: 3px;
|
|
254
|
+
}
|
|
255
|
+
.bl-adapters:empty {
|
|
256
|
+
display: none;
|
|
257
|
+
}
|
|
258
|
+
.bl-adapter {
|
|
259
|
+
appearance: none;
|
|
260
|
+
background: #16181f;
|
|
261
|
+
color: #8b91a3;
|
|
262
|
+
border: 1px solid rgba(139, 145, 163, 0.45);
|
|
263
|
+
font: 700 9px ui-monospace, Consolas, monospace;
|
|
264
|
+
letter-spacing: 0.06em;
|
|
265
|
+
padding: 5px 7px;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
.bl-adapter[data-hot="true"] {
|
|
269
|
+
color: #fff6d8;
|
|
270
|
+
border-color: #ffd36a;
|
|
271
|
+
}
|
|
272
|
+
.bl-adapter[data-flash="true"] {
|
|
273
|
+
animation: bl-adapter-flash 0.55s ease;
|
|
274
|
+
}
|
|
275
|
+
@keyframes bl-adapter-flash {
|
|
276
|
+
0% {
|
|
277
|
+
background: #ffd36a;
|
|
278
|
+
color: #05060a;
|
|
279
|
+
border-color: #ffd36a;
|
|
280
|
+
}
|
|
281
|
+
100% {
|
|
282
|
+
background: #16181f;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
.bl-provider {
|
|
286
|
+
appearance: none;
|
|
287
|
+
width: 22px;
|
|
288
|
+
height: 22px;
|
|
289
|
+
padding: 0;
|
|
290
|
+
background: #16181f;
|
|
291
|
+
color: #fff6d8;
|
|
292
|
+
border: 1px solid rgba(255, 211, 106, 0.45);
|
|
293
|
+
font: 700 8px ui-monospace, Consolas, monospace;
|
|
294
|
+
letter-spacing: 0.04em;
|
|
295
|
+
cursor: pointer;
|
|
296
|
+
}
|
|
297
|
+
.bl-provider[data-hot="true"] {
|
|
298
|
+
background: #6ee7ff;
|
|
299
|
+
border-color: #6ee7ff;
|
|
300
|
+
color: #05060a;
|
|
301
|
+
}
|
|
192
302
|
.bl-hint {
|
|
193
303
|
margin-left: auto;
|
|
194
304
|
font-size: 9px;
|
|
@@ -196,6 +306,12 @@ const STYLES = `
|
|
|
196
306
|
}
|
|
197
307
|
@media (prefers-reduced-motion: reduce) {
|
|
198
308
|
.bl-scan { display: none; }
|
|
309
|
+
.bl-adapter[data-flash="true"] {
|
|
310
|
+
animation: none;
|
|
311
|
+
background: #ffd36a;
|
|
312
|
+
color: #05060a;
|
|
313
|
+
border-color: #ffd36a;
|
|
314
|
+
}
|
|
199
315
|
}
|
|
200
316
|
`;
|
|
201
317
|
|
|
@@ -213,6 +329,10 @@ export class UIOverlay {
|
|
|
213
329
|
this.#stickBottom = true;
|
|
214
330
|
this.#lastLogId = 0;
|
|
215
331
|
this.#mounted = false;
|
|
332
|
+
this.#providerSig = '';
|
|
333
|
+
this.#adapterSig = '';
|
|
334
|
+
this.#flashAt = new Map();
|
|
335
|
+
this.#flashTimers = new Map();
|
|
216
336
|
}
|
|
217
337
|
|
|
218
338
|
#host;
|
|
@@ -221,6 +341,10 @@ export class UIOverlay {
|
|
|
221
341
|
#stickBottom;
|
|
222
342
|
#lastLogId;
|
|
223
343
|
#mounted;
|
|
344
|
+
#providerSig;
|
|
345
|
+
#adapterSig;
|
|
346
|
+
#flashAt;
|
|
347
|
+
#flashTimers;
|
|
224
348
|
|
|
225
349
|
/** @param {'auto' | HTMLElement} [target] */
|
|
226
350
|
mount(target = 'auto') {
|
|
@@ -247,7 +371,13 @@ export class UIOverlay {
|
|
|
247
371
|
<span class="bl-mark" aria-hidden="true"></span>
|
|
248
372
|
<span class="bl-title">BEE LADYBUG</span>
|
|
249
373
|
<span class="bl-badge">CORE</span>
|
|
250
|
-
<span class="bl-mode
|
|
374
|
+
<span class="bl-mode-wrap">
|
|
375
|
+
<span class="bl-mode" data-el="mode">LIVE</span>
|
|
376
|
+
<button class="bl-info" type="button" aria-describedby="bl-freeze-tip" aria-label="Freeze stops the simulated clock, not this overlay">
|
|
377
|
+
<span aria-hidden="true">ⓘ</span>
|
|
378
|
+
<span class="bl-info-tip" id="bl-freeze-tip">Freeze does not stop this overlay. It only stops the simulated clock. Adapters must listen for the sys event (op: freeze).</span>
|
|
379
|
+
</button>
|
|
380
|
+
</span>
|
|
251
381
|
</div>
|
|
252
382
|
<div class="bl-stats">
|
|
253
383
|
<div>HOST <b data-el="hostFps">—</b></div>
|
|
@@ -269,6 +399,8 @@ export class UIOverlay {
|
|
|
269
399
|
<button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
|
|
270
400
|
<button class="bl-btn" data-act="live" type="button">1x LIVE</button>
|
|
271
401
|
<button class="bl-btn" data-act="ask" type="button">ASK AI</button>
|
|
402
|
+
<span class="bl-providers" data-el="providers"></span>
|
|
403
|
+
<span class="bl-adapters" data-el="adapters"></span>
|
|
272
404
|
<span class="bl-hint">F2 overlay</span>
|
|
273
405
|
</div>
|
|
274
406
|
</div>
|
|
@@ -286,7 +418,9 @@ export class UIOverlay {
|
|
|
286
418
|
graphName: this.#root.querySelector('[data-el="graphName"]'),
|
|
287
419
|
graphStats: this.#root.querySelector('[data-el="graphStats"]'),
|
|
288
420
|
hud: this.#root.querySelector('[data-el="hud"]'),
|
|
289
|
-
console: this.#root.querySelector('[data-el="console"]')
|
|
421
|
+
console: this.#root.querySelector('[data-el="console"]'),
|
|
422
|
+
providers: this.#root.querySelector('[data-el="providers"]'),
|
|
423
|
+
adapters: this.#root.querySelector('[data-el="adapters"]')
|
|
290
424
|
};
|
|
291
425
|
|
|
292
426
|
this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
|
|
@@ -297,6 +431,8 @@ export class UIOverlay {
|
|
|
297
431
|
else if (act === 'freeze') this.core.toggleFreeze();
|
|
298
432
|
else if (act === 'live') this.core.restoreRealtime();
|
|
299
433
|
else if (act === 'ask') this.core.ask();
|
|
434
|
+
else if (act === 'assist') this.core.setActiveAssistant(btn.getAttribute('data-name'));
|
|
435
|
+
else if (act === 'adapter') this.core.toggleAdapter(btn.getAttribute('data-name'));
|
|
300
436
|
});
|
|
301
437
|
|
|
302
438
|
this.#els.console.addEventListener('scroll', () => {
|
|
@@ -318,6 +454,10 @@ export class UIOverlay {
|
|
|
318
454
|
this.#root = null;
|
|
319
455
|
this.#els = null;
|
|
320
456
|
this.#mounted = false;
|
|
457
|
+
this.#providerSig = '';
|
|
458
|
+
this.#adapterSig = '';
|
|
459
|
+
this.#clearFlashTimers();
|
|
460
|
+
this.#flashAt.clear();
|
|
321
461
|
return this;
|
|
322
462
|
}
|
|
323
463
|
|
|
@@ -336,6 +476,7 @@ export class UIOverlay {
|
|
|
336
476
|
onPacket(packet) {
|
|
337
477
|
if (!this.#els || !packet) return;
|
|
338
478
|
if (packet.type === 'sys') return;
|
|
479
|
+
this.#flashAdapterButton(packet);
|
|
339
480
|
if (packet.level || packet.type === 'log' || packet.type === 'warn' || packet.type === 'error') {
|
|
340
481
|
this.#appendLine(packet);
|
|
341
482
|
}
|
|
@@ -366,6 +507,8 @@ export class UIOverlay {
|
|
|
366
507
|
this.#paintGraph(primary);
|
|
367
508
|
this.#renderHud(state.hud);
|
|
368
509
|
this.#syncButtons(state);
|
|
510
|
+
this.#syncProviders(state);
|
|
511
|
+
this.#syncAdapters(state);
|
|
369
512
|
|
|
370
513
|
if (this.#stickBottom) {
|
|
371
514
|
this.#els.console.scrollTop = this.#els.console.scrollHeight;
|
|
@@ -424,6 +567,80 @@ export class UIOverlay {
|
|
|
424
567
|
}
|
|
425
568
|
}
|
|
426
569
|
|
|
570
|
+
#syncProviders(state) {
|
|
571
|
+
const wrap = this.#els.providers;
|
|
572
|
+
if (!wrap) return;
|
|
573
|
+
const names = Array.isArray(state.assistants) ? state.assistants : [];
|
|
574
|
+
const active = state.assistant ?? '';
|
|
575
|
+
const sig = `${names.join('\0')}\0${active}`;
|
|
576
|
+
if (sig === this.#providerSig) return;
|
|
577
|
+
this.#providerSig = sig;
|
|
578
|
+
let html = '';
|
|
579
|
+
for (let i = 0; i < names.length; i++) {
|
|
580
|
+
const name = names[i];
|
|
581
|
+
const hot = name === active ? 'true' : 'false';
|
|
582
|
+
html += `<button class="bl-provider" data-act="assist" data-name="${escapeHtml(name)}" data-hot="${hot}" type="button" title="${escapeHtml(name)}">${escapeHtml(abbrevName(name))}</button>`;
|
|
583
|
+
}
|
|
584
|
+
wrap.innerHTML = html;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
#syncAdapters(state) {
|
|
588
|
+
const wrap = this.#els.adapters;
|
|
589
|
+
if (!wrap) return;
|
|
590
|
+
const list = Array.isArray(state.adapters) ? state.adapters : [];
|
|
591
|
+
const sig = list.map((entry) => `${entry.name}\0${entry.label}\0${entry.enabled ? 1 : 0}`).join('\n');
|
|
592
|
+
if (sig === this.#adapterSig) return;
|
|
593
|
+
this.#adapterSig = sig;
|
|
594
|
+
let html = '';
|
|
595
|
+
for (let i = 0; i < list.length; i++) {
|
|
596
|
+
const entry = list[i];
|
|
597
|
+
const hot = entry.enabled ? 'true' : 'false';
|
|
598
|
+
html += `<button class="bl-adapter" data-act="adapter" data-name="${escapeHtml(entry.name)}" data-hot="${hot}" type="button" title="${escapeHtml(entry.label)}">${escapeHtml(entry.label)}</button>`;
|
|
599
|
+
}
|
|
600
|
+
wrap.innerHTML = html;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#flashAdapterButton(packet) {
|
|
604
|
+
if (packet.type !== 'warn' && packet.type !== 'telemetry') return;
|
|
605
|
+
const wrap = this.#els.adapters;
|
|
606
|
+
if (!wrap || !this.core || typeof this.core.getAdapters !== 'function') return;
|
|
607
|
+
const name = packet.source;
|
|
608
|
+
if (!this.core.getAdapters().some((entry) => entry.name === name)) return;
|
|
609
|
+
|
|
610
|
+
const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
|
|
611
|
+
? CSS.escape(name)
|
|
612
|
+
: name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
613
|
+
const btn = wrap.querySelector(`[data-name="${escaped}"]`);
|
|
614
|
+
if (!btn) return;
|
|
615
|
+
|
|
616
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
617
|
+
const last = this.#flashAt.get(name) ?? 0;
|
|
618
|
+
if (now - last < 1500) return;
|
|
619
|
+
this.#flashAt.set(name, now);
|
|
620
|
+
|
|
621
|
+
btn.dataset.flash = 'false';
|
|
622
|
+
void btn.offsetWidth;
|
|
623
|
+
btn.dataset.flash = 'true';
|
|
624
|
+
|
|
625
|
+
const prev = this.#flashTimers.get(name);
|
|
626
|
+
if (prev && typeof clearTimeout === 'function') clearTimeout(prev);
|
|
627
|
+
if (typeof setTimeout !== 'function') return;
|
|
628
|
+
const timer = setTimeout(() => {
|
|
629
|
+
this.#flashTimers.delete(name);
|
|
630
|
+
if (btn.dataset.flash === 'true') btn.dataset.flash = 'false';
|
|
631
|
+
}, 560);
|
|
632
|
+
this.#flashTimers.set(name, timer);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
#clearFlashTimers() {
|
|
636
|
+
if (typeof clearTimeout !== 'function') {
|
|
637
|
+
this.#flashTimers.clear();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
for (const timer of this.#flashTimers.values()) clearTimeout(timer);
|
|
641
|
+
this.#flashTimers.clear();
|
|
642
|
+
}
|
|
643
|
+
|
|
427
644
|
#paintGraph(series) {
|
|
428
645
|
const canvas = this.#els.graph;
|
|
429
646
|
const ctx = canvas.getContext('2d');
|
|
@@ -475,6 +692,11 @@ export class UIOverlay {
|
|
|
475
692
|
}
|
|
476
693
|
}
|
|
477
694
|
|
|
695
|
+
function abbrevName(name) {
|
|
696
|
+
const s = String(name || '?');
|
|
697
|
+
return s.slice(0, 2).toUpperCase();
|
|
698
|
+
}
|
|
699
|
+
|
|
478
700
|
function fmtHud(value) {
|
|
479
701
|
if (value == null) return '—';
|
|
480
702
|
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
|