fluxy-bot 0.5.61 → 0.5.63
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/README.md +76 -23
- package/bin/cli.js +52 -9
- package/package.json +1 -1
- package/supervisor/chat/OnboardWizard.tsx +312 -171
- package/worker/index.ts +3 -0
- package/workspace/.backend.log +1 -0
package/README.md
CHANGED
|
@@ -9,10 +9,12 @@ A self-hosted AI agent that runs on the user's machine with a full-stack workspa
|
|
|
9
9
|
Fluxy is three separate codebases working together:
|
|
10
10
|
|
|
11
11
|
1. **Fluxy Bot** (this repo) -- runs on the user's machine. Supervisor process, worker API, workspace app, chat UI.
|
|
12
|
-
2. **Fluxy Relay** (separate server at `api.fluxy.bot`) -- cloud service that maps `username.fluxy.bot` to the user's Cloudflare tunnel. Routes HTTP and WebSocket traffic.
|
|
13
|
-
3. **Cloudflare
|
|
12
|
+
2. **Fluxy Relay** (separate server at `api.fluxy.bot`) -- optional cloud service that maps `username.fluxy.bot` to the user's Cloudflare tunnel. Routes HTTP and WebSocket traffic. Only used with Quick Tunnels.
|
|
13
|
+
3. **Cloudflare Tunnel** -- exposes `localhost:3000` to the internet. Two modes:
|
|
14
|
+
- **Quick Tunnel** -- zero-config, no account needed, random `*.trycloudflare.com` URL that changes on restart. Optionally paired with the relay for a permanent domain.
|
|
15
|
+
- **Named Tunnel** -- persistent URL with the user's own domain. Requires a Cloudflare account and DNS setup. No relay needed.
|
|
14
16
|
|
|
15
|
-
The
|
|
17
|
+
The user chooses their tunnel mode during `fluxy init` via an interactive selector.
|
|
16
18
|
|
|
17
19
|
---
|
|
18
20
|
|
|
@@ -30,7 +32,7 @@ Supervisor (supervisor/index.ts) port 3000 HTTP server + WebSocket + r
|
|
|
30
32
|
+-- Worker (worker/index.ts) port 3001 Express API, SQLite, auth, conversations
|
|
31
33
|
+-- Vite Dev Server port 3002 Serves workspace/client with HMR
|
|
32
34
|
+-- Backend (workspace/backend/) port 3004 User's custom Express server
|
|
33
|
-
+-- cloudflared (tunnel) -- Exposes port 3000 to the internet
|
|
35
|
+
+-- cloudflared (tunnel) -- Exposes port 3000 to the internet (quick or named)
|
|
34
36
|
+-- Scheduler (supervisor/scheduler) -- PULSE + CRON job runner (in-process)
|
|
35
37
|
```
|
|
36
38
|
|
|
@@ -58,8 +60,8 @@ WebSocket upgrades:
|
|
|
58
60
|
- Everything else -- proxied to Vite dev server for HMR.
|
|
59
61
|
|
|
60
62
|
The supervisor also:
|
|
61
|
-
- Manages the Cloudflare tunnel lifecycle (start, stop, health watchdog every 30s)
|
|
62
|
-
- Registers with the relay and maintains heartbeats
|
|
63
|
+
- Manages the Cloudflare tunnel lifecycle (start, stop, health watchdog every 30s) for both quick and named tunnels
|
|
64
|
+
- Registers with the relay and maintains heartbeats (quick tunnel mode only)
|
|
63
65
|
- Runs the Claude Agent SDK when users send chat messages
|
|
64
66
|
- Restarts the backend process when Claude edits workspace files
|
|
65
67
|
- Broadcasts `app:hmr-update` to all connected dashboard clients after file changes
|
|
@@ -299,35 +301,68 @@ For non-Anthropic providers (OpenAI, Ollama), the supervisor falls back to `ai.c
|
|
|
299
301
|
|
|
300
302
|
The user's machine is behind NAT. We need a public URL so they can access their bot from their phone.
|
|
301
303
|
|
|
302
|
-
###
|
|
304
|
+
### Tunnel Modes
|
|
305
|
+
|
|
306
|
+
The user selects a tunnel mode during `fluxy init`. The mode is stored in `~/.fluxy/config.json` as `tunnel.mode`.
|
|
307
|
+
|
|
308
|
+
#### Quick Tunnel (default)
|
|
309
|
+
|
|
310
|
+
Zero configuration. No Cloudflare account needed.
|
|
303
311
|
|
|
304
312
|
```
|
|
305
313
|
Phone browser
|
|
306
314
|
|
|
|
307
|
-
| https://bruno.fluxy.bot
|
|
315
|
+
| https://bruno.fluxy.bot (via relay, optional)
|
|
308
316
|
v
|
|
309
|
-
Fluxy Relay (api.fluxy.bot)
|
|
317
|
+
Fluxy Relay (api.fluxy.bot) Cloud server, maps username -> tunnel URL
|
|
310
318
|
|
|
|
311
319
|
| https://random-abc.trycloudflare.com
|
|
312
320
|
v
|
|
313
|
-
Cloudflare Quick Tunnel
|
|
321
|
+
Cloudflare Quick Tunnel Ephemeral tunnel, changes on restart
|
|
314
322
|
|
|
|
315
323
|
| http://localhost:3000
|
|
316
324
|
v
|
|
317
|
-
Supervisor
|
|
325
|
+
Supervisor User's machine
|
|
318
326
|
```
|
|
319
327
|
|
|
320
|
-
### Cloudflare Tunnel (supervisor/tunnel.ts)
|
|
321
|
-
|
|
322
|
-
- Auto-downloads `cloudflared` binary to `~/.fluxy/bin/` on first run (validates minimum 10MB file size)
|
|
323
328
|
- Spawns: `cloudflared tunnel --url http://localhost:3000 --no-autoupdate`
|
|
324
329
|
- Extracts tunnel URL from stdout (regex match for `*.trycloudflare.com`)
|
|
330
|
+
- URL changes on every restart -- the relay provides the stable domain layer on top
|
|
331
|
+
- Optionally register with Fluxy Relay for a permanent `my.fluxy.bot/username` or `fluxy.bot/username` URL
|
|
332
|
+
|
|
333
|
+
#### Named Tunnel
|
|
334
|
+
|
|
335
|
+
Persistent URL with the user's own domain. Requires a Cloudflare account + domain.
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
Phone browser
|
|
339
|
+
|
|
|
340
|
+
| https://bot.mydomain.com
|
|
341
|
+
v
|
|
342
|
+
Cloudflare Named Tunnel Persistent tunnel, URL never changes
|
|
343
|
+
|
|
|
344
|
+
| http://localhost:3000
|
|
345
|
+
v
|
|
346
|
+
Supervisor User's machine
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
- Setup via `fluxy tunnel setup` (interactive: login, create tunnel, generate config, print CNAME instructions)
|
|
350
|
+
- Spawns: `cloudflared tunnel --config <configPath> run <name>`
|
|
351
|
+
- URL is the user's domain (from config), no stdout parsing needed
|
|
352
|
+
- No relay needed -- the user's domain is already permanent
|
|
353
|
+
- Requires a DNS CNAME record pointing to `<uuid>.cfargotunnel.com`
|
|
354
|
+
|
|
355
|
+
### Cloudflare Tunnel Binary (supervisor/tunnel.ts)
|
|
356
|
+
|
|
357
|
+
- Auto-downloads `cloudflared` binary to `~/.fluxy/bin/` on first run (validates minimum 10MB file size)
|
|
325
358
|
- Health check: HEAD request to tunnel URL with 5s timeout
|
|
326
359
|
- Watchdog runs every 30s, detects sleep/wake gaps (>60s between ticks), auto-restarts dead tunnels
|
|
360
|
+
- Quick tunnel restart: re-extracts new URL from stdout, updates relay if configured
|
|
361
|
+
- Named tunnel restart: restarts the process, URL doesn't change
|
|
327
362
|
|
|
328
|
-
### Relay Server (separate codebase)
|
|
363
|
+
### Relay Server (separate codebase, Quick Tunnel only)
|
|
329
364
|
|
|
330
|
-
Node.js/Express + http-proxy + MongoDB. Hosted on Railway.
|
|
365
|
+
Node.js/Express + http-proxy + MongoDB. Hosted on Railway. Only used when the user opts into Quick Tunnel mode and registers a handle.
|
|
331
366
|
|
|
332
367
|
**Registration flow:**
|
|
333
368
|
1. User picks a username during onboarding
|
|
@@ -392,13 +427,30 @@ The CLI is the user-facing entry point. Commands:
|
|
|
392
427
|
|
|
393
428
|
| Command | Description |
|
|
394
429
|
|---|---|
|
|
395
|
-
| `fluxy init` | First-time setup: creates config, installs cloudflared, boots server, optionally installs systemd daemon |
|
|
430
|
+
| `fluxy init` | First-time setup: interactive tunnel mode chooser (Quick or Named), creates config, installs cloudflared, boots server, optionally installs systemd daemon |
|
|
396
431
|
| `fluxy start` | Boot the supervisor (or detect existing daemon and show status) |
|
|
397
|
-
| `fluxy status` | Health check via `/api/health`, shows uptime and relay URL |
|
|
432
|
+
| `fluxy status` | Health check via `/api/health`, shows uptime, tunnel URL, and relay URL |
|
|
398
433
|
| `fluxy update` | Downloads latest from npm registry, updates code directories, rebuilds UI, restarts daemon |
|
|
434
|
+
| `fluxy tunnel` | Named tunnel management (subcommands below) |
|
|
399
435
|
| `fluxy daemon` | Linux systemd management: install, start, stop, restart, status, logs, uninstall |
|
|
400
436
|
|
|
401
|
-
|
|
437
|
+
**`fluxy tunnel` subcommands:**
|
|
438
|
+
| Subcommand | Description |
|
|
439
|
+
|---|---|
|
|
440
|
+
| `fluxy tunnel setup` | Interactive named tunnel setup: login to Cloudflare, create tunnel, enter domain, generate config YAML, print CNAME instructions |
|
|
441
|
+
| `fluxy tunnel status` | Show current tunnel mode and configuration |
|
|
442
|
+
| `fluxy tunnel reset` | Switch back to quick tunnel mode |
|
|
443
|
+
|
|
444
|
+
**`fluxy init` tunnel chooser:**
|
|
445
|
+
|
|
446
|
+
During init, the user is presented with an interactive arrow-key menu to choose their tunnel mode:
|
|
447
|
+
|
|
448
|
+
- **Quick Tunnel** (Easy and Fast) -- Random CloudFlare tunnel URL on every start/update. Optionally use Fluxy Relay for a permanent `my.fluxy.bot/username` handle (free) or a premium `fluxy.bot/username` handle ($5 one-time).
|
|
449
|
+
- **Named Tunnel** (Advanced) -- Persistent URL with your own domain. Requires a Cloudflare account + domain. Use a subdomain like `bot.yourdomain.com` or the root domain.
|
|
450
|
+
|
|
451
|
+
If Named Tunnel is selected, `fluxy init` immediately runs the named tunnel setup flow inline (same as `fluxy tunnel setup`).
|
|
452
|
+
|
|
453
|
+
The CLI spawns the supervisor via `node --import tsx/esm supervisor/index.ts` and waits for readiness markers on stdout (`__TUNNEL_URL__`, `__RELAY_URL__`, `__VITE_WARM__`, `__READY__`, `__TUNNEL_FAILED__`) with a 45-second timeout.
|
|
402
454
|
|
|
403
455
|
On Linux, `fluxy daemon` generates a systemd unit file that runs the supervisor as a service with auto-restart on failure.
|
|
404
456
|
|
|
@@ -428,7 +480,8 @@ Windows: `scripts/install.ps1` (PowerShell equivalent).
|
|
|
428
480
|
|
|
429
481
|
| Path | Contents |
|
|
430
482
|
|---|---|
|
|
431
|
-
| `~/.fluxy/config.json` | Port, username, AI provider, relay token, tunnel URL |
|
|
483
|
+
| `~/.fluxy/config.json` | Port, username, AI provider, tunnel mode/config, relay token, tunnel URL |
|
|
484
|
+
| `~/.fluxy/cloudflared-config.yml` | Named tunnel config (generated by `fluxy tunnel setup`) |
|
|
432
485
|
| `~/.fluxy/memory.db` | SQLite -- conversations, messages, settings, sessions, push subscriptions |
|
|
433
486
|
| `~/.fluxy/bin/cloudflared` | Cloudflare tunnel binary |
|
|
434
487
|
| `~/.fluxy/workspace/` | User's workspace copy (client, backend, memory files, skills, config) |
|
|
@@ -448,7 +501,7 @@ supervisor/
|
|
|
448
501
|
index.ts HTTP server, request routing, WebSocket handler, process orchestration
|
|
449
502
|
worker.ts Worker process spawn/stop/restart
|
|
450
503
|
backend.ts Backend process spawn/stop/restart
|
|
451
|
-
tunnel.ts Cloudflare tunnel lifecycle, health watchdog
|
|
504
|
+
tunnel.ts Cloudflare tunnel lifecycle (quick + named), health watchdog
|
|
452
505
|
vite-dev.ts Vite dev server startup for dashboard HMR
|
|
453
506
|
fluxy-agent.ts Claude Agent SDK wrapper, session management, memory injection
|
|
454
507
|
scheduler.ts PULSE + CRON scheduler, 60s tick, push notification dispatch
|
|
@@ -531,8 +584,8 @@ The relay couldn't reliably forward POST bodies (now fixed). WebSocket was the w
|
|
|
531
584
|
**Why bypassPermissions on the agent?**
|
|
532
585
|
The whole point is that the user talks to Claude from their phone and Claude does whatever's needed. Confirmation prompts would require a terminal session that doesn't exist. The workspace directory boundary + the system prompt are the safety rails.
|
|
533
586
|
|
|
534
|
-
**Why
|
|
535
|
-
|
|
587
|
+
**Why two tunnel modes?**
|
|
588
|
+
Quick Tunnel is the default for simplicity -- zero configuration, no Cloudflare account needed. The tradeoff is the URL changes on restart, which is why the relay exists as an optional stable domain layer. Named Tunnel is for advanced users who want full control -- their own domain, no dependency on the relay, permanent URLs. Both modes are offered during `fluxy init`.
|
|
536
589
|
|
|
537
590
|
**Why two Vite configs?**
|
|
538
591
|
`vite.config.ts` builds the workspace dashboard (user-facing app). `vite.fluxy.config.ts` builds the Fluxy chat SPA. They're separate apps with separate entry points, bundled independently. The chat is pre-built at publish time; the dashboard runs as a dev server with HMR.
|
package/bin/cli.js
CHANGED
|
@@ -140,11 +140,12 @@ function chooseTunnelMode() {
|
|
|
140
140
|
{
|
|
141
141
|
label: 'Quick Tunnel',
|
|
142
142
|
mode: 'quick',
|
|
143
|
-
tag: '
|
|
143
|
+
tag: 'Easy and Fast',
|
|
144
144
|
tagColor: c.green,
|
|
145
145
|
desc: [
|
|
146
|
-
'Random CloudFlare tunnel URL on every start',
|
|
147
|
-
`Optional: ${c.reset}${c.pink}fluxy.bot/YOURBOT${c.reset}${c.dim}
|
|
146
|
+
'Random CloudFlare tunnel URL on every start/update',
|
|
147
|
+
`Optional: Use Fluxy Relay Server and access your bot at ${c.reset}${c.pink}my.fluxy.bot/YOURBOT${c.reset}${c.dim} (Free)`,
|
|
148
|
+
`Or use a premium handle like ${c.reset}${c.pink}fluxy.bot/YOURBOT${c.reset}${c.dim} ($5 one-time fee)`,
|
|
148
149
|
],
|
|
149
150
|
},
|
|
150
151
|
{
|
|
@@ -155,6 +156,7 @@ function chooseTunnelMode() {
|
|
|
155
156
|
desc: [
|
|
156
157
|
'Persistent URL with your own domain',
|
|
157
158
|
'Requires a CloudFlare account + domain',
|
|
159
|
+
`Use a subdomain like ${c.reset}${c.white}bot.YOURDOMAIN.COM${c.reset}${c.dim} or the root domain`,
|
|
158
160
|
],
|
|
159
161
|
},
|
|
160
162
|
];
|
|
@@ -403,6 +405,28 @@ function banner() {
|
|
|
403
405
|
${c.dim}v${pkg.version} · Self-hosted AI agent${c.reset}`);
|
|
404
406
|
}
|
|
405
407
|
|
|
408
|
+
function tunnelFailedMessage(localUrl) {
|
|
409
|
+
console.log(`
|
|
410
|
+
${c.dim}─────────────────────────────────${c.reset}
|
|
411
|
+
|
|
412
|
+
${c.yellow}⚠${c.reset} ${c.bold}${c.white}Tunnel failed to connect${c.reset}
|
|
413
|
+
|
|
414
|
+
${c.dim}CloudFlare quick tunnels are rate-limited.${c.reset}
|
|
415
|
+
${c.dim}This usually resolves itself after a few minutes.${c.reset}
|
|
416
|
+
|
|
417
|
+
${c.bold}${c.white}Your dashboard is available locally:${c.reset}
|
|
418
|
+
|
|
419
|
+
${c.blue}${c.bold}${link(localUrl)}${c.reset}
|
|
420
|
+
${c.dim}(cmd+click or ctrl+click to open)${c.reset}
|
|
421
|
+
|
|
422
|
+
${c.bold}${c.white}To retry the tunnel:${c.reset}
|
|
423
|
+
${c.pink}fluxy start${c.reset}
|
|
424
|
+
|
|
425
|
+
${c.bold}${c.white}For a persistent tunnel, use a named tunnel:${c.reset}
|
|
426
|
+
${c.pink}fluxy tunnel setup${c.reset}
|
|
427
|
+
`);
|
|
428
|
+
}
|
|
429
|
+
|
|
406
430
|
function finalMessage(tunnelUrl, relayUrl) {
|
|
407
431
|
console.log(`
|
|
408
432
|
${c.dim}─────────────────────────────────${c.reset}
|
|
@@ -539,6 +563,7 @@ function bootServer({ onTunnelUp, onReady } = {}) {
|
|
|
539
563
|
let resolved = false;
|
|
540
564
|
let stderrBuf = '';
|
|
541
565
|
let tunnelFired = false;
|
|
566
|
+
let tunnelFailed = false;
|
|
542
567
|
|
|
543
568
|
// Vite warmup tracking
|
|
544
569
|
let viteWarmResolve;
|
|
@@ -554,6 +579,7 @@ function bootServer({ onTunnelUp, onReady } = {}) {
|
|
|
554
579
|
child,
|
|
555
580
|
tunnelUrl: tunnelUrl || `http://localhost:${config.port}`,
|
|
556
581
|
relayUrl: relayUrl || config.relay?.url || null,
|
|
582
|
+
tunnelFailed,
|
|
557
583
|
viteWarm,
|
|
558
584
|
});
|
|
559
585
|
};
|
|
@@ -580,6 +606,7 @@ function bootServer({ onTunnelUp, onReady } = {}) {
|
|
|
580
606
|
}
|
|
581
607
|
|
|
582
608
|
if (text.includes('__TUNNEL_FAILED__')) {
|
|
609
|
+
tunnelFailed = true;
|
|
583
610
|
doResolve();
|
|
584
611
|
}
|
|
585
612
|
};
|
|
@@ -687,7 +714,7 @@ async function init() {
|
|
|
687
714
|
console.error(` ${c.dim}${err.message}${c.reset}\n`);
|
|
688
715
|
process.exit(1);
|
|
689
716
|
}
|
|
690
|
-
const { child, tunnelUrl, relayUrl, viteWarm } = result;
|
|
717
|
+
const { child, tunnelUrl, relayUrl, tunnelFailed, viteWarm } = result;
|
|
691
718
|
|
|
692
719
|
// Wait for Vite to finish pre-transforming all modules (with timeout)
|
|
693
720
|
await Promise.race([viteWarm, new Promise(r => setTimeout(r, 30_000))]);
|
|
@@ -704,7 +731,11 @@ async function init() {
|
|
|
704
731
|
});
|
|
705
732
|
stepper.advance();
|
|
706
733
|
stepper.finish();
|
|
707
|
-
|
|
734
|
+
if (tunnelFailed) {
|
|
735
|
+
tunnelFailedMessage(tunnelUrl);
|
|
736
|
+
} else {
|
|
737
|
+
finalMessage(tunnelUrl, relayUrl);
|
|
738
|
+
}
|
|
708
739
|
if (res.status === 0) {
|
|
709
740
|
console.log(` ${c.blue}✔${c.reset} Daemon installed — Fluxy will auto-start on boot.`);
|
|
710
741
|
} else {
|
|
@@ -715,7 +746,11 @@ async function init() {
|
|
|
715
746
|
}
|
|
716
747
|
|
|
717
748
|
stepper.finish();
|
|
718
|
-
|
|
749
|
+
if (tunnelFailed) {
|
|
750
|
+
tunnelFailedMessage(tunnelUrl);
|
|
751
|
+
} else {
|
|
752
|
+
finalMessage(tunnelUrl, relayUrl);
|
|
753
|
+
}
|
|
719
754
|
|
|
720
755
|
child.stdout.on('data', (d) => {
|
|
721
756
|
process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
|
|
@@ -795,7 +830,7 @@ async function start() {
|
|
|
795
830
|
console.error(` ${c.dim}${err.message}${c.reset}\n`);
|
|
796
831
|
process.exit(1);
|
|
797
832
|
}
|
|
798
|
-
const { child, tunnelUrl, relayUrl, viteWarm } = result;
|
|
833
|
+
const { child, tunnelUrl, relayUrl, tunnelFailed, viteWarm } = result;
|
|
799
834
|
|
|
800
835
|
// Wait for Vite to finish pre-transforming all modules (with timeout)
|
|
801
836
|
await Promise.race([viteWarm, new Promise(r => setTimeout(r, 30_000))]);
|
|
@@ -812,7 +847,11 @@ async function start() {
|
|
|
812
847
|
});
|
|
813
848
|
stepper.advance();
|
|
814
849
|
stepper.finish();
|
|
815
|
-
|
|
850
|
+
if (tunnelFailed) {
|
|
851
|
+
tunnelFailedMessage(tunnelUrl);
|
|
852
|
+
} else {
|
|
853
|
+
finalMessage(tunnelUrl, relayUrl);
|
|
854
|
+
}
|
|
816
855
|
if (res.status === 0) {
|
|
817
856
|
console.log(` ${c.blue}✔${c.reset} Daemon installed — Fluxy will auto-start on boot.`);
|
|
818
857
|
} else {
|
|
@@ -823,7 +862,11 @@ async function start() {
|
|
|
823
862
|
}
|
|
824
863
|
|
|
825
864
|
stepper.finish();
|
|
826
|
-
|
|
865
|
+
if (tunnelFailed) {
|
|
866
|
+
tunnelFailedMessage(tunnelUrl);
|
|
867
|
+
} else {
|
|
868
|
+
finalMessage(tunnelUrl, relayUrl);
|
|
869
|
+
}
|
|
827
870
|
|
|
828
871
|
child.stdout.on('data', (d) => {
|
|
829
872
|
process.stdout.write(` ${c.dim}${d.toString().trim()}${c.reset}\n`);
|
package/package.json
CHANGED
|
@@ -127,6 +127,12 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
127
127
|
const [registeredUrl, setRegisteredUrl] = useState('');
|
|
128
128
|
const handleDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
129
129
|
|
|
130
|
+
// Tunnel mode (step 2 branching)
|
|
131
|
+
const [tunnelMode, setTunnelMode] = useState<'quick' | 'named'>('quick');
|
|
132
|
+
const [tunnelDomain, setTunnelDomain] = useState('');
|
|
133
|
+
const [tunnelUrl, setTunnelUrl] = useState('');
|
|
134
|
+
const [handleChoice, setHandleChoice] = useState<'tunnel' | 'relay'>('relay');
|
|
135
|
+
|
|
130
136
|
// Existing handle (for re-run / change flow)
|
|
131
137
|
const [existingHandle, setExistingHandle] = useState<{ username: string; tier: string; url: string } | null>(null);
|
|
132
138
|
const [showChangeConfirm, setShowChangeConfirm] = useState(false);
|
|
@@ -173,6 +179,11 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
173
179
|
if (data.provider) setProvider(data.provider);
|
|
174
180
|
if (data.model) setModel(data.model);
|
|
175
181
|
if (data.whisperEnabled) { setWhisperEnabled(true); setWhisperKey(data.whisperKey || ''); }
|
|
182
|
+
if (data.tunnelMode) setTunnelMode(data.tunnelMode);
|
|
183
|
+
if (data.tunnelDomain) setTunnelDomain(data.tunnelDomain);
|
|
184
|
+
if (data.tunnelUrl) setTunnelUrl(data.tunnelUrl);
|
|
185
|
+
// If user has existing handle, default to 'relay'; otherwise default to 'tunnel'
|
|
186
|
+
if (!data.handle) setHandleChoice('tunnel');
|
|
176
187
|
prefillDone.current = true;
|
|
177
188
|
})
|
|
178
189
|
.catch(() => { prefillDone.current = true; });
|
|
@@ -462,7 +473,11 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
462
473
|
switch (step) {
|
|
463
474
|
case 0: return true;
|
|
464
475
|
case 1: return userName.trim().length > 0;
|
|
465
|
-
case 2:
|
|
476
|
+
case 2: {
|
|
477
|
+
if (tunnelMode === 'named') return true;
|
|
478
|
+
if (handleChoice === 'tunnel') return true;
|
|
479
|
+
return registered;
|
|
480
|
+
}
|
|
466
481
|
case 3: return portalCanContinue;
|
|
467
482
|
case 4: return !!(provider && model && isConnected);
|
|
468
483
|
case 5: return true;
|
|
@@ -608,190 +623,306 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
608
623
|
)}
|
|
609
624
|
|
|
610
625
|
{/* ── Step 2: Name your bot + Claim handle ── */}
|
|
611
|
-
{step === 2 && (
|
|
626
|
+
{step === 2 && tunnelMode === 'named' && (
|
|
627
|
+
<div>
|
|
628
|
+
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
629
|
+
Your Bot's Domain
|
|
630
|
+
</h1>
|
|
631
|
+
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
632
|
+
Your named tunnel is configured with a custom domain.
|
|
633
|
+
</p>
|
|
634
|
+
|
|
635
|
+
<div className="mt-5 flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
636
|
+
<span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">https://{tunnelDomain}</span>
|
|
637
|
+
<button
|
|
638
|
+
onClick={() => {
|
|
639
|
+
navigator.clipboard.writeText(`https://${tunnelDomain}`);
|
|
640
|
+
setPortalCopied(true);
|
|
641
|
+
setTimeout(() => setPortalCopied(false), 2000);
|
|
642
|
+
}}
|
|
643
|
+
className="shrink-0 text-white/30 hover:text-white/60 transition-colors"
|
|
644
|
+
>
|
|
645
|
+
{portalCopied ? (
|
|
646
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
647
|
+
) : (
|
|
648
|
+
<ClipboardPaste className="h-4 w-4" />
|
|
649
|
+
)}
|
|
650
|
+
</button>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<button
|
|
654
|
+
onClick={next}
|
|
655
|
+
className="w-full mt-5 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
656
|
+
>
|
|
657
|
+
Continue
|
|
658
|
+
<ArrowRight className="h-4 w-4" />
|
|
659
|
+
</button>
|
|
660
|
+
</div>
|
|
661
|
+
)}
|
|
662
|
+
|
|
663
|
+
{step === 2 && tunnelMode === 'quick' && (
|
|
612
664
|
<div>
|
|
613
665
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
614
666
|
Name your bot
|
|
615
667
|
</h1>
|
|
616
668
|
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
617
|
-
|
|
669
|
+
Choose how you'll access your bot from anywhere.
|
|
618
670
|
</p>
|
|
619
671
|
|
|
620
|
-
{/*
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
<div className="flex
|
|
631
|
-
<
|
|
632
|
-
|
|
633
|
-
className="flex-1 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
634
|
-
>
|
|
635
|
-
Continue
|
|
636
|
-
<ArrowRight className="h-4 w-4" />
|
|
637
|
-
</button>
|
|
638
|
-
<button
|
|
639
|
-
onClick={() => {
|
|
640
|
-
setShowChangeConfirm(true);
|
|
641
|
-
setRegistered(false);
|
|
642
|
-
setBotName('');
|
|
643
|
-
setHandleStatus(null);
|
|
644
|
-
setTierAvailability({});
|
|
645
|
-
}}
|
|
646
|
-
className="px-5 py-3 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 text-[13px] font-medium rounded-full transition-colors"
|
|
647
|
-
>
|
|
648
|
-
Change
|
|
649
|
-
</button>
|
|
672
|
+
{/* Option cards */}
|
|
673
|
+
<div className="space-y-2 mt-5">
|
|
674
|
+
<button
|
|
675
|
+
onClick={() => setHandleChoice('tunnel')}
|
|
676
|
+
className={`w-full text-left px-4 py-3 rounded-xl border transition-all duration-200 ${
|
|
677
|
+
handleChoice === 'tunnel'
|
|
678
|
+
? 'border-[#04D1FE]/40 bg-[#04D1FE]/[0.06]'
|
|
679
|
+
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
680
|
+
}`}
|
|
681
|
+
>
|
|
682
|
+
<div className="flex items-center justify-between">
|
|
683
|
+
<span className="text-[13px] font-medium text-white/80">Random Tunnel URL</span>
|
|
684
|
+
<span className="text-[11px] font-medium px-2.5 py-0.5 rounded-full border bg-emerald-500/10 text-emerald-400 border-emerald-500/20">Free</span>
|
|
650
685
|
</div>
|
|
651
|
-
|
|
652
|
-
|
|
686
|
+
<p className="text-white/35 text-[11px] mt-1 leading-relaxed">
|
|
687
|
+
Every start, restart, or update will generate a new random URL like <span className="font-mono">abc-xyz.trycloudflare.com</span>. You can bookmark it or use your tunnel URL directly.
|
|
688
|
+
</p>
|
|
689
|
+
</button>
|
|
653
690
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
691
|
+
<button
|
|
692
|
+
onClick={() => setHandleChoice('relay')}
|
|
693
|
+
className={`w-full text-left px-4 py-3 rounded-xl border transition-all duration-200 ${
|
|
694
|
+
handleChoice === 'relay'
|
|
695
|
+
? 'border-[#AF27E3]/40 bg-[#AF27E3]/[0.06]'
|
|
696
|
+
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
697
|
+
}`}
|
|
698
|
+
>
|
|
699
|
+
<div className="flex items-center justify-between">
|
|
700
|
+
<span className="text-[13px] font-medium text-white/80">Custom Domain via Fluxy Relay</span>
|
|
701
|
+
<span className="text-[11px] font-medium px-2.5 py-0.5 rounded-full border bg-[#AF27E3]/15 text-[#AF27E3] border-[#AF27E3]/20">Permanent</span>
|
|
702
|
+
</div>
|
|
703
|
+
<p className="text-white/35 text-[11px] mt-1 leading-relaxed">
|
|
704
|
+
Get a permanent domain that never changes. Choose a free <span className="font-mono">my.fluxy.bot/name</span> or premium <span className="font-mono">fluxy.bot/name</span> handle.
|
|
660
705
|
</p>
|
|
706
|
+
</button>
|
|
707
|
+
</div>
|
|
708
|
+
|
|
709
|
+
{/* Random Tunnel URL — show current URL when selected */}
|
|
710
|
+
{handleChoice === 'tunnel' && (
|
|
711
|
+
<div className="mt-4">
|
|
712
|
+
{tunnelUrl ? (
|
|
713
|
+
<div className="flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
714
|
+
<span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">{tunnelUrl}</span>
|
|
715
|
+
<button
|
|
716
|
+
onClick={() => {
|
|
717
|
+
const url = tunnelUrl.startsWith('http') ? tunnelUrl : `https://${tunnelUrl}`;
|
|
718
|
+
navigator.clipboard.writeText(url);
|
|
719
|
+
setPortalCopied(true);
|
|
720
|
+
setTimeout(() => setPortalCopied(false), 2000);
|
|
721
|
+
}}
|
|
722
|
+
className="shrink-0 text-white/30 hover:text-white/60 transition-colors"
|
|
723
|
+
>
|
|
724
|
+
{portalCopied ? (
|
|
725
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
726
|
+
) : (
|
|
727
|
+
<ClipboardPaste className="h-4 w-4" />
|
|
728
|
+
)}
|
|
729
|
+
</button>
|
|
730
|
+
</div>
|
|
731
|
+
) : (
|
|
732
|
+
<div className="bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
733
|
+
<p className="text-white/35 text-[12px]">Tunnel URL will appear once the tunnel starts.</p>
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
<button
|
|
737
|
+
onClick={next}
|
|
738
|
+
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
739
|
+
>
|
|
740
|
+
Continue
|
|
741
|
+
<ArrowRight className="h-4 w-4" />
|
|
742
|
+
</button>
|
|
661
743
|
</div>
|
|
662
744
|
)}
|
|
663
745
|
|
|
664
|
-
{/*
|
|
665
|
-
{
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
autoCorrect="off"
|
|
677
|
-
autoFocus
|
|
678
|
-
disabled={registered}
|
|
679
|
-
className={inputCls + ' pr-10 font-mono' + (registered ? ' opacity-50' : '')}
|
|
680
|
-
/>
|
|
681
|
-
{handleStatus && botName.length > 0 && !registered && (
|
|
682
|
-
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
|
683
|
-
{handleStatus === 'checking' && (
|
|
684
|
-
<div className="w-5 h-5 border-2 border-white/10 border-t-[#04D1FE] rounded-full animate-spin" />
|
|
685
|
-
)}
|
|
686
|
-
{handleStatus === 'invalid' && (
|
|
687
|
-
<div className="w-6 h-6 rounded-full bg-amber-500/15 flex items-center justify-center">
|
|
688
|
-
<svg className="w-3.5 h-3.5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
689
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3m0 4h.01" />
|
|
690
|
-
</svg>
|
|
691
|
-
</div>
|
|
692
|
-
)}
|
|
746
|
+
{/* Relay registration — existing UI */}
|
|
747
|
+
{handleChoice === 'relay' && (
|
|
748
|
+
<div className="mt-4">
|
|
749
|
+
{/* Existing handle banner */}
|
|
750
|
+
{existingHandle && registered && !showChangeConfirm && (
|
|
751
|
+
<>
|
|
752
|
+
<div className="bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
|
|
753
|
+
<div className="flex items-center gap-2">
|
|
754
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
755
|
+
<p className="text-emerald-400/90 text-[13px] font-medium">Current handle</p>
|
|
756
|
+
</div>
|
|
757
|
+
<p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
|
|
693
758
|
</div>
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
759
|
+
<div className="flex gap-2 mt-4">
|
|
760
|
+
<button
|
|
761
|
+
onClick={next}
|
|
762
|
+
className="flex-1 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
763
|
+
>
|
|
764
|
+
Continue
|
|
765
|
+
<ArrowRight className="h-4 w-4" />
|
|
766
|
+
</button>
|
|
767
|
+
<button
|
|
768
|
+
onClick={() => {
|
|
769
|
+
setShowChangeConfirm(true);
|
|
770
|
+
setRegistered(false);
|
|
771
|
+
setBotName('');
|
|
772
|
+
setHandleStatus(null);
|
|
773
|
+
setTierAvailability({});
|
|
774
|
+
}}
|
|
775
|
+
className="px-5 py-3 bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.08] text-white/60 text-[13px] font-medium rounded-full transition-colors"
|
|
776
|
+
>
|
|
777
|
+
Change
|
|
778
|
+
</button>
|
|
779
|
+
</div>
|
|
780
|
+
</>
|
|
700
781
|
)}
|
|
701
782
|
|
|
702
|
-
{/*
|
|
703
|
-
{
|
|
704
|
-
<div className="
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
<button
|
|
710
|
-
key={h.tier}
|
|
711
|
-
onClick={() => !taken && setSelectedTier(h.tier)}
|
|
712
|
-
disabled={taken}
|
|
713
|
-
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl border transition-all duration-200 text-left ${
|
|
714
|
-
taken
|
|
715
|
-
? 'border-white/[0.04] bg-transparent opacity-50 cursor-not-allowed'
|
|
716
|
-
: selectedTier === h.tier
|
|
717
|
-
? h.highlight
|
|
718
|
-
? 'border-[#AF27E3]/40 bg-[#AF27E3]/[0.06]'
|
|
719
|
-
: 'border-[#AF27E3]/30 bg-white/[0.04]'
|
|
720
|
-
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
721
|
-
}`}
|
|
722
|
-
>
|
|
723
|
-
<span className="font-mono text-[13px] text-white/70">
|
|
724
|
-
{h.label(botName)}
|
|
725
|
-
</span>
|
|
726
|
-
{taken ? (
|
|
727
|
-
<span className="text-[11px] font-medium px-2.5 py-0.5 rounded-full border bg-red-500/10 text-red-400 border-red-500/20">
|
|
728
|
-
Taken
|
|
729
|
-
</span>
|
|
730
|
-
) : (
|
|
731
|
-
<span className={`text-[11px] font-medium px-2.5 py-0.5 rounded-full border ${h.badgeCls}`}>
|
|
732
|
-
{h.badge}
|
|
733
|
-
</span>
|
|
734
|
-
)}
|
|
735
|
-
</button>
|
|
736
|
-
);
|
|
737
|
-
})}
|
|
783
|
+
{/* Change confirmation alert */}
|
|
784
|
+
{showChangeConfirm && !registered && (
|
|
785
|
+
<div className="bg-amber-500/8 border border-amber-500/20 rounded-xl px-4 py-3">
|
|
786
|
+
<p className="text-amber-400/90 text-[13px] font-medium">Changing your handle</p>
|
|
787
|
+
<p className="text-amber-400/60 text-[12px] mt-1">
|
|
788
|
+
Your current handle <span className="font-mono">{existingHandle?.url}</span> will be released and become available for others.
|
|
789
|
+
</p>
|
|
738
790
|
</div>
|
|
739
791
|
)}
|
|
740
792
|
|
|
741
|
-
{/*
|
|
742
|
-
{registered && (
|
|
743
|
-
|
|
744
|
-
<div className="
|
|
745
|
-
<
|
|
746
|
-
|
|
793
|
+
{/* Input — shown for new claim or change flow */}
|
|
794
|
+
{(!existingHandle || showChangeConfirm || !registered) && !(existingHandle && registered && !showChangeConfirm) && (
|
|
795
|
+
<>
|
|
796
|
+
<div className="relative mt-3">
|
|
797
|
+
<input
|
|
798
|
+
type="text"
|
|
799
|
+
value={botName}
|
|
800
|
+
onChange={(e) => onBotNameInput(e.target.value)}
|
|
801
|
+
maxLength={30}
|
|
802
|
+
placeholder="your-bot-name"
|
|
803
|
+
spellCheck={false}
|
|
804
|
+
autoCapitalize="none"
|
|
805
|
+
autoCorrect="off"
|
|
806
|
+
autoFocus
|
|
807
|
+
disabled={registered}
|
|
808
|
+
className={inputCls + ' pr-10 font-mono' + (registered ? ' opacity-50' : '')}
|
|
809
|
+
/>
|
|
810
|
+
{handleStatus && botName.length > 0 && !registered && (
|
|
811
|
+
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
|
812
|
+
{handleStatus === 'checking' && (
|
|
813
|
+
<div className="w-5 h-5 border-2 border-white/10 border-t-[#04D1FE] rounded-full animate-spin" />
|
|
814
|
+
)}
|
|
815
|
+
{handleStatus === 'invalid' && (
|
|
816
|
+
<div className="w-6 h-6 rounded-full bg-amber-500/15 flex items-center justify-center">
|
|
817
|
+
<svg className="w-3.5 h-3.5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
818
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3m0 4h.01" />
|
|
819
|
+
</svg>
|
|
820
|
+
</div>
|
|
821
|
+
)}
|
|
822
|
+
</div>
|
|
823
|
+
)}
|
|
747
824
|
</div>
|
|
748
|
-
<p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
|
|
749
|
-
</div>
|
|
750
|
-
)}
|
|
751
825
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
onClick={showChangeConfirm ? onChangeHandle : onClaimHandle}
|
|
756
|
-
disabled={registering || changingHandle}
|
|
757
|
-
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
758
|
-
>
|
|
759
|
-
{(registering || changingHandle) ? (
|
|
760
|
-
<><LoaderCircle className="h-4 w-4 animate-spin" />{showChangeConfirm ? 'Changing...' : 'Claiming...'}</>
|
|
761
|
-
) : (
|
|
762
|
-
<>{showChangeConfirm ? 'Change Handle' : 'Claim & Continue'}<ArrowRight className="h-4 w-4" /></>
|
|
826
|
+
{/* Status messages */}
|
|
827
|
+
{handleStatus === 'invalid' && handleError && (
|
|
828
|
+
<p className="text-amber-400 text-[12px] mt-2">{handleError}</p>
|
|
763
829
|
)}
|
|
764
|
-
</button>
|
|
765
|
-
)}
|
|
766
830
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
831
|
+
{/* Handle tier options — per-tier availability */}
|
|
832
|
+
{handleStatus === 'ready' && botName.length > 0 && !registered && (
|
|
833
|
+
<div className="space-y-2 mt-4">
|
|
834
|
+
{HANDLES.map((h) => {
|
|
835
|
+
const available = tierAvailability[h.tier];
|
|
836
|
+
const taken = available === false;
|
|
837
|
+
return (
|
|
838
|
+
<button
|
|
839
|
+
key={h.tier}
|
|
840
|
+
onClick={() => !taken && setSelectedTier(h.tier)}
|
|
841
|
+
disabled={taken}
|
|
842
|
+
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl border transition-all duration-200 text-left ${
|
|
843
|
+
taken
|
|
844
|
+
? 'border-white/[0.04] bg-transparent opacity-50 cursor-not-allowed'
|
|
845
|
+
: selectedTier === h.tier
|
|
846
|
+
? h.highlight
|
|
847
|
+
? 'border-[#AF27E3]/40 bg-[#AF27E3]/[0.06]'
|
|
848
|
+
: 'border-[#AF27E3]/30 bg-white/[0.04]'
|
|
849
|
+
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
850
|
+
}`}
|
|
851
|
+
>
|
|
852
|
+
<span className="font-mono text-[13px] text-white/70">
|
|
853
|
+
{h.label(botName)}
|
|
854
|
+
</span>
|
|
855
|
+
{taken ? (
|
|
856
|
+
<span className="text-[11px] font-medium px-2.5 py-0.5 rounded-full border bg-red-500/10 text-red-400 border-red-500/20">
|
|
857
|
+
Taken
|
|
858
|
+
</span>
|
|
859
|
+
) : (
|
|
860
|
+
<span className={`text-[11px] font-medium px-2.5 py-0.5 rounded-full border ${h.badgeCls}`}>
|
|
861
|
+
{h.badge}
|
|
862
|
+
</span>
|
|
863
|
+
)}
|
|
864
|
+
</button>
|
|
865
|
+
);
|
|
866
|
+
})}
|
|
867
|
+
</div>
|
|
868
|
+
)}
|
|
777
869
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
870
|
+
{/* Registered success (after claiming) */}
|
|
871
|
+
{registered && (
|
|
872
|
+
<div className="mt-4 bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
|
|
873
|
+
<div className="flex items-center gap-2">
|
|
874
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
875
|
+
<p className="text-emerald-400/90 text-[13px] font-medium">Handle claimed!</p>
|
|
876
|
+
</div>
|
|
877
|
+
<p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
|
|
878
|
+
</div>
|
|
879
|
+
)}
|
|
880
|
+
|
|
881
|
+
{/* Claim / Change button */}
|
|
882
|
+
{handleStatus === 'ready' && tierAvailability[selectedTier] && botName.length > 0 && !registered && (
|
|
883
|
+
<button
|
|
884
|
+
onClick={showChangeConfirm ? onChangeHandle : onClaimHandle}
|
|
885
|
+
disabled={registering || changingHandle}
|
|
886
|
+
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
887
|
+
>
|
|
888
|
+
{(registering || changingHandle) ? (
|
|
889
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />{showChangeConfirm ? 'Changing...' : 'Claiming...'}</>
|
|
890
|
+
) : (
|
|
891
|
+
<>{showChangeConfirm ? 'Change Handle' : 'Claim & Continue'}<ArrowRight className="h-4 w-4" /></>
|
|
892
|
+
)}
|
|
893
|
+
</button>
|
|
894
|
+
)}
|
|
895
|
+
|
|
896
|
+
{/* Continue after claim */}
|
|
897
|
+
{registered && (
|
|
898
|
+
<button
|
|
899
|
+
onClick={next}
|
|
900
|
+
className="w-full mt-4 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
901
|
+
>
|
|
902
|
+
Continue
|
|
903
|
+
<ArrowRight className="h-4 w-4" />
|
|
904
|
+
</button>
|
|
905
|
+
)}
|
|
906
|
+
|
|
907
|
+
{/* Cancel change */}
|
|
908
|
+
{showChangeConfirm && !registered && (
|
|
909
|
+
<button
|
|
910
|
+
onClick={() => {
|
|
911
|
+
setShowChangeConfirm(false);
|
|
912
|
+
setBotName(existingHandle!.username);
|
|
913
|
+
setRegistered(true);
|
|
914
|
+
setRegisteredUrl(existingHandle!.url);
|
|
915
|
+
setSelectedTier(existingHandle!.tier);
|
|
916
|
+
setHandleStatus(null);
|
|
917
|
+
}}
|
|
918
|
+
className="w-full mt-2 py-2 text-white/25 hover:text-white/40 text-[12px] transition-colors"
|
|
919
|
+
>
|
|
920
|
+
Cancel — keep current handle
|
|
921
|
+
</button>
|
|
922
|
+
)}
|
|
923
|
+
</>
|
|
793
924
|
)}
|
|
794
|
-
|
|
925
|
+
</div>
|
|
795
926
|
)}
|
|
796
927
|
</div>
|
|
797
928
|
)}
|
|
@@ -1241,7 +1372,19 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1241
1372
|
)}
|
|
1242
1373
|
|
|
1243
1374
|
{/* ── Step 6: All Set (initial onboard only) ── */}
|
|
1244
|
-
{step === 6 && isInitialSetup && (
|
|
1375
|
+
{step === 6 && isInitialSetup && (() => {
|
|
1376
|
+
const finalUrl = (() => {
|
|
1377
|
+
if (tunnelMode === 'named') return `https://${tunnelDomain}`;
|
|
1378
|
+
if (handleChoice === 'relay' && registeredUrl) return registeredUrl;
|
|
1379
|
+
return tunnelUrl || `http://localhost:${3000}`;
|
|
1380
|
+
})();
|
|
1381
|
+
const finalUrlFull = finalUrl.startsWith('http') ? finalUrl : `https://${finalUrl}`;
|
|
1382
|
+
const descriptionText = tunnelMode === 'named'
|
|
1383
|
+
? 'Access your agent at your custom domain.'
|
|
1384
|
+
: handleChoice === 'relay' && registeredUrl
|
|
1385
|
+
? 'Your agent is live and ready. From now on, access it using your custom URL below.'
|
|
1386
|
+
: 'Your agent is live and ready. Your tunnel URL is shown below. Note: this URL changes on restart.';
|
|
1387
|
+
return (
|
|
1245
1388
|
<div className="flex flex-col items-center text-center">
|
|
1246
1389
|
<div className="w-16 h-16 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center mb-5">
|
|
1247
1390
|
<Check className="h-8 w-8 text-emerald-400" />
|
|
@@ -1250,16 +1393,15 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1250
1393
|
All Set!
|
|
1251
1394
|
</h1>
|
|
1252
1395
|
<p className="text-white/40 text-[13px] mt-2 leading-relaxed max-w-[340px]">
|
|
1253
|
-
|
|
1396
|
+
{descriptionText}
|
|
1254
1397
|
</p>
|
|
1255
1398
|
|
|
1256
|
-
{/*
|
|
1399
|
+
{/* URL */}
|
|
1257
1400
|
<div className="w-full mt-6 flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
1258
|
-
<span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">{
|
|
1401
|
+
<span className="font-mono text-[13px] text-white/70 truncate flex-1 text-left">{finalUrl}</span>
|
|
1259
1402
|
<button
|
|
1260
1403
|
onClick={() => {
|
|
1261
|
-
|
|
1262
|
-
navigator.clipboard.writeText(url);
|
|
1404
|
+
navigator.clipboard.writeText(finalUrlFull);
|
|
1263
1405
|
setPortalCopied(true);
|
|
1264
1406
|
setTimeout(() => setPortalCopied(false), 2000);
|
|
1265
1407
|
}}
|
|
@@ -1295,9 +1437,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1295
1437
|
{/* Redirect button */}
|
|
1296
1438
|
<button
|
|
1297
1439
|
onClick={() => {
|
|
1298
|
-
|
|
1299
|
-
// Navigate the top-level window (escapes iframe if embedded)
|
|
1300
|
-
(window.top || window).location.href = url;
|
|
1440
|
+
(window.top || window).location.href = finalUrlFull;
|
|
1301
1441
|
}}
|
|
1302
1442
|
className="w-full mt-6 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
1303
1443
|
>
|
|
@@ -1306,10 +1446,11 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
|
|
|
1306
1446
|
</button>
|
|
1307
1447
|
|
|
1308
1448
|
<p className="text-white/20 text-[11px] mt-3 leading-relaxed">
|
|
1309
|
-
You'll be redirected to your custom URL.
|
|
1449
|
+
You'll be redirected to your {tunnelMode === 'named' ? 'custom domain' : handleChoice === 'relay' ? 'custom URL' : 'tunnel URL'}.
|
|
1310
1450
|
</p>
|
|
1311
1451
|
</div>
|
|
1312
|
-
|
|
1452
|
+
);
|
|
1453
|
+
})()}
|
|
1313
1454
|
</motion.div>
|
|
1314
1455
|
</AnimatePresence>
|
|
1315
1456
|
|
package/worker/index.ts
CHANGED
|
@@ -291,6 +291,9 @@ app.get('/api/onboard/status', (_, res) => {
|
|
|
291
291
|
tier: cfg.relay.tier,
|
|
292
292
|
url: cfg.relay.url,
|
|
293
293
|
} : null,
|
|
294
|
+
tunnelMode: cfg.tunnel?.mode || 'quick',
|
|
295
|
+
tunnelDomain: cfg.tunnel?.domain || '',
|
|
296
|
+
tunnelUrl: cfg.tunnelUrl || '',
|
|
294
297
|
});
|
|
295
298
|
});
|
|
296
299
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[backend] Listening on port 3004
|