import { useEffect, useMemo, useRef, useState, type ChangeEvent } from 'react'; import { Search, Trash2, Upload } from 'lucide-react'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { prepareAgentAvatar } from '@/lib/agent-avatar-upload'; import { agentAvatarOptions } from '@/lib/agent-avatars'; import { codingModelKey, type CodingModelOption } from '@/lib/coding-model-options'; import { cn } from '@/lib/utils'; import type { CodingProjectAgent } from '@/types/coding-project'; import { DEFAULT_PROJECT_AGENT_SKILL_IDS } from '../../../shared/coding-skills'; export type AgentCreationSkillOption = { id: string; name: string; description: string; available?: boolean; effective?: boolean; }; export type AgentCreationInput = { name: string; avatarId: string; avatarDataUrl?: string; model: string; responsibility: string; prompt: string; skillIds: string[]; }; type AgentCreationDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; modelOptions: CodingModelOption[]; skills?: AgentCreationSkillOption[]; existingAgentNames?: string[]; agent?: CodingProjectAgent | null; onCreate?: (input: AgentCreationInput) => Promise; onUpdate?: (input: AgentCreationInput) => Promise; onArchive?: () => void; title?: string; submitLabel?: string; }; function createInitialInput(modelOptions: CodingModelOption[], agent?: CodingProjectAgent | null): AgentCreationInput { if (agent) { return { name: agent.name, avatarId: agent.avatarId, avatarDataUrl: agent.avatarDataUrl, model: agent.model ? codingModelKey(agent.model) : '', responsibility: agent.responsibility.mission, prompt: agent.prompt, skillIds: [...agent.skillIds], }; } return { name: '', avatarId: agentAvatarOptions[0]?.id ?? 'avatar-01', avatarDataUrl: undefined, model: modelOptions[0]?.key ?? '', responsibility: '', prompt: '', skillIds: [], }; } export function AgentCreationDialog({ open, onOpenChange, modelOptions, skills = [], existingAgentNames = [], agent = null, onCreate, onUpdate, onArchive, title, submitLabel, }: AgentCreationDialogProps) { const editing = Boolean(agent); const [input, setInput] = useState(() => createInitialInput(modelOptions, agent)); const defaultSkillsAppliedRef = useRef(false); const [submitting, setSubmitting] = useState(false); const [skillQuery, setSkillQuery] = useState(''); const [avatarPickerOpen, setAvatarPickerOpen] = useState(false); const [avatarProcessing, setAvatarProcessing] = useState(false); const avatarUploadInputRef = useRef(null); useEffect(() => { if (!open) { defaultSkillsAppliedRef.current = false; return; } defaultSkillsAppliedRef.current = editing; setInput(createInitialInput(modelOptions, agent)); setSkillQuery(''); setAvatarPickerOpen(false); }, [agent, editing, modelOptions, open]); useEffect(() => { if (!open || editing || defaultSkillsAppliedRef.current) return; const defaultSkillIds = DEFAULT_PROJECT_AGENT_SKILL_IDS.filter((skillId) => skills.some((skill) => skill.id === skillId)); if (defaultSkillIds.length === 0) return; setInput((current) => current.skillIds.length > 0 ? current : { ...current, skillIds: [...defaultSkillIds] }); defaultSkillsAppliedRef.current = true; }, [editing, open, skills]); const handleAvatarFileChange = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ''; if (!file) return; setAvatarProcessing(true); try { const prepared = await prepareAgentAvatar(file); setInput((current) => ({ ...current, avatarDataUrl: prepared.previewUrl })); setAvatarPickerOpen(false); } catch (error) { toast.error(error instanceof Error ? error.message : '伙伴头像图片处理失败'); } finally { setAvatarProcessing(false); } }; const filteredSkills = useMemo(() => { const query = skillQuery.trim().toLocaleLowerCase(); if (!query) return skills; return skills.filter((skill) => `${skill.name} ${skill.description}`.toLocaleLowerCase().includes(query)); }, [skillQuery, skills]); const selectedAvatar = agentAvatarOptions.find((option) => option.id === input.avatarId) ?? agentAvatarOptions[0]; const selectedAvatarSrc = input.avatarDataUrl ?? selectedAvatar?.src; const handleSubmit = async () => { const name = input.name.trim(); const responsibility = input.responsibility.trim(); if (!name || !responsibility || !input.model) { toast.error('请填写伙伴名称、职责,并配置伙伴模型。'); return; } if (existingAgentNames.some((agentName) => agentName.trim() === name)) { toast.error('伙伴名称需要在当前项目内唯一。'); return; } const submit = editing ? onUpdate : onCreate; if (!submit) return; setSubmitting(true); try { await submit({ ...input, name, responsibility }); onOpenChange(false); } catch (error) { toast.error(error instanceof Error ? error.message : String(error)); } finally { setSubmitting(false); } }; return ( {title ?? (editing ? '维护项目伙伴' : '创建项目伙伴')} 设置伙伴的基础信息,也可以补充提示词和技能。
setInput((current) => ({ ...current, name: event.target.value }))} className="mt-0 flex-1" />

当前伙伴的消息固定使用此模型;如需切换,请在这里修改。

setInput((current) => ({ ...current, responsibility: event.target.value }))} className="mt-2" />

高级设置(提示词与技能)