bloby-bot 0.17.9 → 0.18.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/cli/commands/update.ts +1 -0
- package/dist-bloby/assets/{bloby-Cnt49fF0.js → bloby-B8Qpaiaq.js} +59 -59
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-7F_qKdIR.js → highlighted-body-OFNGDK62-CEYRDN-L.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-6uL8XX_T.js +1 -0
- package/dist-bloby/bloby.html +1 -1
- package/package.json +1 -1
- package/supervisor/chat/bloby-main.tsx +35 -0
- package/supervisor/chat/src/components/Chat/InputBar.tsx +14 -1
- package/supervisor/widget.js +417 -23
- package/worker/prompts/bloby-system-prompt.txt +1 -1
- package/workspace/client/public/headphones_spritesheet.webp +0 -0
- package/dist-bloby/assets/mermaid-GHXKKRXX-DE0dm5QR.js +0 -1
package/supervisor/widget.js
CHANGED
|
@@ -30,14 +30,12 @@
|
|
|
30
30
|
|
|
31
31
|
var iframe = document.createElement('iframe');
|
|
32
32
|
iframe.src = '/bloby/';
|
|
33
|
-
iframe.setAttribute('loading', 'lazy');
|
|
34
33
|
panel.appendChild(iframe);
|
|
35
34
|
|
|
36
35
|
// ══════════════════════════════════════════════════════════════════
|
|
37
|
-
// ── Sprite Sheet
|
|
36
|
+
// ── Blob Sprite Sheet Config ─────────────────────────────────────
|
|
38
37
|
// ══════════════════════════════════════════════════════════════════
|
|
39
38
|
|
|
40
|
-
// ── Sprite config ──
|
|
41
39
|
var COLS = 16;
|
|
42
40
|
var FRAME_W = 125;
|
|
43
41
|
var FRAME_H = 120;
|
|
@@ -65,9 +63,37 @@
|
|
|
65
63
|
var BUBBLE_SIZE = 60;
|
|
66
64
|
var BUBBLE_MARGIN = 24;
|
|
67
65
|
|
|
66
|
+
// ══════════════════════════════════════════════════════════════════
|
|
67
|
+
// ── Headphones Sprite Sheet Config ───────────────────────────────
|
|
68
|
+
// ══════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
var HP_COLS = 16;
|
|
71
|
+
var HP_FRAME_W = 126;
|
|
72
|
+
var HP_FRAME_H = 84;
|
|
73
|
+
var HP_DISPLAY_H = 56;
|
|
74
|
+
var HP_DISPLAY_W = HP_DISPLAY_H * (HP_FRAME_W / HP_FRAME_H);
|
|
75
|
+
|
|
76
|
+
var HP_ACTIVATE_START = 11, HP_ACTIVATE_END = 40;
|
|
77
|
+
var HP_RECORD_START = 41, HP_RECORD_END = 85;
|
|
78
|
+
var HP_DEACTIVATE_START = 85, HP_DEACTIVATE_END = 191;
|
|
79
|
+
var HP_FPS = 24;
|
|
80
|
+
var HP_FRAME_MS = 1000 / HP_FPS;
|
|
81
|
+
|
|
82
|
+
// Expanded canvas for headphones mode
|
|
83
|
+
var HP_CANVAS_W = 90;
|
|
84
|
+
var HP_CANVAS_H = 66;
|
|
85
|
+
|
|
86
|
+
// Long-press threshold (ms)
|
|
87
|
+
var HP_HOLD_MS = 300;
|
|
88
|
+
|
|
68
89
|
// ── Easing ──
|
|
69
90
|
function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
|
70
91
|
|
|
92
|
+
// ── Splash guard (skip on soft refresh within same tab) ──
|
|
93
|
+
var SPLASH_KEY = 'bloby_splash_played';
|
|
94
|
+
var splashSeen = false;
|
|
95
|
+
try { splashSeen = sessionStorage.getItem(SPLASH_KEY) === '1'; } catch(e) {}
|
|
96
|
+
|
|
71
97
|
// ── Canvas setup (starts fullscreen for splash) ──
|
|
72
98
|
var canvas = document.createElement('canvas');
|
|
73
99
|
canvas.id = 'bloby-widget-bubble';
|
|
@@ -82,7 +108,7 @@
|
|
|
82
108
|
var dpr = window.devicePixelRatio || 1;
|
|
83
109
|
var W = 0, H = 0;
|
|
84
110
|
|
|
85
|
-
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | 'disabled' ──
|
|
111
|
+
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | 'headphones' | 'disabled' ──
|
|
86
112
|
var canvasPhase = 'splash';
|
|
87
113
|
var canvasCreatedAt = Date.now();
|
|
88
114
|
var appReady = false;
|
|
@@ -93,6 +119,9 @@
|
|
|
93
119
|
if (canvasPhase === 'bubble') {
|
|
94
120
|
W = BUBBLE_SIZE;
|
|
95
121
|
H = BUBBLE_SIZE;
|
|
122
|
+
} else if (canvasPhase === 'headphones') {
|
|
123
|
+
W = HP_CANVAS_W;
|
|
124
|
+
H = HP_CANVAS_H;
|
|
96
125
|
} else {
|
|
97
126
|
W = window.innerWidth;
|
|
98
127
|
H = window.innerHeight;
|
|
@@ -112,7 +141,7 @@
|
|
|
112
141
|
}
|
|
113
142
|
});
|
|
114
143
|
|
|
115
|
-
// ──
|
|
144
|
+
// ── Blob animation state ──
|
|
116
145
|
var spriteSheet = null;
|
|
117
146
|
var animState = 'loading';
|
|
118
147
|
var currentFrame = 0;
|
|
@@ -128,7 +157,27 @@
|
|
|
128
157
|
var travelA = { x: 0, y: 0 };
|
|
129
158
|
var target = { x: 0, y: 0 };
|
|
130
159
|
|
|
131
|
-
// ──
|
|
160
|
+
// ── Headphones animation state ──
|
|
161
|
+
var hpSpriteSheet = null;
|
|
162
|
+
var hpState = 'none'; // none | activating | recording | deactivating | activating_then_deactivate
|
|
163
|
+
var hpFrame = 0;
|
|
164
|
+
var hpPingPong = 1;
|
|
165
|
+
var hpLastFrameTime = 0;
|
|
166
|
+
|
|
167
|
+
// ── Headphones mic recording state ──
|
|
168
|
+
var hpMediaRecorder = null;
|
|
169
|
+
var hpAudioChunks = [];
|
|
170
|
+
var hpStream = null;
|
|
171
|
+
var hpPointerDown = false;
|
|
172
|
+
var hpHoldTimer = null;
|
|
173
|
+
var hpWasHold = false;
|
|
174
|
+
var hpMicPermitted = false;
|
|
175
|
+
var hpWhisperEnabled = false;
|
|
176
|
+
var hpWebSpeechCtor = window.SpeechRecognition || window.webkitSpeechRecognition || null;
|
|
177
|
+
var hpSpeechInstance = null;
|
|
178
|
+
var hpSpeechTranscript = '';
|
|
179
|
+
|
|
180
|
+
// ── Load blob sprite sheet ──
|
|
132
181
|
function loadSprite(onDone, onFail) {
|
|
133
182
|
var img = new Image();
|
|
134
183
|
img.onload = function () {
|
|
@@ -150,7 +199,16 @@
|
|
|
150
199
|
img.src = '/spritesheet.webp';
|
|
151
200
|
}
|
|
152
201
|
|
|
153
|
-
// ──
|
|
202
|
+
// ── Load headphones sprite sheet (lazy, called after bubble ready) ──
|
|
203
|
+
function loadHpSprite(cb) {
|
|
204
|
+
if (hpSpriteSheet) { if (cb) cb(); return; }
|
|
205
|
+
var img = new Image();
|
|
206
|
+
img.onload = function () { hpSpriteSheet = img; if (cb) cb(); };
|
|
207
|
+
img.onerror = function () { console.error('[widget] headphones spritesheet failed to load'); };
|
|
208
|
+
img.src = '/headphones_spritesheet.webp';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Trigger blob move ──
|
|
154
212
|
function moveTo(tx, ty) {
|
|
155
213
|
if (animState !== 'idle') return;
|
|
156
214
|
target.x = tx;
|
|
@@ -174,9 +232,46 @@
|
|
|
174
232
|
animState = 'melting';
|
|
175
233
|
}
|
|
176
234
|
|
|
177
|
-
//
|
|
235
|
+
// ══════════════════════════════════════════════════════════════════
|
|
236
|
+
// ── Update (blob + headphones) ───────────────────────────────────
|
|
237
|
+
// ══════════════════════════════════════════════════════════════════
|
|
238
|
+
|
|
178
239
|
function update(now) {
|
|
179
|
-
//
|
|
240
|
+
// ── Headphones animation ──
|
|
241
|
+
if (hpState !== 'none') {
|
|
242
|
+
var hpDelta = now - hpLastFrameTime;
|
|
243
|
+
if (hpDelta >= HP_FRAME_MS) {
|
|
244
|
+
hpLastFrameTime = now - (hpDelta % HP_FRAME_MS);
|
|
245
|
+
|
|
246
|
+
if (hpState === 'activating' || hpState === 'activating_then_deactivate') {
|
|
247
|
+
hpFrame++;
|
|
248
|
+
if (hpFrame > HP_ACTIVATE_END) {
|
|
249
|
+
if (hpState === 'activating_then_deactivate') {
|
|
250
|
+
hpState = 'deactivating';
|
|
251
|
+
hpFrame = HP_DEACTIVATE_START;
|
|
252
|
+
} else {
|
|
253
|
+
hpState = 'recording';
|
|
254
|
+
hpFrame = HP_RECORD_START;
|
|
255
|
+
hpPingPong = 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else if (hpState === 'recording') {
|
|
259
|
+
hpFrame += hpPingPong;
|
|
260
|
+
if (hpFrame >= HP_RECORD_END) { hpFrame = HP_RECORD_END; hpPingPong = -1; }
|
|
261
|
+
else if (hpFrame <= HP_RECORD_START) { hpFrame = HP_RECORD_START; hpPingPong = 1; }
|
|
262
|
+
} else if (hpState === 'deactivating') {
|
|
263
|
+
hpFrame++;
|
|
264
|
+
if (hpFrame > HP_DEACTIVATE_END) {
|
|
265
|
+
hpState = 'none';
|
|
266
|
+
endHpCanvasMode();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return; // Skip blob update while headphones active
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ── Blob animation ──
|
|
274
|
+
// Smooth position during travel (every rAF)
|
|
180
275
|
if (animState === 'traveling') {
|
|
181
276
|
var elapsed = now - travelStartTime;
|
|
182
277
|
var progress = Math.min(1, elapsed / travelDuration);
|
|
@@ -233,6 +328,10 @@
|
|
|
233
328
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
234
329
|
center.x = BUBBLE_SIZE / 2;
|
|
235
330
|
center.y = BUBBLE_SIZE / 2;
|
|
331
|
+
// Mark splash as played for this session
|
|
332
|
+
try { sessionStorage.setItem(SPLASH_KEY, '1'); } catch(e) {}
|
|
333
|
+
// Preload headphones sprite in background
|
|
334
|
+
loadHpSprite();
|
|
236
335
|
if (hideAfterTransition) {
|
|
237
336
|
canvas.style.display = 'none';
|
|
238
337
|
}
|
|
@@ -241,8 +340,11 @@
|
|
|
241
340
|
}
|
|
242
341
|
}
|
|
243
342
|
|
|
244
|
-
//
|
|
245
|
-
|
|
343
|
+
// ══════════════════════════════════════════════════════════════════
|
|
344
|
+
// ── Draw (blob + headphones) ─────────────────────────────────────
|
|
345
|
+
// ══════════════════════════════════════════════════════════════════
|
|
346
|
+
|
|
347
|
+
function drawBlobFrame(frame, x, y, rotate, flipX) {
|
|
246
348
|
if (!spriteSheet) return;
|
|
247
349
|
var col = frame % COLS;
|
|
248
350
|
var row = Math.floor(frame / COLS);
|
|
@@ -264,35 +366,278 @@
|
|
|
264
366
|
ctx.restore();
|
|
265
367
|
}
|
|
266
368
|
|
|
369
|
+
function drawHpFrame(frame) {
|
|
370
|
+
if (!hpSpriteSheet) return;
|
|
371
|
+
var col = frame % HP_COLS;
|
|
372
|
+
var row = Math.floor(frame / HP_COLS);
|
|
373
|
+
var sx = col * HP_FRAME_W;
|
|
374
|
+
var sy = row * HP_FRAME_H;
|
|
375
|
+
|
|
376
|
+
ctx.save();
|
|
377
|
+
ctx.translate(W / 2, H / 2);
|
|
378
|
+
ctx.imageSmoothingEnabled = true;
|
|
379
|
+
ctx.imageSmoothingQuality = 'high';
|
|
380
|
+
ctx.drawImage(
|
|
381
|
+
hpSpriteSheet,
|
|
382
|
+
sx, sy, HP_FRAME_W, HP_FRAME_H,
|
|
383
|
+
-HP_DISPLAY_W / 2, -HP_DISPLAY_H / 2,
|
|
384
|
+
HP_DISPLAY_W, HP_DISPLAY_H
|
|
385
|
+
);
|
|
386
|
+
ctx.restore();
|
|
387
|
+
}
|
|
388
|
+
|
|
267
389
|
function draw(now) {
|
|
268
390
|
ctx.clearRect(0, 0, W, H);
|
|
391
|
+
|
|
392
|
+
// Headphones mode
|
|
393
|
+
if (hpState !== 'none') {
|
|
394
|
+
drawHpFrame(hpFrame);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Blob mode
|
|
269
399
|
if (!spriteSheet || animState === 'loading') return;
|
|
270
400
|
|
|
271
401
|
if (animState === 'melting') {
|
|
272
402
|
var p = (currentFrame - MELT_START) / (MELT_END - MELT_START);
|
|
273
403
|
var eased = p * p;
|
|
274
|
-
|
|
404
|
+
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle * eased, travelFlip);
|
|
275
405
|
} else if (animState === 'traveling') {
|
|
276
|
-
|
|
406
|
+
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle, travelFlip);
|
|
277
407
|
} else if (animState === 'reforming') {
|
|
278
408
|
var p2 = (currentFrame - REFORM_START) / (REFORM_END - REFORM_START);
|
|
279
409
|
var eased2 = 1 - (1 - p2) * (1 - p2);
|
|
280
|
-
|
|
410
|
+
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle * (1 - eased2));
|
|
281
411
|
} else {
|
|
282
|
-
|
|
412
|
+
drawBlobFrame(currentFrame, center.x, center.y);
|
|
283
413
|
}
|
|
284
414
|
}
|
|
285
415
|
|
|
286
416
|
// ── Animation loop ──
|
|
287
417
|
function loop(now) {
|
|
288
|
-
if (animState !== 'loading') {
|
|
418
|
+
if (animState !== 'loading' || hpState !== 'none') {
|
|
289
419
|
update(now);
|
|
290
420
|
draw(now);
|
|
291
421
|
}
|
|
292
422
|
requestAnimationFrame(loop);
|
|
293
423
|
}
|
|
294
424
|
|
|
295
|
-
//
|
|
425
|
+
// ══════════════════════════════════════════════════════════════════
|
|
426
|
+
// ── Headphones Canvas Mode ───────────────────────────────────────
|
|
427
|
+
// ══════════════════════════════════════════════════════════════════
|
|
428
|
+
|
|
429
|
+
function enterHpCanvasMode() {
|
|
430
|
+
canvasPhase = 'headphones';
|
|
431
|
+
// Expand canvas, keeping center roughly aligned with the bubble center
|
|
432
|
+
var bubbleCenterFromRight = BUBBLE_MARGIN + BUBBLE_SIZE / 2;
|
|
433
|
+
var bubbleCenterFromBottom = BUBBLE_MARGIN + BUBBLE_SIZE / 2;
|
|
434
|
+
var hpRight = Math.round(bubbleCenterFromRight - HP_CANVAS_W / 2);
|
|
435
|
+
var hpBottom = Math.round(bubbleCenterFromBottom - HP_CANVAS_H / 2);
|
|
436
|
+
canvas.style.cssText = 'position:fixed;bottom:' + hpBottom + 'px;right:' + hpRight + 'px;width:' + HP_CANVAS_W + 'px;height:' + HP_CANVAS_H + 'px;z-index:99998;cursor:pointer;border-radius:12px;pointer-events:auto;';
|
|
437
|
+
W = HP_CANVAS_W;
|
|
438
|
+
H = HP_CANVAS_H;
|
|
439
|
+
canvas.width = HP_CANVAS_W * dpr;
|
|
440
|
+
canvas.height = HP_CANVAS_H * dpr;
|
|
441
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
442
|
+
|
|
443
|
+
hpState = 'activating';
|
|
444
|
+
hpFrame = HP_ACTIVATE_START;
|
|
445
|
+
hpLastFrameTime = performance.now();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function endHpCanvasMode() {
|
|
449
|
+
canvasPhase = 'bubble';
|
|
450
|
+
canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;transition:transform .15s ease;-webkit-tap-highlight-color:transparent;';
|
|
451
|
+
W = BUBBLE_SIZE;
|
|
452
|
+
H = BUBBLE_SIZE;
|
|
453
|
+
canvas.width = BUBBLE_SIZE * dpr;
|
|
454
|
+
canvas.height = BUBBLE_SIZE * dpr;
|
|
455
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
456
|
+
center.x = BUBBLE_SIZE / 2;
|
|
457
|
+
center.y = BUBBLE_SIZE / 2;
|
|
458
|
+
animState = 'idle';
|
|
459
|
+
currentFrame = IDLE_START;
|
|
460
|
+
idleDirection = 1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ══════════════════════════════════════════════════════════════════
|
|
464
|
+
// ── Headphones Mic Recording ─────────────────────────────────────
|
|
465
|
+
// ══════════════════════════════════════════════════════════════════
|
|
466
|
+
|
|
467
|
+
/** Check mic permission state without triggering a prompt */
|
|
468
|
+
function checkMicPermission() {
|
|
469
|
+
if (hpMicPermitted) return Promise.resolve('granted');
|
|
470
|
+
if (navigator.permissions && navigator.permissions.query) {
|
|
471
|
+
return navigator.permissions.query({ name: 'microphone' }).then(function(status) {
|
|
472
|
+
if (status.state === 'granted') { hpMicPermitted = true; return 'granted'; }
|
|
473
|
+
return status.state; // 'prompt' or 'denied'
|
|
474
|
+
}).catch(function() { return 'unknown'; });
|
|
475
|
+
}
|
|
476
|
+
return Promise.resolve('unknown');
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Start mic capture (shared by initial recording and re-activation) */
|
|
480
|
+
function startMicCapture() {
|
|
481
|
+
if (hpWhisperEnabled) {
|
|
482
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
|
|
483
|
+
// If user released while getUserMedia was pending, clean up
|
|
484
|
+
if (!hpPointerDown) {
|
|
485
|
+
stream.getTracks().forEach(function(t) { t.stop(); });
|
|
486
|
+
if (hpState === 'activating') hpState = 'activating_then_deactivate';
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
hpStream = stream;
|
|
490
|
+
var mimeType = (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported('audio/webm;codecs=opus'))
|
|
491
|
+
? 'audio/webm;codecs=opus' : 'audio/webm';
|
|
492
|
+
var recorder = new MediaRecorder(stream, { mimeType: mimeType });
|
|
493
|
+
hpAudioChunks = [];
|
|
494
|
+
recorder.ondataavailable = function(ev) {
|
|
495
|
+
if (ev.data.size > 0) hpAudioChunks.push(ev.data);
|
|
496
|
+
};
|
|
497
|
+
hpMediaRecorder = recorder;
|
|
498
|
+
recorder.start();
|
|
499
|
+
}).catch(function(err) {
|
|
500
|
+
console.error('[widget] mic start failed:', err);
|
|
501
|
+
if (hpState === 'activating') hpState = 'activating_then_deactivate';
|
|
502
|
+
});
|
|
503
|
+
} else if (hpWebSpeechCtor) {
|
|
504
|
+
var recognition = new hpWebSpeechCtor();
|
|
505
|
+
recognition.continuous = false;
|
|
506
|
+
recognition.interimResults = true;
|
|
507
|
+
recognition.lang = navigator.language || 'en-US';
|
|
508
|
+
hpSpeechTranscript = '';
|
|
509
|
+
hpSpeechInstance = recognition;
|
|
510
|
+
|
|
511
|
+
recognition.onresult = function(event) {
|
|
512
|
+
var last = event.results[event.results.length - 1];
|
|
513
|
+
hpSpeechTranscript = last[0].transcript;
|
|
514
|
+
};
|
|
515
|
+
recognition.onend = function() { hpSpeechInstance = null; };
|
|
516
|
+
recognition.onerror = function(event) {
|
|
517
|
+
console.error('[widget] speech error:', event.error);
|
|
518
|
+
hpSpeechInstance = null;
|
|
519
|
+
};
|
|
520
|
+
try { recognition.start(); } catch(e) {
|
|
521
|
+
console.error('[widget] speech start failed:', e);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** Entry point: permission check → sprite load → begin recording */
|
|
527
|
+
function startHpRecording() {
|
|
528
|
+
// Need at least one voice backend
|
|
529
|
+
if (!hpWhisperEnabled && !hpWebSpeechCtor) return;
|
|
530
|
+
|
|
531
|
+
checkMicPermission().then(function(permState) {
|
|
532
|
+
if (permState === 'denied') return;
|
|
533
|
+
|
|
534
|
+
if (permState === 'prompt' || permState === 'unknown') {
|
|
535
|
+
// Request permission — this may show a dialog
|
|
536
|
+
navigator.mediaDevices.getUserMedia({ audio: true }).then(function(testStream) {
|
|
537
|
+
testStream.getTracks().forEach(function(t) { t.stop(); });
|
|
538
|
+
hpMicPermitted = true;
|
|
539
|
+
// User may have released to click Allow — check before proceeding
|
|
540
|
+
if (!hpPointerDown) return;
|
|
541
|
+
finishStartHpRecording();
|
|
542
|
+
}).catch(function() { /* denied */ });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Permission already granted
|
|
547
|
+
finishStartHpRecording();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function finishStartHpRecording() {
|
|
552
|
+
if (!hpPointerDown) return;
|
|
553
|
+
if (!hpSpriteSheet) {
|
|
554
|
+
loadHpSprite(function() {
|
|
555
|
+
if (!hpPointerDown) return;
|
|
556
|
+
enterHpCanvasMode();
|
|
557
|
+
startMicCapture();
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
enterHpCanvasMode();
|
|
562
|
+
startMicCapture();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Stop recording and optionally send audio to iframe */
|
|
566
|
+
function stopHpRecording(cancelled) {
|
|
567
|
+
if (cancelled) {
|
|
568
|
+
if (hpStream) { hpStream.getTracks().forEach(function(t) { t.stop(); }); hpStream = null; }
|
|
569
|
+
if (hpMediaRecorder) { try { hpMediaRecorder.stop(); } catch(e) {} hpMediaRecorder = null; }
|
|
570
|
+
hpAudioChunks = [];
|
|
571
|
+
if (hpSpeechInstance) { try { hpSpeechInstance.abort(); } catch(e) {} hpSpeechInstance = null; }
|
|
572
|
+
hpSpeechTranscript = '';
|
|
573
|
+
} else {
|
|
574
|
+
// Normal stop — send the recorded audio/transcript to the iframe
|
|
575
|
+
if (hpWhisperEnabled && hpMediaRecorder && hpMediaRecorder.state !== 'inactive') {
|
|
576
|
+
var recorder = hpMediaRecorder;
|
|
577
|
+
var stream = hpStream;
|
|
578
|
+
recorder.onstop = function() {
|
|
579
|
+
if (stream) stream.getTracks().forEach(function(t) { t.stop(); });
|
|
580
|
+
var blob = new Blob(hpAudioChunks, { type: 'audio/webm' });
|
|
581
|
+
hpAudioChunks = [];
|
|
582
|
+
hpMediaRecorder = null;
|
|
583
|
+
hpStream = null;
|
|
584
|
+
if (blob.size < 1000) return;
|
|
585
|
+
|
|
586
|
+
var reader = new FileReader();
|
|
587
|
+
reader.onloadend = function() {
|
|
588
|
+
var dataUrl = reader.result;
|
|
589
|
+
var base64 = dataUrl.split(',')[1];
|
|
590
|
+
if (base64 && iframe.contentWindow) {
|
|
591
|
+
iframe.contentWindow.postMessage({ type: 'bloby:voice-record', audio: base64 }, '*');
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
reader.readAsDataURL(blob);
|
|
595
|
+
};
|
|
596
|
+
recorder.stop();
|
|
597
|
+
} else if (hpSpeechInstance) {
|
|
598
|
+
var instance = hpSpeechInstance;
|
|
599
|
+
var sent = false;
|
|
600
|
+
var sendTranscript = function() {
|
|
601
|
+
if (sent) return;
|
|
602
|
+
sent = true;
|
|
603
|
+
var text = hpSpeechTranscript.trim();
|
|
604
|
+
hpSpeechTranscript = '';
|
|
605
|
+
if (text && iframe.contentWindow) {
|
|
606
|
+
iframe.contentWindow.postMessage({ type: 'bloby:voice-record', transcript: text }, '*');
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
instance.onend = function() {
|
|
610
|
+
hpSpeechInstance = null;
|
|
611
|
+
sendTranscript();
|
|
612
|
+
};
|
|
613
|
+
try { instance.stop(); } catch(e) {
|
|
614
|
+
hpSpeechInstance = null;
|
|
615
|
+
sendTranscript();
|
|
616
|
+
}
|
|
617
|
+
// Safety timeout
|
|
618
|
+
setTimeout(function() {
|
|
619
|
+
if (hpSpeechInstance === instance) hpSpeechInstance = null;
|
|
620
|
+
sendTranscript();
|
|
621
|
+
}, 2000);
|
|
622
|
+
} else {
|
|
623
|
+
if (hpStream) { hpStream.getTracks().forEach(function(t) { t.stop(); }); hpStream = null; }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Trigger deactivation animation
|
|
628
|
+
if (hpState === 'activating') {
|
|
629
|
+
hpState = 'activating_then_deactivate';
|
|
630
|
+
} else if (hpState === 'recording') {
|
|
631
|
+
hpState = 'deactivating';
|
|
632
|
+
hpFrame = HP_DEACTIVATE_START;
|
|
633
|
+
hpLastFrameTime = performance.now();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ══════════════════════════════════════════════════════════════════
|
|
638
|
+
// ── App-ready Coordination ───────────────────────────────────────
|
|
639
|
+
// ══════════════════════════════════════════════════════════════════
|
|
640
|
+
|
|
296
641
|
function maybeTransition() {
|
|
297
642
|
if (!appReady || animState !== 'idle' || canvasPhase !== 'splash') return;
|
|
298
643
|
var elapsed = Date.now() - canvasCreatedAt;
|
|
@@ -310,7 +655,7 @@
|
|
|
310
655
|
moveTo(tx, ty);
|
|
311
656
|
}
|
|
312
657
|
|
|
313
|
-
// Skip splash and go straight to bubble mode (
|
|
658
|
+
// Skip splash and go straight to bubble mode (onboard, soft refresh, etc.)
|
|
314
659
|
function skipToBubble() {
|
|
315
660
|
canvasPhase = 'bubble';
|
|
316
661
|
canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;transition:transform .15s ease;-webkit-tap-highlight-color:transparent;';
|
|
@@ -327,6 +672,8 @@
|
|
|
327
672
|
// Hide the HTML splash fallback
|
|
328
673
|
var splash = document.getElementById('splash');
|
|
329
674
|
if (splash) splash.style.display = 'none';
|
|
675
|
+
// Preload headphones sprite in background
|
|
676
|
+
loadHpSprite();
|
|
330
677
|
}
|
|
331
678
|
|
|
332
679
|
window.addEventListener('bloby:app-ready', function () {
|
|
@@ -337,8 +684,7 @@
|
|
|
337
684
|
appReady = true;
|
|
338
685
|
}
|
|
339
686
|
|
|
340
|
-
// ── Check onboard status, then start ──
|
|
341
|
-
// First-time users get the onboard wizard — skip the splash animation.
|
|
687
|
+
// ── Check onboard status + settings, then start ──
|
|
342
688
|
var onboardActive = false;
|
|
343
689
|
fetch('/api/settings')
|
|
344
690
|
.then(function (r) { return r.json(); })
|
|
@@ -347,9 +693,15 @@
|
|
|
347
693
|
onboardActive = true;
|
|
348
694
|
skipSplash = true;
|
|
349
695
|
}
|
|
696
|
+
if (s.whisper_enabled === 'true') {
|
|
697
|
+
hpWhisperEnabled = true;
|
|
698
|
+
}
|
|
699
|
+
// Skip splash if already played this session
|
|
700
|
+
if (splashSeen) skipSplash = true;
|
|
350
701
|
startAnimation();
|
|
351
702
|
})
|
|
352
703
|
.catch(function () {
|
|
704
|
+
if (splashSeen) skipSplash = true;
|
|
353
705
|
startAnimation();
|
|
354
706
|
});
|
|
355
707
|
|
|
@@ -371,7 +723,7 @@
|
|
|
371
723
|
}
|
|
372
724
|
|
|
373
725
|
// ══════════════════════════════════════════════════════════════════
|
|
374
|
-
// ── Widget Interaction (Panel, Toggle,
|
|
726
|
+
// ── Widget Interaction (Panel, Toggle, Bubble Pointer) ───────────
|
|
375
727
|
// ══════════════════════════════════════════════════════════════════
|
|
376
728
|
|
|
377
729
|
var isOpen = false;
|
|
@@ -384,9 +736,51 @@
|
|
|
384
736
|
canvas.style.display = isOpen ? 'none' : 'block';
|
|
385
737
|
}
|
|
386
738
|
|
|
387
|
-
|
|
388
|
-
|
|
739
|
+
// ── Bubble pointer handlers (tap → open panel, long-press → headphones mic) ──
|
|
740
|
+
bubble.addEventListener('pointerdown', function(e) {
|
|
741
|
+
// Allow re-activation during headphones deactivation
|
|
742
|
+
if (canvasPhase === 'headphones' && hpState === 'deactivating') {
|
|
743
|
+
hpPointerDown = true;
|
|
744
|
+
hpWasHold = true;
|
|
745
|
+
hpState = 'activating';
|
|
746
|
+
hpFrame = HP_ACTIVATE_START;
|
|
747
|
+
hpLastFrameTime = performance.now();
|
|
748
|
+
startMicCapture();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (canvasPhase !== 'bubble' || hpState !== 'none') return;
|
|
753
|
+
hpPointerDown = true;
|
|
754
|
+
hpWasHold = false;
|
|
755
|
+
|
|
756
|
+
hpHoldTimer = setTimeout(function() {
|
|
757
|
+
hpWasHold = true;
|
|
758
|
+
startHpRecording();
|
|
759
|
+
}, HP_HOLD_MS);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
bubble.addEventListener('pointerup', function() {
|
|
763
|
+
hpPointerDown = false;
|
|
764
|
+
if (hpHoldTimer) { clearTimeout(hpHoldTimer); hpHoldTimer = null; }
|
|
765
|
+
|
|
766
|
+
if (hpState === 'activating' || hpState === 'recording') {
|
|
767
|
+
stopHpRecording(false);
|
|
768
|
+
} else if (hpState === 'none' && !hpWasHold && canvasPhase === 'bubble') {
|
|
769
|
+
toggle();
|
|
770
|
+
}
|
|
389
771
|
});
|
|
772
|
+
|
|
773
|
+
bubble.addEventListener('pointercancel', function() {
|
|
774
|
+
hpPointerDown = false;
|
|
775
|
+
if (hpHoldTimer) { clearTimeout(hpHoldTimer); hpHoldTimer = null; }
|
|
776
|
+
if (hpState !== 'none' && hpState !== 'deactivating') stopHpRecording(true);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// Prevent context menu on long-press (mobile)
|
|
780
|
+
bubble.addEventListener('contextmenu', function(e) {
|
|
781
|
+
e.preventDefault();
|
|
782
|
+
});
|
|
783
|
+
|
|
390
784
|
backdrop.addEventListener('click', toggle);
|
|
391
785
|
|
|
392
786
|
// Close on Escape
|
|
@@ -356,7 +356,7 @@ Do things, don't describe them. When asked to build something, build it. When as
|
|
|
356
356
|
Always read code before changing it. Understand what exists. Never propose changes to code you haven't read. If your human asks about or wants you to modify a file, read it first.
|
|
357
357
|
|
|
358
358
|
## Simplicity
|
|
359
|
-
No over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
|
|
359
|
+
No over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused. Before writing a new integration or skill, check the marketplace. If an audited version exists, recommend it — even if building it would be straightforward.
|
|
360
360
|
- Don't add features, refactoring, or "improvements" beyond what was asked
|
|
361
361
|
- Don't add docstrings, comments, or type annotations to code you didn't change
|
|
362
362
|
- Don't add error handling for scenarios that can't happen
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e}from"./bloby-Cnt49fF0.js";export{e as Mermaid};
|