fluxy-bot 0.1.0
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 +469 -0
- package/client/index.html +13 -0
- package/client/public/fluxy.png +0 -0
- package/client/public/icons/claude.png +0 -0
- package/client/public/icons/codex.png +0 -0
- package/client/public/icons/openai.svg +15 -0
- package/client/src/App.tsx +81 -0
- package/client/src/components/Chat/ChatView.tsx +19 -0
- package/client/src/components/Chat/InputBar.tsx +242 -0
- package/client/src/components/Chat/MessageBubble.tsx +20 -0
- package/client/src/components/Chat/MessageList.tsx +39 -0
- package/client/src/components/Chat/TypingIndicator.tsx +10 -0
- package/client/src/components/Dashboard/ConversationAnalytics.tsx +84 -0
- package/client/src/components/Dashboard/DashboardPage.tsx +52 -0
- package/client/src/components/Dashboard/PromoCard.tsx +44 -0
- package/client/src/components/Dashboard/ReportCard.tsx +35 -0
- package/client/src/components/Dashboard/TodayStats.tsx +28 -0
- package/client/src/components/ErrorBoundary.tsx +23 -0
- package/client/src/components/FluxyFab.tsx +25 -0
- package/client/src/components/Layout/ConnectionStatus.tsx +8 -0
- package/client/src/components/Layout/DashboardHeader.tsx +90 -0
- package/client/src/components/Layout/DashboardLayout.tsx +24 -0
- package/client/src/components/Layout/Header.tsx +10 -0
- package/client/src/components/Layout/MobileNav.tsx +30 -0
- package/client/src/components/Layout/Sidebar.tsx +55 -0
- package/client/src/components/Onboard/OnboardWizard.tsx +763 -0
- package/client/src/components/ui/avatar.tsx +109 -0
- package/client/src/components/ui/badge.tsx +48 -0
- package/client/src/components/ui/button.tsx +64 -0
- package/client/src/components/ui/card.tsx +92 -0
- package/client/src/components/ui/dialog.tsx +156 -0
- package/client/src/components/ui/dropdown-menu.tsx +257 -0
- package/client/src/components/ui/input.tsx +21 -0
- package/client/src/components/ui/scroll-area.tsx +58 -0
- package/client/src/components/ui/select.tsx +190 -0
- package/client/src/components/ui/separator.tsx +28 -0
- package/client/src/components/ui/sheet.tsx +141 -0
- package/client/src/components/ui/skeleton.tsx +13 -0
- package/client/src/components/ui/switch.tsx +33 -0
- package/client/src/components/ui/tabs.tsx +89 -0
- package/client/src/components/ui/textarea.tsx +18 -0
- package/client/src/components/ui/tooltip.tsx +55 -0
- package/client/src/hooks/useChat.ts +69 -0
- package/client/src/hooks/useMobile.ts +16 -0
- package/client/src/hooks/useWebSocket.ts +24 -0
- package/client/src/lib/mock-data.ts +104 -0
- package/client/src/lib/utils.ts +6 -0
- package/client/src/lib/ws-client.ts +52 -0
- package/client/src/main.tsx +10 -0
- package/client/src/styles/globals.css +55 -0
- package/components.json +20 -0
- package/dist/assets/index-BkNWpS06.css +1 -0
- package/dist/assets/index-CX3QeqQ8.js +64 -0
- package/dist/fluxy.png +0 -0
- package/dist/icons/claude.png +0 -0
- package/dist/icons/codex.png +0 -0
- package/dist/icons/openai.svg +15 -0
- package/dist/index.html +14 -0
- package/dist/manifest.webmanifest +1 -0
- package/dist/registerSW.js +1 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-8c29f6e4.js +1 -0
- package/package.json +82 -0
- package/postcss.config.js +5 -0
- package/shared/ai.ts +141 -0
- package/shared/config.ts +37 -0
- package/shared/logger.ts +13 -0
- package/shared/paths.ts +14 -0
- package/shared/relay.ts +101 -0
- package/supervisor/fluxy.html +94 -0
- package/supervisor/index.ts +173 -0
- package/supervisor/tunnel.ts +62 -0
- package/supervisor/worker.ts +55 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +38 -0
- package/worker/claude-auth.ts +224 -0
- package/worker/codex-auth.ts +199 -0
- package/worker/db.ts +75 -0
- package/worker/index.ts +169 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, execSync } from 'child_process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import readline from 'readline';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
12
|
+
const DATA_DIR = path.join(os.homedir(), '.fluxy');
|
|
13
|
+
const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
|
|
14
|
+
const BIN_DIR = path.join(DATA_DIR, 'bin');
|
|
15
|
+
const CF_PATH = path.join(BIN_DIR, 'cloudflared');
|
|
16
|
+
|
|
17
|
+
const RELAY_API = 'https://api.fluxy.bot/api';
|
|
18
|
+
|
|
19
|
+
const [, , command] = process.argv;
|
|
20
|
+
|
|
21
|
+
// ── UI helpers ──
|
|
22
|
+
|
|
23
|
+
const c = {
|
|
24
|
+
reset: '\x1b[0m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
bold: '\x1b[1m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
yellow: '\x1b[33m',
|
|
30
|
+
red: '\x1b[31m',
|
|
31
|
+
white: '\x1b[97m',
|
|
32
|
+
bg: '\x1b[48;5;236m',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
36
|
+
const BAR_WIDTH = 30;
|
|
37
|
+
|
|
38
|
+
function progressBar(ratio, width = BAR_WIDTH) {
|
|
39
|
+
const filled = Math.round(ratio * width);
|
|
40
|
+
const empty = width - filled;
|
|
41
|
+
return `${c.cyan}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class Stepper {
|
|
45
|
+
constructor(steps) {
|
|
46
|
+
this.steps = steps;
|
|
47
|
+
this.current = 0;
|
|
48
|
+
this.frame = 0;
|
|
49
|
+
this.interval = null;
|
|
50
|
+
this.done = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
start() {
|
|
54
|
+
console.log('');
|
|
55
|
+
this.interval = setInterval(() => {
|
|
56
|
+
this.frame = (this.frame + 1) % SPINNER.length;
|
|
57
|
+
this.render();
|
|
58
|
+
}, 80);
|
|
59
|
+
this.render();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render() {
|
|
63
|
+
if (this.done) return;
|
|
64
|
+
|
|
65
|
+
if (this.current > 0 || this.frame > 0) {
|
|
66
|
+
process.stdout.write(`\x1b[${this.steps.length + 2}A`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ratio = this.current / this.steps.length;
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < this.steps.length; i++) {
|
|
72
|
+
if (i < this.current) {
|
|
73
|
+
console.log(` ${c.green}✔${c.reset} ${this.steps[i]}`);
|
|
74
|
+
} else if (i === this.current) {
|
|
75
|
+
console.log(` ${c.cyan}${SPINNER[this.frame]}${c.reset} ${this.steps[i]}${c.dim}...${c.reset}`);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(` ${c.dim}○ ${this.steps[i]}${c.reset}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`\n ${progressBar(ratio)} ${c.dim}${Math.round(ratio * 100)}%${c.reset}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
advance() {
|
|
85
|
+
this.current++;
|
|
86
|
+
this.render();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
finish() {
|
|
90
|
+
this.done = true;
|
|
91
|
+
if (this.interval) clearInterval(this.interval);
|
|
92
|
+
|
|
93
|
+
process.stdout.write(`\x1b[${this.steps.length + 2}A`);
|
|
94
|
+
for (const step of this.steps) {
|
|
95
|
+
console.log(` ${c.green}✔${c.reset} ${step}`);
|
|
96
|
+
}
|
|
97
|
+
console.log(`\n ${progressBar(1)} ${c.green}Done${c.reset}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function banner() {
|
|
102
|
+
console.log(`
|
|
103
|
+
${c.cyan}${c.bold}╔═══════════════════════════════╗
|
|
104
|
+
║ FLUXY v0.1.0 ║
|
|
105
|
+
╚═══════════════════════════════╝${c.reset}
|
|
106
|
+
${c.dim}Self-hosted AI bot${c.reset}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function finalMessage(tunnelUrl, relayUrl) {
|
|
110
|
+
console.log(`
|
|
111
|
+
${c.dim}─────────────────────────────────${c.reset}
|
|
112
|
+
|
|
113
|
+
${c.bold}${c.white}Continue your setup at:${c.reset}
|
|
114
|
+
|
|
115
|
+
${c.cyan}${c.bold}${tunnelUrl}${c.reset}`);
|
|
116
|
+
|
|
117
|
+
if (relayUrl) {
|
|
118
|
+
console.log(`
|
|
119
|
+
${c.bold}${c.white}Your permanent URL:${c.reset}
|
|
120
|
+
|
|
121
|
+
${c.cyan}${c.bold}${relayUrl}${c.reset}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`
|
|
125
|
+
${c.dim}─────────────────────────────────${c.reset}
|
|
126
|
+
${c.dim}Press Ctrl+C to stop the bot${c.reset}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Terminal input ──
|
|
131
|
+
|
|
132
|
+
function ask(question) {
|
|
133
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
rl.question(question, (answer) => {
|
|
136
|
+
rl.close();
|
|
137
|
+
resolve(answer.trim());
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Steps ──
|
|
143
|
+
|
|
144
|
+
function createConfig() {
|
|
145
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
146
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
147
|
+
const config = {
|
|
148
|
+
port: 3000,
|
|
149
|
+
username: '',
|
|
150
|
+
ai: { provider: '', model: '', apiKey: '' },
|
|
151
|
+
tunnel: { enabled: true },
|
|
152
|
+
relay: { token: '', tier: '', url: '' },
|
|
153
|
+
};
|
|
154
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hasCloudflared() {
|
|
159
|
+
try {
|
|
160
|
+
execSync('which cloudflared', { stdio: 'ignore' });
|
|
161
|
+
return true;
|
|
162
|
+
} catch {}
|
|
163
|
+
return fs.existsSync(CF_PATH);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function installCloudflared() {
|
|
167
|
+
if (hasCloudflared()) return;
|
|
168
|
+
|
|
169
|
+
fs.mkdirSync(BIN_DIR, { recursive: true });
|
|
170
|
+
|
|
171
|
+
const platform = os.platform();
|
|
172
|
+
const arch = os.arch();
|
|
173
|
+
let url;
|
|
174
|
+
|
|
175
|
+
if (platform === 'darwin') {
|
|
176
|
+
url = arch === 'arm64'
|
|
177
|
+
? 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz'
|
|
178
|
+
: 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz';
|
|
179
|
+
} else if (platform === 'linux') {
|
|
180
|
+
if (arch === 'arm64' || arch === 'aarch64') {
|
|
181
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64';
|
|
182
|
+
} else if (arch === 'arm' || arch === 'armv7l') {
|
|
183
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm';
|
|
184
|
+
} else {
|
|
185
|
+
url = 'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64';
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
throw new Error(`Unsupported platform: ${platform}/${arch}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (url.endsWith('.tgz')) {
|
|
192
|
+
execSync(`curl -fsSL "${url}" | tar xz -C "${BIN_DIR}"`, { stdio: 'ignore' });
|
|
193
|
+
} else {
|
|
194
|
+
execSync(`curl -fsSL -o "${CF_PATH}" "${url}"`, { stdio: 'ignore' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fs.chmodSync(CF_PATH, 0o755);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Relay registration ──
|
|
201
|
+
|
|
202
|
+
async function registerWithRelay() {
|
|
203
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
204
|
+
|
|
205
|
+
// Skip if already registered
|
|
206
|
+
if (config.relay?.token) return;
|
|
207
|
+
|
|
208
|
+
console.log(`
|
|
209
|
+
${c.bold}${c.white}Choose your bot handle${c.reset}
|
|
210
|
+
${c.dim}This is your permanent URL to access your bot from anywhere.${c.reset}
|
|
211
|
+
`);
|
|
212
|
+
|
|
213
|
+
let username = '';
|
|
214
|
+
let registered = false;
|
|
215
|
+
|
|
216
|
+
while (!registered) {
|
|
217
|
+
username = await ask(` ${c.cyan}Handle:${c.reset} `);
|
|
218
|
+
|
|
219
|
+
if (!username) {
|
|
220
|
+
console.log(` ${c.dim}Skipping relay registration. You can set this up later.${c.reset}\n`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
username = username.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
225
|
+
|
|
226
|
+
if (username.length < 3) {
|
|
227
|
+
console.log(` ${c.yellow}Must be at least 3 characters.${c.reset}\n`);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check availability
|
|
232
|
+
try {
|
|
233
|
+
const checkRes = await fetch(`${RELAY_API}/availability/${encodeURIComponent(username)}`);
|
|
234
|
+
const check = await checkRes.json();
|
|
235
|
+
|
|
236
|
+
if (!check.valid) {
|
|
237
|
+
console.log(` ${c.yellow}${check.error}${c.reset}\n`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!check.available) {
|
|
242
|
+
console.log(` ${c.yellow}That handle is taken. Try another.${c.reset}\n`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
console.log(` ${c.yellow}Could not reach relay server. Skipping registration.${c.reset}\n`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Show options
|
|
251
|
+
console.log(`
|
|
252
|
+
${c.bold}${c.white}Available handles:${c.reset}
|
|
253
|
+
${c.dim}1)${c.reset} ${c.cyan}${username}.fluxy.bot${c.reset} ${c.dim}(Premium — $5)${c.reset}
|
|
254
|
+
${c.dim}2)${c.reset} ${c.cyan}my.fluxy.bot/${username}${c.reset} ${c.green}Free${c.reset}
|
|
255
|
+
${c.dim}3)${c.reset} ${c.cyan}at.fluxy.bot/${username}${c.reset} ${c.green}Free${c.reset}
|
|
256
|
+
${c.dim}4)${c.reset} ${c.cyan}on.fluxy.bot/${username}${c.reset} ${c.green}Free${c.reset}
|
|
257
|
+
`);
|
|
258
|
+
|
|
259
|
+
const choice = await ask(` ${c.cyan}Pick (1-4):${c.reset} `);
|
|
260
|
+
const tiers = ['premium', 'my', 'at', 'on'];
|
|
261
|
+
const tierIndex = parseInt(choice, 10) - 1;
|
|
262
|
+
const tier = tiers[tierIndex] ?? 'my'; // default to free
|
|
263
|
+
|
|
264
|
+
if (tier === 'premium') {
|
|
265
|
+
console.log(` ${c.yellow}Premium handles require payment (coming soon). Using free tier.${c.reset}\n`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const finalTier = tier === 'premium' ? 'my' : tier;
|
|
269
|
+
|
|
270
|
+
// Register
|
|
271
|
+
try {
|
|
272
|
+
const regRes = await fetch(`${RELAY_API}/register`, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify({ username, tier: finalTier }),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const reg = await regRes.json();
|
|
279
|
+
|
|
280
|
+
if (!regRes.ok) {
|
|
281
|
+
console.log(` ${c.yellow}${reg.error}${c.reset}\n`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Save to config
|
|
286
|
+
config.username = username;
|
|
287
|
+
config.relay = {
|
|
288
|
+
token: reg.token,
|
|
289
|
+
tier: finalTier,
|
|
290
|
+
url: reg.relayUrl,
|
|
291
|
+
};
|
|
292
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
293
|
+
|
|
294
|
+
console.log(` ${c.green}✔${c.reset} Registered! Your URL: ${c.cyan}${c.bold}${reg.relayUrl}${c.reset}\n`);
|
|
295
|
+
registered = true;
|
|
296
|
+
} catch {
|
|
297
|
+
console.log(` ${c.yellow}Registration failed. Skipping — you can try again later.${c.reset}\n`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Boot server ──
|
|
304
|
+
|
|
305
|
+
function bootServer() {
|
|
306
|
+
return new Promise((resolve) => {
|
|
307
|
+
const child = spawn(
|
|
308
|
+
'node',
|
|
309
|
+
['--import', 'tsx/esm', path.join(ROOT, 'supervisor/index.ts')],
|
|
310
|
+
{ cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } },
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
let tunnelUrl = null;
|
|
314
|
+
let relayUrl = null;
|
|
315
|
+
|
|
316
|
+
const fallbackLocal = () => {
|
|
317
|
+
if (!tunnelUrl) {
|
|
318
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
319
|
+
resolve({ child, tunnelUrl: `http://localhost:${config.port}`, relayUrl: config.relay?.url || null });
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const handleData = (data) => {
|
|
324
|
+
const text = data.toString();
|
|
325
|
+
|
|
326
|
+
const tunnelMatch = text.match(/__TUNNEL_URL__=(\S+)/);
|
|
327
|
+
if (tunnelMatch) {
|
|
328
|
+
tunnelUrl = tunnelMatch[1];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const relayMatch = text.match(/__RELAY_URL__=(\S+)/);
|
|
332
|
+
if (relayMatch) {
|
|
333
|
+
relayUrl = relayMatch[1];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Resolve once we have both tunnel URL and relay (or relay fails)
|
|
337
|
+
if (tunnelUrl && !relayUrl) {
|
|
338
|
+
// Give relay a moment to register, then resolve anyway
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
if (!relayUrl) {
|
|
341
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
342
|
+
resolve({ child, tunnelUrl, relayUrl: config.relay?.url || null });
|
|
343
|
+
}
|
|
344
|
+
}, 3000);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (tunnelUrl && relayUrl) {
|
|
348
|
+
resolve({ child, tunnelUrl, relayUrl });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (text.includes('__TUNNEL_FAILED__')) {
|
|
353
|
+
fallbackLocal();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
child.stdout.on('data', handleData);
|
|
359
|
+
child.stderr.on('data', handleData);
|
|
360
|
+
|
|
361
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
362
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
363
|
+
|
|
364
|
+
child.on('exit', (code) => process.exit(code ?? 1));
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Main flows ──
|
|
369
|
+
|
|
370
|
+
async function init() {
|
|
371
|
+
banner();
|
|
372
|
+
|
|
373
|
+
const steps = [
|
|
374
|
+
'Creating config',
|
|
375
|
+
'Registering handle',
|
|
376
|
+
'Installing cloudflared',
|
|
377
|
+
'Starting server',
|
|
378
|
+
'Connecting tunnel',
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
// Step 1: Config
|
|
382
|
+
createConfig();
|
|
383
|
+
|
|
384
|
+
// Step 2: Relay registration (interactive — before stepper)
|
|
385
|
+
await registerWithRelay();
|
|
386
|
+
|
|
387
|
+
// Now the non-interactive steps
|
|
388
|
+
const buildSteps = [
|
|
389
|
+
'Installing cloudflared',
|
|
390
|
+
'Starting server',
|
|
391
|
+
'Connecting tunnel',
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
const stepper = new Stepper(buildSteps);
|
|
395
|
+
stepper.start();
|
|
396
|
+
|
|
397
|
+
// Step 3: Cloudflared
|
|
398
|
+
await installCloudflared();
|
|
399
|
+
stepper.advance();
|
|
400
|
+
|
|
401
|
+
// Step 4+5: Server + Tunnel
|
|
402
|
+
stepper.advance();
|
|
403
|
+
const { child, tunnelUrl, relayUrl } = await bootServer();
|
|
404
|
+
stepper.advance();
|
|
405
|
+
|
|
406
|
+
stepper.finish();
|
|
407
|
+
finalMessage(tunnelUrl, relayUrl);
|
|
408
|
+
|
|
409
|
+
child.stdout.on('data', (d) => {
|
|
410
|
+
process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
|
|
411
|
+
});
|
|
412
|
+
child.stderr.on('data', (d) => {
|
|
413
|
+
process.stderr.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function start() {
|
|
418
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
419
|
+
return init();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
banner();
|
|
423
|
+
|
|
424
|
+
const steps = ['Loading config', 'Starting server', 'Connecting tunnel'];
|
|
425
|
+
const stepper = new Stepper(steps);
|
|
426
|
+
stepper.start();
|
|
427
|
+
|
|
428
|
+
stepper.advance(); // config exists
|
|
429
|
+
stepper.advance(); // starting
|
|
430
|
+
|
|
431
|
+
const { child, tunnelUrl, relayUrl } = await bootServer();
|
|
432
|
+
stepper.advance();
|
|
433
|
+
|
|
434
|
+
stepper.finish();
|
|
435
|
+
finalMessage(tunnelUrl, relayUrl);
|
|
436
|
+
|
|
437
|
+
child.stdout.on('data', (d) => {
|
|
438
|
+
process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
|
|
439
|
+
});
|
|
440
|
+
child.stderr.on('data', (d) => {
|
|
441
|
+
process.stderr.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function status() {
|
|
446
|
+
try {
|
|
447
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
448
|
+
const res = await fetch(`http://localhost:${config.port}/api/health`);
|
|
449
|
+
const data = await res.json();
|
|
450
|
+
console.log(`\n ${c.green}●${c.reset} Bot is running`);
|
|
451
|
+
console.log(` ${c.dim}Uptime: ${data.uptime}s${c.reset}`);
|
|
452
|
+
if (config.relay?.url) {
|
|
453
|
+
console.log(` ${c.dim}URL: ${config.relay.url}${c.reset}`);
|
|
454
|
+
}
|
|
455
|
+
console.log(` ${c.dim}Config: ${CONFIG_PATH}${c.reset}\n`);
|
|
456
|
+
} catch {
|
|
457
|
+
console.log(`\n ${c.dim}●${c.reset} Bot is not running.\n`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Route ──
|
|
462
|
+
|
|
463
|
+
switch (command) {
|
|
464
|
+
case 'init': init(); break;
|
|
465
|
+
case 'start': start(); break;
|
|
466
|
+
case 'status': status(); break;
|
|
467
|
+
default:
|
|
468
|
+
fs.existsSync(CONFIG_PATH) ? start() : init();
|
|
469
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="theme-color" content="#212121" />
|
|
7
|
+
<title>Fluxy</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-background text-foreground">
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg width="721" height="721" viewBox="0 0 721 721" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0_1637_2935)">
|
|
3
|
+
<g clip-path="url(#clip1_1637_2935)">
|
|
4
|
+
<path d="M304.246 295.411V249.828C304.246 245.989 305.687 243.109 309.044 241.191L400.692 188.412C413.167 181.215 428.042 177.858 443.394 177.858C500.971 177.858 537.44 222.482 537.44 269.982C537.44 273.34 537.44 277.179 536.959 281.018L441.954 225.358C436.197 222 430.437 222 424.68 225.358L304.246 295.411ZM518.245 472.945V364.024C518.245 357.304 515.364 352.507 509.608 349.149L389.174 279.096L428.519 256.543C431.877 254.626 434.757 254.626 438.115 256.543L529.762 309.323C556.154 324.679 573.905 357.304 573.905 388.971C573.905 425.436 552.315 459.024 518.245 472.941V472.945ZM275.937 376.982L236.592 353.952C233.235 352.034 231.794 349.154 231.794 345.315V239.756C231.794 188.416 271.139 149.548 324.4 149.548C344.555 149.548 363.264 156.268 379.102 168.262L284.578 222.964C278.822 226.321 275.942 231.119 275.942 237.838V376.986L275.937 376.982ZM360.626 425.922L304.246 394.255V327.083L360.626 295.416L417.002 327.083V394.255L360.626 425.922ZM396.852 571.789C376.698 571.789 357.989 565.07 342.151 553.075L436.674 498.374C442.431 495.017 445.311 490.219 445.311 483.499V344.352L485.138 367.382C488.495 369.299 489.936 372.179 489.936 376.018V481.577C489.936 532.917 450.109 571.785 396.852 571.785V571.789ZM283.134 464.79L191.486 412.01C165.094 396.654 147.343 364.029 147.343 332.362C147.343 295.416 169.415 262.309 203.48 248.393V357.791C203.48 364.51 206.361 369.308 212.117 372.665L332.074 442.237L292.729 464.79C289.372 466.707 286.491 466.707 283.134 464.79ZM277.859 543.48C223.639 543.48 183.813 502.695 183.813 452.314C183.813 448.475 184.294 444.636 184.771 440.797L279.295 495.498C285.051 498.856 290.812 498.856 296.568 495.498L417.002 425.927V471.509C417.002 475.349 415.562 478.229 412.204 480.146L320.557 532.926C308.081 540.122 293.206 543.48 277.854 543.48H277.859ZM396.852 600.576C454.911 600.576 503.37 559.313 514.41 504.612C568.149 490.696 602.696 440.315 602.696 388.976C602.696 355.387 588.303 322.762 562.392 299.25C564.791 289.173 566.231 279.096 566.231 269.024C566.231 200.411 510.571 149.067 446.274 149.067C433.322 149.067 420.846 150.984 408.37 155.305C386.775 134.192 357.026 120.758 324.4 120.758C266.342 120.758 217.883 162.02 206.843 216.721C153.104 230.637 118.557 281.018 118.557 332.357C118.557 365.946 132.95 398.571 158.861 422.083C156.462 432.16 155.022 442.237 155.022 452.309C155.022 520.922 210.682 572.266 274.978 572.266C287.931 572.266 300.407 570.349 312.883 566.028C334.473 587.141 364.222 600.576 396.852 600.576Z" fill="white"/>
|
|
5
|
+
</g>
|
|
6
|
+
</g>
|
|
7
|
+
<defs>
|
|
8
|
+
<clipPath id="clip0_1637_2935">
|
|
9
|
+
<rect width="720" height="720" fill="white" transform="translate(0.606934 0.899902)"/>
|
|
10
|
+
</clipPath>
|
|
11
|
+
<clipPath id="clip1_1637_2935">
|
|
12
|
+
<rect width="484.139" height="479.818" fill="white" transform="translate(118.557 120.758)"/>
|
|
13
|
+
</clipPath>
|
|
14
|
+
</defs>
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useWebSocket } from './hooks/useWebSocket';
|
|
3
|
+
import ErrorBoundary from './components/ErrorBoundary';
|
|
4
|
+
import DashboardLayout from './components/Layout/DashboardLayout';
|
|
5
|
+
import DashboardPage from './components/Dashboard/DashboardPage';
|
|
6
|
+
import ChatView from './components/Chat/ChatView';
|
|
7
|
+
import FluxyFab from './components/FluxyFab';
|
|
8
|
+
import OnboardWizard from './components/Onboard/OnboardWizard';
|
|
9
|
+
import {
|
|
10
|
+
Sheet,
|
|
11
|
+
SheetContent,
|
|
12
|
+
SheetTitle,
|
|
13
|
+
SheetDescription,
|
|
14
|
+
} from './components/ui/sheet';
|
|
15
|
+
|
|
16
|
+
function DashboardError() {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex items-center justify-center h-dvh p-6 text-center">
|
|
19
|
+
<div>
|
|
20
|
+
<h1 className="text-xl font-semibold mb-2">Something went wrong</h1>
|
|
21
|
+
<p className="text-sm text-muted-foreground">
|
|
22
|
+
The dashboard encountered an error. Use the Fluxy button to continue chatting.
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function App() {
|
|
30
|
+
const [chatOpen, setChatOpen] = useState(false);
|
|
31
|
+
const [showOnboard, setShowOnboard] = useState(false);
|
|
32
|
+
const { ws, connected } = useWebSocket();
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
fetch('/api/settings')
|
|
36
|
+
.then((r) => r.json())
|
|
37
|
+
.then((s) => { if (s.onboard_complete !== 'true') setShowOnboard(true); })
|
|
38
|
+
.catch(() => setShowOnboard(true));
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<ErrorBoundary fallback={<DashboardError />}>
|
|
44
|
+
<DashboardLayout onOpenOnboard={() => setShowOnboard(true)}>
|
|
45
|
+
<DashboardPage />
|
|
46
|
+
</DashboardLayout>
|
|
47
|
+
</ErrorBoundary>
|
|
48
|
+
|
|
49
|
+
{/* Chat sheet — slides from right */}
|
|
50
|
+
<Sheet open={chatOpen} onOpenChange={setChatOpen}>
|
|
51
|
+
<SheetContent
|
|
52
|
+
side="right"
|
|
53
|
+
className="w-full sm:max-w-md !gap-0 !p-0 !border-l-0"
|
|
54
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
55
|
+
>
|
|
56
|
+
{/* Header — pr-12 to avoid overlap with built-in X button */}
|
|
57
|
+
<div className="flex items-center gap-3 px-4 pr-12 py-3 border-b border-border shrink-0">
|
|
58
|
+
<img src="/fluxy.png" alt="Fluxy" className="h-5 w-auto" />
|
|
59
|
+
<SheetTitle className="text-sm font-semibold">Fluxy</SheetTitle>
|
|
60
|
+
<div
|
|
61
|
+
className={`h-2 w-2 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}
|
|
62
|
+
/>
|
|
63
|
+
<SheetDescription className="sr-only">
|
|
64
|
+
Chat with your Fluxy AI assistant
|
|
65
|
+
</SheetDescription>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Chat body — fills remaining height */}
|
|
69
|
+
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
70
|
+
<ChatView ws={ws} />
|
|
71
|
+
</div>
|
|
72
|
+
</SheetContent>
|
|
73
|
+
</Sheet>
|
|
74
|
+
|
|
75
|
+
<FluxyFab onClick={() => setChatOpen((o) => !o)} />
|
|
76
|
+
|
|
77
|
+
{/* Onboarding wizard overlay */}
|
|
78
|
+
{showOnboard && <OnboardWizard onComplete={() => setShowOnboard(false)} />}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WsClient } from '../../lib/ws-client';
|
|
2
|
+
import { useChat } from '../../hooks/useChat';
|
|
3
|
+
import MessageList from './MessageList';
|
|
4
|
+
import InputBar from './InputBar';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
ws: WsClient | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function ChatView({ ws }: Props) {
|
|
11
|
+
const { messages, streaming, streamBuffer, sendMessage, stopStreaming } = useChat(ws);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex flex-col h-full overflow-hidden">
|
|
15
|
+
<MessageList messages={messages} streaming={streaming} streamBuffer={streamBuffer} />
|
|
16
|
+
<InputBar onSend={sendMessage} onStop={stopStreaming} streaming={streaming} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|