fluxy-bot 0.1.46 → 0.2.1
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/bin/cli.js +1 -3
- package/client/fluxy-main.tsx +75 -0
- package/client/fluxy.html +12 -0
- package/client/index.html +1 -0
- package/client/src/App.tsx +2 -83
- package/client/src/components/Layout/DashboardLayout.tsx +1 -1
- package/client/src/hooks/useChat.ts +51 -62
- package/client/src/hooks/useFluxyChat.ts +119 -0
- package/client/src/lib/ws-client.ts +1 -1
- package/dist/assets/index-BxQ8et35.js +64 -0
- package/dist/assets/index-D2PQx64r.css +1 -0
- package/dist/index.html +3 -2
- package/dist/sw.js +1 -1
- package/dist-fluxy/assets/fluxy-B49yi-07.js +53 -0
- package/dist-fluxy/assets/fluxy-D2PQx64r.css +1 -0
- package/dist-fluxy/fluxy.html +13 -0
- package/dist-fluxy/fluxy.png +0 -0
- package/dist-fluxy/fluxy_frame1.png +0 -0
- package/dist-fluxy/fluxy_say_hi.webm +0 -0
- package/dist-fluxy/fluxy_tilts.webm +0 -0
- package/dist-fluxy/icons/claude.png +0 -0
- package/dist-fluxy/icons/codex.png +0 -0
- package/dist-fluxy/icons/openai.svg +15 -0
- package/package.json +14 -9
- package/scripts/postinstall.js +10 -26
- package/shared/paths.ts +2 -11
- package/supervisor/index.ts +130 -176
- package/supervisor/widget.js +75 -0
- package/supervisor/worker.ts +3 -16
- package/tsconfig.json +2 -3
- package/vite.config.ts +4 -1
- package/vite.fluxy.config.ts +19 -0
- package/{supervisor → worker}/claude-agent.ts +43 -50
- package/{shared → worker}/db.ts +1 -9
- package/{shared → worker}/file-storage.ts +1 -1
- package/worker/index.ts +133 -31
- package/worker/prompts/fluxy-system-prompt.txt +8 -0
- package/client/src/components/BuildOverlay.tsx +0 -75
- package/client/src/components/FluxyFab.tsx +0 -29
- package/client/src/hooks/useWebSocket.ts +0 -22
- package/dist/assets/index-BAUWfBMW.js +0 -100
- package/dist/assets/index-CiN0-4-O.css +0 -1
- package/shared/workspace.ts +0 -196
- package/supervisor/prompts/fluxy-system-prompt.txt +0 -35
- package/supervisor/vite-dev.ts +0 -75
package/supervisor/index.ts
CHANGED
|
@@ -1,234 +1,190 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import net from 'net';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
3
5
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
6
|
import { loadConfig, saveConfig } from '../shared/config.js';
|
|
5
7
|
import { createProvider, type AiProvider, type ChatMessage } from '../shared/ai.js';
|
|
6
|
-
import { paths
|
|
8
|
+
import { paths } from '../shared/paths.js';
|
|
7
9
|
import { log } from '../shared/logger.js';
|
|
8
|
-
import { initDb, closeDb, createConversation, addMessage, getMessages, getSetting } from '../shared/db.js';
|
|
9
|
-
import { initWorkspace } from '../shared/workspace.js';
|
|
10
|
-
import { ensureFileDirs, saveFile } from '../shared/file-storage.js';
|
|
11
|
-
import { startAgentQuery, stopAgentQuery } from './claude-agent.js';
|
|
12
10
|
import { startTunnel, stopTunnel, isTunnelAlive, restartTunnel } from './tunnel.js';
|
|
13
11
|
import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.js';
|
|
14
|
-
import { spawnVite, stopVite, getVitePort, isViteAlive } from './vite-dev.js';
|
|
15
12
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
16
13
|
|
|
17
14
|
const RECOVERING_HTML = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Recovering</title>
|
|
18
15
|
<style>body{background:#0a0a0f;color:#94a3b8;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
19
16
|
div{text-align:center}h1{font-size:18px;margin-bottom:8px;color:#e2e8f0}p{font-size:14px}a{color:#60a5fa}</style></head>
|
|
20
17
|
<body><div><h1>Dashboard is restarting...</h1><p>Refreshing automatically.</p></div>
|
|
21
|
-
<script>setTimeout(()=>location.reload(),3000)</script
|
|
22
|
-
|
|
23
|
-
/** Broadcast a message to all connected chat clients */
|
|
24
|
-
function broadcastWs(chatWss: WebSocketServer, type: string, data?: any): void {
|
|
25
|
-
const payload = JSON.stringify({ type, data });
|
|
26
|
-
for (const client of chatWss.clients) {
|
|
27
|
-
if (client.readyState === WebSocket.OPEN) client.send(payload);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Notify clients after every agent turn — HMR handles the actual update */
|
|
32
|
-
function runPostIteration(chatWss: WebSocketServer): void {
|
|
33
|
-
log.info('[post-iteration] Changes applied — HMR active');
|
|
34
|
-
broadcastWs(chatWss, 'changes:applied', { timestamp: Date.now() });
|
|
35
|
-
}
|
|
18
|
+
<script>setTimeout(()=>location.reload(),3000)</script>
|
|
19
|
+
<script src="/fluxy/widget.js"></script></body></html>`;
|
|
36
20
|
|
|
37
21
|
export async function startSupervisor() {
|
|
38
22
|
const config = loadConfig();
|
|
39
23
|
const workerPort = getWorkerPort(config.port);
|
|
40
24
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Active non-Anthropic streams (keyed by conversationId)
|
|
47
|
-
const activeStreams = new Map<string, AbortController>();
|
|
25
|
+
// Fluxy's AI brain
|
|
26
|
+
let ai: AiProvider | null = null;
|
|
27
|
+
if (config.ai.provider && (config.ai.apiKey || config.ai.provider === 'ollama')) {
|
|
28
|
+
ai = createProvider(config.ai.provider, config.ai.apiKey, config.ai.baseUrl);
|
|
29
|
+
}
|
|
48
30
|
|
|
49
|
-
|
|
31
|
+
// Fluxy chat conversations (in-memory for now)
|
|
32
|
+
const conversations = new Map<WebSocket, ChatMessage[]>();
|
|
50
33
|
|
|
51
|
-
// HTTP server
|
|
34
|
+
// HTTP server
|
|
52
35
|
const server = http.createServer((req, res) => {
|
|
53
|
-
|
|
36
|
+
// Fluxy routes — served directly by supervisor, never proxied
|
|
37
|
+
if (req.url === '/fluxy/widget.js') {
|
|
38
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript', 'Cache-Control': 'no-cache' });
|
|
39
|
+
res.end(fs.readFileSync(paths.widgetJs));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
54
42
|
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
43
|
+
if (req.url?.startsWith('/fluxy/assets/')) {
|
|
44
|
+
const assetPath = path.join(paths.distFluxy, req.url.slice('/fluxy'.length));
|
|
45
|
+
if (fs.existsSync(assetPath)) {
|
|
46
|
+
const ext = path.extname(assetPath);
|
|
47
|
+
const mimeTypes: Record<string, string> = {
|
|
48
|
+
'.js': 'application/javascript',
|
|
49
|
+
'.css': 'text/css',
|
|
50
|
+
'.woff2': 'font/woff2',
|
|
51
|
+
'.woff': 'font/woff',
|
|
52
|
+
'.png': 'image/png',
|
|
53
|
+
'.svg': 'image/svg+xml',
|
|
54
|
+
};
|
|
55
|
+
res.writeHead(200, {
|
|
56
|
+
'Content-Type': mimeTypes[ext] || 'application/octet-stream',
|
|
57
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
58
|
+
});
|
|
59
|
+
fs.createReadStream(assetPath).pipe(res);
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
|
+
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
if (req.url === '/fluxy' || req.url === '/fluxy/') {
|
|
65
|
+
const indexPath = path.join(paths.distFluxy, 'fluxy.html');
|
|
66
|
+
if (fs.existsSync(indexPath)) {
|
|
67
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
68
|
+
res.end(fs.readFileSync(indexPath));
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback: dev mode or build not yet done
|
|
71
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
72
|
+
res.end('<html><body style="background:#212121;color:#f5f5f5;display:flex;align-items:center;justify-content:center;height:100vh;font-family:system-ui"><p>Fluxy chat not built yet. Run <code>npm run build</code></p></body></html>');
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Override Host header to localhost so Vite accepts the request
|
|
82
|
-
const headers = { ...req.headers, host: `127.0.0.1:${targetPort}` };
|
|
83
|
-
|
|
84
|
-
const proxy = http.request(
|
|
85
|
-
{ host: '127.0.0.1', port: targetPort, path: req.url, method: req.method, headers },
|
|
86
|
-
(proxyRes) => {
|
|
87
|
-
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
88
|
-
proxyRes.pipe(res);
|
|
89
|
-
},
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
proxy.on('error', () => {
|
|
93
|
-
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
94
|
-
res.end(RECOVERING_HTML);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
req.pipe(proxy);
|
|
77
|
+
// Everything else → proxy to worker
|
|
78
|
+
if (!isWorkerAlive()) {
|
|
79
|
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
80
|
+
res.end(RECOVERING_HTML);
|
|
81
|
+
return;
|
|
98
82
|
}
|
|
83
|
+
|
|
84
|
+
const proxy = http.request(
|
|
85
|
+
{ host: '127.0.0.1', port: workerPort, path: req.url, method: req.method, headers: req.headers },
|
|
86
|
+
(proxyRes) => {
|
|
87
|
+
res.writeHead(proxyRes.statusCode!, proxyRes.headers);
|
|
88
|
+
proxyRes.pipe(res);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
proxy.on('error', () => {
|
|
93
|
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
94
|
+
res.end(RECOVERING_HTML);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
req.pipe(proxy);
|
|
99
98
|
});
|
|
100
99
|
|
|
101
|
-
// WebSocket: chat
|
|
102
|
-
const
|
|
100
|
+
// WebSocket: Fluxy chat + proxy worker WS
|
|
101
|
+
const fluxyWss = new WebSocketServer({ noServer: true });
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
log.info('
|
|
103
|
+
fluxyWss.on('connection', (ws) => {
|
|
104
|
+
log.info('Fluxy chat connected');
|
|
105
|
+
let convId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
106
|
+
conversations.set(ws, []);
|
|
106
107
|
|
|
107
108
|
ws.on('message', (raw) => {
|
|
108
|
-
const
|
|
109
|
+
const rawStr = raw.toString();
|
|
109
110
|
|
|
110
|
-
// Heartbeat
|
|
111
|
-
if (
|
|
111
|
+
// Heartbeat
|
|
112
|
+
if (rawStr === 'ping') {
|
|
113
|
+
ws.send('pong');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
112
116
|
|
|
113
|
-
const msg = JSON.parse(
|
|
117
|
+
const msg = JSON.parse(rawStr);
|
|
114
118
|
|
|
119
|
+
// New protocol: { type: 'user:message', data: { content, conversationId? } }
|
|
115
120
|
if (msg.type === 'user:message') {
|
|
116
|
-
|
|
117
|
-
const
|
|
121
|
+
const data = msg.data || {};
|
|
122
|
+
const content = data.content;
|
|
123
|
+
if (!content) return;
|
|
124
|
+
if (data.conversationId) convId = data.conversationId;
|
|
118
125
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
if (!convId) convId = createConversation(content.slice(0, 50), freshConfig.ai.model).id;
|
|
126
|
+
const history = conversations.get(ws) || [];
|
|
127
|
+
history.push({ role: 'user', content });
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
audioPath = saveFile(audioData, 'audio', 'webm');
|
|
128
|
-
} catch (err: any) {
|
|
129
|
-
log.warn(`Failed to save audio file: ${err.message}`);
|
|
130
|
-
audioPath = audioData; // fallback to inline
|
|
131
|
-
}
|
|
129
|
+
if (!ai) {
|
|
130
|
+
ws.send(JSON.stringify({ type: 'bot:error', data: { error: 'AI not configured. Set up your provider first.' } }));
|
|
131
|
+
return;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
type StoredAttachment = { type: string; name: string; mediaType: string; filePath: string };
|
|
136
|
-
let storedAttachments: StoredAttachment[] | undefined;
|
|
137
|
-
if (attachments?.length) {
|
|
138
|
-
storedAttachments = [];
|
|
139
|
-
for (const att of attachments) {
|
|
140
|
-
try {
|
|
141
|
-
const isImage = att.mediaType?.startsWith('image/');
|
|
142
|
-
const ext = att.name?.split('.').pop() || (isImage ? 'jpg' : 'bin');
|
|
143
|
-
const category = isImage ? 'images' as const : 'documents' as const;
|
|
144
|
-
const filePath = saveFile(att.data, category, ext);
|
|
145
|
-
storedAttachments.push({ type: att.type, name: att.name, mediaType: att.mediaType, filePath });
|
|
146
|
-
} catch (err: any) {
|
|
147
|
-
log.warn(`Failed to save attachment ${att.name}: ${err.message}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
134
|
+
ws.send(JSON.stringify({ type: 'bot:typing' }));
|
|
151
135
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
ws.send(JSON.stringify({ type, data: { ...data, conversationId: convId } }));
|
|
166
|
-
}, attachments).then(() => {
|
|
167
|
-
if (usedTools) runPostIteration(chatWss);
|
|
168
|
-
});
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
136
|
+
ai.chat(
|
|
137
|
+
[{ role: 'system', content: 'You are Fluxy, a helpful AI assistant. You help users manage and customize their self-hosted bot.' }, ...history],
|
|
138
|
+
config.ai.model,
|
|
139
|
+
(token) => ws.send(JSON.stringify({ type: 'bot:token', data: { token, conversationId: convId } })),
|
|
140
|
+
(full) => {
|
|
141
|
+
history.push({ role: 'assistant', content: full });
|
|
142
|
+
ws.send(JSON.stringify({ type: 'bot:response', data: { conversationId: convId, content: full } }));
|
|
143
|
+
},
|
|
144
|
+
(err) => ws.send(JSON.stringify({ type: 'bot:error', data: { error: err.message } })),
|
|
145
|
+
);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
171
148
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
149
|
+
if (msg.type === 'user:stop') {
|
|
150
|
+
// Stop streaming (best-effort — AI provider may not support cancellation)
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Legacy protocol support (old fluxy.html format)
|
|
155
|
+
if (msg.type === 'message' && msg.content) {
|
|
156
|
+
const history = conversations.get(ws) || [];
|
|
157
|
+
history.push({ role: 'user', content: msg.content });
|
|
177
158
|
|
|
178
159
|
if (!ai) {
|
|
179
|
-
ws.send(JSON.stringify({ type: '
|
|
160
|
+
ws.send(JSON.stringify({ type: 'error', error: 'AI not configured. Set up your provider first.' }));
|
|
180
161
|
return;
|
|
181
162
|
}
|
|
182
163
|
|
|
183
|
-
const history = getMessages(convId) as { role: string; content: string }[];
|
|
184
|
-
const systemPrompt = getSetting('system_prompt');
|
|
185
|
-
const messages: ChatMessage[] = [
|
|
186
|
-
...(systemPrompt ? [{ role: 'system' as const, content: systemPrompt }] : []),
|
|
187
|
-
...history.map((m) => ({ role: m.role as ChatMessage['role'], content: m.content })),
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
ws.send(JSON.stringify({ type: 'bot:typing', data: { conversationId: convId } }));
|
|
191
|
-
const ctrl = new AbortController();
|
|
192
|
-
activeStreams.set(convId, ctrl);
|
|
193
|
-
|
|
194
164
|
ai.chat(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
(token) => ws.send(JSON.stringify({ type: '
|
|
198
|
-
(full
|
|
199
|
-
|
|
200
|
-
ws.send(JSON.stringify({ type: '
|
|
201
|
-
activeStreams.delete(convId);
|
|
202
|
-
},
|
|
203
|
-
(err) => {
|
|
204
|
-
ws.send(JSON.stringify({ type: 'bot:error', data: { conversationId: convId, error: err.message } }));
|
|
205
|
-
activeStreams.delete(convId);
|
|
165
|
+
[{ role: 'system', content: 'You are Fluxy, a helpful AI assistant. You help users manage and customize their self-hosted bot.' }, ...history],
|
|
166
|
+
config.ai.model,
|
|
167
|
+
(token) => ws.send(JSON.stringify({ type: 'token', token })),
|
|
168
|
+
(full) => {
|
|
169
|
+
history.push({ role: 'assistant', content: full });
|
|
170
|
+
ws.send(JSON.stringify({ type: 'done' }));
|
|
206
171
|
},
|
|
207
|
-
|
|
172
|
+
(err) => ws.send(JSON.stringify({ type: 'error', error: err.message })),
|
|
208
173
|
);
|
|
209
174
|
}
|
|
210
|
-
|
|
211
|
-
if (msg.type === 'user:stop') {
|
|
212
|
-
// Stop Agent SDK queries
|
|
213
|
-
stopAgentQuery(msg.data.conversationId);
|
|
214
|
-
// Stop legacy streams
|
|
215
|
-
const ctrl = activeStreams.get(msg.data.conversationId);
|
|
216
|
-
if (ctrl) { ctrl.abort(); activeStreams.delete(msg.data.conversationId); }
|
|
217
|
-
}
|
|
218
175
|
});
|
|
176
|
+
|
|
177
|
+
ws.on('close', () => conversations.delete(ws));
|
|
219
178
|
});
|
|
220
179
|
|
|
221
180
|
server.on('upgrade', (req, socket: net.Socket, head) => {
|
|
222
|
-
if (req.url === '/ws') {
|
|
223
|
-
|
|
181
|
+
if (req.url === '/fluxy/ws') {
|
|
182
|
+
fluxyWss.handleUpgrade(req, socket, head, (ws) => fluxyWss.emit('connection', ws, req));
|
|
224
183
|
return;
|
|
225
184
|
}
|
|
226
185
|
|
|
227
|
-
// Proxy
|
|
228
|
-
const
|
|
229
|
-
if (!targetPort) { socket.destroy(); return; }
|
|
230
|
-
|
|
231
|
-
const proxy = net.connect(targetPort, () => {
|
|
186
|
+
// Proxy WS upgrade to worker
|
|
187
|
+
const proxy = net.connect(workerPort, () => {
|
|
232
188
|
const headers = Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`).join('\r\n');
|
|
233
189
|
proxy.write(`GET ${req.url} HTTP/1.1\r\n${headers}\r\n\r\n`);
|
|
234
190
|
if (head.length > 0) proxy.write(head);
|
|
@@ -251,11 +207,11 @@ export async function startSupervisor() {
|
|
|
251
207
|
|
|
252
208
|
server.listen(config.port, () => {
|
|
253
209
|
log.ok(`Supervisor on http://localhost:${config.port}`);
|
|
210
|
+
log.ok(`Fluxy chat at http://localhost:${config.port}/fluxy`);
|
|
254
211
|
});
|
|
255
212
|
|
|
256
|
-
// Spawn worker
|
|
213
|
+
// Spawn worker
|
|
257
214
|
spawnWorker(workerPort);
|
|
258
|
-
await spawnVite(vitePort);
|
|
259
215
|
|
|
260
216
|
// Tunnel
|
|
261
217
|
let tunnelUrl: string | null = null;
|
|
@@ -335,9 +291,7 @@ export async function startSupervisor() {
|
|
|
335
291
|
delete latestConfig.tunnelUrl;
|
|
336
292
|
saveConfig(latestConfig);
|
|
337
293
|
stopWorker();
|
|
338
|
-
stopVite();
|
|
339
294
|
stopTunnel();
|
|
340
|
-
closeDb();
|
|
341
295
|
server.close();
|
|
342
296
|
process.exit(0);
|
|
343
297
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
if (document.getElementById('fluxy-widget')) return;
|
|
3
|
+
|
|
4
|
+
var PANEL_WIDTH = '400px';
|
|
5
|
+
|
|
6
|
+
// ── Styles ──
|
|
7
|
+
var style = document.createElement('style');
|
|
8
|
+
style.textContent = [
|
|
9
|
+
'#fluxy-widget-bubble{position:fixed;bottom:24px;right:24px;z-index:99998;cursor:pointer;width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:transform .15s ease;-webkit-tap-highlight-color:transparent}',
|
|
10
|
+
'#fluxy-widget-bubble:hover{transform:scale(1.1)}',
|
|
11
|
+
'#fluxy-widget-bubble:active{transform:scale(0.95)}',
|
|
12
|
+
'#fluxy-widget-bubble video,#fluxy-widget-bubble img{height:44px;width:auto;pointer-events:none;-webkit-user-drag:none}',
|
|
13
|
+
'#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}',
|
|
14
|
+
'#fluxy-widget-backdrop.open{opacity:1;pointer-events:auto}',
|
|
15
|
+
'#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}',
|
|
16
|
+
'#fluxy-widget-panel.open{transform:translateX(0)}',
|
|
17
|
+
'#fluxy-widget-panel iframe{width:100%;height:100%;border:none;background:#212121}',
|
|
18
|
+
'@media(max-width:480px){#fluxy-widget-panel{width:100vw}}',
|
|
19
|
+
].join('\n');
|
|
20
|
+
document.head.appendChild(style);
|
|
21
|
+
|
|
22
|
+
// ── Backdrop ──
|
|
23
|
+
var backdrop = document.createElement('div');
|
|
24
|
+
backdrop.id = 'fluxy-widget-backdrop';
|
|
25
|
+
document.body.appendChild(backdrop);
|
|
26
|
+
|
|
27
|
+
// ── Panel ──
|
|
28
|
+
var panel = document.createElement('div');
|
|
29
|
+
panel.id = 'fluxy-widget-panel';
|
|
30
|
+
document.body.appendChild(panel);
|
|
31
|
+
|
|
32
|
+
var iframe = document.createElement('iframe');
|
|
33
|
+
iframe.src = '/fluxy/';
|
|
34
|
+
iframe.setAttribute('loading', 'lazy');
|
|
35
|
+
panel.appendChild(iframe);
|
|
36
|
+
|
|
37
|
+
// ── Bubble ──
|
|
38
|
+
var bubble = document.createElement('div');
|
|
39
|
+
bubble.id = 'fluxy-widget-bubble';
|
|
40
|
+
bubble.setAttribute('role', 'button');
|
|
41
|
+
bubble.setAttribute('aria-label', 'Open Fluxy chat');
|
|
42
|
+
|
|
43
|
+
var video = document.createElement('video');
|
|
44
|
+
video.src = '/fluxy_tilts.webm';
|
|
45
|
+
video.poster = '/fluxy_frame1.png';
|
|
46
|
+
video.autoplay = true;
|
|
47
|
+
video.loop = true;
|
|
48
|
+
video.muted = true;
|
|
49
|
+
video.playsInline = true;
|
|
50
|
+
video.setAttribute('playsinline', '');
|
|
51
|
+
video.draggable = false;
|
|
52
|
+
bubble.appendChild(video);
|
|
53
|
+
|
|
54
|
+
// Mark widget present
|
|
55
|
+
bubble.dataset.fluxyWidget = '1';
|
|
56
|
+
document.body.appendChild(bubble);
|
|
57
|
+
|
|
58
|
+
// ── Toggle ──
|
|
59
|
+
var isOpen = false;
|
|
60
|
+
|
|
61
|
+
function toggle() {
|
|
62
|
+
isOpen = !isOpen;
|
|
63
|
+
panel.classList.toggle('open', isOpen);
|
|
64
|
+
backdrop.classList.toggle('open', isOpen);
|
|
65
|
+
bubble.style.display = isOpen ? 'none' : 'flex';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bubble.addEventListener('click', toggle);
|
|
69
|
+
backdrop.addEventListener('click', toggle);
|
|
70
|
+
|
|
71
|
+
// Close on Escape
|
|
72
|
+
document.addEventListener('keydown', function (e) {
|
|
73
|
+
if (e.key === 'Escape' && isOpen) toggle();
|
|
74
|
+
});
|
|
75
|
+
})();
|
package/supervisor/worker.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { PKG_DIR
|
|
3
|
+
import { PKG_DIR } from '../shared/paths.js';
|
|
4
4
|
import { log } from '../shared/logger.js';
|
|
5
5
|
|
|
6
6
|
let child: ChildProcess | null = null;
|
|
@@ -14,20 +14,7 @@ export function getWorkerPort(basePort: number): number {
|
|
|
14
14
|
export function spawnWorker(port: number): ChildProcess {
|
|
15
15
|
const workerPath = path.join(PKG_DIR, 'worker', 'index.ts');
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
// Keeps the parent process alive and internally restarts
|
|
19
|
-
// the worker when imported files change.
|
|
20
|
-
// --watch-path: scope watching to worker/ and shared/ so frontend
|
|
21
|
-
// edits don't cause unnecessary restarts.
|
|
22
|
-
child = spawn(process.execPath, [
|
|
23
|
-
'--watch',
|
|
24
|
-
'--watch-preserve-output',
|
|
25
|
-
`--watch-path=${path.join(PKG_DIR, 'worker')}`,
|
|
26
|
-
`--watch-path=${path.join(PKG_DIR, 'shared')}`,
|
|
27
|
-
`--watch-path=${paths.env}`,
|
|
28
|
-
'--import', 'tsx/esm',
|
|
29
|
-
workerPath,
|
|
30
|
-
], {
|
|
17
|
+
child = spawn(process.execPath, ['--import', 'tsx/esm', workerPath], {
|
|
31
18
|
cwd: PKG_DIR,
|
|
32
19
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
20
|
env: { ...process.env, WORKER_PORT: String(port) },
|
|
@@ -54,7 +41,7 @@ export function spawnWorker(port: number): ChildProcess {
|
|
|
54
41
|
}
|
|
55
42
|
});
|
|
56
43
|
|
|
57
|
-
log.ok(`Worker spawned on port ${port}
|
|
44
|
+
log.ok(`Worker spawned on port ${port}`);
|
|
58
45
|
return child;
|
|
59
46
|
}
|
|
60
47
|
|
package/tsconfig.json
CHANGED
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
"outDir": "dist",
|
|
10
10
|
"rootDir": ".",
|
|
11
11
|
"jsx": "react-jsx",
|
|
12
|
-
"types": [
|
|
12
|
+
"types": [],
|
|
13
13
|
"paths": {
|
|
14
14
|
"@server/*": ["./server/*"],
|
|
15
|
-
"@client/*": ["./client/src/*"]
|
|
16
|
-
"@/*": ["./client/src/*"]
|
|
15
|
+
"@client/*": ["./client/src/*"]
|
|
17
16
|
}
|
|
18
17
|
},
|
|
19
18
|
"include": ["server/**/*", "client/src/**/*", "vite.config.ts"],
|
package/vite.config.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
root: 'client',
|
|
7
|
+
base: '/fluxy/',
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: { '@': path.resolve(__dirname, 'client/src') },
|
|
10
|
+
},
|
|
11
|
+
build: {
|
|
12
|
+
outDir: '../dist-fluxy',
|
|
13
|
+
emptyOutDir: true,
|
|
14
|
+
rollupOptions: {
|
|
15
|
+
input: path.resolve(__dirname, 'client/fluxy.html'),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
plugins: [react()],
|
|
19
|
+
});
|