bee-ladybug 0.2.0 → 0.3.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.
@@ -1,5 +1,8 @@
1
+
1
2
  # BeeLadybug
2
3
 
4
+
5
+
3
6
  Universal telemetry and visual inspection overlay.
4
7
 
5
8
  BeeLadybug is **not** a Canvas widget. It is a small open-source debugger
@@ -113,6 +116,8 @@ bee.subscribe((packet) => {
113
116
  });
114
117
  ```
115
118
 
119
+ ![Overlay Core: la ⓘ accanto a LIVE spiega che il freeze ferma solo il clock fittizio](docs/overlay-core.jpg)
120
+
116
121
  ## Tasti
117
122
 
118
123
  | Tasto | Azione |
@@ -197,6 +202,8 @@ python examples/python/sender.py
197
202
 
198
203
  Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
199
204
 
205
+ ![Overlay PythonBridge: ASK AI e quadratini mock/echo per scegliere il provider](docs/overlay-python.jpg)
206
+
200
207
  ## npm / TypeScript
201
208
 
202
209
  ```bash
@@ -216,14 +223,19 @@ npm run typecheck
216
223
  npm run lint
217
224
  ```
218
225
 
219
- Versione runtime e pacchetto: **0.2.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali). Su npm ora risulta `0.1.0`: il prossimo `npm publish` sarà `0.2.0`.
226
+ ```bash
227
+ npm login
228
+ npm publish --access public
229
+ ```
230
+
231
+ Versione runtime e pacchetto: **0.3.0** (`BEE_LADYBUG_VERSION` e `package.json` devono restare uguali).
220
232
 
221
233
  ## Assistente AI (gancio, nessun vendor)
222
234
 
223
- BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta il proprio modello:
235
+ BeeLadybug **non** chiama OpenAI, Claude o Ollama. Lo sviluppatore innesta i propri modelli. `registerAssistant` **aggiunge** (non sostituisce gli altri). `setActiveAssistant(name)` sceglie chi risponde a **ASK AI**.
224
236
 
225
237
  ```js
226
- bee.setAssistant({
238
+ bee.registerAssistant({
227
239
  name: 'ollama',
228
240
  async complete({ question, snapshot }) {
229
241
  const res = await fetch('http://127.0.0.1:11434/api/generate', {
@@ -240,17 +252,18 @@ bee.setAssistant({
240
252
  }
241
253
  });
242
254
 
255
+ bee.registerAssistant({
256
+ name: 'mock',
257
+ complete({ snapshot }) {
258
+ return `logs=${snapshot.logs.length}`;
259
+ }
260
+ });
261
+
262
+ bee.setActiveAssistant('ollama');
243
263
  await bee.ask('perché il loss sale?');
244
264
  ```
245
265
 
246
- Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI**. Senza `setAssistant`, il bottone spiega come collegarlo.
247
-
248
- ## API Core
249
-
250
- ```bash
251
- npm login
252
- npm publish --access public
253
- ```
266
+ Il Core manda un **snapshot** (log recenti, HUD, metriche) a `complete()`. Chiavi tipo `token`/`password` vengono redacted. In overlay: pulsante **ASK AI** e un quadratino per ogni provider; quello attivo è evidenziato. Senza provider, il bottone spiega come collegarlo. `setAssistant` resta come scorciatoia: registra e attiva, senza cancellare gli altri.
254
267
 
255
268
  ## API Core
256
269
 
@@ -277,15 +290,16 @@ bee.getHud();
277
290
  bee.show(); bee.hide(); bee.toggle();
278
291
  bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
279
292
  bee.applySlowMo(); bee.restoreRealtime();
280
- bee.setAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
293
+ bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
294
+ bee.setActiveAssistant('mock');
281
295
  await bee.ask('why fps drop?');
282
296
  bee.destroy();
283
297
  ```
284
298
 
285
299
  ## Roadmap
286
300
 
287
- 1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI. Fatto.
288
- 2. **npm 0.2.0** — `npm publish` (registry è ancora 0.1.0).
301
+ 1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
302
+ 2. **npm 0.3.0** — `npm publish`.
289
303
 
290
304
  ## Come contribuire
291
305
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bee-ladybug",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Universal telemetry and visual inspection overlay for Canvas, DOM, Python and AI flows.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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.2.0';
12
+ export const BEE_LADYBUG_VERSION = '0.3.0';
13
13
 
14
14
  export const CORE_DEFAULTS = Object.freeze({
15
15
  toggleKey: 'F2',
@@ -93,7 +93,8 @@ export class BeeLadybugCore {
93
93
  this.#onKeyDown = (event) => this.#handleKey(event);
94
94
  this.#onTick = (now) => this.#tick(now);
95
95
  this.#started = false;
96
- this.#assistant = null;
96
+ this.#assistants = new Map();
97
+ this.#activeAssistant = null;
97
98
  this.#askBusy = false;
98
99
 
99
100
  this.ui = null;
@@ -124,7 +125,10 @@ export class BeeLadybugCore {
124
125
  #onKeyDown;
125
126
  #onTick;
126
127
  #started;
127
- #assistant;
128
+ /** @type {Map<string, { name: string, complete: Function }>} */
129
+ #assistants;
130
+ /** @type {string | null} */
131
+ #activeAssistant;
128
132
  #askBusy;
129
133
 
130
134
  /**
@@ -159,51 +163,86 @@ export class BeeLadybugCore {
159
163
  }
160
164
 
161
165
  get hasAssistant() {
162
- return Boolean(this.#assistant);
166
+ return Boolean(this.#activeProvider());
163
167
  }
164
168
 
165
169
  get assistantBusy() {
166
170
  return this.#askBusy;
167
171
  }
168
172
 
173
+ /** Registered provider names, insertion order. */
174
+ getAssistants() {
175
+ return [...this.#assistants.keys()];
176
+ }
177
+
169
178
  /**
170
- * Plug in any model. BeeLadybug does not call OpenAI/Ollama itself.
179
+ * Add a model without dropping the others. Same `name` updates that slot.
180
+ * The first registration becomes active; later ones do not steal focus.
171
181
  * @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
172
182
  */
173
- setAssistant(provider) {
174
- const next = normalizeAssistant(provider);
175
- if (!next) {
183
+ registerAssistant(provider) {
184
+ return this.#putAssistant(provider, { activate: !this.#activeAssistant, via: 'registerAssistant' });
185
+ }
186
+
187
+ /**
188
+ * @param {string} name
189
+ */
190
+ setActiveAssistant(name) {
191
+ const key = String(name || '');
192
+ if (!this.#assistants.has(key)) {
176
193
  this.sendData('warn', {
177
194
  source: 'ai',
178
- message: 'setAssistant() needs { complete(ctx) }. No vendor is bundled.'
195
+ message: `No assistant named "${key}". bee.registerAssistant({ name, complete }) first.`
179
196
  });
180
197
  return this;
181
198
  }
182
- this.#assistant = next;
183
- this.sendData('state', { source: 'ai', key: 'ai.assist', value: next.name });
184
- this.sendData('log', {
185
- source: 'ai',
186
- message: `assistant ready (${next.name})`
187
- });
199
+ this.#activeAssistant = key;
200
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: key });
201
+ this.sendData('log', { source: 'ai', message: `assistant active (${key})` });
188
202
  return this;
189
203
  }
190
204
 
191
- clearAssistant() {
192
- this.#assistant = null;
193
- this.sendData('state', { source: 'ai', key: 'ai.assist', value: 'off' });
205
+ /**
206
+ * Register and activate one provider. Does not remove the others.
207
+ * @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
208
+ */
209
+ setAssistant(provider) {
210
+ return this.#putAssistant(provider, { activate: true, via: 'setAssistant' });
211
+ }
212
+
213
+ /**
214
+ * @param {string} [name] omit to drop every provider
215
+ */
216
+ clearAssistant(name) {
217
+ if (name == null || name === '') {
218
+ this.#assistants.clear();
219
+ this.#activeAssistant = null;
220
+ } else {
221
+ const key = String(name);
222
+ this.#assistants.delete(key);
223
+ if (this.#activeAssistant === key) {
224
+ this.#activeAssistant = this.#assistants.keys().next().value ?? null;
225
+ }
226
+ }
227
+ this.sendData('state', {
228
+ source: 'ai',
229
+ key: 'ai.assist',
230
+ value: this.#activeAssistant ?? 'off'
231
+ });
194
232
  return this;
195
233
  }
196
234
 
197
235
  /**
198
- * Send recent telemetry to the registered assistant and print the answer.
236
+ * Send recent telemetry to the active assistant and print the answer.
199
237
  * @param {string} [question]
200
238
  * @returns {Promise<string|null>}
201
239
  */
202
240
  async ask(question = 'Diagnose the current telemetry.') {
203
- if (!this.#assistant) {
241
+ const assistant = this.#activeProvider();
242
+ if (!assistant) {
204
243
  this.sendData('warn', {
205
244
  source: 'ai',
206
- message: 'No assistant. bee.setAssistant({ name, complete }) then ASK AI.'
245
+ message: 'No assistant. bee.registerAssistant({ name, complete }) then ASK AI.'
207
246
  });
208
247
  return null;
209
248
  }
@@ -217,10 +256,10 @@ export class BeeLadybugCore {
217
256
  this.sendData('log', { source: 'ai', message: `ask: ${q}` });
218
257
  try {
219
258
  const snapshot = buildAssistantSnapshot(this, q);
220
- const raw = await this.#assistant.complete({ question: q, snapshot });
259
+ const raw = await assistant.complete({ question: q, snapshot });
221
260
  const text = String(raw ?? '').trim() || '(empty answer)';
222
261
  this.sendData('log', { source: 'ai', message: text });
223
- this.sendData('state', { source: 'ai', key: 'ai.assist', value: this.#assistant.name });
262
+ this.sendData('state', { source: 'ai', key: 'ai.assist', value: assistant.name });
224
263
  return text;
225
264
  } catch (err) {
226
265
  this.sendData('error', {
@@ -230,7 +269,7 @@ export class BeeLadybugCore {
230
269
  this.sendData('state', {
231
270
  source: 'ai',
232
271
  key: 'ai.assist',
233
- value: this.#assistant?.name ?? 'error'
272
+ value: assistant?.name ?? 'error'
234
273
  });
235
274
  return null;
236
275
  } finally {
@@ -238,6 +277,37 @@ export class BeeLadybugCore {
238
277
  }
239
278
  }
240
279
 
280
+ #activeProvider() {
281
+ return this.#activeAssistant ? this.#assistants.get(this.#activeAssistant) ?? null : null;
282
+ }
283
+
284
+ /**
285
+ * @param {{ name?: string, complete: Function }} provider
286
+ * @param {{ activate: boolean, via: string }} opts
287
+ */
288
+ #putAssistant(provider, opts) {
289
+ const next = normalizeAssistant(provider);
290
+ if (!next) {
291
+ this.sendData('warn', {
292
+ source: 'ai',
293
+ message: `${opts.via}() needs { complete(ctx) }. No vendor is bundled.`
294
+ });
295
+ return this;
296
+ }
297
+ this.#assistants.set(next.name, next);
298
+ if (opts.activate) this.#activeAssistant = next.name;
299
+ this.sendData('state', {
300
+ source: 'ai',
301
+ key: 'ai.assist',
302
+ value: this.#activeAssistant ?? next.name
303
+ });
304
+ this.sendData('log', {
305
+ source: 'ai',
306
+ message: `assistant ready (${next.name})`
307
+ });
308
+ return this;
309
+ }
310
+
241
311
 
242
312
  start() {
243
313
  if (this.#started) return this;
@@ -280,7 +350,8 @@ export class BeeLadybugCore {
280
350
  this.#history.clear();
281
351
  this.#hud.clear();
282
352
  this.#latest.clear();
283
- this.#assistant = null;
353
+ this.#assistants.clear();
354
+ this.#activeAssistant = null;
284
355
  this.#askBusy = false;
285
356
  this.ui?.destroy();
286
357
  this.ui = null;
@@ -307,6 +378,10 @@ export class BeeLadybugCore {
307
378
  return this.visible ? this.hide() : this.show();
308
379
  }
309
380
 
381
+ /**
382
+ * Stops the simulated clock only. Overlay RAF keeps running.
383
+ * Adapters must listen for `sys` packets with `op: 'freeze'`.
384
+ */
310
385
  freeze() {
311
386
  if (this.frozen) return this;
312
387
  this.frozen = true;
@@ -365,7 +440,8 @@ export class BeeLadybugCore {
365
440
  logCount: this.#logs.length,
366
441
  hud: this.getHud(),
367
442
  mode: this.#modeLabel(),
368
- assistant: this.#assistant ? this.#assistant.name : null
443
+ assistant: this.#activeAssistant,
444
+ assistants: this.getAssistants()
369
445
  };
370
446
  }
371
447
 
@@ -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,31 @@ 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-provider {
251
+ appearance: none;
252
+ width: 22px;
253
+ height: 22px;
254
+ padding: 0;
255
+ background: #16181f;
256
+ color: #fff6d8;
257
+ border: 1px solid rgba(255, 211, 106, 0.45);
258
+ font: 700 8px ui-monospace, Consolas, monospace;
259
+ letter-spacing: 0.04em;
260
+ cursor: pointer;
261
+ }
262
+ .bl-provider[data-hot="true"] {
263
+ background: #6ee7ff;
264
+ border-color: #6ee7ff;
265
+ color: #05060a;
266
+ }
192
267
  .bl-hint {
193
268
  margin-left: auto;
194
269
  font-size: 9px;
@@ -213,6 +288,7 @@ export class UIOverlay {
213
288
  this.#stickBottom = true;
214
289
  this.#lastLogId = 0;
215
290
  this.#mounted = false;
291
+ this.#providerSig = '';
216
292
  }
217
293
 
218
294
  #host;
@@ -221,6 +297,7 @@ export class UIOverlay {
221
297
  #stickBottom;
222
298
  #lastLogId;
223
299
  #mounted;
300
+ #providerSig;
224
301
 
225
302
  /** @param {'auto' | HTMLElement} [target] */
226
303
  mount(target = 'auto') {
@@ -247,7 +324,13 @@ export class UIOverlay {
247
324
  <span class="bl-mark" aria-hidden="true"></span>
248
325
  <span class="bl-title">BEE LADYBUG</span>
249
326
  <span class="bl-badge">CORE</span>
250
- <span class="bl-mode" data-el="mode">LIVE</span>
327
+ <span class="bl-mode-wrap">
328
+ <span class="bl-mode" data-el="mode">LIVE</span>
329
+ <button class="bl-info" type="button" aria-describedby="bl-freeze-tip" aria-label="Freeze stops the simulated clock, not this overlay">
330
+ <span aria-hidden="true">ⓘ</span>
331
+ <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>
332
+ </button>
333
+ </span>
251
334
  </div>
252
335
  <div class="bl-stats">
253
336
  <div>HOST <b data-el="hostFps">—</b></div>
@@ -269,6 +352,7 @@ export class UIOverlay {
269
352
  <button class="bl-btn" data-act="freeze" type="button">F4 STOP</button>
270
353
  <button class="bl-btn" data-act="live" type="button">1x LIVE</button>
271
354
  <button class="bl-btn" data-act="ask" type="button">ASK AI</button>
355
+ <span class="bl-providers" data-el="providers"></span>
272
356
  <span class="bl-hint">F2 overlay</span>
273
357
  </div>
274
358
  </div>
@@ -286,7 +370,8 @@ export class UIOverlay {
286
370
  graphName: this.#root.querySelector('[data-el="graphName"]'),
287
371
  graphStats: this.#root.querySelector('[data-el="graphStats"]'),
288
372
  hud: this.#root.querySelector('[data-el="hud"]'),
289
- console: this.#root.querySelector('[data-el="console"]')
373
+ console: this.#root.querySelector('[data-el="console"]'),
374
+ providers: this.#root.querySelector('[data-el="providers"]')
290
375
  };
291
376
 
292
377
  this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
@@ -297,6 +382,7 @@ export class UIOverlay {
297
382
  else if (act === 'freeze') this.core.toggleFreeze();
298
383
  else if (act === 'live') this.core.restoreRealtime();
299
384
  else if (act === 'ask') this.core.ask();
385
+ else if (act === 'assist') this.core.setActiveAssistant(btn.getAttribute('data-name'));
300
386
  });
301
387
 
302
388
  this.#els.console.addEventListener('scroll', () => {
@@ -318,6 +404,7 @@ export class UIOverlay {
318
404
  this.#root = null;
319
405
  this.#els = null;
320
406
  this.#mounted = false;
407
+ this.#providerSig = '';
321
408
  return this;
322
409
  }
323
410
 
@@ -366,6 +453,7 @@ export class UIOverlay {
366
453
  this.#paintGraph(primary);
367
454
  this.#renderHud(state.hud);
368
455
  this.#syncButtons(state);
456
+ this.#syncProviders(state);
369
457
 
370
458
  if (this.#stickBottom) {
371
459
  this.#els.console.scrollTop = this.#els.console.scrollHeight;
@@ -424,6 +512,23 @@ export class UIOverlay {
424
512
  }
425
513
  }
426
514
 
515
+ #syncProviders(state) {
516
+ const wrap = this.#els.providers;
517
+ if (!wrap) return;
518
+ const names = Array.isArray(state.assistants) ? state.assistants : [];
519
+ const active = state.assistant ?? '';
520
+ const sig = `${names.join('\0')}\0${active}`;
521
+ if (sig === this.#providerSig) return;
522
+ this.#providerSig = sig;
523
+ let html = '';
524
+ for (let i = 0; i < names.length; i++) {
525
+ const name = names[i];
526
+ const hot = name === active ? 'true' : 'false';
527
+ html += `<button class="bl-provider" data-act="assist" data-name="${escapeHtml(name)}" data-hot="${hot}" type="button" title="${escapeHtml(name)}">${escapeHtml(abbrevName(name))}</button>`;
528
+ }
529
+ wrap.innerHTML = html;
530
+ }
531
+
427
532
  #paintGraph(series) {
428
533
  const canvas = this.#els.graph;
429
534
  const ctx = canvas.getContext('2d');
@@ -475,6 +580,11 @@ export class UIOverlay {
475
580
  }
476
581
  }
477
582
 
583
+ function abbrevName(name) {
584
+ const s = String(name || '?');
585
+ return s.slice(0, 2).toUpperCase();
586
+ }
587
+
478
588
  function fmtHud(value) {
479
589
  if (value == null) return '—';
480
590
  if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
package/src/index.d.ts CHANGED
@@ -58,6 +58,7 @@ export interface CoreState {
58
58
  hud: Record<string, HudEntry>;
59
59
  mode: string;
60
60
  assistant: string | null;
61
+ assistants: string[];
61
62
  }
62
63
 
63
64
  export interface CoreOptions {
@@ -148,8 +149,11 @@ export class BeeLadybugCore {
148
149
  getLatest(type: string): BeePacket | null;
149
150
  readonly hasAssistant: boolean;
150
151
  readonly assistantBusy: boolean;
152
+ getAssistants(): string[];
153
+ registerAssistant(provider: AssistantProvider): this;
154
+ setActiveAssistant(name: string): this;
151
155
  setAssistant(provider: AssistantProvider): this;
152
- clearAssistant(): this;
156
+ clearAssistant(name?: string): this;
153
157
  ask(question?: string): Promise<string | null>;
154
158
  }
155
159