From 18dc3bf53ff4007b1548a762f44d2d6e1c590ea0 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Thu, 5 Feb 2026 23:18:43 +0800 Subject: [PATCH] feat(setup): implement functional setup wizard with multi-step flow Add complete setup wizard implementation with the following features: - Welcome step with feature highlights - Environment check step with Node.js, OpenClaw, and Gateway verification - AI Provider selection with API key input and validation UI - Channel connection step with QR code placeholder - Skill bundle selection with recommended bundles pre-selected - Completion summary showing all configured options Additional changes: - Add setupComplete state and markSetupComplete action to settings store - Auto-redirect to setup wizard on first launch - Track setup completion in persisted settings --- build_process/process.md | 3 +- src/App.tsx | 11 +- src/pages/Setup/index.tsx | 707 ++++++++++++++++++++++++++++++++++---- src/stores/settings.ts | 6 + 4 files changed, 649 insertions(+), 78 deletions(-) diff --git a/build_process/process.md b/build_process/process.md index 8c55960b5..e60e4d9d5 100644 --- a/build_process/process.md +++ b/build_process/process.md @@ -7,10 +7,11 @@ ### Completed: * [commit_1] Project skeleton - Electron + React + TypeScript foundation (v0.1.0-alpha) +* [commit_2] Gateway refinements - Auto-reconnection, health checks, better state management ### Plan: 1. ~~Initialize project structure~~ ✅ -2. Add Gateway process management refinements +2. ~~Add Gateway process management refinements~~ ✅ 3. Implement Setup wizard with actual functionality 4. Add Provider configuration (API Key management) 5. Implement Channel connection flows diff --git a/src/App.tsx b/src/App.tsx index 7df80edca..f8998d752 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ * Root Application Component * Handles routing and global providers */ -import { Routes, Route, useNavigate } from 'react-router-dom'; +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom'; import { useEffect } from 'react'; import { Toaster } from 'sonner'; import { MainLayout } from './components/layout/MainLayout'; @@ -18,7 +18,9 @@ import { useGatewayStore } from './stores/gateway'; function App() { const navigate = useNavigate(); + const location = useLocation(); const theme = useSettingsStore((state) => state.theme); + const setupComplete = useSettingsStore((state) => state.setupComplete); const initGateway = useGatewayStore((state) => state.init); // Initialize Gateway connection on mount @@ -26,6 +28,13 @@ function App() { initGateway(); }, [initGateway]); + // Redirect to setup wizard if not complete + useEffect(() => { + if (!setupComplete && !location.pathname.startsWith('/setup')) { + navigate('/setup'); + } + }, [setupComplete, location.pathname, navigate]); + // Listen for navigation events from main process useEffect(() => { const handleNavigate = (...args: unknown[]) => { diff --git a/src/pages/Setup/index.tsx b/src/pages/Setup/index.tsx index 3def98070..2a2df92fc 100644 --- a/src/pages/Setup/index.tsx +++ b/src/pages/Setup/index.tsx @@ -2,12 +2,28 @@ * Setup Wizard Page * First-time setup experience for new users */ -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; -import { Check, ChevronLeft, ChevronRight } from 'lucide-react'; +import { + Check, + ChevronLeft, + ChevronRight, + Loader2, + AlertCircle, + Eye, + EyeOff, + RefreshCw, + CheckCircle2, + XCircle, +} from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; +import { useGatewayStore } from '@/stores/gateway'; +import { useSettingsStore } from '@/stores/settings'; +import { toast } from 'sonner'; interface SetupStep { id: string; @@ -48,17 +64,101 @@ const steps: SetupStep[] = [ }, ]; +// Provider types +interface Provider { + id: string; + name: string; + model: string; + icon: string; + placeholder: string; +} + +const providers: Provider[] = [ + { id: 'anthropic', name: 'Anthropic', model: 'Claude', icon: '🤖', placeholder: 'sk-ant-...' }, + { id: 'openai', name: 'OpenAI', model: 'GPT-4', icon: '💚', placeholder: 'sk-...' }, + { id: 'google', name: 'Google', model: 'Gemini', icon: '🔷', placeholder: 'AI...' }, +]; + +// Channel types +interface Channel { + type: string; + name: string; + icon: string; + description: string; +} + +const channels: Channel[] = [ + { type: 'whatsapp', name: 'WhatsApp', icon: '📱', description: 'Connect via QR code scan' }, + { type: 'telegram', name: 'Telegram', icon: '✈️', description: 'Connect via bot token' }, + { type: 'discord', name: 'Discord', icon: '🎮', description: 'Connect via bot token' }, + { type: 'slack', name: 'Slack', icon: '💼', description: 'Connect via OAuth' }, +]; + +// Skill bundle types +interface SkillBundle { + id: string; + name: string; + icon: string; + description: string; + skills: string[]; + recommended?: boolean; +} + +const skillBundles: SkillBundle[] = [ + { + id: 'productivity', + name: 'Productivity', + icon: '📋', + description: 'Task management, reminders, notes', + skills: ['todo', 'reminder', 'notes', 'calendar'], + recommended: true, + }, + { + id: 'developer', + name: 'Developer', + icon: '💻', + description: 'Code assistance, git, terminal', + skills: ['code-assist', 'git', 'terminal', 'docs'], + recommended: true, + }, + { + id: 'smart-home', + name: 'Smart Home', + icon: '🏠', + description: 'Home automation, IoT control', + skills: ['lights', 'thermostat', 'security', 'iot'], + }, + { + id: 'media', + name: 'Media', + icon: '🎨', + description: 'Image generation, music, video', + skills: ['image-gen', 'music', 'video', 'transcribe'], + }, +]; + export function Setup() { const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(0); + const [canProceed, setCanProceed] = useState(true); + + // Setup state + const [selectedProvider, setSelectedProvider] = useState(null); + const [apiKey, setApiKey] = useState(''); + const [selectedChannel, setSelectedChannel] = useState(null); + const [selectedBundles, setSelectedBundles] = useState>(new Set(['productivity', 'developer'])); const step = steps[currentStep]; const isFirstStep = currentStep === 0; const isLastStep = currentStep === steps.length - 1; - const handleNext = () => { + const markSetupComplete = useSettingsStore((state) => state.markSetupComplete); + + const handleNext = async () => { if (isLastStep) { - // Complete setup and go to dashboard + // Complete setup + markSetupComplete(); + toast.success('Setup complete! Welcome to ClawX'); navigate('/'); } else { setCurrentStep((i) => i + 1); @@ -70,9 +170,34 @@ export function Setup() { }; const handleSkip = () => { + markSetupComplete(); navigate('/'); }; + // Update canProceed based on current step + useEffect(() => { + switch (currentStep) { + case 0: // Welcome + setCanProceed(true); + break; + case 1: // Runtime + // Will be managed by RuntimeContent + break; + case 2: // Provider + setCanProceed(selectedProvider !== null && apiKey.length > 0); + break; + case 3: // Channel + setCanProceed(true); // Channel is optional + break; + case 4: // Skills + setCanProceed(selectedBundles.size > 0); + break; + case 5: // Complete + setCanProceed(true); + break; + } + }, [currentStep, selectedProvider, apiKey, selectedChannel, selectedBundles]); + return (
{/* Progress Indicator */} @@ -126,11 +251,46 @@ export function Setup() { {/* Step-specific content */}
{currentStep === 0 && } - {currentStep === 1 && } - {currentStep === 2 && } - {currentStep === 3 && } - {currentStep === 4 && } - {currentStep === 5 && } + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + {currentStep === 3 && ( + + )} + {currentStep === 4 && ( + { + const newSet = new Set(selectedBundles); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + setSelectedBundles(newSet); + }} + /> + )} + {currentStep === 5 && ( + + )}
{/* Navigation */} @@ -144,17 +304,17 @@ export function Setup() { )}
- {!isLastStep && ( + {!isLastStep && currentStep !== 1 && ( )} - +
Node.js Runtime - ✓ Installed + {renderStatus(checks.nodejs.status, checks.nodejs.message)}
OpenClaw Package - ✓ Ready + {renderStatus(checks.openclaw.status, checks.openclaw.message)}
- Gateway Service - ✓ Running +
+ Gateway Service + {checks.gateway.status === 'error' && ( + + )} +
+ {renderStatus(checks.gateway.status, checks.gateway.message)}
+ + {(checks.nodejs.status === 'error' || checks.openclaw.status === 'error') && ( +
+
+ +
+

Environment issue detected

+

+ Please ensure Node.js is installed and OpenClaw is properly set up. +

+
+
+
+ )} ); } -function ProviderContent() { +interface ProviderContentProps { + providers: Provider[]; + selectedProvider: string | null; + onSelectProvider: (id: string | null) => void; + apiKey: string; + onApiKeyChange: (key: string) => void; +} + +function ProviderContent({ + providers, + selectedProvider, + onSelectProvider, + apiKey, + onApiKeyChange +}: ProviderContentProps) { + const [showKey, setShowKey] = useState(false); + const [validating, setValidating] = useState(false); + const [keyValid, setKeyValid] = useState(null); + + const selectedProviderData = providers.find((p) => p.id === selectedProvider); + + const handleValidateKey = async () => { + if (!apiKey || !selectedProvider) return; + + setValidating(true); + setKeyValid(null); + + // Simulate API key validation + await new Promise((resolve) => setTimeout(resolve, 1500)); + + // Basic validation - just check format + const isValid = apiKey.length > 10; + setKeyValid(isValid); + setValidating(false); + + if (isValid) { + toast.success('API key validated successfully'); + } else { + toast.error('Invalid API key format'); + } + }; + return ( -
-

Select AI Provider

-

- Choose your preferred AI model provider -

+
+
+

Select AI Provider

+

+ Choose your preferred AI model provider +

+
+
- {[ - { id: 'anthropic', name: 'Anthropic', model: 'Claude', icon: '🤖' }, - { id: 'openai', name: 'OpenAI', model: 'GPT-4', icon: '💚' }, - { id: 'google', name: 'Google', model: 'Gemini', icon: '🔷' }, - ].map((provider) => ( + {providers.map((provider) => ( ))}
+ + {selectedProvider && ( + +
+ +
+
+ { + onApiKeyChange(e.target.value); + setKeyValid(null); + }} + className="pr-10 bg-white/5 border-white/10" + /> + +
+ +
+ {keyValid !== null && ( +

+ {keyValid ? '✓ API key is valid' : '✗ Invalid API key'} +

+ )} +
+ +

+ Your API key will be securely stored in the system keychain. +

+
+ )}
); } -function ChannelContent() { +interface ChannelContentProps { + channels: Channel[]; + selectedChannel: string | null; + onSelectChannel: (type: string | null) => void; +} + +function ChannelContent({ channels, selectedChannel, onSelectChannel }: ChannelContentProps) { + const [connecting, setConnecting] = useState(false); + const [qrCode, setQrCode] = useState(null); + + const handleConnect = async (channelType: string) => { + onSelectChannel(channelType); + setConnecting(true); + + // Simulate QR code generation for WhatsApp + if (channelType === 'whatsapp') { + await new Promise((resolve) => setTimeout(resolve, 1000)); + // In real app, this would be a real QR code + setQrCode('placeholder'); + } + + setConnecting(false); + }; + return (
-

Connect a Channel

-

- Link a messaging app to start chatting with your AI -

-
- {[ - { type: 'whatsapp', name: 'WhatsApp', icon: '📱' }, - { type: 'telegram', name: 'Telegram', icon: '✈️' }, - { type: 'discord', name: 'Discord', icon: '🎮' }, - { type: 'slack', name: 'Slack', icon: '💼' }, - ].map((channel) => ( - - ))} +
+

Connect a Channel

+

+ Link a messaging app to start chatting with your AI +

+ + {!selectedChannel ? ( +
+ {channels.map((channel) => ( + + ))} +
+ ) : ( + +
+ + {channels.find((c) => c.type === selectedChannel)?.icon} + + + {channels.find((c) => c.type === selectedChannel)?.name} + +
+ + {connecting ? ( +
+ +

Generating QR code...

+
+ ) : selectedChannel === 'whatsapp' && qrCode ? ( +
+
+ {/* Placeholder QR code */} +
+ QR Code Placeholder +
+
+

+ Scan this QR code with WhatsApp to connect +

+
+ ) : ( +
+

+ Follow the instructions to connect your {channels.find((c) => c.type === selectedChannel)?.name} account. +

+
+ )} + + +
+ )} +

You can add more channels later in Settings

@@ -266,29 +766,43 @@ function ChannelContent() { ); } -function SkillsContent() { +interface SkillsContentProps { + bundles: SkillBundle[]; + selectedBundles: Set; + onToggleBundle: (id: string) => void; +} + +function SkillsContent({ bundles, selectedBundles, onToggleBundle }: SkillsContentProps) { return (
-

Choose Skill Bundles

-

- Select pre-configured skill packages -

+
+

Choose Skill Bundles

+

+ Select pre-configured skill packages to enable +

+
+
- {[ - { id: 'productivity', name: 'Productivity', icon: '📋', recommended: true }, - { id: 'developer', name: 'Developer', icon: '💻', recommended: true }, - { id: 'smart-home', name: 'Smart Home', icon: '🏠' }, - { id: 'media', name: 'Media', icon: '🎨' }, - ].map((bundle) => ( + {bundles.map((bundle) => ( ))}
+ +

+ Selected: {selectedBundles.size} bundle{selectedBundles.size !== 1 ? 's' : ''} +

); } -function CompleteContent() { +interface CompleteContentProps { + selectedProvider: string | null; + selectedChannel: string | null; + selectedBundles: Set; + bundles: SkillBundle[]; +} + +function CompleteContent({ selectedProvider, selectedChannel, selectedBundles, bundles }: CompleteContentProps) { + const gatewayStatus = useGatewayStore((state) => state.status); + + const providerData = providers.find((p) => p.id === selectedProvider); + const channelData = channels.find((c) => c.type === selectedChannel); + const selectedBundleNames = bundles + .filter((b) => selectedBundles.has(b.id)) + .map((b) => b.name) + .join(', '); + return ( -
+
🎉

Setup Complete!

ClawX is configured and ready to use. You can now start chatting with your AI assistant.

-
-

✅ AI Provider configured

-

✅ Channel connected

-

✅ Skills enabled

-

✅ Gateway running

+ +
+
+ AI Provider + + {providerData ? `${providerData.icon} ${providerData.name}` : '—'} + +
+
+ Channel + + {channelData ? `${channelData.icon} ${channelData.name}` : 'Skipped'} + +
+
+ Skills + + {selectedBundleNames || 'None selected'} + +
+
+ Gateway + + {gatewayStatus.state === 'running' ? '✓ Running' : gatewayStatus.state} + +
); diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 27da3de0b..4d0361724 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -28,6 +28,9 @@ interface SettingsState { sidebarCollapsed: boolean; devModeUnlocked: boolean; + // Setup + setupComplete: boolean; + // Actions setTheme: (theme: Theme) => void; setLanguage: (language: string) => void; @@ -40,6 +43,7 @@ interface SettingsState { setAutoDownloadUpdate: (value: boolean) => void; setSidebarCollapsed: (value: boolean) => void; setDevModeUnlocked: (value: boolean) => void; + markSetupComplete: () => void; resetSettings: () => void; } @@ -55,6 +59,7 @@ const defaultSettings = { autoDownloadUpdate: false, sidebarCollapsed: false, devModeUnlocked: false, + setupComplete: false, }; export const useSettingsStore = create()( @@ -73,6 +78,7 @@ export const useSettingsStore = create()( setAutoDownloadUpdate: (autoDownloadUpdate) => set({ autoDownloadUpdate }), setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }), setDevModeUnlocked: (devModeUnlocked) => set({ devModeUnlocked }), + markSetupComplete: () => set({ setupComplete: true }), resetSettings: () => set(defaultSettings), }), {