bloby-bot 0.18.7 → 0.18.9
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/dist-bloby/assets/{bloby-BB0UI7YK.js → bloby-C0lGB4pP.js} +4 -4
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-DjVujd9M.js → highlighted-body-OFNGDK62-CQTQ53Zc.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-BZKpcY-H.js +1 -0
- package/dist-bloby/bloby.html +1 -1
- package/package.json +1 -1
- package/supervisor/chat/src/components/Chat/HeadphonesAnimation.tsx +31 -29
- package/supervisor/widget.js +150 -352
- package/dist-bloby/assets/mermaid-GHXKKRXX-CzCkjlOj.js +0 -1
package/supervisor/widget.js
CHANGED
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
// ── Styles ──
|
|
7
7
|
var style = document.createElement('style');
|
|
8
8
|
style.textContent = [
|
|
9
|
-
'#bloby-widget-bubble{
|
|
10
|
-
'#bloby-widget-bubble:hover{transform:scale(1.1)}',
|
|
11
|
-
'#bloby-widget-bubble:active{transform:scale(0.95)}',
|
|
9
|
+
'#bloby-widget-bubble{-webkit-tap-highlight-color:transparent}',
|
|
12
10
|
'#bloby-widget-backdrop{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.4);opacity:0;transition:opacity .2s ease;pointer-events:none}',
|
|
13
11
|
'#bloby-widget-backdrop.open{opacity:1;pointer-events:auto}',
|
|
14
12
|
'#bloby-widget-panel{position:fixed;top:0;right:0;bottom:0;z-index:99999;width:' + PANEL_WIDTH + ';max-width:100vw;transform:translateX(100%);transition:transform .25s cubic-bezier(.4,0,.2,1);box-shadow:-4px 0 24px rgba(0,0,0,0.3);border-left:1px solid #3a3a3a;overflow:hidden}',
|
|
@@ -33,7 +31,7 @@
|
|
|
33
31
|
panel.appendChild(iframe);
|
|
34
32
|
|
|
35
33
|
// ══════════════════════════════════════════════════════════════════
|
|
36
|
-
// ── Blob Sprite Sheet Config
|
|
34
|
+
// ── Blob Sprite Sheet Config (splash only) ───────────────────────
|
|
37
35
|
// ══════════════════════════════════════════════════════════════════
|
|
38
36
|
|
|
39
37
|
var COLS = 16;
|
|
@@ -42,13 +40,11 @@
|
|
|
42
40
|
var DISPLAY_H = 58;
|
|
43
41
|
var DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
44
42
|
|
|
45
|
-
// Frame ranges (0-indexed)
|
|
46
43
|
var IDLE_START = 0, IDLE_END = 29;
|
|
47
44
|
var MELT_START = 30, MELT_END = 52;
|
|
48
45
|
var TRAVEL_START = 52, TRAVEL_END = 84;
|
|
49
46
|
var REFORM_START = 84, REFORM_END = 191;
|
|
50
47
|
|
|
51
|
-
// Timing
|
|
52
48
|
var FPS = 29;
|
|
53
49
|
var FRAME_MS = 1000 / FPS;
|
|
54
50
|
var IDLE_FPS = 22;
|
|
@@ -63,47 +59,47 @@
|
|
|
63
59
|
var BUBBLE_SIZE = 60;
|
|
64
60
|
var BUBBLE_MARGIN = 24;
|
|
65
61
|
|
|
66
|
-
// ── Badge element
|
|
62
|
+
// ── Badge element ──
|
|
67
63
|
var badgeEl = document.createElement('div');
|
|
68
64
|
badgeEl.id = 'bloby-widget-badge';
|
|
69
65
|
badgeEl.style.cssText = 'display:none;position:fixed;bottom:' + (BUBBLE_MARGIN + BUBBLE_SIZE - 14) + 'px;right:' + (BUBBLE_MARGIN - 5) + 'px;z-index:99999;min-width:20px;height:20px;border-radius:10px;background:#F04D68;color:#fff;font:bold 11px -apple-system,BlinkMacSystemFont,sans-serif;text-align:center;line-height:20px;padding:0 5px;box-sizing:border-box;pointer-events:none;';
|
|
70
66
|
document.body.appendChild(badgeEl);
|
|
71
67
|
|
|
72
68
|
// ══════════════════════════════════════════════════════════════════
|
|
73
|
-
// ── Headphones Sprite Sheet Config
|
|
69
|
+
// ── Headphones Sprite Sheet Config (v2 — bubble + recording) ─────
|
|
74
70
|
// ══════════════════════════════════════════════════════════════════
|
|
75
71
|
|
|
76
72
|
var HP_COLS = 16;
|
|
77
73
|
var HP_FRAME_W = 126;
|
|
78
74
|
var HP_FRAME_H = 84;
|
|
79
|
-
|
|
80
|
-
var
|
|
75
|
+
// Bubble display: fill 60px width exactly
|
|
76
|
+
var HP_BUBBLE_H = 40;
|
|
77
|
+
var HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H); // 60
|
|
81
78
|
|
|
79
|
+
// v2 frame ranges
|
|
80
|
+
var HP_IDLE_FRAME = 10;
|
|
82
81
|
var HP_ACTIVATE_START = 11, HP_ACTIVATE_END = 40;
|
|
83
|
-
var HP_RECORD_START =
|
|
84
|
-
var HP_DEACTIVATE_START = 85, HP_DEACTIVATE_END = 191;
|
|
85
|
-
var HP_FPS = 24;
|
|
86
|
-
var HP_FRAME_MS = 1000 / HP_FPS;
|
|
82
|
+
var HP_RECORD_START = 40, HP_RECORD_END = 85;
|
|
87
83
|
|
|
88
|
-
//
|
|
89
|
-
var
|
|
90
|
-
var
|
|
84
|
+
// v2 FPS: 2x speed for activate/deactivate, normal for recording
|
|
85
|
+
var HP_BASE_FPS = 24;
|
|
86
|
+
var HP_FAST_FPS = 48;
|
|
91
87
|
|
|
92
|
-
// Long-press threshold
|
|
93
|
-
var HP_HOLD_MS =
|
|
88
|
+
// Long-press threshold
|
|
89
|
+
var HP_HOLD_MS = 500;
|
|
94
90
|
|
|
95
|
-
// ── Bubble CSS (shared
|
|
96
|
-
var BUBBLE_CSS = '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
|
|
91
|
+
// ── Bubble CSS (shared) ──
|
|
92
|
+
var BUBBLE_CSS = '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%;-webkit-tap-highlight-color:transparent;touch-action:none;';
|
|
97
93
|
|
|
98
94
|
// ── Easing ──
|
|
99
95
|
function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
|
100
96
|
|
|
101
|
-
// ── Splash guard
|
|
97
|
+
// ── Splash guard ──
|
|
102
98
|
var SPLASH_KEY = 'bloby_splash_played';
|
|
103
99
|
var splashSeen = false;
|
|
104
100
|
try { splashSeen = sessionStorage.getItem(SPLASH_KEY) === '1'; } catch(e) {}
|
|
105
101
|
|
|
106
|
-
// ── Canvas setup
|
|
102
|
+
// ── Canvas setup ──
|
|
107
103
|
var canvas = document.createElement('canvas');
|
|
108
104
|
canvas.id = 'bloby-widget-bubble';
|
|
109
105
|
canvas.setAttribute('role', 'button');
|
|
@@ -117,7 +113,7 @@
|
|
|
117
113
|
var dpr = window.devicePixelRatio || 1;
|
|
118
114
|
var W = 0, H = 0;
|
|
119
115
|
|
|
120
|
-
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | '
|
|
116
|
+
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | 'disabled' ──
|
|
121
117
|
var canvasPhase = 'splash';
|
|
122
118
|
var canvasCreatedAt = Date.now();
|
|
123
119
|
var appReady = false;
|
|
@@ -126,14 +122,9 @@
|
|
|
126
122
|
|
|
127
123
|
function resizeCanvas() {
|
|
128
124
|
if (canvasPhase === 'bubble') {
|
|
129
|
-
W = BUBBLE_SIZE;
|
|
130
|
-
H = BUBBLE_SIZE;
|
|
131
|
-
} else if (canvasPhase === 'headphones') {
|
|
132
|
-
W = HP_CANVAS_W;
|
|
133
|
-
H = HP_CANVAS_H;
|
|
125
|
+
W = BUBBLE_SIZE; H = BUBBLE_SIZE;
|
|
134
126
|
} else {
|
|
135
|
-
W = window.innerWidth;
|
|
136
|
-
H = window.innerHeight;
|
|
127
|
+
W = window.innerWidth; H = window.innerHeight;
|
|
137
128
|
}
|
|
138
129
|
canvas.width = W * dpr;
|
|
139
130
|
canvas.height = H * dpr;
|
|
@@ -150,7 +141,7 @@
|
|
|
150
141
|
}
|
|
151
142
|
});
|
|
152
143
|
|
|
153
|
-
// ── Blob animation state ──
|
|
144
|
+
// ── Blob animation state (splash only) ──
|
|
154
145
|
var spriteSheet = null;
|
|
155
146
|
var animState = 'loading';
|
|
156
147
|
var currentFrame = 0;
|
|
@@ -161,22 +152,22 @@
|
|
|
161
152
|
var travelAngle = 0;
|
|
162
153
|
var travelDrawAngle = 0;
|
|
163
154
|
var travelFlip = 1;
|
|
164
|
-
|
|
165
155
|
var center = { x: 0, y: 0 };
|
|
166
156
|
var travelA = { x: 0, y: 0 };
|
|
167
157
|
var target = { x: 0, y: 0 };
|
|
168
158
|
|
|
169
|
-
// ── Headphones animation state ──
|
|
159
|
+
// ── Headphones animation state (bubble) ──
|
|
170
160
|
var hpSpriteSheet = null;
|
|
171
|
-
|
|
172
|
-
var
|
|
161
|
+
// idle | activating | activating_then_deactivate | recording | deactivating
|
|
162
|
+
var hpState = 'idle';
|
|
163
|
+
var hpFrame = HP_IDLE_FRAME;
|
|
173
164
|
var hpPingPong = 1;
|
|
174
165
|
var hpLastFrameTime = 0;
|
|
175
166
|
|
|
176
167
|
// ── Unread badge ──
|
|
177
168
|
var unreadCount = 0;
|
|
178
169
|
|
|
179
|
-
// ──
|
|
170
|
+
// ── Mic recording state ──
|
|
180
171
|
var hpMediaRecorder = null;
|
|
181
172
|
var hpAudioChunks = [];
|
|
182
173
|
var hpStream = null;
|
|
@@ -198,69 +189,65 @@
|
|
|
198
189
|
center.y = Math.round(H / 2);
|
|
199
190
|
currentFrame = IDLE_START;
|
|
200
191
|
animState = 'idle';
|
|
201
|
-
|
|
202
|
-
// Hide the HTML splash fallback
|
|
203
192
|
var splash = document.getElementById('splash');
|
|
204
193
|
if (splash) splash.style.display = 'none';
|
|
205
|
-
|
|
206
194
|
onDone();
|
|
207
195
|
};
|
|
208
|
-
img.onerror = function () {
|
|
209
|
-
if (onFail) onFail();
|
|
210
|
-
};
|
|
196
|
+
img.onerror = function () { if (onFail) onFail(); };
|
|
211
197
|
img.src = '/spritesheet.webp';
|
|
212
198
|
}
|
|
213
199
|
|
|
214
|
-
// ── Load headphones sprite sheet
|
|
200
|
+
// ── Load headphones sprite sheet ──
|
|
215
201
|
function loadHpSprite(cb) {
|
|
216
202
|
if (hpSpriteSheet) { if (cb) cb(); return; }
|
|
217
203
|
var img = new Image();
|
|
218
204
|
img.onload = function () { hpSpriteSheet = img; if (cb) cb(); };
|
|
219
|
-
img.onerror = function () { console.error('[widget] headphones
|
|
205
|
+
img.onerror = function () { console.error('[widget] headphones sprite failed'); };
|
|
220
206
|
img.src = '/headphones_spritesheet.webp';
|
|
221
207
|
}
|
|
222
208
|
|
|
223
|
-
// ── Trigger blob move ──
|
|
209
|
+
// ── Trigger blob move (splash) ──
|
|
224
210
|
function moveTo(tx, ty) {
|
|
225
211
|
if (animState !== 'idle') return;
|
|
226
|
-
target.x = tx;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
travelA.y = center.y;
|
|
230
|
-
|
|
231
|
-
var dx = target.x - center.x;
|
|
232
|
-
var dy = target.y - center.y;
|
|
212
|
+
target.x = tx; target.y = ty;
|
|
213
|
+
travelA.x = center.x; travelA.y = center.y;
|
|
214
|
+
var dx = target.x - center.x, dy = target.y - center.y;
|
|
233
215
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
234
|
-
|
|
235
216
|
travelAngle = Math.atan2(dy, dx);
|
|
236
217
|
travelDrawAngle = travelAngle;
|
|
237
218
|
travelFlip = 1;
|
|
238
219
|
if (travelDrawAngle > Math.PI / 2) { travelDrawAngle -= Math.PI; travelFlip = -1; }
|
|
239
220
|
else if (travelDrawAngle < -Math.PI / 2) { travelDrawAngle += Math.PI; travelFlip = -1; }
|
|
240
221
|
travelDuration = Math.min(TRAVEL_MAX, Math.max(TRAVEL_MIN, dist / TRAVEL_PX_PER_MS));
|
|
241
|
-
|
|
242
222
|
currentFrame = MELT_START;
|
|
243
223
|
lastFrameTime = performance.now();
|
|
244
224
|
animState = 'melting';
|
|
245
225
|
}
|
|
246
226
|
|
|
247
227
|
// ══════════════════════════════════════════════════════════════════
|
|
248
|
-
// ── Update
|
|
228
|
+
// ── Update ───────────────────────────────────────────────────────
|
|
249
229
|
// ══════════════════════════════════════════════════════════════════
|
|
250
230
|
|
|
231
|
+
function hpFps() {
|
|
232
|
+
if (hpState === 'activating' || hpState === 'activating_then_deactivate' || hpState === 'deactivating') return HP_FAST_FPS;
|
|
233
|
+
return HP_BASE_FPS;
|
|
234
|
+
}
|
|
235
|
+
|
|
251
236
|
function update(now) {
|
|
252
|
-
// ── Headphones animation ──
|
|
253
|
-
if (hpState !== '
|
|
237
|
+
// ── Headphones animation (bubble mode) ──
|
|
238
|
+
if (canvasPhase === 'bubble' && hpState !== 'idle') {
|
|
239
|
+
var hpInterval = 1000 / hpFps();
|
|
254
240
|
var hpDelta = now - hpLastFrameTime;
|
|
255
|
-
if (hpDelta >=
|
|
256
|
-
hpLastFrameTime = now - (hpDelta %
|
|
241
|
+
if (hpDelta >= hpInterval) {
|
|
242
|
+
hpLastFrameTime = now - (hpDelta % hpInterval);
|
|
257
243
|
|
|
258
244
|
if (hpState === 'activating' || hpState === 'activating_then_deactivate') {
|
|
259
245
|
hpFrame++;
|
|
260
246
|
if (hpFrame > HP_ACTIVATE_END) {
|
|
261
247
|
if (hpState === 'activating_then_deactivate') {
|
|
248
|
+
// Reverse back to idle
|
|
262
249
|
hpState = 'deactivating';
|
|
263
|
-
hpFrame =
|
|
250
|
+
hpFrame = HP_ACTIVATE_END;
|
|
264
251
|
} else {
|
|
265
252
|
hpState = 'recording';
|
|
266
253
|
hpFrame = HP_RECORD_START;
|
|
@@ -272,28 +259,26 @@
|
|
|
272
259
|
if (hpFrame >= HP_RECORD_END) { hpFrame = HP_RECORD_END; hpPingPong = -1; }
|
|
273
260
|
else if (hpFrame <= HP_RECORD_START) { hpFrame = HP_RECORD_START; hpPingPong = 1; }
|
|
274
261
|
} else if (hpState === 'deactivating') {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
262
|
+
// v2: reverse 40→11
|
|
263
|
+
hpFrame--;
|
|
264
|
+
if (hpFrame < HP_ACTIVATE_START) {
|
|
265
|
+
hpState = 'idle';
|
|
266
|
+
hpFrame = HP_IDLE_FRAME;
|
|
279
267
|
}
|
|
280
268
|
}
|
|
281
269
|
}
|
|
282
|
-
return; //
|
|
270
|
+
return; // skip blob update
|
|
283
271
|
}
|
|
284
272
|
|
|
285
|
-
// ── Blob animation ──
|
|
286
|
-
// Smooth position during travel (every rAF)
|
|
273
|
+
// ── Blob animation (splash/transition only) ──
|
|
287
274
|
if (animState === 'traveling') {
|
|
288
275
|
var elapsed = now - travelStartTime;
|
|
289
276
|
var progress = Math.min(1, elapsed / travelDuration);
|
|
290
277
|
var e = easeInOutCubic(progress);
|
|
291
278
|
center.x = travelA.x + (target.x - travelA.x) * e;
|
|
292
279
|
center.y = travelA.y + (target.y - travelA.y) * e;
|
|
293
|
-
|
|
294
280
|
if (progress >= 1) {
|
|
295
|
-
center.x = target.x;
|
|
296
|
-
center.y = target.y;
|
|
281
|
+
center.x = target.x; center.y = target.y;
|
|
297
282
|
animState = 'reforming';
|
|
298
283
|
currentFrame = REFORM_START;
|
|
299
284
|
lastFrameTime = now;
|
|
@@ -301,7 +286,6 @@
|
|
|
301
286
|
}
|
|
302
287
|
}
|
|
303
288
|
|
|
304
|
-
// Sprite frame advance (at configured FPS)
|
|
305
289
|
var frameInterval = animState === 'idle' ? IDLE_FRAME_MS
|
|
306
290
|
: animState === 'reforming' ? REFORM_FRAME_MS
|
|
307
291
|
: FRAME_MS;
|
|
@@ -315,11 +299,7 @@
|
|
|
315
299
|
else if (currentFrame <= IDLE_START) { currentFrame = IDLE_START; idleDirection = 1; }
|
|
316
300
|
} else if (animState === 'melting') {
|
|
317
301
|
currentFrame++;
|
|
318
|
-
if (currentFrame > MELT_END) {
|
|
319
|
-
animState = 'traveling';
|
|
320
|
-
currentFrame = TRAVEL_START;
|
|
321
|
-
travelStartTime = now;
|
|
322
|
-
}
|
|
302
|
+
if (currentFrame > MELT_END) { animState = 'traveling'; currentFrame = TRAVEL_START; travelStartTime = now; }
|
|
323
303
|
} else if (animState === 'traveling') {
|
|
324
304
|
currentFrame++;
|
|
325
305
|
if (currentFrame > TRAVEL_END) currentFrame = TRAVEL_START;
|
|
@@ -329,73 +309,37 @@
|
|
|
329
309
|
animState = 'idle';
|
|
330
310
|
currentFrame = IDLE_START;
|
|
331
311
|
idleDirection = 1;
|
|
332
|
-
// Transition canvas to bubble mode
|
|
333
312
|
if (canvasPhase === 'transitioning') {
|
|
334
|
-
|
|
335
|
-
canvas.style.cssText = BUBBLE_CSS;
|
|
336
|
-
W = BUBBLE_SIZE;
|
|
337
|
-
H = BUBBLE_SIZE;
|
|
338
|
-
canvas.width = BUBBLE_SIZE * dpr;
|
|
339
|
-
canvas.height = BUBBLE_SIZE * dpr;
|
|
340
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
341
|
-
center.x = BUBBLE_SIZE / 2;
|
|
342
|
-
center.y = BUBBLE_SIZE / 2;
|
|
343
|
-
// Mark splash as played for this session
|
|
344
|
-
try { sessionStorage.setItem(SPLASH_KEY, '1'); } catch(e) {}
|
|
345
|
-
// Preload headphones sprite in background
|
|
346
|
-
loadHpSprite();
|
|
347
|
-
if (hideAfterTransition) {
|
|
348
|
-
canvas.style.display = 'none';
|
|
349
|
-
}
|
|
313
|
+
enterBubbleMode();
|
|
350
314
|
}
|
|
351
315
|
}
|
|
352
316
|
}
|
|
353
317
|
}
|
|
354
318
|
|
|
355
319
|
// ══════════════════════════════════════════════════════════════════
|
|
356
|
-
// ── Draw
|
|
320
|
+
// ── Draw ─────────────────────────────────────────────────────────
|
|
357
321
|
// ══════════════════════════════════════════════════════════════════
|
|
358
322
|
|
|
359
323
|
function drawBlobFrame(frame, x, y, rotate, flipX) {
|
|
360
324
|
if (!spriteSheet) return;
|
|
361
|
-
var col = frame % COLS;
|
|
362
|
-
var row = Math.floor(frame / COLS);
|
|
363
|
-
var sx = col * FRAME_W;
|
|
364
|
-
var sy = row * FRAME_H;
|
|
365
|
-
|
|
325
|
+
var col = frame % COLS, row = Math.floor(frame / COLS);
|
|
366
326
|
ctx.save();
|
|
367
327
|
ctx.translate(x, y);
|
|
368
328
|
if (rotate) ctx.rotate(rotate);
|
|
369
329
|
if (flipX === -1) ctx.scale(-1, 1);
|
|
370
330
|
ctx.imageSmoothingEnabled = true;
|
|
371
331
|
ctx.imageSmoothingQuality = 'high';
|
|
372
|
-
ctx.drawImage(
|
|
373
|
-
spriteSheet,
|
|
374
|
-
sx, sy, FRAME_W, FRAME_H,
|
|
375
|
-
-DISPLAY_W / 2, -DISPLAY_H / 2,
|
|
376
|
-
DISPLAY_W, DISPLAY_H
|
|
377
|
-
);
|
|
332
|
+
ctx.drawImage(spriteSheet, col * FRAME_W, row * FRAME_H, FRAME_W, FRAME_H, -DISPLAY_W / 2, -DISPLAY_H / 2, DISPLAY_W, DISPLAY_H);
|
|
378
333
|
ctx.restore();
|
|
379
334
|
}
|
|
380
335
|
|
|
381
336
|
function drawHpFrame(frame) {
|
|
382
337
|
if (!hpSpriteSheet) return;
|
|
383
|
-
var col = frame % HP_COLS;
|
|
384
|
-
var
|
|
385
|
-
var sx = col * HP_FRAME_W;
|
|
386
|
-
var sy = row * HP_FRAME_H;
|
|
387
|
-
|
|
388
|
-
ctx.save();
|
|
389
|
-
ctx.translate(W / 2, H / 2);
|
|
338
|
+
var col = frame % HP_COLS, row = Math.floor(frame / HP_COLS);
|
|
339
|
+
var dx = (W - HP_BUBBLE_W) / 2, dy = (H - HP_BUBBLE_H) / 2;
|
|
390
340
|
ctx.imageSmoothingEnabled = true;
|
|
391
341
|
ctx.imageSmoothingQuality = 'high';
|
|
392
|
-
ctx.drawImage(
|
|
393
|
-
hpSpriteSheet,
|
|
394
|
-
sx, sy, HP_FRAME_W, HP_FRAME_H,
|
|
395
|
-
-HP_DISPLAY_W / 2, -HP_DISPLAY_H / 2,
|
|
396
|
-
HP_DISPLAY_W, HP_DISPLAY_H
|
|
397
|
-
);
|
|
398
|
-
ctx.restore();
|
|
342
|
+
ctx.drawImage(hpSpriteSheet, col * HP_FRAME_W, row * HP_FRAME_H, HP_FRAME_W, HP_FRAME_H, dx, dy, HP_BUBBLE_W, HP_BUBBLE_H);
|
|
399
343
|
}
|
|
400
344
|
|
|
401
345
|
function updateBadge() {
|
|
@@ -407,28 +351,25 @@
|
|
|
407
351
|
badgeEl.style.display = 'block';
|
|
408
352
|
}
|
|
409
353
|
|
|
410
|
-
function draw(
|
|
354
|
+
function draw() {
|
|
411
355
|
ctx.clearRect(0, 0, W, H);
|
|
412
356
|
|
|
413
|
-
//
|
|
414
|
-
if (
|
|
357
|
+
// Bubble mode → headphones sprite
|
|
358
|
+
if (canvasPhase === 'bubble') {
|
|
415
359
|
drawHpFrame(hpFrame);
|
|
416
360
|
return;
|
|
417
361
|
}
|
|
418
362
|
|
|
419
|
-
//
|
|
363
|
+
// Splash / transition → blob sprite
|
|
420
364
|
if (!spriteSheet || animState === 'loading') return;
|
|
421
|
-
|
|
422
365
|
if (animState === 'melting') {
|
|
423
366
|
var p = (currentFrame - MELT_START) / (MELT_END - MELT_START);
|
|
424
|
-
|
|
425
|
-
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle * eased, travelFlip);
|
|
367
|
+
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle * p * p, travelFlip);
|
|
426
368
|
} else if (animState === 'traveling') {
|
|
427
369
|
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle, travelFlip);
|
|
428
370
|
} else if (animState === 'reforming') {
|
|
429
371
|
var p2 = (currentFrame - REFORM_START) / (REFORM_END - REFORM_START);
|
|
430
|
-
|
|
431
|
-
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle * (1 - eased2));
|
|
372
|
+
drawBlobFrame(currentFrame, center.x, center.y, travelDrawAngle * (1 - (1 - (1 - p2) * (1 - p2))));
|
|
432
373
|
} else {
|
|
433
374
|
drawBlobFrame(currentFrame, center.x, center.y);
|
|
434
375
|
}
|
|
@@ -436,73 +377,49 @@
|
|
|
436
377
|
|
|
437
378
|
// ── Animation loop ──
|
|
438
379
|
function loop(now) {
|
|
439
|
-
if (animState !== 'loading' ||
|
|
380
|
+
if (animState !== 'loading' || canvasPhase === 'bubble') {
|
|
440
381
|
update(now);
|
|
441
|
-
draw(
|
|
382
|
+
draw();
|
|
442
383
|
}
|
|
443
384
|
requestAnimationFrame(loop);
|
|
444
385
|
}
|
|
445
386
|
|
|
446
387
|
// ══════════════════════════════════════════════════════════════════
|
|
447
|
-
// ──
|
|
388
|
+
// ── Bubble Mode ──────────────────────────────────────────────────
|
|
448
389
|
// ══════════════════════════════════════════════════════════════════
|
|
449
390
|
|
|
450
|
-
function
|
|
451
|
-
canvasPhase = 'headphones';
|
|
452
|
-
badgeEl.style.display = 'none';
|
|
453
|
-
// Expand canvas, keeping center roughly aligned with the bubble center
|
|
454
|
-
var bubbleCenterFromRight = BUBBLE_MARGIN + BUBBLE_SIZE / 2;
|
|
455
|
-
var bubbleCenterFromBottom = BUBBLE_MARGIN + BUBBLE_SIZE / 2;
|
|
456
|
-
var hpRight = Math.round(bubbleCenterFromRight - HP_CANVAS_W / 2);
|
|
457
|
-
var hpBottom = Math.round(bubbleCenterFromBottom - HP_CANVAS_H / 2);
|
|
458
|
-
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;touch-action:none;';
|
|
459
|
-
W = HP_CANVAS_W;
|
|
460
|
-
H = HP_CANVAS_H;
|
|
461
|
-
canvas.width = HP_CANVAS_W * dpr;
|
|
462
|
-
canvas.height = HP_CANVAS_H * dpr;
|
|
463
|
-
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
464
|
-
|
|
465
|
-
hpState = 'activating';
|
|
466
|
-
hpFrame = HP_ACTIVATE_START;
|
|
467
|
-
hpLastFrameTime = performance.now();
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function endHpCanvasMode() {
|
|
391
|
+
function enterBubbleMode() {
|
|
471
392
|
canvasPhase = 'bubble';
|
|
472
393
|
canvas.style.cssText = BUBBLE_CSS;
|
|
473
|
-
W = BUBBLE_SIZE;
|
|
474
|
-
H = BUBBLE_SIZE;
|
|
394
|
+
W = BUBBLE_SIZE; H = BUBBLE_SIZE;
|
|
475
395
|
canvas.width = BUBBLE_SIZE * dpr;
|
|
476
396
|
canvas.height = BUBBLE_SIZE * dpr;
|
|
477
397
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
398
|
+
hpState = 'idle';
|
|
399
|
+
hpFrame = HP_IDLE_FRAME;
|
|
400
|
+
try { sessionStorage.setItem(SPLASH_KEY, '1'); } catch(e) {}
|
|
401
|
+
loadHpSprite();
|
|
402
|
+
if (hideAfterTransition) canvas.style.display = 'none';
|
|
483
403
|
}
|
|
484
404
|
|
|
485
405
|
// ══════════════════════════════════════════════════════════════════
|
|
486
|
-
// ──
|
|
406
|
+
// ── Mic Recording ────────────────────────────────────────────────
|
|
487
407
|
// ══════════════════════════════════════════════════════════════════
|
|
488
408
|
|
|
489
|
-
/** Check mic permission state without triggering a prompt */
|
|
490
409
|
function checkMicPermission() {
|
|
491
410
|
if (hpMicPermitted) return Promise.resolve('granted');
|
|
492
411
|
if (navigator.permissions && navigator.permissions.query) {
|
|
493
|
-
return navigator.permissions.query({ name: 'microphone' }).then(function(
|
|
494
|
-
if (
|
|
495
|
-
return
|
|
412
|
+
return navigator.permissions.query({ name: 'microphone' }).then(function(s) {
|
|
413
|
+
if (s.state === 'granted') { hpMicPermitted = true; return 'granted'; }
|
|
414
|
+
return s.state;
|
|
496
415
|
}).catch(function() { return 'unknown'; });
|
|
497
416
|
}
|
|
498
417
|
return Promise.resolve('unknown');
|
|
499
418
|
}
|
|
500
419
|
|
|
501
|
-
/** Start mic capture (shared by initial recording and re-activation) */
|
|
502
420
|
function startMicCapture() {
|
|
503
421
|
if (hpWhisperEnabled) {
|
|
504
422
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(function(stream) {
|
|
505
|
-
// If user released while getUserMedia was pending, clean up
|
|
506
423
|
if (!hpPointerDown) {
|
|
507
424
|
stream.getTracks().forEach(function(t) { t.stop(); });
|
|
508
425
|
if (hpState === 'activating') hpState = 'activating_then_deactivate';
|
|
@@ -513,9 +430,7 @@
|
|
|
513
430
|
? 'audio/webm;codecs=opus' : 'audio/webm';
|
|
514
431
|
var recorder = new MediaRecorder(stream, { mimeType: mimeType });
|
|
515
432
|
hpAudioChunks = [];
|
|
516
|
-
recorder.ondataavailable = function(ev) {
|
|
517
|
-
if (ev.data.size > 0) hpAudioChunks.push(ev.data);
|
|
518
|
-
};
|
|
433
|
+
recorder.ondataavailable = function(ev) { if (ev.data.size > 0) hpAudioChunks.push(ev.data); };
|
|
519
434
|
hpMediaRecorder = recorder;
|
|
520
435
|
recorder.start();
|
|
521
436
|
}).catch(function(err) {
|
|
@@ -529,62 +444,45 @@
|
|
|
529
444
|
recognition.lang = navigator.language || 'en-US';
|
|
530
445
|
hpSpeechTranscript = '';
|
|
531
446
|
hpSpeechInstance = recognition;
|
|
532
|
-
|
|
533
|
-
recognition.onresult = function(event) {
|
|
534
|
-
var last = event.results[event.results.length - 1];
|
|
535
|
-
hpSpeechTranscript = last[0].transcript;
|
|
536
|
-
};
|
|
447
|
+
recognition.onresult = function(event) { hpSpeechTranscript = event.results[event.results.length - 1][0].transcript; };
|
|
537
448
|
recognition.onend = function() { hpSpeechInstance = null; };
|
|
538
|
-
recognition.onerror = function(
|
|
539
|
-
|
|
540
|
-
hpSpeechInstance = null;
|
|
541
|
-
};
|
|
542
|
-
try { recognition.start(); } catch(e) {
|
|
543
|
-
console.error('[widget] speech start failed:', e);
|
|
544
|
-
}
|
|
449
|
+
recognition.onerror = function(ev) { console.error('[widget] speech error:', ev.error); hpSpeechInstance = null; };
|
|
450
|
+
try { recognition.start(); } catch(e) {}
|
|
545
451
|
}
|
|
546
452
|
}
|
|
547
453
|
|
|
548
|
-
/** Entry point: permission check → sprite load → begin recording */
|
|
549
454
|
function startHpRecording() {
|
|
550
|
-
// Need at least one voice backend
|
|
551
455
|
if (!hpWhisperEnabled && !hpWebSpeechCtor) return;
|
|
552
456
|
|
|
553
457
|
checkMicPermission().then(function(permState) {
|
|
554
458
|
if (permState === 'denied') return;
|
|
555
|
-
|
|
556
459
|
if (permState === 'prompt' || permState === 'unknown') {
|
|
557
|
-
// Request permission — this may show a dialog
|
|
558
460
|
navigator.mediaDevices.getUserMedia({ audio: true }).then(function(testStream) {
|
|
559
461
|
testStream.getTracks().forEach(function(t) { t.stop(); });
|
|
560
462
|
hpMicPermitted = true;
|
|
561
|
-
// User may have released to click Allow — check before proceeding
|
|
562
463
|
if (!hpPointerDown) return;
|
|
563
|
-
|
|
564
|
-
}).catch(function() {
|
|
464
|
+
beginHpAnimation();
|
|
465
|
+
}).catch(function() {});
|
|
565
466
|
return;
|
|
566
467
|
}
|
|
567
|
-
|
|
568
|
-
// Permission already granted
|
|
569
|
-
finishStartHpRecording();
|
|
468
|
+
beginHpAnimation();
|
|
570
469
|
});
|
|
571
470
|
}
|
|
572
471
|
|
|
573
|
-
function
|
|
472
|
+
function beginHpAnimation() {
|
|
574
473
|
if (!hpPointerDown) return;
|
|
575
474
|
if (!hpSpriteSheet) {
|
|
576
|
-
loadHpSprite(function() {
|
|
577
|
-
if (!hpPointerDown) return;
|
|
578
|
-
enterHpCanvasMode();
|
|
579
|
-
startMicCapture();
|
|
580
|
-
});
|
|
475
|
+
loadHpSprite(function() { if (hpPointerDown) beginHpAnimation(); });
|
|
581
476
|
return;
|
|
582
477
|
}
|
|
583
|
-
|
|
478
|
+
badgeEl.style.display = 'none';
|
|
479
|
+
hpState = 'activating';
|
|
480
|
+
hpFrame = HP_ACTIVATE_START;
|
|
481
|
+
hpPingPong = 1;
|
|
482
|
+
hpLastFrameTime = performance.now();
|
|
584
483
|
startMicCapture();
|
|
585
484
|
}
|
|
586
485
|
|
|
587
|
-
/** Stop recording and optionally send audio to iframe */
|
|
588
486
|
function stopHpRecording(cancelled) {
|
|
589
487
|
if (cancelled) {
|
|
590
488
|
if (hpStream) { hpStream.getTracks().forEach(function(t) { t.stop(); }); hpStream = null; }
|
|
@@ -593,25 +491,18 @@
|
|
|
593
491
|
if (hpSpeechInstance) { try { hpSpeechInstance.abort(); } catch(e) {} hpSpeechInstance = null; }
|
|
594
492
|
hpSpeechTranscript = '';
|
|
595
493
|
} else {
|
|
596
|
-
// Normal stop — send the recorded audio/transcript to the iframe
|
|
597
494
|
if (hpWhisperEnabled && hpMediaRecorder && hpMediaRecorder.state !== 'inactive') {
|
|
598
495
|
var recorder = hpMediaRecorder;
|
|
599
496
|
var stream = hpStream;
|
|
600
497
|
recorder.onstop = function() {
|
|
601
498
|
if (stream) stream.getTracks().forEach(function(t) { t.stop(); });
|
|
602
499
|
var blob = new Blob(hpAudioChunks, { type: 'audio/webm' });
|
|
603
|
-
hpAudioChunks = [];
|
|
604
|
-
hpMediaRecorder = null;
|
|
605
|
-
hpStream = null;
|
|
500
|
+
hpAudioChunks = []; hpMediaRecorder = null; hpStream = null;
|
|
606
501
|
if (blob.size < 1000) return;
|
|
607
|
-
|
|
608
502
|
var reader = new FileReader();
|
|
609
503
|
reader.onloadend = function() {
|
|
610
|
-
var
|
|
611
|
-
|
|
612
|
-
if (base64 && iframe.contentWindow) {
|
|
613
|
-
iframe.contentWindow.postMessage({ type: 'bloby:voice-record', audio: base64 }, '*');
|
|
614
|
-
}
|
|
504
|
+
var base64 = reader.result.split(',')[1];
|
|
505
|
+
if (base64 && iframe.contentWindow) iframe.contentWindow.postMessage({ type: 'bloby:voice-record', audio: base64 }, '*');
|
|
615
506
|
};
|
|
616
507
|
reader.readAsDataURL(blob);
|
|
617
508
|
};
|
|
@@ -620,38 +511,25 @@
|
|
|
620
511
|
var instance = hpSpeechInstance;
|
|
621
512
|
var sent = false;
|
|
622
513
|
var sendTranscript = function() {
|
|
623
|
-
if (sent) return;
|
|
624
|
-
sent = true;
|
|
514
|
+
if (sent) return; sent = true;
|
|
625
515
|
var text = hpSpeechTranscript.trim();
|
|
626
516
|
hpSpeechTranscript = '';
|
|
627
|
-
if (text && iframe.contentWindow) {
|
|
628
|
-
iframe.contentWindow.postMessage({ type: 'bloby:voice-record', transcript: text }, '*');
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
instance.onend = function() {
|
|
632
|
-
hpSpeechInstance = null;
|
|
633
|
-
sendTranscript();
|
|
517
|
+
if (text && iframe.contentWindow) iframe.contentWindow.postMessage({ type: 'bloby:voice-record', transcript: text }, '*');
|
|
634
518
|
};
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
// Safety timeout
|
|
640
|
-
setTimeout(function() {
|
|
641
|
-
if (hpSpeechInstance === instance) hpSpeechInstance = null;
|
|
642
|
-
sendTranscript();
|
|
643
|
-
}, 2000);
|
|
519
|
+
instance.onend = function() { hpSpeechInstance = null; sendTranscript(); };
|
|
520
|
+
try { instance.stop(); } catch(e) { hpSpeechInstance = null; sendTranscript(); }
|
|
521
|
+
setTimeout(function() { if (hpSpeechInstance === instance) hpSpeechInstance = null; sendTranscript(); }, 2000);
|
|
644
522
|
} else {
|
|
645
523
|
if (hpStream) { hpStream.getTracks().forEach(function(t) { t.stop(); }); hpStream = null; }
|
|
646
524
|
}
|
|
647
525
|
}
|
|
648
526
|
|
|
649
|
-
//
|
|
527
|
+
// v2 deactivation: reverse 40→11 (activate played backwards)
|
|
650
528
|
if (hpState === 'activating') {
|
|
651
529
|
hpState = 'activating_then_deactivate';
|
|
652
530
|
} else if (hpState === 'recording') {
|
|
653
531
|
hpState = 'deactivating';
|
|
654
|
-
hpFrame =
|
|
532
|
+
hpFrame = HP_ACTIVATE_END;
|
|
655
533
|
hpLastFrameTime = performance.now();
|
|
656
534
|
}
|
|
657
535
|
}
|
|
@@ -663,100 +541,71 @@
|
|
|
663
541
|
function maybeTransition() {
|
|
664
542
|
if (!appReady || animState !== 'idle' || canvasPhase !== 'splash') return;
|
|
665
543
|
var elapsed = Date.now() - canvasCreatedAt;
|
|
666
|
-
if (elapsed < MIN_SPLASH_MS) {
|
|
667
|
-
setTimeout(maybeTransition, MIN_SPLASH_MS - elapsed);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
triggerTransition();
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
function triggerTransition() {
|
|
544
|
+
if (elapsed < MIN_SPLASH_MS) { setTimeout(maybeTransition, MIN_SPLASH_MS - elapsed); return; }
|
|
674
545
|
canvasPhase = 'transitioning';
|
|
675
546
|
var tx = W - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
|
|
676
547
|
var ty = H - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
|
|
677
548
|
moveTo(tx, ty);
|
|
678
549
|
}
|
|
679
550
|
|
|
680
|
-
// Skip splash and go straight to bubble mode (onboard, soft refresh, etc.)
|
|
681
551
|
function skipToBubble() {
|
|
682
552
|
canvasPhase = 'bubble';
|
|
683
553
|
canvas.style.cssText = BUBBLE_CSS;
|
|
684
|
-
W = BUBBLE_SIZE;
|
|
685
|
-
H = BUBBLE_SIZE;
|
|
554
|
+
W = BUBBLE_SIZE; H = BUBBLE_SIZE;
|
|
686
555
|
canvas.width = BUBBLE_SIZE * dpr;
|
|
687
556
|
canvas.height = BUBBLE_SIZE * dpr;
|
|
688
557
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
currentFrame = IDLE_START;
|
|
692
|
-
idleDirection = 1;
|
|
558
|
+
hpState = 'idle';
|
|
559
|
+
hpFrame = HP_IDLE_FRAME;
|
|
693
560
|
animState = 'idle';
|
|
694
|
-
// Hide the HTML splash fallback
|
|
695
561
|
var splash = document.getElementById('splash');
|
|
696
562
|
if (splash) splash.style.display = 'none';
|
|
697
|
-
// Preload headphones sprite in background
|
|
698
563
|
loadHpSprite();
|
|
699
564
|
}
|
|
700
565
|
|
|
701
|
-
window.addEventListener('bloby:app-ready', function () {
|
|
702
|
-
|
|
703
|
-
maybeTransition();
|
|
704
|
-
});
|
|
705
|
-
if (window.__blobyAppReady) {
|
|
706
|
-
appReady = true;
|
|
707
|
-
}
|
|
566
|
+
window.addEventListener('bloby:app-ready', function () { appReady = true; maybeTransition(); });
|
|
567
|
+
if (window.__blobyAppReady) appReady = true;
|
|
708
568
|
|
|
709
|
-
// ── Check onboard status + settings, then start ──
|
|
710
569
|
var onboardActive = false;
|
|
711
570
|
fetch('/api/settings')
|
|
712
571
|
.then(function (r) { return r.json(); })
|
|
713
572
|
.then(function (s) {
|
|
714
|
-
if (s.onboard_complete !== 'true') {
|
|
715
|
-
|
|
716
|
-
skipSplash = true;
|
|
717
|
-
}
|
|
718
|
-
if (s.whisper_enabled === 'true') {
|
|
719
|
-
hpWhisperEnabled = true;
|
|
720
|
-
}
|
|
721
|
-
// Skip splash if already played this session
|
|
573
|
+
if (s.onboard_complete !== 'true') { onboardActive = true; skipSplash = true; }
|
|
574
|
+
if (s.whisper_enabled === 'true') hpWhisperEnabled = true;
|
|
722
575
|
if (splashSeen) skipSplash = true;
|
|
723
576
|
startAnimation();
|
|
724
577
|
})
|
|
725
|
-
.catch(function () {
|
|
726
|
-
if (splashSeen) skipSplash = true;
|
|
727
|
-
startAnimation();
|
|
728
|
-
});
|
|
578
|
+
.catch(function () { if (splashSeen) skipSplash = true; startAnimation(); });
|
|
729
579
|
|
|
730
580
|
function startAnimation() {
|
|
731
581
|
loadSprite(
|
|
732
|
-
function
|
|
733
|
-
if (skipSplash) {
|
|
734
|
-
skipToBubble();
|
|
735
|
-
if (onboardActive) canvas.style.display = 'none';
|
|
736
|
-
}
|
|
582
|
+
function () {
|
|
583
|
+
if (skipSplash) { skipToBubble(); if (onboardActive) canvas.style.display = 'none'; }
|
|
737
584
|
requestAnimationFrame(loop);
|
|
738
585
|
maybeTransition();
|
|
739
586
|
},
|
|
740
|
-
function
|
|
741
|
-
canvas.style.display = 'none';
|
|
742
|
-
canvasPhase = 'disabled';
|
|
743
|
-
}
|
|
587
|
+
function () { canvas.style.display = 'none'; canvasPhase = 'disabled'; }
|
|
744
588
|
);
|
|
745
589
|
}
|
|
746
590
|
|
|
747
591
|
// ══════════════════════════════════════════════════════════════════
|
|
748
|
-
// ── Widget Interaction
|
|
592
|
+
// ── Widget Interaction ───────────────────────────────────────────
|
|
749
593
|
// ══════════════════════════════════════════════════════════════════
|
|
750
594
|
|
|
751
595
|
var isOpen = false;
|
|
752
596
|
|
|
753
597
|
function toggle() {
|
|
754
|
-
// If clicked during splash or travel animation, skip straight to bubble first
|
|
755
598
|
if (canvasPhase === 'splash' || canvasPhase === 'transitioning') {
|
|
756
599
|
skipToBubble();
|
|
757
600
|
try { sessionStorage.setItem(SPLASH_KEY, '1'); } catch(e) {}
|
|
758
601
|
}
|
|
759
602
|
if (canvasPhase !== 'bubble') return;
|
|
603
|
+
// If headphones animation is running, reset to idle
|
|
604
|
+
if (hpState !== 'idle') {
|
|
605
|
+
stopHpRecording(true);
|
|
606
|
+
hpState = 'idle';
|
|
607
|
+
hpFrame = HP_IDLE_FRAME;
|
|
608
|
+
}
|
|
760
609
|
isOpen = !isOpen;
|
|
761
610
|
panel.classList.toggle('open', isOpen);
|
|
762
611
|
backdrop.classList.toggle('open', isOpen);
|
|
@@ -765,18 +614,13 @@
|
|
|
765
614
|
updateBadge();
|
|
766
615
|
}
|
|
767
616
|
|
|
768
|
-
// ── Bubble pointer handlers
|
|
769
|
-
//
|
|
617
|
+
// ── Bubble pointer handlers ──
|
|
618
|
+
// Tap = open chat (always, any state). Hold ≥500ms = record.
|
|
770
619
|
var hpPointerId = -1;
|
|
771
620
|
|
|
772
621
|
bubble.addEventListener('pointerdown', function(e) {
|
|
622
|
+
if (canvasPhase !== 'bubble') return;
|
|
773
623
|
e.preventDefault();
|
|
774
|
-
|
|
775
|
-
// During headphones deactivation or activating, a tap opens chat
|
|
776
|
-
if (canvasPhase === 'headphones') return;
|
|
777
|
-
|
|
778
|
-
if (canvasPhase !== 'bubble' || hpState !== 'none') return;
|
|
779
|
-
|
|
780
624
|
hpPointerId = e.pointerId;
|
|
781
625
|
bubble.setPointerCapture(e.pointerId);
|
|
782
626
|
hpPointerDown = true;
|
|
@@ -784,27 +628,22 @@
|
|
|
784
628
|
|
|
785
629
|
hpHoldTimer = setTimeout(function() {
|
|
786
630
|
hpWasHold = true;
|
|
787
|
-
|
|
631
|
+
if (hpState === 'idle' || hpState === 'deactivating') {
|
|
632
|
+
startHpRecording();
|
|
633
|
+
}
|
|
788
634
|
}, HP_HOLD_MS);
|
|
789
635
|
});
|
|
790
636
|
|
|
791
|
-
bubble.addEventListener('pointerup', function(
|
|
637
|
+
bubble.addEventListener('pointerup', function() {
|
|
792
638
|
hpPointerDown = false;
|
|
793
639
|
if (hpHoldTimer) { clearTimeout(hpHoldTimer); hpHoldTimer = null; }
|
|
794
|
-
if (hpPointerId !== -1) {
|
|
795
|
-
try { bubble.releasePointerCapture(hpPointerId); } catch(ex) {}
|
|
796
|
-
hpPointerId = -1;
|
|
797
|
-
}
|
|
640
|
+
if (hpPointerId !== -1) { try { bubble.releasePointerCapture(hpPointerId); } catch(ex) {} hpPointerId = -1; }
|
|
798
641
|
|
|
799
642
|
if (hpState === 'activating' || hpState === 'recording') {
|
|
643
|
+
// Release during recording → stop & deactivate
|
|
800
644
|
stopHpRecording(false);
|
|
801
645
|
} else if (!hpWasHold) {
|
|
802
|
-
// Short tap
|
|
803
|
-
if (canvasPhase === 'headphones') {
|
|
804
|
-
// Skip deactivation animation, go straight to bubble, then open
|
|
805
|
-
hpState = 'none';
|
|
806
|
-
endHpCanvasMode();
|
|
807
|
-
}
|
|
646
|
+
// Short tap → open chat (works from any state)
|
|
808
647
|
toggle();
|
|
809
648
|
}
|
|
810
649
|
});
|
|
@@ -812,83 +651,42 @@
|
|
|
812
651
|
bubble.addEventListener('pointercancel', function() {
|
|
813
652
|
hpPointerDown = false;
|
|
814
653
|
if (hpHoldTimer) { clearTimeout(hpHoldTimer); hpHoldTimer = null; }
|
|
815
|
-
if (hpPointerId !== -1) {
|
|
816
|
-
|
|
817
|
-
hpPointerId = -1;
|
|
818
|
-
}
|
|
819
|
-
if (hpState !== 'none' && hpState !== 'deactivating') stopHpRecording(true);
|
|
654
|
+
if (hpPointerId !== -1) { try { bubble.releasePointerCapture(hpPointerId); } catch(ex) {} hpPointerId = -1; }
|
|
655
|
+
if (hpState === 'activating' || hpState === 'recording') stopHpRecording(true);
|
|
820
656
|
});
|
|
821
657
|
|
|
822
|
-
//
|
|
658
|
+
// Click on blob during splash → skip and open
|
|
823
659
|
document.addEventListener('click', function(e) {
|
|
824
660
|
if (canvasPhase !== 'splash' && canvasPhase !== 'transitioning') return;
|
|
825
661
|
if (animState === 'loading') return;
|
|
826
|
-
var dx = e.clientX - center.x;
|
|
827
|
-
|
|
828
|
-
if (Math.sqrt(dx * dx + dy * dy) < DISPLAY_H / 2 + 15) {
|
|
829
|
-
toggle();
|
|
830
|
-
}
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
// Prevent context menu on long-press (mobile)
|
|
834
|
-
bubble.addEventListener('contextmenu', function(e) {
|
|
835
|
-
e.preventDefault();
|
|
662
|
+
var dx = e.clientX - center.x, dy = e.clientY - center.y;
|
|
663
|
+
if (Math.sqrt(dx * dx + dy * dy) < DISPLAY_H / 2 + 15) toggle();
|
|
836
664
|
});
|
|
837
665
|
|
|
666
|
+
bubble.addEventListener('contextmenu', function(e) { e.preventDefault(); });
|
|
838
667
|
backdrop.addEventListener('click', toggle);
|
|
839
|
-
|
|
840
|
-
// Close on Escape
|
|
841
|
-
document.addEventListener('keydown', function (e) {
|
|
842
|
-
if (e.key === 'Escape' && isOpen) toggle();
|
|
843
|
-
});
|
|
668
|
+
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && isOpen) toggle(); });
|
|
844
669
|
|
|
845
670
|
// ── PWA Install ──
|
|
846
671
|
var deferredInstallPrompt = null;
|
|
847
|
-
window.addEventListener('beforeinstallprompt', function (e) {
|
|
848
|
-
e.preventDefault();
|
|
849
|
-
deferredInstallPrompt = e;
|
|
850
|
-
});
|
|
672
|
+
window.addEventListener('beforeinstallprompt', function (e) { e.preventDefault(); deferredInstallPrompt = e; });
|
|
851
673
|
|
|
852
674
|
// Handle messages from iframe
|
|
853
675
|
window.addEventListener('message', function (e) {
|
|
854
676
|
if (!e.data || !e.data.type) return;
|
|
855
|
-
|
|
856
677
|
if (e.data.type === 'bloby:close' && isOpen) toggle();
|
|
857
|
-
|
|
858
|
-
// New message from agent — badge the bubble if panel is closed
|
|
859
|
-
if (e.data.type === 'bloby:new-message' && !isOpen) {
|
|
860
|
-
unreadCount++;
|
|
861
|
-
updateBadge();
|
|
862
|
-
}
|
|
863
|
-
|
|
678
|
+
if (e.data.type === 'bloby:new-message' && !isOpen) { unreadCount++; updateBadge(); }
|
|
864
679
|
if (e.data.type === 'bloby:install-app') {
|
|
865
680
|
if (deferredInstallPrompt) {
|
|
866
681
|
deferredInstallPrompt.prompt();
|
|
867
|
-
deferredInstallPrompt.userChoice.then(function (
|
|
868
|
-
|
|
869
|
-
});
|
|
870
|
-
} else {
|
|
871
|
-
iframe.contentWindow.postMessage({ type: 'bloby:show-ios-install' }, '*');
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Onboard complete — show the bubble
|
|
876
|
-
if (e.data.type === 'bloby:onboard-complete') {
|
|
877
|
-
onboardActive = false;
|
|
878
|
-
hideAfterTransition = false;
|
|
879
|
-
canvas.style.display = 'block';
|
|
682
|
+
deferredInstallPrompt.userChoice.then(function (r) { if (r.outcome === 'accepted') deferredInstallPrompt = null; });
|
|
683
|
+
} else { iframe.contentWindow.postMessage({ type: 'bloby:show-ios-install' }, '*'); }
|
|
880
684
|
}
|
|
685
|
+
if (e.data.type === 'bloby:onboard-complete') { onboardActive = false; hideAfterTransition = false; canvas.style.display = 'block'; }
|
|
881
686
|
});
|
|
882
687
|
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
if (sessionStorage.getItem('bloby_widget_open') === '1') {
|
|
886
|
-
sessionStorage.removeItem('bloby_widget_open');
|
|
887
|
-
if (canvasPhase === 'bubble') toggle();
|
|
888
|
-
}
|
|
889
|
-
} catch (e) {}
|
|
688
|
+
try { if (sessionStorage.getItem('bloby_widget_open') === '1') { sessionStorage.removeItem('bloby_widget_open'); if (canvasPhase === 'bubble') toggle(); } } catch (e) {}
|
|
890
689
|
|
|
891
|
-
// Inject app-ws.js — proxies /app/api/* fetch calls through WebSocket
|
|
892
690
|
var awsScript = document.createElement('script');
|
|
893
691
|
awsScript.src = '/bloby/app-ws.js';
|
|
894
692
|
document.head.appendChild(awsScript);
|