fluxy-bot 0.1.10 → 0.1.12
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/client/src/components/Onboard/OnboardWizard.tsx +384 -126
- package/dist/assets/index-CQ58CZb9.css +1 -0
- package/dist/assets/index-sq7pCwXB.js +64 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/package.json +1 -1
- package/shared/relay.ts +19 -1
- package/worker/index.ts +77 -2
- package/dist/assets/index-CX_6AQUX.css +0 -1
- package/dist/assets/index-DVzlk0FS.js +0 -64
|
@@ -32,7 +32,7 @@ const MODELS: Record<string, { id: string; label: string }[]> = {
|
|
|
32
32
|
],
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const TOTAL_STEPS =
|
|
35
|
+
const TOTAL_STEPS = 6; // 0..5
|
|
36
36
|
|
|
37
37
|
const HANDLES = [
|
|
38
38
|
{ tier: 'premium', label: (n: string) => `${n}.fluxy.bot`, badge: '$5', badgeCls: 'bg-primary/15 text-primary border-primary/20', highlight: true },
|
|
@@ -125,19 +125,52 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
125
125
|
|
|
126
126
|
// Bot name + Handle (step 2)
|
|
127
127
|
const [botName, setBotName] = useState('');
|
|
128
|
-
const [handleStatus, setHandleStatus] = useState<null | 'checking' | '
|
|
128
|
+
const [handleStatus, setHandleStatus] = useState<null | 'checking' | 'ready' | 'invalid'>(null);
|
|
129
129
|
const [handleError, setHandleError] = useState('');
|
|
130
|
+
const [tierAvailability, setTierAvailability] = useState<Record<string, boolean>>({});
|
|
130
131
|
const [selectedTier, setSelectedTier] = useState('at');
|
|
131
132
|
const [registering, setRegistering] = useState(false);
|
|
132
133
|
const [registered, setRegistered] = useState(false);
|
|
133
134
|
const [registeredUrl, setRegisteredUrl] = useState('');
|
|
134
135
|
const handleDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
135
136
|
|
|
136
|
-
//
|
|
137
|
+
// Existing handle (for re-run / change flow)
|
|
138
|
+
const [existingHandle, setExistingHandle] = useState<{ username: string; tier: string; url: string } | null>(null);
|
|
139
|
+
const [showChangeConfirm, setShowChangeConfirm] = useState(false);
|
|
140
|
+
const [changingHandle, setChangingHandle] = useState(false);
|
|
141
|
+
|
|
142
|
+
// Portal credentials (step 3)
|
|
143
|
+
const [portalUser, setPortalUser] = useState('');
|
|
144
|
+
const [portalPass, setPortalPass] = useState('');
|
|
145
|
+
const [portalPassConfirm, setPortalPassConfirm] = useState('');
|
|
146
|
+
const [portalCopied, setPortalCopied] = useState(false);
|
|
147
|
+
const [portalExists, setPortalExists] = useState(false);
|
|
148
|
+
|
|
149
|
+
// Whisper (step 5)
|
|
137
150
|
const [whisperEnabled, setWhisperEnabled] = useState(false);
|
|
151
|
+
const [whisperKey, setWhisperKey] = useState('');
|
|
138
152
|
|
|
139
153
|
const isConnected = authState[provider] === 'connected';
|
|
140
154
|
|
|
155
|
+
// Pre-fill from existing settings (re-run wizard)
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
Promise.all([
|
|
158
|
+
fetch('/api/settings').then((r) => r.json()),
|
|
159
|
+
fetch('/api/handle/status').then((r) => r.json()),
|
|
160
|
+
]).then(([settings, handle]) => {
|
|
161
|
+
if (settings.user_name) setUserName(settings.user_name);
|
|
162
|
+
if (settings.portal_user) { setPortalUser(settings.portal_user); setPortalExists(true); }
|
|
163
|
+
if (settings.whisper_key) { setWhisperEnabled(true); setWhisperKey(settings.whisper_key); }
|
|
164
|
+
if (handle.registered && handle.username) {
|
|
165
|
+
setBotName(handle.username);
|
|
166
|
+
setSelectedTier(handle.tier || 'at');
|
|
167
|
+
setExistingHandle({ username: handle.username, tier: handle.tier, url: handle.url });
|
|
168
|
+
setRegistered(true);
|
|
169
|
+
setRegisteredUrl(handle.url);
|
|
170
|
+
}
|
|
171
|
+
}).catch(() => {});
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
141
174
|
// Check if Claude is already authenticated when selecting Anthropic
|
|
142
175
|
useEffect(() => {
|
|
143
176
|
if (provider !== 'anthropic' || authState.anthropic === 'connected') return;
|
|
@@ -176,11 +209,12 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
176
209
|
return () => clearInterval(interval);
|
|
177
210
|
}, [openaiWaiting]);
|
|
178
211
|
|
|
179
|
-
// Handle availability check (debounced)
|
|
212
|
+
// Handle availability check (debounced, per-tier)
|
|
180
213
|
useEffect(() => {
|
|
181
214
|
if (handleDebounce.current) clearTimeout(handleDebounce.current);
|
|
182
215
|
setHandleStatus(null);
|
|
183
216
|
setHandleError('');
|
|
217
|
+
setTierAvailability({});
|
|
184
218
|
setRegistered(false);
|
|
185
219
|
setRegisteredUrl('');
|
|
186
220
|
|
|
@@ -200,10 +234,18 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
200
234
|
if (!data.valid) {
|
|
201
235
|
setHandleStatus('invalid');
|
|
202
236
|
setHandleError(data.error);
|
|
203
|
-
} else if (data.available) {
|
|
204
|
-
setHandleStatus('available');
|
|
205
237
|
} else {
|
|
206
|
-
|
|
238
|
+
const avail: Record<string, boolean> = {};
|
|
239
|
+
for (const h of data.handles) {
|
|
240
|
+
avail[h.tier] = h.available;
|
|
241
|
+
}
|
|
242
|
+
setTierAvailability(avail);
|
|
243
|
+
setHandleStatus('ready');
|
|
244
|
+
// Auto-select first available tier
|
|
245
|
+
const firstAvailable = HANDLES.find((h) => avail[h.tier]);
|
|
246
|
+
if (firstAvailable && !avail[selectedTier]) {
|
|
247
|
+
setSelectedTier(firstAvailable.tier);
|
|
248
|
+
}
|
|
207
249
|
}
|
|
208
250
|
} catch {
|
|
209
251
|
setHandleStatus(null);
|
|
@@ -218,7 +260,7 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
218
260
|
};
|
|
219
261
|
|
|
220
262
|
const onClaimHandle = async () => {
|
|
221
|
-
if (!botName || handleStatus !== '
|
|
263
|
+
if (!botName || handleStatus !== 'ready' || !tierAvailability[selectedTier]) return;
|
|
222
264
|
setRegistering(true);
|
|
223
265
|
try {
|
|
224
266
|
const res = await fetch('/api/handle/register', {
|
|
@@ -242,6 +284,33 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
242
284
|
}
|
|
243
285
|
};
|
|
244
286
|
|
|
287
|
+
const onChangeHandle = async () => {
|
|
288
|
+
if (!botName || handleStatus !== 'ready' || !tierAvailability[selectedTier]) return;
|
|
289
|
+
setChangingHandle(true);
|
|
290
|
+
try {
|
|
291
|
+
const res = await fetch('/api/handle/change', {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: { 'Content-Type': 'application/json' },
|
|
294
|
+
body: JSON.stringify({ username: botName, tier: selectedTier }),
|
|
295
|
+
});
|
|
296
|
+
const data = await res.json();
|
|
297
|
+
if (data.ok) {
|
|
298
|
+
setRegistered(true);
|
|
299
|
+
setRegisteredUrl(data.url);
|
|
300
|
+
setExistingHandle({ username: botName, tier: selectedTier, url: data.url });
|
|
301
|
+
setShowChangeConfirm(false);
|
|
302
|
+
} else {
|
|
303
|
+
setHandleError(data.error || 'Handle change failed');
|
|
304
|
+
setHandleStatus('invalid');
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
setHandleError('Could not reach server');
|
|
308
|
+
setHandleStatus('invalid');
|
|
309
|
+
} finally {
|
|
310
|
+
setChangingHandle(false);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
245
314
|
const handleProviderChange = (id: string) => {
|
|
246
315
|
if (provider === 'openai' && id !== 'openai' && openaiWaiting) {
|
|
247
316
|
fetch('/api/auth/codex/cancel', { method: 'POST' });
|
|
@@ -347,14 +416,19 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
347
416
|
|
|
348
417
|
/* ── Navigation ── */
|
|
349
418
|
|
|
350
|
-
// Steps: 0=Welcome, 1=Name, 2=Bot name + Handle, 3=
|
|
419
|
+
// Steps: 0=Welcome, 1=Name, 2=Bot name + Handle, 3=Portal credentials, 4=Provider, 5=Whisper+Complete
|
|
420
|
+
const portalPassMatch = portalPass === portalPassConfirm;
|
|
421
|
+
const portalValid = portalUser.trim().length >= 3 && portalPass.length >= 6 && portalPassMatch;
|
|
422
|
+
const portalCanContinue = portalValid || (portalExists && portalPass.length === 0);
|
|
423
|
+
|
|
351
424
|
const canNext = (() => {
|
|
352
425
|
switch (step) {
|
|
353
426
|
case 0: return true;
|
|
354
427
|
case 1: return userName.trim().length > 0;
|
|
355
428
|
case 2: return botName.trim().length >= 3; // Must have a bot name
|
|
356
|
-
case 3: return
|
|
357
|
-
case 4: return
|
|
429
|
+
case 3: return portalCanContinue;
|
|
430
|
+
case 4: return !!(provider && model && isConnected);
|
|
431
|
+
case 5: return true;
|
|
358
432
|
default: return false;
|
|
359
433
|
}
|
|
360
434
|
})();
|
|
@@ -380,6 +454,9 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
380
454
|
apiKey: '',
|
|
381
455
|
baseUrl: provider === 'ollama' ? baseUrl || undefined : undefined,
|
|
382
456
|
whisperEnabled,
|
|
457
|
+
whisperKey: whisperEnabled ? whisperKey : '',
|
|
458
|
+
portalUser: portalUser.trim(),
|
|
459
|
+
portalPass,
|
|
383
460
|
}),
|
|
384
461
|
});
|
|
385
462
|
onComplete();
|
|
@@ -492,135 +569,301 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
492
569
|
This is your bot's name and permanent handle — access it from anywhere.
|
|
493
570
|
</p>
|
|
494
571
|
|
|
495
|
-
{/*
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
572
|
+
{/* Existing handle banner */}
|
|
573
|
+
{existingHandle && registered && !showChangeConfirm && (
|
|
574
|
+
<>
|
|
575
|
+
<div className="mt-4 bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
|
|
576
|
+
<div className="flex items-center gap-2">
|
|
577
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
578
|
+
<p className="text-emerald-400/90 text-[13px] font-medium">Current handle</p>
|
|
579
|
+
</div>
|
|
580
|
+
<p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
|
|
581
|
+
</div>
|
|
582
|
+
<div className="flex gap-2 mt-4">
|
|
583
|
+
<button
|
|
584
|
+
onClick={next}
|
|
585
|
+
className="flex-1 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"
|
|
586
|
+
>
|
|
587
|
+
Continue
|
|
588
|
+
<ArrowRight className="h-4 w-4" />
|
|
589
|
+
</button>
|
|
590
|
+
<button
|
|
591
|
+
onClick={() => {
|
|
592
|
+
setShowChangeConfirm(true);
|
|
593
|
+
setRegistered(false);
|
|
594
|
+
setBotName('');
|
|
595
|
+
setHandleStatus(null);
|
|
596
|
+
setTierAvailability({});
|
|
597
|
+
}}
|
|
598
|
+
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"
|
|
599
|
+
>
|
|
600
|
+
Change
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
</>
|
|
604
|
+
)}
|
|
605
|
+
|
|
606
|
+
{/* Change confirmation alert */}
|
|
607
|
+
{showChangeConfirm && !registered && (
|
|
608
|
+
<div className="mt-4 bg-amber-500/8 border border-amber-500/20 rounded-xl px-4 py-3">
|
|
609
|
+
<p className="text-amber-400/90 text-[13px] font-medium">Changing your handle</p>
|
|
610
|
+
<p className="text-amber-400/60 text-[12px] mt-1">
|
|
611
|
+
Your current handle <span className="font-mono">{existingHandle?.url}</span> will be released and become available for others.
|
|
612
|
+
</p>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
616
|
+
{/* Input — shown for new claim or change flow */}
|
|
617
|
+
{(!existingHandle || showChangeConfirm || !registered) && !(existingHandle && registered && !showChangeConfirm) && (
|
|
618
|
+
<>
|
|
619
|
+
<div className="relative mt-5">
|
|
620
|
+
<input
|
|
621
|
+
type="text"
|
|
622
|
+
value={botName}
|
|
623
|
+
onChange={(e) => onBotNameInput(e.target.value)}
|
|
624
|
+
maxLength={30}
|
|
625
|
+
placeholder="your-bot-name"
|
|
626
|
+
spellCheck={false}
|
|
627
|
+
autoCapitalize="none"
|
|
628
|
+
autoCorrect="off"
|
|
629
|
+
autoFocus
|
|
630
|
+
disabled={registered}
|
|
631
|
+
className={inputCls + ' pr-10 font-mono' + (registered ? ' opacity-50' : '')}
|
|
632
|
+
/>
|
|
633
|
+
{handleStatus && botName.length > 0 && !registered && (
|
|
634
|
+
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
|
635
|
+
{handleStatus === 'checking' && (
|
|
636
|
+
<div className="w-5 h-5 border-2 border-white/10 border-t-primary rounded-full animate-spin" />
|
|
637
|
+
)}
|
|
638
|
+
{handleStatus === 'invalid' && (
|
|
639
|
+
<div className="w-6 h-6 rounded-full bg-amber-500/15 flex items-center justify-center">
|
|
640
|
+
<svg className="w-3.5 h-3.5 text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
|
|
641
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3m0 4h.01" />
|
|
642
|
+
</svg>
|
|
643
|
+
</div>
|
|
644
|
+
)}
|
|
532
645
|
</div>
|
|
533
646
|
)}
|
|
534
647
|
</div>
|
|
535
|
-
)}
|
|
536
|
-
</div>
|
|
537
648
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
{handleStatus === 'taken' && (
|
|
543
|
-
<p className="text-red-400 text-[12px] mt-2">This name is already taken</p>
|
|
544
|
-
)}
|
|
649
|
+
{/* Status messages */}
|
|
650
|
+
{handleStatus === 'invalid' && handleError && (
|
|
651
|
+
<p className="text-amber-400 text-[12px] mt-2">{handleError}</p>
|
|
652
|
+
)}
|
|
545
653
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
654
|
+
{/* Handle tier options — per-tier availability */}
|
|
655
|
+
{handleStatus === 'ready' && botName.length > 0 && !registered && (
|
|
656
|
+
<div className="space-y-2 mt-4">
|
|
657
|
+
{HANDLES.map((h) => {
|
|
658
|
+
const available = tierAvailability[h.tier];
|
|
659
|
+
const taken = available === false;
|
|
660
|
+
return (
|
|
661
|
+
<button
|
|
662
|
+
key={h.tier}
|
|
663
|
+
onClick={() => !taken && setSelectedTier(h.tier)}
|
|
664
|
+
disabled={taken}
|
|
665
|
+
className={`w-full flex items-center justify-between px-4 py-3 rounded-xl border transition-all duration-200 text-left ${
|
|
666
|
+
taken
|
|
667
|
+
? 'border-white/[0.04] bg-transparent opacity-50 cursor-not-allowed'
|
|
668
|
+
: selectedTier === h.tier
|
|
669
|
+
? h.highlight
|
|
670
|
+
? 'border-primary/40 bg-primary/[0.06]'
|
|
671
|
+
: 'border-primary/30 bg-white/[0.04]'
|
|
672
|
+
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
673
|
+
}`}
|
|
674
|
+
>
|
|
675
|
+
<span className="font-mono text-[13px] text-white/70">
|
|
676
|
+
{h.label(botName)}
|
|
677
|
+
</span>
|
|
678
|
+
{taken ? (
|
|
679
|
+
<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">
|
|
680
|
+
Taken
|
|
681
|
+
</span>
|
|
682
|
+
) : (
|
|
683
|
+
<span className={`text-[11px] font-medium px-2.5 py-0.5 rounded-full border ${h.badgeCls}`}>
|
|
684
|
+
{h.badge}
|
|
685
|
+
</span>
|
|
686
|
+
)}
|
|
687
|
+
</button>
|
|
688
|
+
);
|
|
689
|
+
})}
|
|
690
|
+
</div>
|
|
691
|
+
)}
|
|
692
|
+
|
|
693
|
+
{/* Registered success (after claiming) */}
|
|
694
|
+
{registered && (
|
|
695
|
+
<div className="mt-4 bg-emerald-500/8 border border-emerald-500/15 rounded-xl px-4 py-3">
|
|
696
|
+
<div className="flex items-center gap-2">
|
|
697
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
698
|
+
<p className="text-emerald-400/90 text-[13px] font-medium">Handle claimed!</p>
|
|
699
|
+
</div>
|
|
700
|
+
<p className="text-emerald-400/60 text-[12px] mt-1 font-mono">{registeredUrl}</p>
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
|
|
704
|
+
{/* Claim / Change button */}
|
|
705
|
+
{handleStatus === 'ready' && tierAvailability[selectedTier] && botName.length > 0 && !registered && (
|
|
550
706
|
<button
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
className=
|
|
554
|
-
selectedTier === h.tier
|
|
555
|
-
? h.highlight
|
|
556
|
-
? 'border-primary/40 bg-primary/[0.06]'
|
|
557
|
-
: 'border-primary/30 bg-white/[0.04]'
|
|
558
|
-
: 'border-white/[0.06] bg-transparent hover:border-white/10 hover:bg-white/[0.02]'
|
|
559
|
-
}`}
|
|
707
|
+
onClick={showChangeConfirm ? onChangeHandle : onClaimHandle}
|
|
708
|
+
disabled={registering || changingHandle}
|
|
709
|
+
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"
|
|
560
710
|
>
|
|
561
|
-
|
|
562
|
-
{
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
</span>
|
|
711
|
+
{(registering || changingHandle) ? (
|
|
712
|
+
<><LoaderCircle className="h-4 w-4 animate-spin" />{showChangeConfirm ? 'Changing...' : 'Claiming...'}</>
|
|
713
|
+
) : (
|
|
714
|
+
<>{showChangeConfirm ? 'Change Handle' : 'Claim & Continue'}<ArrowRight className="h-4 w-4" /></>
|
|
715
|
+
)}
|
|
567
716
|
</button>
|
|
568
|
-
)
|
|
569
|
-
</div>
|
|
570
|
-
)}
|
|
717
|
+
)}
|
|
571
718
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
719
|
+
{/* Continue after claim */}
|
|
720
|
+
{registered && (
|
|
721
|
+
<button
|
|
722
|
+
onClick={next}
|
|
723
|
+
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"
|
|
724
|
+
>
|
|
725
|
+
Continue
|
|
726
|
+
<ArrowRight className="h-4 w-4" />
|
|
727
|
+
</button>
|
|
728
|
+
)}
|
|
582
729
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
) : (
|
|
593
|
-
<>Claim & Continue<ArrowRight className="h-4 w-4" /></>
|
|
730
|
+
{/* Skip */}
|
|
731
|
+
{!registered && (!tierAvailability[selectedTier] || handleStatus !== 'ready') && botName.length >= 3 && (
|
|
732
|
+
<button
|
|
733
|
+
onClick={next}
|
|
734
|
+
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"
|
|
735
|
+
>
|
|
736
|
+
Skip handle for now
|
|
737
|
+
<ArrowRight className="h-3.5 w-3.5" />
|
|
738
|
+
</button>
|
|
594
739
|
)}
|
|
595
|
-
|
|
740
|
+
|
|
741
|
+
{/* Cancel change */}
|
|
742
|
+
{showChangeConfirm && !registered && (
|
|
743
|
+
<button
|
|
744
|
+
onClick={() => {
|
|
745
|
+
setShowChangeConfirm(false);
|
|
746
|
+
setBotName(existingHandle!.username);
|
|
747
|
+
setRegistered(true);
|
|
748
|
+
setRegisteredUrl(existingHandle!.url);
|
|
749
|
+
setSelectedTier(existingHandle!.tier);
|
|
750
|
+
setHandleStatus(null);
|
|
751
|
+
}}
|
|
752
|
+
className="w-full mt-2 py-2 text-white/25 hover:text-white/40 text-[12px] transition-colors"
|
|
753
|
+
>
|
|
754
|
+
Cancel — keep current handle
|
|
755
|
+
</button>
|
|
756
|
+
)}
|
|
757
|
+
</>
|
|
596
758
|
)}
|
|
759
|
+
</div>
|
|
760
|
+
)}
|
|
597
761
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
762
|
+
{/* ── Step 3: Portal credentials ── */}
|
|
763
|
+
{step === 3 && (
|
|
764
|
+
<div>
|
|
765
|
+
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
766
|
+
Secure your portal
|
|
767
|
+
</h1>
|
|
768
|
+
<p className="text-white/40 text-[13px] mt-1.5 leading-relaxed">
|
|
769
|
+
Create credentials to protect your dashboard. Anyone with your URL will need these to log in.
|
|
770
|
+
</p>
|
|
771
|
+
|
|
772
|
+
{/* Relay URL display + copy */}
|
|
773
|
+
{(registered && registeredUrl) && (
|
|
774
|
+
<div className="mt-4 flex items-center gap-2 bg-white/[0.03] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
775
|
+
<span className="font-mono text-[13px] text-white/60 truncate flex-1">{registeredUrl}</span>
|
|
776
|
+
<button
|
|
777
|
+
onClick={() => {
|
|
778
|
+
navigator.clipboard.writeText(registeredUrl);
|
|
779
|
+
setPortalCopied(true);
|
|
780
|
+
setTimeout(() => setPortalCopied(false), 2000);
|
|
781
|
+
}}
|
|
782
|
+
className="shrink-0 text-white/30 hover:text-white/60 transition-colors"
|
|
783
|
+
>
|
|
784
|
+
{portalCopied ? (
|
|
785
|
+
<Check className="h-4 w-4 text-emerald-400" />
|
|
786
|
+
) : (
|
|
787
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
788
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
789
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
790
|
+
</svg>
|
|
791
|
+
)}
|
|
792
|
+
</button>
|
|
793
|
+
</div>
|
|
607
794
|
)}
|
|
608
795
|
|
|
609
|
-
{/*
|
|
610
|
-
{
|
|
611
|
-
<
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
>
|
|
615
|
-
Skip handle for now
|
|
616
|
-
<ArrowRight className="h-3.5 w-3.5" />
|
|
617
|
-
</button>
|
|
796
|
+
{/* Already configured hint */}
|
|
797
|
+
{portalExists && (
|
|
798
|
+
<div className="mt-4 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-2.5">
|
|
799
|
+
<p className="text-white/40 text-[12px]">Credentials already set. Leave password fields empty to keep your current password.</p>
|
|
800
|
+
</div>
|
|
618
801
|
)}
|
|
802
|
+
|
|
803
|
+
{/* Username */}
|
|
804
|
+
<div className="mt-5">
|
|
805
|
+
<label className="text-[12px] text-white/40 font-medium mb-1.5 block">Username</label>
|
|
806
|
+
<input
|
|
807
|
+
type="text"
|
|
808
|
+
value={portalUser}
|
|
809
|
+
onChange={(e) => setPortalUser(e.target.value.replace(/\s/g, '').toLowerCase())}
|
|
810
|
+
placeholder="admin"
|
|
811
|
+
autoFocus
|
|
812
|
+
autoComplete="username"
|
|
813
|
+
className={inputCls + ' font-mono'}
|
|
814
|
+
/>
|
|
815
|
+
{portalUser.length > 0 && portalUser.trim().length < 3 && (
|
|
816
|
+
<p className="text-amber-400/70 text-[11px] mt-1">At least 3 characters</p>
|
|
817
|
+
)}
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
{/* Password */}
|
|
821
|
+
<div className="mt-3">
|
|
822
|
+
<label className="text-[12px] text-white/40 font-medium mb-1.5 block">Password</label>
|
|
823
|
+
<input
|
|
824
|
+
type="password"
|
|
825
|
+
value={portalPass}
|
|
826
|
+
onChange={(e) => setPortalPass(e.target.value)}
|
|
827
|
+
placeholder="••••••••"
|
|
828
|
+
autoComplete="new-password"
|
|
829
|
+
onKeyDown={handleKeyDown}
|
|
830
|
+
className={inputCls}
|
|
831
|
+
/>
|
|
832
|
+
{portalPass.length > 0 && portalPass.length < 6 && (
|
|
833
|
+
<p className="text-amber-400/70 text-[11px] mt-1">At least 6 characters</p>
|
|
834
|
+
)}
|
|
835
|
+
</div>
|
|
836
|
+
|
|
837
|
+
{/* Confirm password */}
|
|
838
|
+
<div className="mt-3">
|
|
839
|
+
<label className="text-[12px] text-white/40 font-medium mb-1.5 block">Confirm password</label>
|
|
840
|
+
<input
|
|
841
|
+
type="password"
|
|
842
|
+
value={portalPassConfirm}
|
|
843
|
+
onChange={(e) => setPortalPassConfirm(e.target.value)}
|
|
844
|
+
placeholder="••••••••"
|
|
845
|
+
autoComplete="new-password"
|
|
846
|
+
onKeyDown={handleKeyDown}
|
|
847
|
+
className={inputCls}
|
|
848
|
+
/>
|
|
849
|
+
{portalPassConfirm.length > 0 && !portalPassMatch && (
|
|
850
|
+
<p className="text-red-400/70 text-[11px] mt-1">Passwords don't match</p>
|
|
851
|
+
)}
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<button
|
|
855
|
+
onClick={next}
|
|
856
|
+
disabled={!canNext}
|
|
857
|
+
className="w-full mt-5 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"
|
|
858
|
+
>
|
|
859
|
+
Continue
|
|
860
|
+
<ArrowRight className="h-4 w-4" />
|
|
861
|
+
</button>
|
|
619
862
|
</div>
|
|
620
863
|
)}
|
|
621
864
|
|
|
622
|
-
{/* ── Step
|
|
623
|
-
{step ===
|
|
865
|
+
{/* ── Step 4: Provider + Auth + Model ── */}
|
|
866
|
+
{step === 4 && (
|
|
624
867
|
<div>
|
|
625
868
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
626
869
|
Choose your AI provider
|
|
@@ -854,8 +1097,8 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
854
1097
|
</div>
|
|
855
1098
|
)}
|
|
856
1099
|
|
|
857
|
-
{/* ── Step
|
|
858
|
-
{step ===
|
|
1100
|
+
{/* ── Step 5: Whisper (optional) + Complete ── */}
|
|
1101
|
+
{step === 5 && (
|
|
859
1102
|
<div>
|
|
860
1103
|
<div className="flex items-center gap-2 mb-1">
|
|
861
1104
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
|
@@ -896,8 +1139,23 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
896
1139
|
</button>
|
|
897
1140
|
|
|
898
1141
|
{whisperEnabled && (
|
|
899
|
-
<div className="mt-3
|
|
900
|
-
<
|
|
1142
|
+
<div className="mt-3">
|
|
1143
|
+
<label className="text-[12px] text-white/40 font-medium mb-1.5 block">OpenAI API Key</label>
|
|
1144
|
+
<input
|
|
1145
|
+
type="password"
|
|
1146
|
+
value={whisperKey}
|
|
1147
|
+
onChange={(e) => setWhisperKey(e.target.value.trim())}
|
|
1148
|
+
placeholder="sk-..."
|
|
1149
|
+
autoComplete="off"
|
|
1150
|
+
className={inputCls + ' font-mono text-[13px]'}
|
|
1151
|
+
/>
|
|
1152
|
+
{whisperKey.length > 0 && !whisperKey.startsWith('sk-') && (
|
|
1153
|
+
<p className="text-amber-400/70 text-[11px] mt-1">Key should start with sk-</p>
|
|
1154
|
+
)}
|
|
1155
|
+
{whisperKey.length > 0 && whisperKey.startsWith('sk-') && whisperKey.length < 20 && (
|
|
1156
|
+
<p className="text-amber-400/70 text-[11px] mt-1">Key looks too short</p>
|
|
1157
|
+
)}
|
|
1158
|
+
<div className="flex items-start gap-2.5 mt-3 bg-white/[0.02] border border-white/[0.06] rounded-xl px-4 py-3">
|
|
901
1159
|
<Mic className="h-4 w-4 text-primary/60 mt-0.5 shrink-0" />
|
|
902
1160
|
<p className="text-white/35 text-[12px] leading-relaxed">
|
|
903
1161
|
Users will see a microphone button in the chat. Audio is sent to OpenAI's Whisper API for transcription, then processed as a regular text message.
|
|
@@ -908,7 +1166,7 @@ export default function OnboardWizard({ onComplete }: Props) {
|
|
|
908
1166
|
|
|
909
1167
|
<button
|
|
910
1168
|
onClick={handleComplete}
|
|
911
|
-
disabled={saving}
|
|
1169
|
+
disabled={saving || (whisperEnabled && (!whisperKey.startsWith('sk-') || whisperKey.length < 20))}
|
|
912
1170
|
className="w-full mt-5 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"
|
|
913
1171
|
>
|
|
914
1172
|
{saving ? (
|