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.
- package/dist-fluxy/assets/{fluxy-sbmFOV3Z.js → fluxy-D_vy8fea.js} +26 -26
- package/dist-fluxy/fluxy.html +1 -1
- package/package.json +1 -1
- package/supervisor/chat/ARCHITECTURE.md +89 -0
- package/supervisor/chat/fluxy-main.tsx +19 -1
- package/supervisor/chat/src/components/Chat/InputBar.tsx +14 -8
- package/supervisor/index.ts +17 -1
- package/worker/index.ts +0 -1
- package/workspace/client/public/sw.js +1 -0
package/dist-fluxy/fluxy.html
CHANGED
|
@@ -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-
|
|
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
|
@@ -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
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
package/supervisor/index.ts
CHANGED
|
@@ -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');
|