fluxy-bot 0.10.16 → 0.10.18
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
CHANGED
|
@@ -88,6 +88,15 @@ export function registerUpdateCommand(program: Command) {
|
|
|
88
88
|
fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// Always update index.html — contains splash screen, SW registration,
|
|
92
|
+
// and meta tags that ship with each version. Safe to overwrite since
|
|
93
|
+
// users don't edit this file (their code lives in src/ and backend/).
|
|
94
|
+
const indexSrc = path.join(extracted, 'workspace', 'client', 'index.html');
|
|
95
|
+
const indexDst = path.join(DATA_DIR, 'workspace', 'client', 'index.html');
|
|
96
|
+
if (fs.existsSync(indexSrc)) {
|
|
97
|
+
fs.cpSync(indexSrc, indexDst, { force: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
|
|
92
101
|
const src = path.join(extracted, file);
|
|
93
102
|
if (fs.existsSync(src)) {
|
package/package.json
CHANGED
package/supervisor/widget.js
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
// ── Styles ──
|
|
7
7
|
var style = document.createElement('style');
|
|
8
8
|
style.textContent = [
|
|
9
|
-
'#fluxy-widget-bubble{
|
|
9
|
+
'#fluxy-widget-bubble{transition:transform .15s ease;-webkit-tap-highlight-color:transparent}',
|
|
10
10
|
'#fluxy-widget-bubble:hover{transform:scale(1.1)}',
|
|
11
11
|
'#fluxy-widget-bubble:active{transform:scale(0.95)}',
|
|
12
|
-
'#fluxy-widget-bubble video{height:60px;width:auto;pointer-events:none;-webkit-user-drag:none}',
|
|
13
|
-
'#fluxy-widget-bubble img{width:60px;height:60px;object-fit:contain;pointer-events:none;-webkit-user-drag:none}',
|
|
14
12
|
'#fluxy-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}',
|
|
15
13
|
'#fluxy-widget-backdrop.open{opacity:1;pointer-events:auto}',
|
|
16
14
|
'#fluxy-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}',
|
|
@@ -35,50 +33,360 @@
|
|
|
35
33
|
iframe.setAttribute('loading', 'lazy');
|
|
36
34
|
panel.appendChild(iframe);
|
|
37
35
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
36
|
+
// ══════════════════════════════════════════════════════════════════
|
|
37
|
+
// ── Sprite Sheet Animation (Splash + Bubble) ─────────────────────
|
|
38
|
+
// ══════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
// ── Sprite config ──
|
|
41
|
+
var COLS = 16;
|
|
42
|
+
var FRAME_W = 125;
|
|
43
|
+
var FRAME_H = 120;
|
|
44
|
+
var DISPLAY_H = 58;
|
|
45
|
+
var DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
46
|
+
|
|
47
|
+
// Frame ranges (0-indexed)
|
|
48
|
+
var IDLE_START = 0, IDLE_END = 29;
|
|
49
|
+
var MELT_START = 30, MELT_END = 52;
|
|
50
|
+
var TRAVEL_START = 52, TRAVEL_END = 84;
|
|
51
|
+
var REFORM_START = 84, REFORM_END = 191;
|
|
52
|
+
|
|
53
|
+
// Timing
|
|
54
|
+
var FPS = 29;
|
|
55
|
+
var FRAME_MS = 1000 / FPS;
|
|
56
|
+
var IDLE_FPS = 22;
|
|
57
|
+
var IDLE_FRAME_MS = 1000 / IDLE_FPS;
|
|
58
|
+
var REFORM_FPS = 70;
|
|
59
|
+
var REFORM_FRAME_MS = 1000 / REFORM_FPS;
|
|
60
|
+
|
|
61
|
+
var TRAVEL_PX_PER_MS = 0.65;
|
|
62
|
+
var TRAVEL_MIN = 385;
|
|
63
|
+
var TRAVEL_MAX = 1150;
|
|
64
|
+
var MIN_SPLASH_MS = 2000;
|
|
65
|
+
var BUBBLE_SIZE = 60;
|
|
66
|
+
var BUBBLE_MARGIN = 24;
|
|
67
|
+
|
|
68
|
+
// ── Easing ──
|
|
69
|
+
function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
|
|
70
|
+
|
|
71
|
+
// ── Canvas setup (starts fullscreen for splash) ──
|
|
72
|
+
var canvas = document.createElement('canvas');
|
|
73
|
+
canvas.id = 'fluxy-widget-bubble';
|
|
74
|
+
canvas.setAttribute('role', 'button');
|
|
75
|
+
canvas.setAttribute('aria-label', 'Open Fluxy chat');
|
|
76
|
+
canvas.dataset.fluxyWidget = '1';
|
|
77
|
+
canvas.style.cssText = 'position:fixed;inset:0;width:100vw;height:100dvh;z-index:9999;pointer-events:none;';
|
|
78
|
+
document.body.appendChild(canvas);
|
|
79
|
+
|
|
80
|
+
var bubble = canvas;
|
|
81
|
+
var ctx = canvas.getContext('2d');
|
|
82
|
+
var dpr = window.devicePixelRatio || 1;
|
|
83
|
+
var W = 0, H = 0;
|
|
84
|
+
|
|
85
|
+
// ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | 'disabled' ──
|
|
86
|
+
var canvasPhase = 'splash';
|
|
87
|
+
var canvasCreatedAt = Date.now();
|
|
88
|
+
var appReady = false;
|
|
89
|
+
var hideAfterTransition = false;
|
|
90
|
+
var skipSplash = false;
|
|
91
|
+
|
|
92
|
+
function resizeCanvas() {
|
|
93
|
+
if (canvasPhase === 'bubble') {
|
|
94
|
+
W = BUBBLE_SIZE;
|
|
95
|
+
H = BUBBLE_SIZE;
|
|
96
|
+
} else {
|
|
97
|
+
W = window.innerWidth;
|
|
98
|
+
H = window.innerHeight;
|
|
99
|
+
}
|
|
100
|
+
canvas.width = W * dpr;
|
|
101
|
+
canvas.height = H * dpr;
|
|
102
|
+
canvas.style.width = W + 'px';
|
|
103
|
+
canvas.style.height = H + 'px';
|
|
104
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
105
|
+
}
|
|
106
|
+
resizeCanvas();
|
|
107
|
+
window.addEventListener('resize', function () {
|
|
108
|
+
resizeCanvas();
|
|
109
|
+
if (canvasPhase === 'splash' && animState === 'idle') {
|
|
110
|
+
center.x = Math.round(W / 2);
|
|
111
|
+
center.y = Math.round(H / 2);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── Animation state ──
|
|
116
|
+
var spriteSheet = null;
|
|
117
|
+
var animState = 'loading';
|
|
118
|
+
var currentFrame = 0;
|
|
119
|
+
var idleDirection = 1;
|
|
120
|
+
var lastFrameTime = 0;
|
|
121
|
+
var travelDuration = 0;
|
|
122
|
+
var travelStartTime = 0;
|
|
123
|
+
var travelAngle = 0;
|
|
124
|
+
var travelDrawAngle = 0;
|
|
125
|
+
var travelFlip = 1;
|
|
126
|
+
|
|
127
|
+
var center = { x: 0, y: 0 };
|
|
128
|
+
var travelA = { x: 0, y: 0 };
|
|
129
|
+
var target = { x: 0, y: 0 };
|
|
130
|
+
|
|
131
|
+
// ── Load sprite sheet ──
|
|
132
|
+
function loadSprite(onDone, onFail) {
|
|
133
|
+
var img = new Image();
|
|
134
|
+
img.onload = function () {
|
|
135
|
+
spriteSheet = img;
|
|
136
|
+
center.x = Math.round(W / 2);
|
|
137
|
+
center.y = Math.round(H / 2);
|
|
138
|
+
currentFrame = IDLE_START;
|
|
139
|
+
animState = 'idle';
|
|
140
|
+
|
|
141
|
+
// Hide the HTML splash fallback
|
|
142
|
+
var splash = document.getElementById('splash');
|
|
143
|
+
if (splash) splash.style.display = 'none';
|
|
144
|
+
|
|
145
|
+
onDone();
|
|
146
|
+
};
|
|
147
|
+
img.onerror = function () {
|
|
148
|
+
if (onFail) onFail();
|
|
149
|
+
};
|
|
150
|
+
img.src = '/spritesheet.webp';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── Trigger move ──
|
|
154
|
+
function moveTo(tx, ty) {
|
|
155
|
+
if (animState !== 'idle') return;
|
|
156
|
+
target.x = tx;
|
|
157
|
+
target.y = ty;
|
|
158
|
+
travelA.x = center.x;
|
|
159
|
+
travelA.y = center.y;
|
|
160
|
+
|
|
161
|
+
var dx = target.x - center.x;
|
|
162
|
+
var dy = target.y - center.y;
|
|
163
|
+
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
164
|
+
|
|
165
|
+
travelAngle = Math.atan2(dy, dx);
|
|
166
|
+
travelDrawAngle = travelAngle;
|
|
167
|
+
travelFlip = 1;
|
|
168
|
+
if (travelDrawAngle > Math.PI / 2) { travelDrawAngle -= Math.PI; travelFlip = -1; }
|
|
169
|
+
else if (travelDrawAngle < -Math.PI / 2) { travelDrawAngle += Math.PI; travelFlip = -1; }
|
|
170
|
+
travelDuration = Math.min(TRAVEL_MAX, Math.max(TRAVEL_MIN, dist / TRAVEL_PX_PER_MS));
|
|
171
|
+
|
|
172
|
+
currentFrame = MELT_START;
|
|
173
|
+
lastFrameTime = performance.now();
|
|
174
|
+
animState = 'melting';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Update ──
|
|
178
|
+
function update(now) {
|
|
179
|
+
// Smooth position update during travel (every rAF)
|
|
180
|
+
if (animState === 'traveling') {
|
|
181
|
+
var elapsed = now - travelStartTime;
|
|
182
|
+
var progress = Math.min(1, elapsed / travelDuration);
|
|
183
|
+
var e = easeInOutCubic(progress);
|
|
184
|
+
center.x = travelA.x + (target.x - travelA.x) * e;
|
|
185
|
+
center.y = travelA.y + (target.y - travelA.y) * e;
|
|
186
|
+
|
|
187
|
+
if (progress >= 1) {
|
|
188
|
+
center.x = target.x;
|
|
189
|
+
center.y = target.y;
|
|
190
|
+
animState = 'reforming';
|
|
191
|
+
currentFrame = REFORM_START;
|
|
192
|
+
lastFrameTime = now;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Sprite frame advance (at configured FPS)
|
|
198
|
+
var frameInterval = animState === 'idle' ? IDLE_FRAME_MS
|
|
199
|
+
: animState === 'reforming' ? REFORM_FRAME_MS
|
|
200
|
+
: FRAME_MS;
|
|
201
|
+
var frameDelta = now - lastFrameTime;
|
|
202
|
+
if (frameDelta < frameInterval) return;
|
|
203
|
+
lastFrameTime = now - (frameDelta % frameInterval);
|
|
204
|
+
|
|
205
|
+
if (animState === 'idle') {
|
|
206
|
+
currentFrame += idleDirection;
|
|
207
|
+
if (currentFrame >= IDLE_END) { currentFrame = IDLE_END; idleDirection = -1; }
|
|
208
|
+
else if (currentFrame <= IDLE_START) { currentFrame = IDLE_START; idleDirection = 1; }
|
|
209
|
+
} else if (animState === 'melting') {
|
|
210
|
+
currentFrame++;
|
|
211
|
+
if (currentFrame > MELT_END) {
|
|
212
|
+
animState = 'traveling';
|
|
213
|
+
currentFrame = TRAVEL_START;
|
|
214
|
+
travelStartTime = now;
|
|
215
|
+
}
|
|
216
|
+
} else if (animState === 'traveling') {
|
|
217
|
+
currentFrame++;
|
|
218
|
+
if (currentFrame > TRAVEL_END) currentFrame = TRAVEL_START;
|
|
219
|
+
} else if (animState === 'reforming') {
|
|
220
|
+
currentFrame++;
|
|
221
|
+
if (currentFrame > REFORM_END) {
|
|
222
|
+
animState = 'idle';
|
|
223
|
+
currentFrame = IDLE_START;
|
|
224
|
+
idleDirection = 1;
|
|
225
|
+
// Transition canvas to bubble mode
|
|
226
|
+
if (canvasPhase === 'transitioning') {
|
|
227
|
+
canvasPhase = 'bubble';
|
|
228
|
+
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;';
|
|
229
|
+
W = BUBBLE_SIZE;
|
|
230
|
+
H = BUBBLE_SIZE;
|
|
231
|
+
canvas.width = BUBBLE_SIZE * dpr;
|
|
232
|
+
canvas.height = BUBBLE_SIZE * dpr;
|
|
233
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
234
|
+
center.x = BUBBLE_SIZE / 2;
|
|
235
|
+
center.y = BUBBLE_SIZE / 2;
|
|
236
|
+
if (hideAfterTransition) {
|
|
237
|
+
canvas.style.display = 'none';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Draw ──
|
|
245
|
+
function drawFrame(frame, x, y, rotate, flipX) {
|
|
246
|
+
if (!spriteSheet) return;
|
|
247
|
+
var col = frame % COLS;
|
|
248
|
+
var row = Math.floor(frame / COLS);
|
|
249
|
+
var sx = col * FRAME_W;
|
|
250
|
+
var sy = row * FRAME_H;
|
|
251
|
+
|
|
252
|
+
ctx.save();
|
|
253
|
+
ctx.translate(x, y);
|
|
254
|
+
if (rotate) ctx.rotate(rotate);
|
|
255
|
+
if (flipX === -1) ctx.scale(-1, 1);
|
|
256
|
+
ctx.imageSmoothingEnabled = true;
|
|
257
|
+
ctx.imageSmoothingQuality = 'high';
|
|
258
|
+
ctx.drawImage(
|
|
259
|
+
spriteSheet,
|
|
260
|
+
sx, sy, FRAME_W, FRAME_H,
|
|
261
|
+
-DISPLAY_W / 2, -DISPLAY_H / 2,
|
|
262
|
+
DISPLAY_W, DISPLAY_H
|
|
263
|
+
);
|
|
264
|
+
ctx.restore();
|
|
65
265
|
}
|
|
66
266
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
267
|
+
function draw(now) {
|
|
268
|
+
ctx.clearRect(0, 0, W, H);
|
|
269
|
+
if (!spriteSheet || animState === 'loading') return;
|
|
270
|
+
|
|
271
|
+
if (animState === 'melting') {
|
|
272
|
+
var p = (currentFrame - MELT_START) / (MELT_END - MELT_START);
|
|
273
|
+
var eased = p * p;
|
|
274
|
+
drawFrame(currentFrame, center.x, center.y, travelDrawAngle * eased, travelFlip);
|
|
275
|
+
} else if (animState === 'traveling') {
|
|
276
|
+
drawFrame(currentFrame, center.x, center.y, travelDrawAngle, travelFlip);
|
|
277
|
+
} else if (animState === 'reforming') {
|
|
278
|
+
var p2 = (currentFrame - REFORM_START) / (REFORM_END - REFORM_START);
|
|
279
|
+
var eased2 = 1 - (1 - p2) * (1 - p2);
|
|
280
|
+
drawFrame(currentFrame, center.x, center.y, travelDrawAngle * (1 - eased2));
|
|
281
|
+
} else {
|
|
282
|
+
drawFrame(currentFrame, center.x, center.y);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Animation loop ──
|
|
287
|
+
function loop(now) {
|
|
288
|
+
if (animState !== 'loading') {
|
|
289
|
+
update(now);
|
|
290
|
+
draw(now);
|
|
291
|
+
}
|
|
292
|
+
requestAnimationFrame(loop);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── App-ready coordination ──
|
|
296
|
+
function maybeTransition() {
|
|
297
|
+
if (!appReady || animState !== 'idle' || canvasPhase !== 'splash') return;
|
|
298
|
+
var elapsed = Date.now() - canvasCreatedAt;
|
|
299
|
+
if (elapsed < MIN_SPLASH_MS) {
|
|
300
|
+
setTimeout(maybeTransition, MIN_SPLASH_MS - elapsed);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
triggerTransition();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function triggerTransition() {
|
|
307
|
+
canvasPhase = 'transitioning';
|
|
308
|
+
var tx = W - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
|
|
309
|
+
var ty = H - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
|
|
310
|
+
moveTo(tx, ty);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Skip splash and go straight to bubble mode (for onboard / instant load)
|
|
314
|
+
function skipToBubble() {
|
|
315
|
+
canvasPhase = 'bubble';
|
|
316
|
+
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;';
|
|
317
|
+
W = BUBBLE_SIZE;
|
|
318
|
+
H = BUBBLE_SIZE;
|
|
319
|
+
canvas.width = BUBBLE_SIZE * dpr;
|
|
320
|
+
canvas.height = BUBBLE_SIZE * dpr;
|
|
321
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
322
|
+
center.x = BUBBLE_SIZE / 2;
|
|
323
|
+
center.y = BUBBLE_SIZE / 2;
|
|
324
|
+
currentFrame = IDLE_START;
|
|
325
|
+
idleDirection = 1;
|
|
326
|
+
animState = 'idle';
|
|
327
|
+
// Hide the HTML splash fallback
|
|
328
|
+
var splash = document.getElementById('splash');
|
|
329
|
+
if (splash) splash.style.display = 'none';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
window.addEventListener('fluxy:app-ready', function () {
|
|
333
|
+
appReady = true;
|
|
334
|
+
maybeTransition();
|
|
335
|
+
});
|
|
336
|
+
if (window.__fluxyAppReady) {
|
|
337
|
+
appReady = true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Check onboard status, then start ──
|
|
341
|
+
// First-time users get the onboard wizard — skip the splash animation.
|
|
342
|
+
var onboardActive = false;
|
|
343
|
+
fetch('/api/settings')
|
|
344
|
+
.then(function (r) { return r.json(); })
|
|
345
|
+
.then(function (s) {
|
|
346
|
+
if (s.onboard_complete !== 'true') {
|
|
347
|
+
onboardActive = true;
|
|
348
|
+
skipSplash = true;
|
|
349
|
+
}
|
|
350
|
+
startAnimation();
|
|
351
|
+
})
|
|
352
|
+
.catch(function () {
|
|
353
|
+
startAnimation();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
function startAnimation() {
|
|
357
|
+
loadSprite(
|
|
358
|
+
function onSuccess() {
|
|
359
|
+
if (skipSplash) {
|
|
360
|
+
skipToBubble();
|
|
361
|
+
if (onboardActive) canvas.style.display = 'none';
|
|
362
|
+
}
|
|
363
|
+
requestAnimationFrame(loop);
|
|
364
|
+
maybeTransition();
|
|
365
|
+
},
|
|
366
|
+
function onFail() {
|
|
367
|
+
canvas.style.display = 'none';
|
|
368
|
+
canvasPhase = 'disabled';
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ══════════════════════════════════════════════════════════════════
|
|
374
|
+
// ── Widget Interaction (Panel, Toggle, Messages) ─────────────────
|
|
375
|
+
// ══════════════════════════════════════════════════════════════════
|
|
70
376
|
|
|
71
|
-
// ── Toggle ──
|
|
72
377
|
var isOpen = false;
|
|
73
378
|
|
|
74
379
|
function toggle() {
|
|
380
|
+
if (canvasPhase !== 'bubble') return;
|
|
75
381
|
isOpen = !isOpen;
|
|
76
382
|
panel.classList.toggle('open', isOpen);
|
|
77
383
|
backdrop.classList.toggle('open', isOpen);
|
|
78
|
-
|
|
384
|
+
canvas.style.display = isOpen ? 'none' : 'block';
|
|
79
385
|
}
|
|
80
386
|
|
|
81
|
-
bubble.addEventListener('click',
|
|
387
|
+
bubble.addEventListener('click', function () {
|
|
388
|
+
if (canvasPhase === 'bubble') toggle();
|
|
389
|
+
});
|
|
82
390
|
backdrop.addEventListener('click', toggle);
|
|
83
391
|
|
|
84
392
|
// Close on Escape
|
|
@@ -97,10 +405,8 @@
|
|
|
97
405
|
window.addEventListener('message', function (e) {
|
|
98
406
|
if (!e.data || !e.data.type) return;
|
|
99
407
|
|
|
100
|
-
// Close chat panel
|
|
101
408
|
if (e.data.type === 'fluxy:close' && isOpen) toggle();
|
|
102
409
|
|
|
103
|
-
// Install App request from chat iframe
|
|
104
410
|
if (e.data.type === 'fluxy:install-app') {
|
|
105
411
|
if (deferredInstallPrompt) {
|
|
106
412
|
deferredInstallPrompt.prompt();
|
|
@@ -108,66 +414,28 @@
|
|
|
108
414
|
if (result.outcome === 'accepted') deferredInstallPrompt = null;
|
|
109
415
|
});
|
|
110
416
|
} else {
|
|
111
|
-
// No native prompt available — tell chat to show instructions modal
|
|
112
417
|
iframe.contentWindow.postMessage({ type: 'fluxy:show-ios-install' }, '*');
|
|
113
418
|
}
|
|
114
419
|
}
|
|
420
|
+
|
|
421
|
+
// Onboard complete — show the bubble
|
|
422
|
+
if (e.data.type === 'fluxy:onboard-complete') {
|
|
423
|
+
onboardActive = false;
|
|
424
|
+
hideAfterTransition = false;
|
|
425
|
+
canvas.style.display = 'block';
|
|
426
|
+
}
|
|
115
427
|
});
|
|
116
428
|
|
|
117
|
-
// Restore open state after HMR reload
|
|
429
|
+
// Restore open state after HMR reload
|
|
118
430
|
try {
|
|
119
431
|
if (sessionStorage.getItem('fluxy_widget_open') === '1') {
|
|
120
432
|
sessionStorage.removeItem('fluxy_widget_open');
|
|
121
|
-
toggle();
|
|
433
|
+
if (canvasPhase === 'bubble') toggle();
|
|
122
434
|
}
|
|
123
435
|
} catch (e) {}
|
|
124
436
|
|
|
125
|
-
// Hide widget during initial onboard
|
|
126
|
-
try {
|
|
127
|
-
fetch('/api/settings')
|
|
128
|
-
.then(function (r) { return r.json(); })
|
|
129
|
-
.then(function (s) {
|
|
130
|
-
if (s.onboard_complete !== 'true') {
|
|
131
|
-
bubble.style.display = 'none';
|
|
132
|
-
window.addEventListener('message', function onMsg(e) {
|
|
133
|
-
if (e.data && e.data.type === 'fluxy:onboard-complete') {
|
|
134
|
-
bubble.style.display = 'flex';
|
|
135
|
-
window.removeEventListener('message', onMsg);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
.catch(function () {});
|
|
141
|
-
} catch (e) {}
|
|
142
|
-
|
|
143
437
|
// Inject app-ws.js — proxies /app/api/* fetch calls through WebSocket
|
|
144
|
-
// (works around POST request failures through Cloudflare tunnel)
|
|
145
438
|
var awsScript = document.createElement('script');
|
|
146
439
|
awsScript.src = '/fluxy/app-ws.js';
|
|
147
440
|
document.head.appendChild(awsScript);
|
|
148
|
-
|
|
149
|
-
// ── Splash / Service Worker upgrade logic ──────────────────────────
|
|
150
|
-
// This runs from the supervisor (always updated), so it fixes existing
|
|
151
|
-
// installs whose workspace/client/index.html is still old.
|
|
152
|
-
|
|
153
|
-
// Re-show splash before page unloads (manual refresh)
|
|
154
|
-
window.addEventListener('beforeunload', function () {
|
|
155
|
-
console.log('[widget] beforeunload — showing splash');
|
|
156
|
-
var s = document.getElementById('splash');
|
|
157
|
-
if (s) { s.style.transition = 'none'; s.style.display = 'flex'; s.style.opacity = '1'; }
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// When a new SW takes control, show splash and reload so the new
|
|
161
|
-
// caching strategy (stale-while-revalidate) kicks in immediately.
|
|
162
|
-
if ('serviceWorker' in navigator) {
|
|
163
|
-
var swRefreshing = false;
|
|
164
|
-
navigator.serviceWorker.addEventListener('controllerchange', function () {
|
|
165
|
-
console.log('[widget] controllerchange — new SW took control, refreshing:', swRefreshing);
|
|
166
|
-
if (swRefreshing) return;
|
|
167
|
-
swRefreshing = true;
|
|
168
|
-
var s = document.getElementById('splash');
|
|
169
|
-
if (s) { s.style.transition = 'none'; s.style.display = 'flex'; s.style.opacity = '1'; }
|
|
170
|
-
location.reload();
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
441
|
})();
|
|
@@ -10,18 +10,11 @@
|
|
|
10
10
|
<link rel="apple-touch-icon" href="/fluxy-icon-192.png" />
|
|
11
11
|
<link rel="manifest" href="/manifest.json" />
|
|
12
12
|
<title>Fluxy</title>
|
|
13
|
-
<style>
|
|
14
|
-
@keyframes _fs{to{transform:rotate(360deg)}}
|
|
15
|
-
</style>
|
|
16
13
|
</head>
|
|
17
14
|
<body class="bg-background text-foreground" style="background:#222122">
|
|
18
|
-
<!--
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<div id="splash" style="background:#222122;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100dvh;width:100vw;position:fixed;inset:0;z-index:9999;font-family:system-ui,-apple-system,sans-serif;transition:opacity .25s ease-out">
|
|
22
|
-
<img src="/fluxy-icon-192.png" width="56" height="56" style="border-radius:14px;margin-bottom:20px" alt="" />
|
|
23
|
-
<div style="width:18px;height:18px;border:2px solid rgba(255,255,255,0.12);border-top-color:rgba(255,255,255,0.7);border-radius:50%;animation:_fs .6s linear infinite"></div>
|
|
24
|
-
</div>
|
|
15
|
+
<!-- Dark background fallback — visible instantly while widget.js loads.
|
|
16
|
+
The canvas animation (in widget.js) takes over as the actual splash. -->
|
|
17
|
+
<div id="splash" style="background:#222122;position:fixed;inset:0;z-index:9998;transition:opacity .25s ease-out"></div>
|
|
25
18
|
|
|
26
19
|
<div id="root"></div>
|
|
27
20
|
|
|
@@ -42,13 +35,10 @@
|
|
|
42
35
|
});
|
|
43
36
|
</script>
|
|
44
37
|
<script>
|
|
45
|
-
// Re-show
|
|
46
|
-
// The last painted frame before teardown will be the dark splash
|
|
47
|
-
// instead of a white gap.
|
|
38
|
+
// Re-show dark background before page unloads (manual refresh / navigation).
|
|
48
39
|
window.addEventListener('beforeunload', function () {
|
|
49
|
-
console.log('[splash] beforeunload — showing splash');
|
|
50
40
|
var s = document.getElementById('splash');
|
|
51
|
-
if (s) { s.style.transition = 'none'; s.style.display = '
|
|
41
|
+
if (s) { s.style.transition = 'none'; s.style.display = 'block'; s.style.opacity = '1'; }
|
|
52
42
|
});
|
|
53
43
|
</script>
|
|
54
44
|
<script type="module" src="/src/main.tsx"></script>
|
|
@@ -63,7 +53,7 @@
|
|
|
63
53
|
if(swRefreshing)return;
|
|
64
54
|
swRefreshing=true;
|
|
65
55
|
var s=document.getElementById('splash');
|
|
66
|
-
if(s){s.style.transition='none';s.style.display='
|
|
56
|
+
if(s){s.style.transition='none';s.style.display='block';s.style.opacity='1'}
|
|
67
57
|
console.log('[sw-reg] reloading after new SW took control');
|
|
68
58
|
location.reload();
|
|
69
59
|
});
|
|
Binary file
|
|
@@ -41,10 +41,10 @@ export default function App() {
|
|
|
41
41
|
const splash = document.getElementById('splash');
|
|
42
42
|
let hiddenAt = 0;
|
|
43
43
|
|
|
44
|
-
// Show the
|
|
44
|
+
// Show the dark background (used before reloads and on resume)
|
|
45
45
|
function showSplash() {
|
|
46
46
|
if (!splash) return;
|
|
47
|
-
splash.style.display = '
|
|
47
|
+
splash.style.display = 'block';
|
|
48
48
|
splash.style.opacity = '1';
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -9,18 +9,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
|
9
9
|
</React.StrictMode>,
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// to ensure the app is visible before removing the splash.
|
|
15
|
-
console.log('[main] React render called, waiting for paint to hide splash');
|
|
12
|
+
// Signal that the app has painted — the canvas animation in widget.js
|
|
13
|
+
// will trigger the melt → travel → reform transition to bubble mode.
|
|
16
14
|
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (splash) {
|
|
20
|
-
splash.style.opacity = '0';
|
|
21
|
-
splash.addEventListener('transitionend', () => {
|
|
22
|
-
splash.style.display = 'none';
|
|
23
|
-
console.log('[main] splash hidden');
|
|
24
|
-
}, { once: true });
|
|
25
|
-
}
|
|
15
|
+
(window as any).__fluxyAppReady = true;
|
|
16
|
+
window.dispatchEvent(new Event('fluxy:app-ready'));
|
|
26
17
|
}));
|