fluxy-bot 0.3.20 → 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.
@@ -4,7 +4,7 @@
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-sbmFOV3Z.js"></script>
7
+ <script type="module" crossorigin src="/fluxy/assets/fluxy-D_vy8fea.js"></script>
8
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.3.20",
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) |
@@ -246,7 +246,25 @@ 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 */}
@@ -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,23 @@ 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
+
308
324
  // Save settings via WebSocket (bypasses relay POST issues)
309
325
  if (msg.type === 'settings:save') {
310
326
  (async () => {
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', () => {});