fluxy-bot 0.3.19 → 0.3.21

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.
@@ -1 +1 @@
1
- import{b as o,j as e,R as n,O as r}from"./globals-A5sTL-kb.js";function a(){const t=()=>{window.parent?.postMessage({type:"fluxy:onboard-complete"},"*")};return e.jsx(r,{onComplete:t,isInitialSetup:!0})}o.createRoot(document.getElementById("root")).render(e.jsx(n.StrictMode,{children:e.jsx(a,{})}));
1
+ import{b as o,j as e,R as n,O as r}from"./globals-BmcQJ_1f.js";function a(){const t=()=>{window.parent?.postMessage({type:"fluxy:onboard-complete"},"*")};return e.jsx(r,{onComplete:t,isInitialSetup:!0})}o.createRoot(document.getElementById("root")).render(e.jsx(n.StrictMode,{children:e.jsx(a,{})}));
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
6
6
  <title>Fluxy Chat</title>
7
- <script type="module" crossorigin src="/fluxy/assets/fluxy-DmbQycIj.js"></script>
8
- <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-A5sTL-kb.js">
7
+ <script type="module" crossorigin src="/fluxy/assets/fluxy-D_vy8fea.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-BmcQJ_1f.js">
9
9
  <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BdY9BJIP.css">
10
10
  </head>
11
11
  <body class="bg-background text-foreground">
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
6
6
  <title>Fluxy Setup</title>
7
- <script type="module" crossorigin src="/fluxy/assets/onboard-DnbGE0aK.js"></script>
8
- <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-A5sTL-kb.js">
7
+ <script type="module" crossorigin src="/fluxy/assets/onboard-Cp7fEIFC.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/fluxy/assets/globals-BmcQJ_1f.js">
9
9
  <link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BdY9BJIP.css">
10
10
  </head>
11
11
  <body class="bg-background text-foreground">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "description": "Self-hosted AI bot — run your own AI assistant from anywhere",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,89 @@
1
+ # Fluxy Chat Architecture
2
+
3
+ ## Network Topology
4
+
5
+ ```
6
+ Browser
7
+ |
8
+ | https://bot-name.at.fluxy.bot
9
+ v
10
+ Fluxy Relay (cloud)
11
+ |
12
+ | Cloudflare Quick Tunnel
13
+ v
14
+ Supervisor (localhost:3000)
15
+ |
16
+ ├── Worker (localhost:3001) — DB, settings, AI providers
17
+ ├── Dashboard Vite (localhost:3002) — dev server for workspace/client
18
+ └── Backend (localhost:3004) — user's app backend
19
+ ```
20
+
21
+ ## How the Chat Loads
22
+
23
+ The dashboard (`workspace/client/`) loads at the root URL. It injects `widget.js`, which creates:
24
+
25
+ 1. A floating bubble (the Fluxy avatar)
26
+ 2. A slide-out panel containing an **iframe** at `/fluxy/`
27
+
28
+ The iframe loads `dist-fluxy/fluxy.html`, which is the pre-built Fluxy chat SPA. This SPA is completely independent from the dashboard — it has its own React tree, its own styles, and its own WebSocket connection.
29
+
30
+ ```
31
+ Dashboard (main page)
32
+ └── widget.js
33
+ ├── Bubble (click to open)
34
+ └── Panel
35
+ └── <iframe src="/fluxy/"> ← Fluxy chat lives here
36
+ ├── fluxy-main.tsx
37
+ ├── InputBar, MessageList, etc.
38
+ └── OnboardWizard (opened from settings menu)
39
+ ```
40
+
41
+ ## The Relay Problem
42
+
43
+ When a user registers a handle (e.g. `bug.at.fluxy.bot`), the relay proxies all HTTP traffic through Cloudflare's tunnel to the local supervisor. This works transparently for:
44
+
45
+ - **GET requests** — settings, onboard status, static assets
46
+ - **WebSocket** — chat messages, heartbeats, real-time events
47
+
48
+ However, **POST requests from the chat iframe fail** (502 / timeout). The relay + tunnel chain cannot reliably forward HTTP POST bodies originating from an iframe context. The exact cause is in the relay/tunnel infrastructure — the request never reaches the supervisor.
49
+
50
+ This does NOT affect:
51
+ - The initial onboard (`/fluxy/onboard.html` in a separate iframe, runs before the relay is configured)
52
+ - Direct `localhost` access
53
+ - GET requests from the chat iframe
54
+ - WebSocket messages from the chat iframe
55
+
56
+ ## The Fix: WebSocket as a Sidecar Channel
57
+
58
+ Since the WebSocket connection works reliably from the chat iframe, any mutation that the chat needs to perform (saving settings, etc.) is sent over WebSocket instead of HTTP POST.
59
+
60
+ ```
61
+ Chat Wizard Supervisor WS Handler Worker
62
+ | | |
63
+ |-- ws: settings:save --------->| |
64
+ | |-- POST /api/onboard ----->|
65
+ | | (localhost, no relay) |
66
+ | |<-- { ok: true } ---------|
67
+ |<-- ws: settings:saved --------| |
68
+ ```
69
+
70
+ The supervisor's WebSocket handler receives the `settings:save` message and makes a **local** HTTP POST to the worker (`localhost:3001`). This completely bypasses the relay.
71
+
72
+ ### When to use WebSocket vs HTTP
73
+
74
+ | From where | GET | POST/PUT | Notes |
75
+ |---|---|---|---|
76
+ | Initial onboard | `fetch()` | `fetch()` | Runs before relay exists, direct tunnel works |
77
+ | Chat iframe | `fetch()` / `authFetch()` | **WebSocket** | POST through relay fails |
78
+ | Dashboard | `fetch()` | `fetch()` | Not in an iframe, relay handles it fine |
79
+
80
+ ## Key Files
81
+
82
+ | File | Purpose |
83
+ |---|---|
84
+ | `supervisor/widget.js` | Creates the chat iframe + bubble on the dashboard |
85
+ | `supervisor/chat/fluxy-main.tsx` | Chat app entry point, WS connection, wizard integration |
86
+ | `supervisor/chat/OnboardWizard.tsx` | Setup wizard (uses `onSave` prop for WS saves) |
87
+ | `supervisor/chat/src/lib/ws-client.ts` | WebSocket client with reconnect + message queue |
88
+ | `supervisor/index.ts` | Supervisor HTTP server + WS handler (`settings:save`) |
89
+ | `worker/index.ts` | Worker API (`POST /api/onboard`, settings DB) |
@@ -87,9 +87,10 @@ function ModelDropdown({ models, value, onChange }: { models: { id: string; labe
87
87
  interface Props {
88
88
  onComplete: () => void;
89
89
  isInitialSetup?: boolean;
90
+ onSave?: (payload: any) => Promise<any>;
90
91
  }
91
92
 
92
- export default function OnboardWizard({ onComplete, isInitialSetup = false }: Props) {
93
+ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSave }: Props) {
93
94
  const TOTAL_STEPS = isInitialSetup ? 7 : 6; // 0..5 normal, +step 6 "All Set" for initial
94
95
 
95
96
  const [step, setStep] = useState(0);
@@ -472,34 +473,18 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false }: Pr
472
473
  portalUser: portalUser.trim(),
473
474
  portalPass,
474
475
  };
475
- console.log('[OnboardWizard] handleComplete called');
476
- console.log('[OnboardWizard] inIframe:', window.parent !== window, 'origin:', window.location.origin, 'href:', window.location.href);
477
476
  try {
478
- // Try XHR as fallback if in iframe (fetch POST can fail through tunnels)
479
- const inIframe = window.parent !== window;
480
- let res: Response;
481
- if (inIframe) {
482
- console.log('[OnboardWizard] Using XHR for POST (iframe context)');
483
- res = await new Promise<Response>((resolve, reject) => {
484
- const xhr = new XMLHttpRequest();
485
- xhr.open('POST', '/api/onboard');
486
- xhr.setRequestHeader('Content-Type', 'application/json');
487
- xhr.onload = () => resolve(new Response(xhr.responseText, { status: xhr.status, statusText: xhr.statusText }));
488
- xhr.onerror = () => reject(new Error('XHR network error'));
489
- xhr.ontimeout = () => reject(new Error('XHR timeout'));
490
- xhr.timeout = 15000;
491
- xhr.send(JSON.stringify(payload));
492
- });
477
+ if (onSave) {
478
+ // Chat context: save via WebSocket (bypasses relay POST issues)
479
+ await onSave(payload);
493
480
  } else {
494
- res = await fetch('/api/onboard', {
481
+ // Initial onboard: direct POST
482
+ await fetch('/api/onboard', {
495
483
  method: 'POST',
496
484
  headers: { 'Content-Type': 'application/json' },
497
485
  body: JSON.stringify(payload),
498
486
  });
499
487
  }
500
- console.log('[OnboardWizard] POST /api/onboard response:', res.status, res.statusText);
501
- const data = await res.json().catch(() => null);
502
- console.log('[OnboardWizard] response body:', data);
503
488
  if (isInitialSetup) {
504
489
  setSaving(false);
505
490
  setStep(6);
@@ -246,12 +246,51 @@ function FluxyApp() {
246
246
  {/* Chat body */}
247
247
  <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
248
248
  <MessageList messages={messages} streaming={streaming} streamBuffer={streamBuffer} tools={tools} />
249
- <InputBar onSend={sendMessage} onStop={stopStreaming} streaming={streaming} whisperEnabled={whisperEnabled} />
249
+ <InputBar
250
+ onSend={sendMessage}
251
+ onStop={stopStreaming}
252
+ streaming={streaming}
253
+ whisperEnabled={whisperEnabled}
254
+ onTranscribe={(audio) => {
255
+ return new Promise((resolve, reject) => {
256
+ const client = clientRef.current;
257
+ if (!client?.connected) { reject(new Error('WebSocket not connected')); return; }
258
+ const unsub = client.on('whisper:result', (data) => {
259
+ unsub();
260
+ clearTimeout(timer);
261
+ resolve(data);
262
+ });
263
+ const timer = setTimeout(() => { unsub(); reject(new Error('Transcription timeout')); }, 30000);
264
+ client.send('whisper:transcribe', { audio });
265
+ });
266
+ }}
267
+ />
250
268
  </div>
251
269
 
252
270
  {/* Setup Wizard overlay */}
253
271
  {showWizard && (
254
272
  <OnboardWizard
273
+ onSave={(payload) => {
274
+ return new Promise((resolve, reject) => {
275
+ const client = clientRef.current;
276
+ if (!client?.connected) {
277
+ reject(new Error('WebSocket not connected'));
278
+ return;
279
+ }
280
+ const unsub = client.on('settings:saved', (data) => {
281
+ unsub();
282
+ clearTimeout(timer);
283
+ resolve(data);
284
+ });
285
+ const unsubErr = client.on('settings:save-error', (data) => {
286
+ unsubErr();
287
+ clearTimeout(timer);
288
+ reject(new Error(data.error || 'Save failed'));
289
+ });
290
+ const timer = setTimeout(() => { unsub(); unsubErr(); reject(new Error('Save timeout')); }, 10000);
291
+ client.send('settings:save', payload);
292
+ });
293
+ }}
255
294
  onComplete={() => {
256
295
  setShowWizard(false);
257
296
  // Reload settings (bot name, whisper, etc.)
@@ -8,6 +8,7 @@ interface Props {
8
8
  onStop: () => void;
9
9
  streaming: boolean;
10
10
  whisperEnabled?: boolean;
11
+ onTranscribe?: (audio: string) => Promise<{ transcript?: string }>;
11
12
  }
12
13
 
13
14
  function formatTime(s: number) {
@@ -53,7 +54,7 @@ function compressImage(dataUrl: string, maxBytes = 4 * 1024 * 1024): Promise<str
53
54
 
54
55
  const DRAFT_KEY = 'fluxy_draft';
55
56
 
56
- export default function InputBar({ onSend, onStop, streaming, whisperEnabled }: Props) {
57
+ export default function InputBar({ onSend, onStop, streaming, whisperEnabled, onTranscribe }: Props) {
57
58
  const [text, setText] = useState(() => {
58
59
  try { return localStorage.getItem(DRAFT_KEY) || ''; } catch { return ''; }
59
60
  });
@@ -136,12 +137,17 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled }:
136
137
  if (!base64) return;
137
138
 
138
139
  try {
139
- const res = await fetch('/api/whisper/transcribe', {
140
- method: 'POST',
141
- headers: { 'Content-Type': 'application/json' },
142
- body: JSON.stringify({ audio: base64 }),
143
- });
144
- const data = await res.json();
140
+ let data: { transcript?: string };
141
+ if (onTranscribe) {
142
+ data = await onTranscribe(base64);
143
+ } else {
144
+ const res = await fetch('/api/whisper/transcribe', {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify({ audio: base64 }),
148
+ });
149
+ data = await res.json();
150
+ }
145
151
  if (data.transcript?.trim()) {
146
152
  onSend(data.transcript.trim(), undefined, dataUrl);
147
153
  }
@@ -158,7 +164,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled }:
158
164
  setIsRecording(false);
159
165
  setRecordingTime(0);
160
166
  dragRef.current = 0;
161
- }, [onSend]);
167
+ }, [onSend, onTranscribe]);
162
168
 
163
169
  // ── File handling ──
164
170
 
@@ -150,7 +150,6 @@ export async function startSupervisor() {
150
150
 
151
151
  // HTTP server — proxies to Vite dev servers + worker API
152
152
  const server = http.createServer(async (req, res) => {
153
- if (req.method === 'POST') console.log(`[supervisor] POST received: ${req.url}`);
154
153
  // Fluxy widget — served directly (not part of Vite build)
155
154
  if (req.url === '/fluxy/widget.js') {
156
155
  console.log('[supervisor] Serving /fluxy/widget.js directly');
@@ -305,6 +304,41 @@ export async function startSupervisor() {
305
304
 
306
305
  const msg = JSON.parse(rawStr);
307
306
 
307
+ // Whisper transcription via WebSocket (bypasses relay POST issues)
308
+ if (msg.type === 'whisper:transcribe') {
309
+ (async () => {
310
+ try {
311
+ const result = await workerApi('/api/whisper/transcribe', 'POST', { audio: msg.data.audio });
312
+ if (ws.readyState === WebSocket.OPEN) {
313
+ ws.send(JSON.stringify({ type: 'whisper:result', data: result }));
314
+ }
315
+ } catch (err: any) {
316
+ if (ws.readyState === WebSocket.OPEN) {
317
+ ws.send(JSON.stringify({ type: 'whisper:result', data: { error: err.message } }));
318
+ }
319
+ }
320
+ })();
321
+ return;
322
+ }
323
+
324
+ // Save settings via WebSocket (bypasses relay POST issues)
325
+ if (msg.type === 'settings:save') {
326
+ (async () => {
327
+ try {
328
+ const result = await workerApi('/api/onboard', 'POST', msg.data);
329
+ if (ws.readyState === WebSocket.OPEN) {
330
+ ws.send(JSON.stringify({ type: 'settings:saved', data: result }));
331
+ }
332
+ } catch (err: any) {
333
+ log.error(`[fluxy] settings:save failed: ${err.message}`);
334
+ if (ws.readyState === WebSocket.OPEN) {
335
+ ws.send(JSON.stringify({ type: 'settings:save-error', data: { error: err.message } }));
336
+ }
337
+ }
338
+ })();
339
+ return;
340
+ }
341
+
308
342
  // New protocol: { type: 'user:message', data: { content, conversationId? } }
309
343
  if (msg.type === 'user:message') {
310
344
  const data = msg.data || {};
package/worker/index.ts CHANGED
@@ -297,7 +297,6 @@ app.get('/api/portal/validate-token', (req, res) => {
297
297
  });
298
298
 
299
299
  app.post('/api/onboard', (req, res) => {
300
- console.log('[worker] POST /api/onboard body:', JSON.stringify(req.body));
301
300
  const { userName, agentName, provider, model, apiKey, baseUrl, portalUser, portalPass, whisperEnabled, whisperKey } = req.body;
302
301
  setSetting('user_name', userName || '');
303
302
  setSetting('agent_name', agentName || 'Fluxy');
@@ -1,3 +1,4 @@
1
1
  // Minimal service worker — required for PWA installability
2
2
  self.addEventListener('install', () => self.skipWaiting());
3
3
  self.addEventListener('activate', (e) => e.waitUntil(self.clients.claim()));
4
+ self.addEventListener('fetch', () => {});