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.
- package/{README.md → README.MD} +28 -14
- package/package.json +1 -1
- package/src/core/BeeLadybugCore.js +102 -26
- package/src/core/UIOverlay.js +113 -3
- package/src/index.d.ts +5 -1
package/{README.md → README.MD}
RENAMED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
@@ -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.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.#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
*
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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:
|
|
195
|
+
message: `No assistant named "${key}". bee.registerAssistant({ name, complete }) first.`
|
|
179
196
|
});
|
|
180
197
|
return this;
|
|
181
198
|
}
|
|
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
|
-
});
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
241
|
+
const assistant = this.#activeProvider();
|
|
242
|
+
if (!assistant) {
|
|
204
243
|
this.sendData('warn', {
|
|
205
244
|
source: 'ai',
|
|
206
|
-
message: 'No assistant. bee.
|
|
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
|
|
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:
|
|
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:
|
|
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.#
|
|
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.#
|
|
443
|
+
assistant: this.#activeAssistant,
|
|
444
|
+
assistants: this.getAssistants()
|
|
369
445
|
};
|
|
370
446
|
}
|
|
371
447
|
|
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,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
|
|
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
|
|