From 4281ce35b4c3cecb30c0c57ffeda248090b495f8 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 3 Apr 2026 21:38:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(saas):=20remove=20hardcoded=20model=20fallb?= =?UTF-8?q?ack=20=E2=80=94=20dynamic=20from=20available=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - service.rs: template model passed as-is (Option), no hardcoded fallback - saas-types.ts: AgentConfigFromTemplate.model → string | null - agentStore.ts: when model is null, resolve from saasStore.availableModels[0] - AgentOnboardingWizard.tsx: restore full file (was corrupted), apply assignTemplate try/catch fix --- .../src/components/AgentOnboardingWizard.tsx | 754 ++++++++++++++++++ desktop/src/lib/saas-types.ts | 2 +- desktop/src/store/agentStore.ts | 10 +- 3 files changed, 763 insertions(+), 3 deletions(-) diff --git a/desktop/src/components/AgentOnboardingWizard.tsx b/desktop/src/components/AgentOnboardingWizard.tsx index c9172be..1c7dfa1 100644 --- a/desktop/src/components/AgentOnboardingWizard.tsx +++ b/desktop/src/components/AgentOnboardingWizard.tsx @@ -1,3 +1,140 @@ +/** + * AgentOnboardingWizard - Guided Agent creation wizard + * + * A 5-step wizard for creating new Agents with personality settings. + * Inspired by OpenClaw's quick configuration modal. + */ +import { useState, useCallback, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + X, + User, + Bot, + Sparkles, + Briefcase, + Folder, + ChevronLeft, + ChevronRight, + Check, + Loader2, + AlertCircle, + LayoutGrid, +} from 'lucide-react'; +import { cn } from '../lib/utils'; +import { useAgentStore, type CloneCreateOptions } from '../store/agentStore'; +import { EmojiPicker } from './ui/EmojiPicker'; +import { PersonalitySelector } from './PersonalitySelector'; +import { ScenarioTags } from './ScenarioTags'; +import type { Clone } from '../store/agentStore'; +import { intelligenceClient } from '../lib/intelligence-client'; +import { generateSoulContent, generateUserContent } from '../lib/personality-presets'; +import { createLogger } from '../lib/logger'; +import { + type AgentTemplateAvailable, + type AgentTemplateFull, + saasClient, +} from '../lib/saas-client'; +import { useSaaSStore } from '../store/saasStore'; + +const log = createLogger('AgentOnboardingWizard'); + +// === Types === + +interface WizardFormData { + userName: string; + userRole: string; + agentName: string; + agentRole: string; + agentNickname: string; + emoji: string; + personality: string; + scenarios: string[]; + workspaceDir: string; + restrictFiles: boolean; + privacyOptIn: boolean; + notes: string; +} + +interface AgentOnboardingWizardProps { + isOpen: boolean; + onClose: () => void; + onSuccess?: (clone: Clone) => void; +} + +const initialFormData: WizardFormData = { + userName: '', + userRole: '', + agentName: '', + agentRole: '', + agentNickname: '', + emoji: '', + personality: '', + scenarios: [], + workspaceDir: '', + restrictFiles: true, + privacyOptIn: false, + notes: '', +}; + +// === Step Configuration === + +const steps = [ + { id: 0, title: '行业模板', description: '选择预设或自定义', icon: LayoutGrid }, + { id: 1, title: '认识用户', description: '让我们了解一下您', icon: User }, + { id: 2, title: 'Agent 身份', description: '给助手起个名字', icon: Bot }, + { id: 3, title: '人格风格', description: '选择沟通风格', icon: Sparkles }, + { id: 4, title: '使用场景', description: '选择应用场景', icon: Briefcase }, + { id: 5, title: '工作环境', description: '配置工作目录', icon: Folder }, +]; + +// === Component === + +export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboardingWizardProps) { + const { createClone, createFromTemplate, updateClone, clones, isLoading, error, clearError } = useAgentStore(); + const availableTemplates = useSaaSStore((s) => s.availableTemplates); + const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState(initialFormData); + const [errors, setErrors] = useState>({}); + const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [selectedTemplate, setSelectedTemplate] = useState(null); + + // Reset form when modal opens + useEffect(() => { + if (isOpen) { + setFormData(initialFormData); + setCurrentStep(0); + setErrors({}); + setSubmitStatus('idle'); + setSelectedTemplate(null); + clearError(); + } + }, [isOpen, clearError]); + + // Update form field + const updateField = (field: K, value: WizardFormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[field]; + return newErrors; + }); + } + }; + + // Handle template selection + const handleSelectTemplate = async (t: AgentTemplateAvailable) => { + try { + const full = await saasClient.fetchTemplateFull(t.id); + setSelectedTemplate(full); + setFormData(prev => ({ + ...prev, + agentName: full.name, + emoji: full.emoji || prev.emoji, + personality: full.personality || prev.personality, + scenarios: full.scenarios.length > 0 ? full.scenarios : prev.scenarios, + })); + // Persist template assignment to SaaS backend (fire-and-forget, let wizard continue with fallback flow) try { await useSaaSStore.getState().assignTemplate(t.id); @@ -11,3 +148,620 @@ setCurrentStep(1); } }; + + // Validate current step + const validateStep = useCallback((step: number): boolean => { + const newErrors: Record = {}; + + switch (step) { + case 0: + // Template selection is always valid (blank agent is an option) + break; + case 1: + if (!formData.userName.trim()) { + newErrors.userName = '请输入您的名字'; + } + break; + case 2: + if (!formData.agentName.trim()) { + newErrors.agentName = '请输入 Agent 名称'; + } + break; + case 3: + if (!formData.emoji) { + newErrors.emoji = '请选择一个 Emoji'; + } + if (!formData.personality) { + newErrors.personality = '请选择一个人格风格'; + } + break; + case 4: + if (formData.scenarios.length === 0) { + newErrors.scenarios = '请至少选择一个使用场景'; + } + break; + case 5: + // Optional step, no validation + break; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }, [formData]); + + // Navigate to next step + const nextStep = () => { + if (validateStep(currentStep)) { + setCurrentStep((prev) => Math.min(prev + 1, steps.length)); + } + }; + + // Navigate to previous step + const prevStep = () => { + setCurrentStep((prev) => Math.max(prev - 1, 0)); + }; + + // Handle form submission + const handleSubmit = async () => { + if (!validateStep(currentStep)) { + return; + } + + setSubmitStatus('idle'); + + try { + let clone: Clone | undefined; + + // Template-based creation path + if (selectedTemplate && clones.length === 0) { + clone = await createFromTemplate(selectedTemplate); + + // Persist USER.md for template-created agents + if (clone) { + try { + const userContent = generateUserContent({ + userName: formData.userName, + userRole: formData.userRole, + scenarios: formData.scenarios, + }); + await intelligenceClient.identity.updateFile(clone.id, 'user_profile', userContent); + } catch (err) { + log.warn('Failed to persist USER.md for template agent:', err); + } + } + } else { + // Manual creation / update path + const personalityUpdates = { + name: formData.agentName, + role: formData.agentRole || undefined, + nickname: formData.agentNickname || undefined, + userName: formData.userName, + userRole: formData.userRole || undefined, + scenarios: formData.scenarios, + workspaceDir: formData.workspaceDir || undefined, + restrictFiles: formData.restrictFiles, + emoji: formData.emoji, + personality: formData.personality, + notes: formData.notes || undefined, + }; + + if (clones && clones.length > 0) { + clone = await updateClone(clones[0].id, personalityUpdates); + } else { + const createOptions: CloneCreateOptions = { + ...personalityUpdates, + privacyOptIn: formData.privacyOptIn, + }; + clone = await createClone(createOptions); + } + } + + if (clone) { + // Persist SOUL.md and USER.md to the identity system (manual path only) + if (!selectedTemplate) { + try { + const soulContent = generateSoulContent({ + agentName: formData.agentName, + emoji: formData.emoji, + personality: formData.personality, + scenarios: formData.scenarios, + }); + + const userContent = generateUserContent({ + userName: formData.userName, + userRole: formData.userRole, + scenarios: formData.scenarios, + }); + + await intelligenceClient.identity.updateFile(clone.id, 'soul', soulContent); + await intelligenceClient.identity.updateFile(clone.id, 'user_profile', userContent); + + log.debug('SOUL.md and USER.md persisted for agent:', clone.id); + } catch (err) { + log.warn('Failed to persist identity files:', err); + } + } + + setSubmitStatus('success'); + setTimeout(() => { + onSuccess?.(clone); + onClose(); + }, 1500); + } else { + setSubmitStatus('error'); + } + } catch { + setSubmitStatus('error'); + } + }; + + if (!isOpen) return null; + + const CurrentStepIcon = steps[currentStep]?.icon || Bot; + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+
+
+ +
+
+

+ 创建新 Agent +

+

+ 步骤 {currentStep + 1}/{steps.length}: {steps[currentStep]?.title} +

+
+
+ +
+ + {/* Progress Bar */} +
+
+ {steps.map((step, index) => { + const StepIcon = step.icon; + const isActive = currentStep === step.id; + const isCompleted = currentStep > step.id; + return ( +
+ + {index < steps.length - 1 && ( +
+ )} +
+ ); + })} +
+
+ + {/* Content */} +
+ + + {/* Step 0: 行业模板 */} + {currentStep === 0 && ( +
+

选择一个行业预设快速开始,或创建空白 Agent

+
+ + {(availableTemplates ?? []).map(t => ( + + ))} +
+
+ )} + + {/* Step 1: 认识用户 */} + {currentStep === 1 && ( + <> +
+

+ 让我们认识一下 +

+

+ 请告诉我们您的名字,让助手更好地为您服务 +

+
+ +
+ + updateField('userName', e.target.value)} + placeholder="例如:张三" + className={cn( + 'w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary', + errors.userName ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' + )} + /> + {errors.userName && ( +

+ + {errors.userName} +

+ )} +
+ +
+ + updateField('userRole', e.target.value)} + placeholder="例如:产品经理、开发工程师" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ + )} + + {/* Step 2: Agent 身份 */} + {currentStep === 2 && ( + <> +
+

+ 给您的助手起个名字 +

+

+ 这将是您助手的身份标识 +

+
+ +
+ + updateField('agentName', e.target.value)} + placeholder="例如:小龙助手" + className={cn( + 'w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary', + errors.agentName ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' + )} + /> + {errors.agentName && ( +

+ + {errors.agentName} +

+ )} +
+ +
+ + updateField('agentRole', e.target.value)} + placeholder="例如:编程助手、写作顾问" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ +
+ + updateField('agentNickname', e.target.value)} + placeholder="例如:小龙" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ + )} + + {/* Step 3: 人格风格 */} + {currentStep === 3 && ( + <> +
+

+ 选择人格风格 +

+

+ 这决定了助手的沟通方式和性格特点 +

+
+ +
+ + updateField('emoji', emoji)} + /> + {errors.emoji && ( +

+ + {errors.emoji} +

+ )} +
+ +
+ + updateField('personality', personality)} + /> + {errors.personality && ( +

+ + {errors.personality} +

+ )} +
+ + )} + + {/* Step 4: 使用场景 */} + {currentStep === 4 && ( + <> +
+

+ 选择使用场景 +

+

+ 选择您希望 Agent 协助的领域(最多5个) +

+
+ + updateField('scenarios', scenarios)} + maxSelections={5} + /> + {errors.scenarios && ( +

+ + {errors.scenarios} +

+ )} + + )} + + {/* Step 5: 工作环境 */} + {currentStep === 5 && ( + <> +
+

+ 配置工作环境 +

+

+ 设置 Agent 的工作目录和权限 +

+
+ +
+ + updateField('workspaceDir', e.target.value)} + placeholder="例如:/home/user/projects/myproject" + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary font-mono" + /> +

+ Agent 将在此目录下工作,留空则使用默认目录 +

+
+ +
+
+
+

+ 限制文件访问 +

+

+ 仅允许访问工作目录内的文件 +

+
+ +
+
+ +
+ +