bee-ladybug 0.1.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/LICENSE +21 -0
- package/{README.md → README.MD} +116 -14
- package/package.json +40 -7
- package/src/adapters/python/PythonBridge.js +217 -15
- package/src/core/AiAssistant.js +60 -0
- package/src/core/BeeLadybugCore.js +167 -2
- package/src/core/UIOverlay.js +122 -5
- package/src/index.d.ts +276 -0
- package/src/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antonio Prosperi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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
|
|
@@ -27,22 +30,24 @@ funziona sopra un gioco, sopra una pagina, o da solo in una sandbox.
|
|
|
27
30
|
bee-ladybug/
|
|
28
31
|
├── src/
|
|
29
32
|
│ ├── index.js # API pubblica
|
|
33
|
+
│ ├── index.d.ts # tipi npm / tsc
|
|
30
34
|
│ ├── core/
|
|
31
|
-
│ │ ├── BeeLadybugCore.js
|
|
32
|
-
│ │ └── UIOverlay.js
|
|
35
|
+
│ │ ├── BeeLadybugCore.js
|
|
36
|
+
│ │ └── UIOverlay.js
|
|
33
37
|
│ └── adapters/
|
|
34
|
-
│ ├── CanvasAdapter.js
|
|
35
|
-
│ ├── WebDOMAdapter.js
|
|
36
|
-
│ └── python/
|
|
37
|
-
│ └── PythonBridge.js # futuro: WebSocket → sendData
|
|
38
|
+
│ ├── CanvasAdapter.js
|
|
39
|
+
│ ├── WebDOMAdapter.js
|
|
40
|
+
│ └── python/PythonBridge.js
|
|
38
41
|
├── examples/
|
|
39
|
-
│ ├── core-sandbox.js # demo senza adapter
|
|
40
42
|
│ ├── core.html
|
|
41
|
-
│ ├── canvas-sandbox.js
|
|
42
|
-
│ ├── dom
|
|
43
|
-
│
|
|
44
|
-
|
|
43
|
+
│ ├── canvas-sandbox.js
|
|
44
|
+
│ ├── dom.html
|
|
45
|
+
│ ├── python.html
|
|
46
|
+
│ └── python/sender.py
|
|
47
|
+
├── tsconfig.json
|
|
45
48
|
├── package.json
|
|
49
|
+
├── LICENSE
|
|
50
|
+
├── index.html
|
|
46
51
|
└── README.md
|
|
47
52
|
```
|
|
48
53
|
|
|
@@ -111,6 +116,8 @@ bee.subscribe((packet) => {
|
|
|
111
116
|
});
|
|
112
117
|
```
|
|
113
118
|
|
|
119
|
+

|
|
120
|
+
|
|
114
121
|
## Tasti
|
|
115
122
|
|
|
116
123
|
| Tasto | Azione |
|
|
@@ -165,6 +172,99 @@ adapter.attach();
|
|
|
165
172
|
|
|
166
173
|
Traduce il DOM in pacchetti: nodo sotto il puntatore, box model (margin / padding / content), conteggio nodi, mutazioni (`+n -n attr`). `F4` congela l'hover. `F2` nasconde overlay e highlight. Il Core non sa che esiste HTML.
|
|
167
174
|
|
|
175
|
+
## PythonBridge
|
|
176
|
+
|
|
177
|
+
Python non vede l'overlay. Manda JSON. Il browser lo srotola in `sendData()`.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { BeeLadybugCore, PythonBridge } from './src/index.js';
|
|
181
|
+
|
|
182
|
+
const bee = new BeeLadybugCore();
|
|
183
|
+
const bridge = new PythonBridge(bee, { url: 'ws://127.0.0.1:8765' });
|
|
184
|
+
bridge.connect();
|
|
185
|
+
|
|
186
|
+
// stesso parser, senza socket:
|
|
187
|
+
bridge.ingest({ type: 'metric', payload: { name: 'loss', value: 0.12 } });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Forma del filo:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{ "type": "log", "payload": { "message": "epoch 3" } }
|
|
194
|
+
{ "type": "error", "message": "CUDA OOM" }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Sender di esempio (solo stdlib, nessun pip):
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
python examples/python/sender.py
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Poi aprire `http://localhost:8080/examples/python.html` e Connect. I pulsanti mock funzionano anche a processo spento.
|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+
## npm / TypeScript
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
npm install
|
|
211
|
+
npm run typecheck
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
import { BeeLadybugCore } from 'bee-ladybug';
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Il runtime resta JavaScript ESM. I tipi stanno in `src/index.d.ts` (`package.json` → `"types"`).
|
|
219
|
+
Keyword e repository puntano a **BeeLadybug-universal**, non a BeeEngine.
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npm run typecheck
|
|
223
|
+
npm run lint
|
|
224
|
+
```
|
|
225
|
+
|
|
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).
|
|
232
|
+
|
|
233
|
+
## Assistente AI (gancio, nessun vendor)
|
|
234
|
+
|
|
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**.
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
bee.registerAssistant({
|
|
239
|
+
name: 'ollama',
|
|
240
|
+
async complete({ question, snapshot }) {
|
|
241
|
+
const res = await fetch('http://127.0.0.1:11434/api/generate', {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
model: 'llama3',
|
|
246
|
+
prompt: `${question}\n${JSON.stringify(snapshot)}`,
|
|
247
|
+
stream: false
|
|
248
|
+
})
|
|
249
|
+
});
|
|
250
|
+
const data = await res.json();
|
|
251
|
+
return data.response;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
bee.registerAssistant({
|
|
256
|
+
name: 'mock',
|
|
257
|
+
complete({ snapshot }) {
|
|
258
|
+
return `logs=${snapshot.logs.length}`;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
bee.setActiveAssistant('ollama');
|
|
263
|
+
await bee.ask('perché il loss sale?');
|
|
264
|
+
```
|
|
265
|
+
|
|
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.
|
|
267
|
+
|
|
168
268
|
## API Core
|
|
169
269
|
|
|
170
270
|
```js
|
|
@@ -190,14 +290,16 @@ bee.getHud();
|
|
|
190
290
|
bee.show(); bee.hide(); bee.toggle();
|
|
191
291
|
bee.freeze(); bee.unfreeze(); bee.toggleFreeze();
|
|
192
292
|
bee.applySlowMo(); bee.restoreRealtime();
|
|
293
|
+
bee.registerAssistant({ name: 'mock', complete: async ({ snapshot }) => 'ok' });
|
|
294
|
+
bee.setActiveAssistant('mock');
|
|
295
|
+
await bee.ask('why fps drop?');
|
|
193
296
|
bee.destroy();
|
|
194
297
|
```
|
|
195
298
|
|
|
196
299
|
## Roadmap
|
|
197
300
|
|
|
198
|
-
1. **Ora** — Core
|
|
199
|
-
2. **
|
|
200
|
-
3. **Pacchetto npm** — `import { BeeLadybugCore } from 'bee-ladybug'`.
|
|
301
|
+
1. **Ora** — Core, adapter, PythonBridge, tipi, ESLint, gancio AI multi-provider. Fatto.
|
|
302
|
+
2. **npm 0.3.0** — `npm publish`.
|
|
201
303
|
|
|
202
304
|
## Come contribuire
|
|
203
305
|
|
package/package.json
CHANGED
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bee-ladybug",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Universal telemetry and visual inspection overlay for Canvas, DOM,
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
7
7
|
"module": "src/index.js",
|
|
8
|
+
"types": "src/index.d.ts",
|
|
8
9
|
"exports": {
|
|
9
|
-
".":
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
}
|
|
10
15
|
},
|
|
11
16
|
"files": [
|
|
12
17
|
"src"
|
|
13
18
|
],
|
|
14
|
-
|
|
19
|
+
"scripts": {
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"lint": "eslint src examples",
|
|
22
|
+
"test": "node --test tests/**/*.test.js"
|
|
23
|
+
},
|
|
15
24
|
"keywords": [
|
|
16
25
|
"debug",
|
|
17
26
|
"telemetry",
|
|
18
27
|
"overlay",
|
|
19
28
|
"observability",
|
|
29
|
+
"devtools",
|
|
20
30
|
"canvas",
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
"dom",
|
|
32
|
+
"python",
|
|
33
|
+
"websocket",
|
|
34
|
+
"ai",
|
|
35
|
+
"esm"
|
|
36
|
+
],
|
|
37
|
+
"author": "Antonio Prosperi",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/antonioprosperi2-svg/BeeLadybug-universal.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/antonioprosperi2-svg/BeeLadybug-universal#readme",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^9.36.0",
|
|
52
|
+
"eslint": "^10.8.1",
|
|
53
|
+
"globals": "^16.0.0",
|
|
54
|
+
"typescript": "^5.7.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,40 +1,242 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PythonBridge —
|
|
2
|
+
* PythonBridge — inbound channel for Python / AI / headless processes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Python never talks to the overlay directly.
|
|
4
|
+
* Speaks WebSocket JSON and calls core.sendData(type, payload).
|
|
5
|
+
* The core stays browser-side; Python never talks to the overlay.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* { "type": "
|
|
7
|
+
* Wire shape (either form):
|
|
8
|
+
* { "type": "metric", "payload": { "name": "loss", "value": 0.12 } }
|
|
9
|
+
* { "type": "log", "message": "epoch 3" }
|
|
10
|
+
*
|
|
11
|
+
* ingest() is the same parser used by the socket, so tests and the mock
|
|
12
|
+
* demo do not need a live Python process.
|
|
10
13
|
*/
|
|
14
|
+
|
|
15
|
+
export const PYTHON_BRIDGE_DEFAULTS = Object.freeze({
|
|
16
|
+
url: 'ws://127.0.0.1:8765',
|
|
17
|
+
autoReconnect: true,
|
|
18
|
+
reconnectMs: 1500,
|
|
19
|
+
source: 'python'
|
|
20
|
+
});
|
|
21
|
+
|
|
11
22
|
export class PythonBridge {
|
|
12
23
|
/**
|
|
13
24
|
* @param {import('../../core/BeeLadybugCore.js').BeeLadybugCore} core
|
|
14
|
-
* @param {
|
|
25
|
+
* @param {Partial<typeof PYTHON_BRIDGE_DEFAULTS>} [options]
|
|
15
26
|
*/
|
|
16
27
|
constructor(core, options = {}) {
|
|
28
|
+
const cfg = { ...PYTHON_BRIDGE_DEFAULTS, ...options };
|
|
17
29
|
this.core = core;
|
|
18
|
-
this.url =
|
|
30
|
+
this.url = cfg.url;
|
|
31
|
+
this.autoReconnect = cfg.autoReconnect;
|
|
32
|
+
this.reconnectMs = cfg.reconnectMs;
|
|
33
|
+
this.source = cfg.source;
|
|
34
|
+
this.status = 'idle';
|
|
35
|
+
|
|
19
36
|
this.#socket = null;
|
|
37
|
+
this.#wanted = false;
|
|
38
|
+
this.#timer = 0;
|
|
39
|
+
this.#packets = 0;
|
|
40
|
+
this.#retries = 0;
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
#socket;
|
|
44
|
+
#wanted;
|
|
45
|
+
#timer;
|
|
46
|
+
#packets;
|
|
47
|
+
#retries;
|
|
23
48
|
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
get connected() {
|
|
50
|
+
return this.status === 'open';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
connect(url) {
|
|
54
|
+
if (url) this.url = url;
|
|
55
|
+
this.#wanted = true;
|
|
56
|
+
this.#retries = 0;
|
|
57
|
+
this.#open();
|
|
30
58
|
return this;
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
disconnect() {
|
|
62
|
+
this.#wanted = false;
|
|
63
|
+
this.#clearTimer();
|
|
34
64
|
if (this.#socket) {
|
|
35
|
-
|
|
65
|
+
try {
|
|
66
|
+
this.#socket.close();
|
|
67
|
+
} catch {
|
|
68
|
+
/* already closing */
|
|
69
|
+
}
|
|
36
70
|
this.#socket = null;
|
|
37
71
|
}
|
|
72
|
+
this.#setStatus('idle');
|
|
73
|
+
this.core?.sendData('log', {
|
|
74
|
+
source: this.source,
|
|
75
|
+
message: 'PythonBridge disconnected.'
|
|
76
|
+
});
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse one wire message (object, JSON string, or array) into sendData().
|
|
82
|
+
* @param {unknown} raw
|
|
83
|
+
*/
|
|
84
|
+
ingest(raw) {
|
|
85
|
+
if (!this.core) return this;
|
|
86
|
+
if (raw == null) return this;
|
|
87
|
+
|
|
88
|
+
let data = raw;
|
|
89
|
+
if (typeof raw === 'string') {
|
|
90
|
+
const text = raw.trim();
|
|
91
|
+
if (!text) return this;
|
|
92
|
+
try {
|
|
93
|
+
data = JSON.parse(text);
|
|
94
|
+
} catch {
|
|
95
|
+
this.core.sendData('error', {
|
|
96
|
+
source: this.source,
|
|
97
|
+
message: `bad JSON: ${text.slice(0, 96)}`
|
|
98
|
+
});
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(data)) {
|
|
104
|
+
for (let i = 0; i < data.length; i++) this.ingest(data[i]);
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (typeof data !== 'object') return this;
|
|
109
|
+
|
|
110
|
+
const type = data.type || data.channel;
|
|
111
|
+
if (typeof type !== 'string' || !type) {
|
|
112
|
+
this.core.sendData('warn', {
|
|
113
|
+
source: this.source,
|
|
114
|
+
message: 'packet missing type'
|
|
115
|
+
});
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const payload = unpackPayload(data, this.source);
|
|
120
|
+
this.#packets += 1;
|
|
121
|
+
this.core.sendData(type, payload);
|
|
122
|
+
this.core.sendData('state', {
|
|
123
|
+
source: this.source,
|
|
124
|
+
key: 'py.packets',
|
|
125
|
+
value: this.#packets
|
|
126
|
+
});
|
|
38
127
|
return this;
|
|
39
128
|
}
|
|
129
|
+
|
|
130
|
+
#open() {
|
|
131
|
+
if (!this.#wanted) return;
|
|
132
|
+
if (typeof WebSocket === 'undefined') {
|
|
133
|
+
this.core?.sendData('error', {
|
|
134
|
+
source: this.source,
|
|
135
|
+
message: 'WebSocket API missing in this runtime.'
|
|
136
|
+
});
|
|
137
|
+
this.#setStatus('closed');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.#clearTimer();
|
|
142
|
+
if (this.#socket) {
|
|
143
|
+
try {
|
|
144
|
+
this.#socket.close();
|
|
145
|
+
} catch {
|
|
146
|
+
/* ignore */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.#setStatus('connecting');
|
|
151
|
+
if (this.#retries === 0) {
|
|
152
|
+
this.core?.sendData('log', {
|
|
153
|
+
source: this.source,
|
|
154
|
+
message: `PythonBridge connecting ${this.url}`
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let socket;
|
|
159
|
+
try {
|
|
160
|
+
socket = new WebSocket(this.url);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
this.core?.sendData('error', {
|
|
163
|
+
source: this.source,
|
|
164
|
+
message: `connect failed: ${err instanceof Error ? err.message : String(err)}`
|
|
165
|
+
});
|
|
166
|
+
this.#setStatus('closed');
|
|
167
|
+
this.#scheduleReconnect();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.#socket = socket;
|
|
172
|
+
socket.addEventListener('open', () => {
|
|
173
|
+
if (socket !== this.#socket) return;
|
|
174
|
+
this.#setStatus('open');
|
|
175
|
+
this.#retries = 0;
|
|
176
|
+
this.core?.sendData('log', {
|
|
177
|
+
source: this.source,
|
|
178
|
+
message: `PythonBridge open (${this.url})`
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
socket.addEventListener('message', (event) => {
|
|
182
|
+
if (socket !== this.#socket) return;
|
|
183
|
+
this.ingest(event.data);
|
|
184
|
+
});
|
|
185
|
+
socket.addEventListener('close', () => {
|
|
186
|
+
if (socket !== this.#socket) return;
|
|
187
|
+
this.#socket = null;
|
|
188
|
+
this.#setStatus('closed');
|
|
189
|
+
if (this.#retries === 0) {
|
|
190
|
+
this.core?.sendData('warn', {
|
|
191
|
+
source: this.source,
|
|
192
|
+
message: 'PythonBridge socket closed.'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
this.#retries += 1;
|
|
196
|
+
this.#scheduleReconnect();
|
|
197
|
+
});
|
|
198
|
+
socket.addEventListener('error', () => {
|
|
199
|
+
if (this.#retries > 0) return;
|
|
200
|
+
this.core?.sendData('error', {
|
|
201
|
+
source: this.source,
|
|
202
|
+
message: `PythonBridge socket error (${this.url})`
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#scheduleReconnect() {
|
|
208
|
+
if (!this.#wanted || !this.autoReconnect) return;
|
|
209
|
+
this.#clearTimer();
|
|
210
|
+
this.#timer = setTimeout(() => this.#open(), this.reconnectMs);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#clearTimer() {
|
|
214
|
+
if (this.#timer && typeof clearTimeout === 'function') clearTimeout(this.#timer);
|
|
215
|
+
this.#timer = 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#setStatus(status) {
|
|
219
|
+
this.status = status;
|
|
220
|
+
this.core?.sendData('state', {
|
|
221
|
+
source: this.source,
|
|
222
|
+
key: 'py.status',
|
|
223
|
+
value: status
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function unpackPayload(data, source) {
|
|
229
|
+
const body = data.payload != null && typeof data.payload === 'object' && !Array.isArray(data.payload)
|
|
230
|
+
? { ...data.payload }
|
|
231
|
+
: omitType(data);
|
|
232
|
+
if (!body.source) body.source = source;
|
|
233
|
+
return body;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function omitType(data) {
|
|
237
|
+
const out = { ...data };
|
|
238
|
+
delete out.type;
|
|
239
|
+
delete out.channel;
|
|
240
|
+
delete out.payload;
|
|
241
|
+
return out;
|
|
40
242
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendor-agnostic AI hook. BeeLadybug never ships an API key or a model:
|
|
3
|
+
* the host app injects complete({ question, snapshot }).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SECRET_KEY = /key|token|secret|password|authorization|cookie|apikey/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import('./BeeLadybugCore.js').BeeLadybugCore} core
|
|
10
|
+
* @param {string} question
|
|
11
|
+
*/
|
|
12
|
+
export function buildAssistantSnapshot(core, question) {
|
|
13
|
+
const logs = core.getLogs().slice(-40).map((packet) => ({
|
|
14
|
+
type: packet.type,
|
|
15
|
+
level: packet.level,
|
|
16
|
+
source: packet.source,
|
|
17
|
+
message: String(packet.payload?.message ?? packet.payload?.value ?? packet.type),
|
|
18
|
+
frame: packet.frame
|
|
19
|
+
}));
|
|
20
|
+
const history = core.getPrimaryHistory();
|
|
21
|
+
return {
|
|
22
|
+
question: String(question || ''),
|
|
23
|
+
version: core.version,
|
|
24
|
+
state: core.getState(),
|
|
25
|
+
hud: scrub(core.getHud()),
|
|
26
|
+
logs,
|
|
27
|
+
history: {
|
|
28
|
+
name: history.name,
|
|
29
|
+
min: history.min,
|
|
30
|
+
max: history.max,
|
|
31
|
+
avg: history.avg,
|
|
32
|
+
last: history.last,
|
|
33
|
+
samples: history.values.slice(-24)
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeAssistant(provider) {
|
|
39
|
+
if (!provider || typeof provider.complete !== 'function') return null;
|
|
40
|
+
const name = typeof provider.name === 'string' && provider.name
|
|
41
|
+
? provider.name
|
|
42
|
+
: 'custom';
|
|
43
|
+
return { name, complete: provider.complete.bind(provider) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scrub(value, depth = 0) {
|
|
47
|
+
if (value == null || typeof value !== 'object') {
|
|
48
|
+
if (typeof value === 'string' && value.length > 400) return `${value.slice(0, 400)}…`;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
if (depth > 4) return '[…]';
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.slice(0, 24).map((item) => scrub(item, depth + 1));
|
|
54
|
+
}
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const key of Object.keys(value)) {
|
|
57
|
+
out[key] = SECRET_KEY.test(key) ? '[redacted]' : scrub(value[key], depth + 1);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { UIOverlay } from './UIOverlay.js';
|
|
10
|
+
import { buildAssistantSnapshot, normalizeAssistant } from './AiAssistant.js';
|
|
10
11
|
|
|
11
|
-
export const BEE_LADYBUG_VERSION = '0.
|
|
12
|
+
export const BEE_LADYBUG_VERSION = '0.3.0';
|
|
12
13
|
|
|
13
14
|
export const CORE_DEFAULTS = Object.freeze({
|
|
14
15
|
toggleKey: 'F2',
|
|
@@ -92,6 +93,9 @@ export class BeeLadybugCore {
|
|
|
92
93
|
this.#onKeyDown = (event) => this.#handleKey(event);
|
|
93
94
|
this.#onTick = (now) => this.#tick(now);
|
|
94
95
|
this.#started = false;
|
|
96
|
+
this.#assistants = new Map();
|
|
97
|
+
this.#activeAssistant = null;
|
|
98
|
+
this.#askBusy = false;
|
|
95
99
|
|
|
96
100
|
this.ui = null;
|
|
97
101
|
if (cfg.ui !== false && typeof document !== 'undefined') {
|
|
@@ -121,6 +125,11 @@ export class BeeLadybugCore {
|
|
|
121
125
|
#onKeyDown;
|
|
122
126
|
#onTick;
|
|
123
127
|
#started;
|
|
128
|
+
/** @type {Map<string, { name: string, complete: Function }>} */
|
|
129
|
+
#assistants;
|
|
130
|
+
/** @type {string | null} */
|
|
131
|
+
#activeAssistant;
|
|
132
|
+
#askBusy;
|
|
124
133
|
|
|
125
134
|
/**
|
|
126
135
|
* Public adapter contract. `type` is a channel name; `payload` is raw data.
|
|
@@ -153,6 +162,153 @@ export class BeeLadybugCore {
|
|
|
153
162
|
return () => this.#listeners.delete(handler);
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
get hasAssistant() {
|
|
166
|
+
return Boolean(this.#activeProvider());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get assistantBusy() {
|
|
170
|
+
return this.#askBusy;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Registered provider names, insertion order. */
|
|
174
|
+
getAssistants() {
|
|
175
|
+
return [...this.#assistants.keys()];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
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.
|
|
181
|
+
* @param {{ name?: string, complete: (input: { question: string, snapshot: object }) => unknown }} provider
|
|
182
|
+
*/
|
|
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)) {
|
|
193
|
+
this.sendData('warn', {
|
|
194
|
+
source: 'ai',
|
|
195
|
+
message: `No assistant named "${key}". bee.registerAssistant({ name, complete }) first.`
|
|
196
|
+
});
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
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})` });
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
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
|
+
});
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Send recent telemetry to the active assistant and print the answer.
|
|
237
|
+
* @param {string} [question]
|
|
238
|
+
* @returns {Promise<string|null>}
|
|
239
|
+
*/
|
|
240
|
+
async ask(question = 'Diagnose the current telemetry.') {
|
|
241
|
+
const assistant = this.#activeProvider();
|
|
242
|
+
if (!assistant) {
|
|
243
|
+
this.sendData('warn', {
|
|
244
|
+
source: 'ai',
|
|
245
|
+
message: 'No assistant. bee.registerAssistant({ name, complete }) then ASK AI.'
|
|
246
|
+
});
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
if (this.#askBusy) {
|
|
250
|
+
this.sendData('warn', { source: 'ai', message: 'assistant busy' });
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const q = String(question || 'Diagnose the current telemetry.');
|
|
254
|
+
this.#askBusy = true;
|
|
255
|
+
this.sendData('state', { source: 'ai', key: 'ai.assist', value: 'thinking' });
|
|
256
|
+
this.sendData('log', { source: 'ai', message: `ask: ${q}` });
|
|
257
|
+
try {
|
|
258
|
+
const snapshot = buildAssistantSnapshot(this, q);
|
|
259
|
+
const raw = await assistant.complete({ question: q, snapshot });
|
|
260
|
+
const text = String(raw ?? '').trim() || '(empty answer)';
|
|
261
|
+
this.sendData('log', { source: 'ai', message: text });
|
|
262
|
+
this.sendData('state', { source: 'ai', key: 'ai.assist', value: assistant.name });
|
|
263
|
+
return text;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
this.sendData('error', {
|
|
266
|
+
source: 'ai',
|
|
267
|
+
message: err instanceof Error ? err.message : String(err)
|
|
268
|
+
});
|
|
269
|
+
this.sendData('state', {
|
|
270
|
+
source: 'ai',
|
|
271
|
+
key: 'ai.assist',
|
|
272
|
+
value: assistant?.name ?? 'error'
|
|
273
|
+
});
|
|
274
|
+
return null;
|
|
275
|
+
} finally {
|
|
276
|
+
this.#askBusy = false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
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
|
+
|
|
311
|
+
|
|
156
312
|
start() {
|
|
157
313
|
if (this.#started) return this;
|
|
158
314
|
this.#started = true;
|
|
@@ -194,6 +350,9 @@ export class BeeLadybugCore {
|
|
|
194
350
|
this.#history.clear();
|
|
195
351
|
this.#hud.clear();
|
|
196
352
|
this.#latest.clear();
|
|
353
|
+
this.#assistants.clear();
|
|
354
|
+
this.#activeAssistant = null;
|
|
355
|
+
this.#askBusy = false;
|
|
197
356
|
this.ui?.destroy();
|
|
198
357
|
this.ui = null;
|
|
199
358
|
return this;
|
|
@@ -219,6 +378,10 @@ export class BeeLadybugCore {
|
|
|
219
378
|
return this.visible ? this.hide() : this.show();
|
|
220
379
|
}
|
|
221
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Stops the simulated clock only. Overlay RAF keeps running.
|
|
383
|
+
* Adapters must listen for `sys` packets with `op: 'freeze'`.
|
|
384
|
+
*/
|
|
222
385
|
freeze() {
|
|
223
386
|
if (this.frozen) return this;
|
|
224
387
|
this.frozen = true;
|
|
@@ -276,7 +439,9 @@ export class BeeLadybugCore {
|
|
|
276
439
|
coreFps: this.coreFps,
|
|
277
440
|
logCount: this.#logs.length,
|
|
278
441
|
hud: this.getHud(),
|
|
279
|
-
mode: this.#modeLabel()
|
|
442
|
+
mode: this.#modeLabel(),
|
|
443
|
+
assistant: this.#activeAssistant,
|
|
444
|
+
assistants: this.getAssistants()
|
|
280
445
|
};
|
|
281
446
|
}
|
|
282
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;
|
|
@@ -169,6 +219,7 @@ const STYLES = `
|
|
|
169
219
|
.bl-foot {
|
|
170
220
|
display: flex;
|
|
171
221
|
align-items: center;
|
|
222
|
+
flex-wrap: wrap;
|
|
172
223
|
gap: 6px;
|
|
173
224
|
padding: 8px 10px;
|
|
174
225
|
border-top: 1px solid rgba(255, 59, 78, 0.22);
|
|
@@ -188,6 +239,31 @@ const STYLES = `
|
|
|
188
239
|
border-color: #ff3b4e;
|
|
189
240
|
color: #fff;
|
|
190
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
|
+
}
|
|
191
267
|
.bl-hint {
|
|
192
268
|
margin-left: auto;
|
|
193
269
|
font-size: 9px;
|
|
@@ -205,22 +281,23 @@ export class UIOverlay {
|
|
|
205
281
|
*/
|
|
206
282
|
constructor(core, options = {}) {
|
|
207
283
|
this.core = core;
|
|
208
|
-
|
|
284
|
+
void options;
|
|
209
285
|
this.#host = null;
|
|
210
286
|
this.#root = null;
|
|
211
287
|
this.#els = null;
|
|
212
288
|
this.#stickBottom = true;
|
|
213
289
|
this.#lastLogId = 0;
|
|
214
290
|
this.#mounted = false;
|
|
291
|
+
this.#providerSig = '';
|
|
215
292
|
}
|
|
216
293
|
|
|
217
|
-
#options;
|
|
218
294
|
#host;
|
|
219
295
|
#root;
|
|
220
296
|
#els;
|
|
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>
|
|
@@ -268,6 +351,8 @@ export class UIOverlay {
|
|
|
268
351
|
<button class="bl-btn" data-act="slow" type="button">F3 SLOW</button>
|
|
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>
|
|
354
|
+
<button class="bl-btn" data-act="ask" type="button">ASK AI</button>
|
|
355
|
+
<span class="bl-providers" data-el="providers"></span>
|
|
271
356
|
<span class="bl-hint">F2 overlay</span>
|
|
272
357
|
</div>
|
|
273
358
|
</div>
|
|
@@ -285,7 +370,8 @@ export class UIOverlay {
|
|
|
285
370
|
graphName: this.#root.querySelector('[data-el="graphName"]'),
|
|
286
371
|
graphStats: this.#root.querySelector('[data-el="graphStats"]'),
|
|
287
372
|
hud: this.#root.querySelector('[data-el="hud"]'),
|
|
288
|
-
console: this.#root.querySelector('[data-el="console"]')
|
|
373
|
+
console: this.#root.querySelector('[data-el="console"]'),
|
|
374
|
+
providers: this.#root.querySelector('[data-el="providers"]')
|
|
289
375
|
};
|
|
290
376
|
|
|
291
377
|
this.#root.querySelector('.bl-foot').addEventListener('click', (event) => {
|
|
@@ -295,6 +381,8 @@ export class UIOverlay {
|
|
|
295
381
|
if (act === 'slow') this.core.applySlowMo();
|
|
296
382
|
else if (act === 'freeze') this.core.toggleFreeze();
|
|
297
383
|
else if (act === 'live') this.core.restoreRealtime();
|
|
384
|
+
else if (act === 'ask') this.core.ask();
|
|
385
|
+
else if (act === 'assist') this.core.setActiveAssistant(btn.getAttribute('data-name'));
|
|
298
386
|
});
|
|
299
387
|
|
|
300
388
|
this.#els.console.addEventListener('scroll', () => {
|
|
@@ -316,6 +404,7 @@ export class UIOverlay {
|
|
|
316
404
|
this.#root = null;
|
|
317
405
|
this.#els = null;
|
|
318
406
|
this.#mounted = false;
|
|
407
|
+
this.#providerSig = '';
|
|
319
408
|
return this;
|
|
320
409
|
}
|
|
321
410
|
|
|
@@ -364,6 +453,7 @@ export class UIOverlay {
|
|
|
364
453
|
this.#paintGraph(primary);
|
|
365
454
|
this.#renderHud(state.hud);
|
|
366
455
|
this.#syncButtons(state);
|
|
456
|
+
this.#syncProviders(state);
|
|
367
457
|
|
|
368
458
|
if (this.#stickBottom) {
|
|
369
459
|
this.#els.console.scrollTop = this.#els.console.scrollHeight;
|
|
@@ -415,6 +505,28 @@ export class UIOverlay {
|
|
|
415
505
|
freeze.dataset.hot = state.frozen ? 'true' : 'false';
|
|
416
506
|
slow.dataset.hot = !state.frozen && state.timeScale !== 1 ? 'true' : 'false';
|
|
417
507
|
live.dataset.hot = !state.frozen && state.timeScale === 1 ? 'true' : 'false';
|
|
508
|
+
const ask = this.#root.querySelector('[data-act="ask"]');
|
|
509
|
+
if (ask) {
|
|
510
|
+
ask.dataset.hot = this.core.hasAssistant ? 'true' : 'false';
|
|
511
|
+
ask.disabled = Boolean(this.core.assistantBusy);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
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;
|
|
418
530
|
}
|
|
419
531
|
|
|
420
532
|
#paintGraph(series) {
|
|
@@ -468,6 +580,11 @@ export class UIOverlay {
|
|
|
468
580
|
}
|
|
469
581
|
}
|
|
470
582
|
|
|
583
|
+
function abbrevName(name) {
|
|
584
|
+
const s = String(name || '?');
|
|
585
|
+
return s.slice(0, 2).toUpperCase();
|
|
586
|
+
}
|
|
587
|
+
|
|
471
588
|
function fmtHud(value) {
|
|
472
589
|
if (value == null) return '—';
|
|
473
590
|
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public TypeScript surface for bee-ladybug.
|
|
3
|
+
* Runtime is still plain ESM JavaScript in src/*.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const BEE_LADYBUG_VERSION: string;
|
|
7
|
+
|
|
8
|
+
export const WELL_KNOWN_TYPES: readonly [
|
|
9
|
+
'fps',
|
|
10
|
+
'metric',
|
|
11
|
+
'coord',
|
|
12
|
+
'log',
|
|
13
|
+
'warn',
|
|
14
|
+
'error',
|
|
15
|
+
'state',
|
|
16
|
+
'sys',
|
|
17
|
+
'clear'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
21
|
+
export type WellKnownType = (typeof WELL_KNOWN_TYPES)[number];
|
|
22
|
+
|
|
23
|
+
export interface BeePacket {
|
|
24
|
+
id: number;
|
|
25
|
+
type: string;
|
|
26
|
+
payload: Record<string, unknown>;
|
|
27
|
+
wallTime: number;
|
|
28
|
+
simTime: number;
|
|
29
|
+
frame: number;
|
|
30
|
+
source: string;
|
|
31
|
+
level: LogLevel | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface HudEntry {
|
|
35
|
+
value: unknown;
|
|
36
|
+
unit?: string;
|
|
37
|
+
type: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MetricHistory {
|
|
41
|
+
name: string;
|
|
42
|
+
values: number[];
|
|
43
|
+
min: number;
|
|
44
|
+
max: number;
|
|
45
|
+
avg: number;
|
|
46
|
+
last: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CoreState {
|
|
50
|
+
version: string;
|
|
51
|
+
visible: boolean;
|
|
52
|
+
frozen: boolean;
|
|
53
|
+
timeScale: number;
|
|
54
|
+
simTime: number;
|
|
55
|
+
frameCount: number;
|
|
56
|
+
coreFps: number;
|
|
57
|
+
logCount: number;
|
|
58
|
+
hud: Record<string, HudEntry>;
|
|
59
|
+
mode: string;
|
|
60
|
+
assistant: string | null;
|
|
61
|
+
assistants: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CoreOptions {
|
|
65
|
+
toggleKey?: string;
|
|
66
|
+
slowKey?: string;
|
|
67
|
+
freezeKey?: string;
|
|
68
|
+
slowScale?: number;
|
|
69
|
+
maxLogs?: number;
|
|
70
|
+
historySize?: number;
|
|
71
|
+
mount?: 'auto' | false | HTMLElement;
|
|
72
|
+
ui?: boolean;
|
|
73
|
+
autoAttach?: boolean;
|
|
74
|
+
autoStart?: boolean;
|
|
75
|
+
overlay?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const CORE_DEFAULTS: Required<Omit<CoreOptions, 'overlay' | 'mount'>> & {
|
|
79
|
+
mount: 'auto';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface AssistantSnapshot {
|
|
83
|
+
question: string;
|
|
84
|
+
version: string;
|
|
85
|
+
state: CoreState;
|
|
86
|
+
hud: Record<string, HudEntry>;
|
|
87
|
+
logs: Array<{
|
|
88
|
+
type: string;
|
|
89
|
+
level: LogLevel | null;
|
|
90
|
+
source: string;
|
|
91
|
+
message: string;
|
|
92
|
+
frame: number;
|
|
93
|
+
}>;
|
|
94
|
+
history: {
|
|
95
|
+
name: string;
|
|
96
|
+
min: number;
|
|
97
|
+
max: number;
|
|
98
|
+
avg: number;
|
|
99
|
+
last: number;
|
|
100
|
+
samples: number[];
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface AssistantProvider {
|
|
105
|
+
name?: string;
|
|
106
|
+
complete(input: { question: string; snapshot: AssistantSnapshot }): unknown;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function formatSimTime(ms: number): string;
|
|
110
|
+
|
|
111
|
+
export class BeeLadybugCore {
|
|
112
|
+
readonly version: string;
|
|
113
|
+
toggleKey: string;
|
|
114
|
+
slowKey: string;
|
|
115
|
+
freezeKey: string;
|
|
116
|
+
slowScale: number;
|
|
117
|
+
maxLogs: number;
|
|
118
|
+
historySize: number;
|
|
119
|
+
visible: boolean;
|
|
120
|
+
frozen: boolean;
|
|
121
|
+
timeScale: number;
|
|
122
|
+
simTime: number;
|
|
123
|
+
frameCount: number;
|
|
124
|
+
coreFps: number;
|
|
125
|
+
ui: UIOverlay | null;
|
|
126
|
+
|
|
127
|
+
constructor(options?: CoreOptions);
|
|
128
|
+
sendData(type: string, payload?: unknown): BeePacket | null;
|
|
129
|
+
subscribe(handler: (packet: BeePacket) => void): () => void;
|
|
130
|
+
start(): this;
|
|
131
|
+
stop(): this;
|
|
132
|
+
attach(): this;
|
|
133
|
+
detach(): this;
|
|
134
|
+
destroy(): this;
|
|
135
|
+
show(): this;
|
|
136
|
+
hide(): this;
|
|
137
|
+
toggle(): this;
|
|
138
|
+
freeze(): this;
|
|
139
|
+
unfreeze(): this;
|
|
140
|
+
toggleFreeze(): this;
|
|
141
|
+
setTimeScale(scale: number): this;
|
|
142
|
+
applySlowMo(): this;
|
|
143
|
+
restoreRealtime(): this;
|
|
144
|
+
getState(): CoreState;
|
|
145
|
+
getLogs(): BeePacket[];
|
|
146
|
+
getHistory(name: string): MetricHistory;
|
|
147
|
+
getPrimaryHistory(): MetricHistory;
|
|
148
|
+
getHud(): Record<string, HudEntry>;
|
|
149
|
+
getLatest(type: string): BeePacket | null;
|
|
150
|
+
readonly hasAssistant: boolean;
|
|
151
|
+
readonly assistantBusy: boolean;
|
|
152
|
+
getAssistants(): string[];
|
|
153
|
+
registerAssistant(provider: AssistantProvider): this;
|
|
154
|
+
setActiveAssistant(name: string): this;
|
|
155
|
+
setAssistant(provider: AssistantProvider): this;
|
|
156
|
+
clearAssistant(name?: string): this;
|
|
157
|
+
ask(question?: string): Promise<string | null>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class UIOverlay {
|
|
161
|
+
core: BeeLadybugCore | null;
|
|
162
|
+
constructor(core: BeeLadybugCore, options?: Record<string, unknown>);
|
|
163
|
+
mount(target?: 'auto' | HTMLElement): this;
|
|
164
|
+
unmount(): this;
|
|
165
|
+
destroy(): this;
|
|
166
|
+
setVisible(visible: boolean): this;
|
|
167
|
+
onPacket(packet: BeePacket): void;
|
|
168
|
+
onClear(channel?: string): void;
|
|
169
|
+
render(): void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface CanvasEntity {
|
|
173
|
+
name?: string;
|
|
174
|
+
label?: string;
|
|
175
|
+
id?: string | number;
|
|
176
|
+
role?: string;
|
|
177
|
+
primary?: boolean;
|
|
178
|
+
x?: number;
|
|
179
|
+
y?: number;
|
|
180
|
+
worldX?: number;
|
|
181
|
+
worldY?: number;
|
|
182
|
+
width?: number;
|
|
183
|
+
height?: number;
|
|
184
|
+
w?: number;
|
|
185
|
+
h?: number;
|
|
186
|
+
active?: boolean;
|
|
187
|
+
colliding?: boolean;
|
|
188
|
+
[key: string]: unknown;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface CanvasAdapterOptions {
|
|
192
|
+
canvas?: HTMLCanvasElement | null;
|
|
193
|
+
entities?: CanvasEntity[];
|
|
194
|
+
overlay?: boolean;
|
|
195
|
+
computeCollisions?: boolean;
|
|
196
|
+
colorActive?: string;
|
|
197
|
+
colorColliding?: string;
|
|
198
|
+
colorInactive?: string;
|
|
199
|
+
source?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export const CANVAS_ADAPTER_DEFAULTS: Required<
|
|
203
|
+
Omit<CanvasAdapterOptions, 'canvas' | 'entities'>
|
|
204
|
+
>;
|
|
205
|
+
|
|
206
|
+
export class CanvasAdapter {
|
|
207
|
+
core: BeeLadybugCore | null;
|
|
208
|
+
canvas: HTMLCanvasElement | null;
|
|
209
|
+
entities: CanvasEntity[];
|
|
210
|
+
overlayEnabled: boolean;
|
|
211
|
+
computeCollisions: boolean;
|
|
212
|
+
frozen: boolean;
|
|
213
|
+
timeScale: number;
|
|
214
|
+
constructor(core: BeeLadybugCore, options?: CanvasAdapterOptions);
|
|
215
|
+
attach(): this;
|
|
216
|
+
detach(): this;
|
|
217
|
+
setCanvas(canvas: HTMLCanvasElement | null): this;
|
|
218
|
+
setEntities(entities: CanvasEntity[]): this;
|
|
219
|
+
pump(ctx?: CanvasRenderingContext2D): this;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface WebDOMAdapterOptions {
|
|
223
|
+
root?: ParentNode | null;
|
|
224
|
+
overlay?: boolean;
|
|
225
|
+
autoTick?: boolean;
|
|
226
|
+
watch?: string[];
|
|
227
|
+
source?: string;
|
|
228
|
+
colorHover?: string;
|
|
229
|
+
colorWatch?: string;
|
|
230
|
+
colorMargin?: string;
|
|
231
|
+
colorPadding?: string;
|
|
232
|
+
colorContent?: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export const WEB_DOM_ADAPTER_DEFAULTS: Required<Omit<WebDOMAdapterOptions, 'root'>>;
|
|
236
|
+
|
|
237
|
+
export class WebDOMAdapter {
|
|
238
|
+
core: BeeLadybugCore | null;
|
|
239
|
+
root: ParentNode | null;
|
|
240
|
+
overlayEnabled: boolean;
|
|
241
|
+
autoTick: boolean;
|
|
242
|
+
watch: string[];
|
|
243
|
+
frozen: boolean;
|
|
244
|
+
timeScale: number;
|
|
245
|
+
hover: Element | null;
|
|
246
|
+
constructor(core: BeeLadybugCore, options?: WebDOMAdapterOptions);
|
|
247
|
+
attach(): this;
|
|
248
|
+
detach(): this;
|
|
249
|
+
setRoot(root: ParentNode | null): this;
|
|
250
|
+
pump(): this;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export type PythonBridgeStatus = 'idle' | 'connecting' | 'open' | 'closed';
|
|
254
|
+
|
|
255
|
+
export interface PythonBridgeOptions {
|
|
256
|
+
url?: string;
|
|
257
|
+
autoReconnect?: boolean;
|
|
258
|
+
reconnectMs?: number;
|
|
259
|
+
source?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const PYTHON_BRIDGE_DEFAULTS: Required<PythonBridgeOptions>;
|
|
263
|
+
|
|
264
|
+
export class PythonBridge {
|
|
265
|
+
core: BeeLadybugCore | null;
|
|
266
|
+
url: string;
|
|
267
|
+
autoReconnect: boolean;
|
|
268
|
+
reconnectMs: number;
|
|
269
|
+
source: string;
|
|
270
|
+
status: PythonBridgeStatus;
|
|
271
|
+
readonly connected: boolean;
|
|
272
|
+
constructor(core: BeeLadybugCore, options?: PythonBridgeOptions);
|
|
273
|
+
connect(url?: string): this;
|
|
274
|
+
disconnect(): this;
|
|
275
|
+
ingest(raw: unknown): this;
|
|
276
|
+
}
|
package/src/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { BeeLadybugCore, BEE_LADYBUG_VERSION, CORE_DEFAULTS, WELL_KNOWN_TYPES, f
|
|
|
2
2
|
export { UIOverlay } from './core/UIOverlay.js';
|
|
3
3
|
export { CanvasAdapter, CANVAS_ADAPTER_DEFAULTS } from './adapters/CanvasAdapter.js';
|
|
4
4
|
export { WebDOMAdapter, WEB_DOM_ADAPTER_DEFAULTS } from './adapters/WebDOMAdapter.js';
|
|
5
|
+
export { PythonBridge, PYTHON_BRIDGE_DEFAULTS } from './adapters/python/PythonBridge.js';
|