fluxy-bot 0.1.0 → 0.1.2
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 +30 -165
- package/client/src/components/Onboard/OnboardWizard.tsx +208 -24
- package/dist/assets/index-CX_6AQUX.css +1 -0
- package/dist/assets/index-qj4hIsKu.js +64 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/supervisor/worker.ts +1 -1
- package/worker/index.ts +48 -0
- package/dist/assets/index-BkNWpS06.css +0 -1
- package/dist/assets/index-CX3QeqQ8.js +0 -64
package/bin/cli.js
CHANGED
|
@@ -4,7 +4,6 @@ import { spawn, execSync } from 'child_process';
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
|
-
import readline from 'readline';
|
|
8
7
|
import { fileURLToPath } from 'url';
|
|
9
8
|
|
|
10
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -14,8 +13,6 @@ const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
|
|
|
14
13
|
const BIN_DIR = path.join(DATA_DIR, 'bin');
|
|
15
14
|
const CF_PATH = path.join(BIN_DIR, 'cloudflared');
|
|
16
15
|
|
|
17
|
-
const RELAY_API = 'https://api.fluxy.bot/api';
|
|
18
|
-
|
|
19
16
|
const [, , command] = process.argv;
|
|
20
17
|
|
|
21
18
|
// ── UI helpers ──
|
|
@@ -29,7 +26,6 @@ const c = {
|
|
|
29
26
|
yellow: '\x1b[33m',
|
|
30
27
|
red: '\x1b[31m',
|
|
31
28
|
white: '\x1b[97m',
|
|
32
|
-
bg: '\x1b[48;5;236m',
|
|
33
29
|
};
|
|
34
30
|
|
|
35
31
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -101,7 +97,7 @@ class Stepper {
|
|
|
101
97
|
function banner() {
|
|
102
98
|
console.log(`
|
|
103
99
|
${c.cyan}${c.bold}╔═══════════════════════════════╗
|
|
104
|
-
║ FLUXY v0.1.
|
|
100
|
+
║ FLUXY v0.1.1 ║
|
|
105
101
|
╚═══════════════════════════════╝${c.reset}
|
|
106
102
|
${c.dim}Self-hosted AI bot${c.reset}`);
|
|
107
103
|
}
|
|
@@ -110,7 +106,7 @@ function finalMessage(tunnelUrl, relayUrl) {
|
|
|
110
106
|
console.log(`
|
|
111
107
|
${c.dim}─────────────────────────────────${c.reset}
|
|
112
108
|
|
|
113
|
-
${c.bold}${c.white}
|
|
109
|
+
${c.bold}${c.white}Open your dashboard to finish setup:${c.reset}
|
|
114
110
|
|
|
115
111
|
${c.cyan}${c.bold}${tunnelUrl}${c.reset}`);
|
|
116
112
|
|
|
@@ -127,18 +123,6 @@ function finalMessage(tunnelUrl, relayUrl) {
|
|
|
127
123
|
`);
|
|
128
124
|
}
|
|
129
125
|
|
|
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
126
|
// ── Steps ──
|
|
143
127
|
|
|
144
128
|
function createConfig() {
|
|
@@ -197,161 +181,51 @@ async function installCloudflared() {
|
|
|
197
181
|
fs.chmodSync(CF_PATH, 0o755);
|
|
198
182
|
}
|
|
199
183
|
|
|
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
184
|
// ── Boot server ──
|
|
304
185
|
|
|
305
186
|
function bootServer() {
|
|
306
187
|
return new Promise((resolve) => {
|
|
307
188
|
const child = spawn(
|
|
308
|
-
|
|
189
|
+
process.execPath,
|
|
309
190
|
['--import', 'tsx/esm', path.join(ROOT, 'supervisor/index.ts')],
|
|
310
191
|
{ cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } },
|
|
311
192
|
);
|
|
312
193
|
|
|
313
194
|
let tunnelUrl = null;
|
|
314
195
|
let relayUrl = null;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
196
|
+
let resolved = false;
|
|
197
|
+
|
|
198
|
+
const doResolve = () => {
|
|
199
|
+
if (resolved) return;
|
|
200
|
+
resolved = true;
|
|
201
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
|
202
|
+
resolve({
|
|
203
|
+
child,
|
|
204
|
+
tunnelUrl: tunnelUrl || `http://localhost:${config.port}`,
|
|
205
|
+
relayUrl: relayUrl || config.relay?.url || null,
|
|
206
|
+
});
|
|
321
207
|
};
|
|
322
208
|
|
|
323
209
|
const handleData = (data) => {
|
|
324
210
|
const text = data.toString();
|
|
325
211
|
|
|
326
212
|
const tunnelMatch = text.match(/__TUNNEL_URL__=(\S+)/);
|
|
327
|
-
if (tunnelMatch)
|
|
328
|
-
tunnelUrl = tunnelMatch[1];
|
|
329
|
-
}
|
|
213
|
+
if (tunnelMatch) tunnelUrl = tunnelMatch[1];
|
|
330
214
|
|
|
331
215
|
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
|
-
}
|
|
216
|
+
if (relayMatch) relayUrl = relayMatch[1];
|
|
346
217
|
|
|
347
218
|
if (tunnelUrl && relayUrl) {
|
|
348
|
-
|
|
219
|
+
doResolve();
|
|
349
220
|
return;
|
|
350
221
|
}
|
|
351
222
|
|
|
223
|
+
if (tunnelUrl && !relayUrl) {
|
|
224
|
+
setTimeout(doResolve, 3000);
|
|
225
|
+
}
|
|
226
|
+
|
|
352
227
|
if (text.includes('__TUNNEL_FAILED__')) {
|
|
353
|
-
|
|
354
|
-
return;
|
|
228
|
+
doResolve();
|
|
355
229
|
}
|
|
356
230
|
};
|
|
357
231
|
|
|
@@ -370,35 +244,26 @@ function bootServer() {
|
|
|
370
244
|
async function init() {
|
|
371
245
|
banner();
|
|
372
246
|
|
|
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
247
|
createConfig();
|
|
383
248
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// Now the non-interactive steps
|
|
388
|
-
const buildSteps = [
|
|
249
|
+
const steps = [
|
|
250
|
+
'Creating config',
|
|
389
251
|
'Installing cloudflared',
|
|
390
252
|
'Starting server',
|
|
391
253
|
'Connecting tunnel',
|
|
392
254
|
];
|
|
393
255
|
|
|
394
|
-
const stepper = new Stepper(
|
|
256
|
+
const stepper = new Stepper(steps);
|
|
395
257
|
stepper.start();
|
|
396
258
|
|
|
397
|
-
//
|
|
259
|
+
// Config already created
|
|
260
|
+
stepper.advance();
|
|
261
|
+
|
|
262
|
+
// Cloudflared
|
|
398
263
|
await installCloudflared();
|
|
399
264
|
stepper.advance();
|
|
400
265
|
|
|
401
|
-
//
|
|
266
|
+
// Server + Tunnel
|
|
402
267
|
stepper.advance();
|
|
403
268
|
const { child, tunnelUrl, relayUrl } = await bootServer();
|
|
404
269
|
stepper.advance();
|
|
@@ -34,6 +34,13 @@ const MODELS: Record<string, { id: string; label: string }[]> = {
|
|
|
34
34
|
|
|
35
35
|
const TOTAL_STEPS = 5; // 0..4
|
|
36
36
|
|
|
37
|
+
const HANDLES = [
|
|
38
|
+
{ tier: 'premium', label: (n: string) => `${n}.fluxy.bot`, badge: '$5', badgeCls: 'bg-primary/15 text-primary border-primary/20', highlight: true },
|
|
39
|
+
{ tier: 'my', label: (n: string) => `my.fluxy.bot/${n}`, badge: 'Free', badgeCls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', highlight: false },
|
|
40
|
+
{ tier: 'at', label: (n: string) => `at.fluxy.bot/${n}`, badge: 'Free', badgeCls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', highlight: false },
|
|
41
|
+
{ tier: 'on', label: (n: string) => `on.fluxy.bot/${n}`, badge: 'Free', badgeCls: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20', highlight: false },
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
37
44
|
/* ── Dropdown ── */
|
|
38
45
|
|
|
39
46
|
function ModelDropdown({ models, value, onChange }: { models: { id: string; label: string }[]; value: string; onChange: (id: string) => void }) {
|
|
@@ -93,7 +100,6 @@ interface Props {
|
|
|
93
100
|
export default function OnboardWizard({ onComplete }: Props) {
|
|
94
101
|
const [step, setStep] = useState(0);
|
|
95
102
|
const [userName, setUserName] = useState('');
|
|
96
|
-
const [agentName, setAgentName] = useState('Fluxy');
|
|
97
103
|
const [provider, setProvider] = useState('anthropic');
|
|
98
104
|
const [model, setModel] = useState('');
|
|
99
105
|
const [saving, setSaving] = useState(false);
|
|
@@ -119,6 +125,16 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
119
125
|
// Ollama-specific
|
|
120
126
|
const [baseUrl, setBaseUrl] = useState('');
|
|
121
127
|
|
|
128
|
+
// Bot name + Handle (step 2)
|
|
129
|
+
const [botName, setBotName] = useState('');
|
|
130
|
+
const [handleStatus, setHandleStatus] = useState<null | 'checking' | 'available' | 'taken' | 'invalid'>(null);
|
|
131
|
+
const [handleError, setHandleError] = useState('');
|
|
132
|
+
const [selectedTier, setSelectedTier] = useState('my');
|
|
133
|
+
const [registering, setRegistering] = useState(false);
|
|
134
|
+
const [registered, setRegistered] = useState(false);
|
|
135
|
+
const [registeredUrl, setRegisteredUrl] = useState('');
|
|
136
|
+
const handleDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
137
|
+
|
|
122
138
|
// Whisper (step 4)
|
|
123
139
|
const [whisperEnabled, setWhisperEnabled] = useState(false);
|
|
124
140
|
|
|
@@ -162,8 +178,73 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
162
178
|
return () => clearInterval(interval);
|
|
163
179
|
}, [openaiWaiting]);
|
|
164
180
|
|
|
181
|
+
// Handle availability check (debounced)
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (handleDebounce.current) clearTimeout(handleDebounce.current);
|
|
184
|
+
setHandleStatus(null);
|
|
185
|
+
setHandleError('');
|
|
186
|
+
setRegistered(false);
|
|
187
|
+
setRegisteredUrl('');
|
|
188
|
+
|
|
189
|
+
const trimmed = botName.trim();
|
|
190
|
+
if (!trimmed) return;
|
|
191
|
+
if (trimmed.length < 3) {
|
|
192
|
+
setHandleStatus('invalid');
|
|
193
|
+
setHandleError('At least 3 characters');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setHandleStatus('checking');
|
|
198
|
+
handleDebounce.current = setTimeout(async () => {
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(`/api/handle/check/${encodeURIComponent(trimmed)}`);
|
|
201
|
+
const data = await res.json();
|
|
202
|
+
if (!data.valid) {
|
|
203
|
+
setHandleStatus('invalid');
|
|
204
|
+
setHandleError(data.error);
|
|
205
|
+
} else if (data.available) {
|
|
206
|
+
setHandleStatus('available');
|
|
207
|
+
} else {
|
|
208
|
+
setHandleStatus('taken');
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
setHandleStatus(null);
|
|
212
|
+
}
|
|
213
|
+
}, 400);
|
|
214
|
+
|
|
215
|
+
return () => { if (handleDebounce.current) clearTimeout(handleDebounce.current); };
|
|
216
|
+
}, [botName]);
|
|
217
|
+
|
|
218
|
+
const onBotNameInput = (val: string) => {
|
|
219
|
+
setBotName(val.toLowerCase().replace(/[^a-z0-9-]/g, ''));
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const onClaimHandle = async () => {
|
|
223
|
+
if (!botName || handleStatus !== 'available') return;
|
|
224
|
+
setRegistering(true);
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch('/api/handle/register', {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: { 'Content-Type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({ username: botName, tier: selectedTier }),
|
|
230
|
+
});
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
if (data.ok) {
|
|
233
|
+
setRegistered(true);
|
|
234
|
+
setRegisteredUrl(data.url);
|
|
235
|
+
} else {
|
|
236
|
+
setHandleError(data.error || 'Registration failed');
|
|
237
|
+
setHandleStatus('invalid');
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
setHandleError('Could not reach server');
|
|
241
|
+
setHandleStatus('invalid');
|
|
242
|
+
} finally {
|
|
243
|
+
setRegistering(false);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
165
247
|
const handleProviderChange = (id: string) => {
|
|
166
|
-
// Cancel Codex OAuth if switching away from OpenAI
|
|
167
248
|
if (provider === 'openai' && id !== 'openai' && openaiWaiting) {
|
|
168
249
|
fetch('/api/auth/codex/cancel', { method: 'POST' });
|
|
169
250
|
setOpenaiWaiting(false);
|
|
@@ -268,13 +349,14 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
268
349
|
|
|
269
350
|
/* ── Navigation ── */
|
|
270
351
|
|
|
352
|
+
// Steps: 0=Welcome, 1=Name, 2=Bot name + Handle, 3=Provider, 4=Whisper+Complete
|
|
271
353
|
const canNext = (() => {
|
|
272
354
|
switch (step) {
|
|
273
355
|
case 0: return true;
|
|
274
356
|
case 1: return userName.trim().length > 0;
|
|
275
|
-
case 2: return
|
|
357
|
+
case 2: return botName.trim().length >= 3; // Must have a bot name
|
|
276
358
|
case 3: return !!(provider && model && isConnected);
|
|
277
|
-
case 4: return true;
|
|
359
|
+
case 4: return true;
|
|
278
360
|
default: return false;
|
|
279
361
|
}
|
|
280
362
|
})();
|
|
@@ -294,7 +376,7 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
294
376
|
headers: { 'Content-Type': 'application/json' },
|
|
295
377
|
body: JSON.stringify({
|
|
296
378
|
userName: userName.trim(),
|
|
297
|
-
agentName:
|
|
379
|
+
agentName: botName.trim() || 'Fluxy',
|
|
298
380
|
provider,
|
|
299
381
|
model,
|
|
300
382
|
apiKey: '',
|
|
@@ -402,33 +484,140 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
402
484
|
</div>
|
|
403
485
|
)}
|
|
404
486
|
|
|
405
|
-
{/* ── Step 2:
|
|
487
|
+
{/* ── Step 2: Name your bot + Claim handle ── */}
|
|
406
488
|
{step === 2 && (
|
|
407
489
|
<div>
|
|
408
490
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
409
|
-
Name your
|
|
491
|
+
Name your bot
|
|
410
492
|
</h1>
|
|
411
493
|
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
412
|
-
|
|
494
|
+
This is your bot's name and permanent handle — access it from anywhere.
|
|
413
495
|
</p>
|
|
414
|
-
|
|
496
|
+
|
|
497
|
+
{/* Input */}
|
|
498
|
+
<div className="relative mt-5">
|
|
415
499
|
<input
|
|
416
500
|
type="text"
|
|
417
|
-
value={
|
|
418
|
-
onChange={(e) =>
|
|
419
|
-
|
|
420
|
-
placeholder="
|
|
501
|
+
value={botName}
|
|
502
|
+
onChange={(e) => onBotNameInput(e.target.value)}
|
|
503
|
+
maxLength={30}
|
|
504
|
+
placeholder="your-bot-name"
|
|
505
|
+
spellCheck={false}
|
|
506
|
+
autoCapitalize="none"
|
|
507
|
+
autoCorrect="off"
|
|
421
508
|
autoFocus
|
|
422
|
-
|
|
509
|
+
disabled={registered}
|
|
510
|
+
className={inputCls + ' pr-10 font-mono' + (registered ? ' opacity-50' : '')}
|
|
423
511
|
/>
|
|
512
|
+
{handleStatus && botName.length > 0 && !registered && (
|
|
513
|
+
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
|
514
|
+
{handleStatus === 'checking' && (
|
|
515
|
+
<div className="w-5 h-5 border-2 border-white/10 border-t-primary rounded-full animate-spin" />
|
|
516
|
+
)}
|
|
517
|
+
{handleStatus === 'available' && (
|
|
518
|
+
<div className="w-6 h-6 rounded-full bg-emerald-500/15 flex items-center justify-center">
|
|
519
|
+
<Check className="h-3.5 w-3.5 text-emerald-400" />
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
{handleStatus === 'taken' && (
|
|
523
|
+
<div className="w-6 h-6 rounded-full bg-red-500/15 flex items-center justify-center">
|
|
524
|
+
<svg className="w-3.5 h-3.5 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
525
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
526
|
+
</svg>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
{handleStatus === 'invalid' && (
|
|
530
|
+
<div className="w-6 h-6 rounded-full bg-amber-500/15 flex items-center justify-center">
|
|
531
|
+
<svg className="w-3.5 h-3.5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
532
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3m0 4h.01" />
|
|
533
|
+
</svg>
|
|
534
|
+
</div>
|
|
535
|
+
)}
|
|
536
|
+
</div>
|
|
537
|
+
)}
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
{/* Status messages */}
|
|
541
|
+
{handleStatus === 'invalid' && handleError && (
|
|
542
|
+
<p className="text-amber-400 text-[12px] mt-2">{handleError}</p>
|
|
543
|
+
)}
|
|
544
|
+
{handleStatus === 'taken' && (
|
|
545
|
+
<p className="text-red-400 text-[12px] mt-2">This name is already taken</p>
|
|
546
|
+
)}
|
|
547
|
+
|
|
548
|
+
{/* Handle tier options */}
|
|
549
|
+
{handleStatus === 'available' && botName.length > 0 && !registered && (
|
|
550
|
+
<div className="space-y-2 mt-4">
|
|
551
|
+
{HANDLES.map((h) => (
|
|
552
|
+
<button
|
|
553
|
+
key={h.tier}
|
|
554
|
+
onClick={() => setSelectedTier(h.tier)}
|
|
555
|
+
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl border transition-all duration-200 text-left ${
|
|
556
|
+
selectedTier === h.tier
|
|
557
|
+
? h.highlight
|
|
558
|
+
? 'border-primary/40 bg-primary/[0.06]'
|
|
559
|
+
: 'border-primary/30 bg-white/[0.04]'
|
|
560
|
+
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
561
|
+
}`}
|
|
562
|
+
>
|
|
563
|
+
<span className="font-mono text-[13px] text-white/70">
|
|
564
|
+
{h.label(botName)}
|
|
565
|
+
</span>
|
|
566
|
+
<span className={`text-[11px] font-medium px-2.5 py-0.5 rounded-full border ${h.badgeCls}`}>
|
|
567
|
+
{h.badge}
|
|
568
|
+
</span>
|
|
569
|
+
</button>
|
|
570
|
+
))}
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
|
|
574
|
+
{/* Registered success */}
|
|
575
|
+
{registered && (
|
|
576
|
+
<div className="mt-4 bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
|
|
577
|
+
<div className="flex items-center gap-2">
|
|
578
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
579
|
+
<p className="text-emerald-400/90 text-[13px] font-medium">Handle claimed!</p>
|
|
580
|
+
</div>
|
|
581
|
+
<p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
|
|
582
|
+
</div>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{/* Claim button */}
|
|
586
|
+
{handleStatus === 'available' && botName.length > 0 && !registered && (
|
|
587
|
+
<button
|
|
588
|
+
onClick={onClaimHandle}
|
|
589
|
+
disabled={registering}
|
|
590
|
+
className="w-full mt-4 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 disabled:opacity-40"
|
|
591
|
+
>
|
|
592
|
+
{registering ? (
|
|
593
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />Claiming...</>
|
|
594
|
+
) : (
|
|
595
|
+
<>Claim & Continue<ArrowRight className="h-4 w-4" /></>
|
|
596
|
+
)}
|
|
597
|
+
</button>
|
|
598
|
+
)}
|
|
599
|
+
|
|
600
|
+
{/* Continue without claiming */}
|
|
601
|
+
{registered && (
|
|
424
602
|
<button
|
|
425
603
|
onClick={next}
|
|
426
|
-
|
|
427
|
-
className="shrink-0 h-12 w-12 flex items-center justify-center rounded-full bg-primary hover:bg-primary/90 text-white transition-colors disabled:opacity-30"
|
|
604
|
+
className="w-full mt-4 py-3 bg-primary hover:bg-primary/90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
|
|
428
605
|
>
|
|
429
|
-
|
|
606
|
+
Continue
|
|
607
|
+
<ArrowRight className="h-4 w-4" />
|
|
430
608
|
</button>
|
|
431
|
-
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{/* Skip if name is taken or not checked yet */}
|
|
612
|
+
{!registered && (handleStatus !== 'available' || botName.length === 0) && botName.length >= 3 && (
|
|
613
|
+
<button
|
|
614
|
+
onClick={next}
|
|
615
|
+
className="w-full mt-4 py-2.5 text-white/30 hover:text-white/50 text-[13px] transition-colors flex items-center justify-center gap-2"
|
|
616
|
+
>
|
|
617
|
+
Skip handle for now
|
|
618
|
+
<ArrowRight className="h-3.5 w-3.5" />
|
|
619
|
+
</button>
|
|
620
|
+
)}
|
|
432
621
|
</div>
|
|
433
622
|
)}
|
|
434
623
|
|
|
@@ -478,7 +667,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
478
667
|
))}
|
|
479
668
|
</div>
|
|
480
669
|
|
|
481
|
-
{/* Divider */}
|
|
482
670
|
<div className="border-t border-white/[0.06] mt-4 mb-3" />
|
|
483
671
|
|
|
484
672
|
{/* ── Auth flow: Anthropic ── */}
|
|
@@ -655,7 +843,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
655
843
|
</>
|
|
656
844
|
)}
|
|
657
845
|
|
|
658
|
-
{/* Continue button */}
|
|
659
846
|
{isConnected && (
|
|
660
847
|
<button
|
|
661
848
|
onClick={next}
|
|
@@ -669,7 +856,7 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
669
856
|
</div>
|
|
670
857
|
)}
|
|
671
858
|
|
|
672
|
-
{/* ── Step 4: Whisper (optional) ── */}
|
|
859
|
+
{/* ── Step 4: Whisper (optional) + Complete ── */}
|
|
673
860
|
{step === 4 && (
|
|
674
861
|
<div>
|
|
675
862
|
<div className="flex items-center gap-2 mb-1">
|
|
@@ -684,7 +871,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
684
871
|
Allow users to send audio messages that are automatically transcribed.
|
|
685
872
|
</p>
|
|
686
873
|
|
|
687
|
-
{/* Whisper card */}
|
|
688
874
|
<button
|
|
689
875
|
onClick={() => setWhisperEnabled((v) => !v)}
|
|
690
876
|
className={`w-full mt-5 rounded-xl border transition-all duration-200 p-4 text-left ${
|
|
@@ -701,7 +887,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
701
887
|
Speech-to-text powered by OpenAI. Requires an OpenAI API key.
|
|
702
888
|
</div>
|
|
703
889
|
</div>
|
|
704
|
-
{/* Toggle */}
|
|
705
890
|
<div className={`w-10 h-[22px] rounded-full transition-colors duration-200 flex items-center px-0.5 shrink-0 ${
|
|
706
891
|
whisperEnabled ? 'bg-primary' : 'bg-white/[0.08]'
|
|
707
892
|
}`}>
|
|
@@ -723,7 +908,6 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
723
908
|
</div>
|
|
724
909
|
)}
|
|
725
910
|
|
|
726
|
-
{/* Actions */}
|
|
727
911
|
<button
|
|
728
912
|
onClick={handleComplete}
|
|
729
913
|
disabled={saving}
|