import { useEffect, useRef, useState } from 'react'; import { Button, Form, Input, InputNumber, Modal, Upload, App as AntApp, List, Popconfirm, Tag, Switch, Select, Collapse } from 'antd'; import type { UploadFile } from 'antd/es/upload/interface'; import { useNavigate, useParams } from 'react-router-dom'; import { Agent, AgentAPI, ImageAPI, KnowledgeStatus, SkillType, Team, TeamAPI, AiModel, ModelAPI } from '../api'; import { PLACEHOLDER_40X40_GRAY } from '../constants'; import SkillEditor from '../components/SkillEditor'; import McpPanel from '../components/McpPanel'; import ChatPreview from '../components/ChatPreview'; import { ArrowLeftOutlined, SaveOutlined, FileTextOutlined, RocketOutlined, ToolOutlined, DatabaseOutlined, SettingOutlined, UploadOutlined } from '@ant-design/icons'; const STATUS_TAG: Record = { pending: { color: 'default', text: '待处理' }, indexing: { color: 'processing', text: '索引中…' }, ready: { color: 'success', text: '已就绪' }, failed: { color: 'error', text: '失败' }, }; const TYPE_TAG: Record = { prompt: { color: 'blue', icon: '📝', label: 'Prompt' }, http: { color: 'green', icon: '🌐', label: 'HTTP' }, js: { color: 'volcano', icon: '⚙️', label: 'JS' }, }; const DEFAULT_AVATAR = '/default_bot_icon.jpg'; const PRESET_AVATARS: string[] = Array.from({ length: 12 }, (_, index) => `/avatars/avatar-${String(index + 1).padStart(2, '0')}.png`); const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('/'); export default function AgentEditor() { const { id } = useParams(); const isNew = !id; const navigate = useNavigate(); const { message } = AntApp.useApp(); const [form] = Form.useForm(); const [initForm] = Form.useForm(); const [agent, setAgent] = useState(null); const [saving, setSaving] = useState(false); const [teams, setTeams] = useState([]); const [models, setModels] = useState([]); const [autoSaveStatus, setAutoSaveStatus] = useState<'saved' | 'saving' | 'error'>('saved'); const [initModalOpen, setInitModalOpen] = useState(isNew); const [selectedAvatar, setSelectedAvatar] = useState(DEFAULT_AVATAR); const [avatarSelectorOpen, setAvatarSelectorOpen] = useState(false); const [avatarUploading, setAvatarUploading] = useState(false); // 监听名称变化以同步头像首字母 const [agentName, setAgentName] = useState(''); // skill editor const [skillEditorOpen, setSkillEditorOpen] = useState(false); const [editingSkillId, setEditingSkillId] = useState(null); const pollTimer = useRef(null); const refresh = async () => { if (!id) return; const data = await AgentAPI.detail(id); setAgent(data); form.setFieldsValue(data); setAgentName(data.name); setSelectedAvatar(data.avatar || DEFAULT_AVATAR); // 若有索引中文件 → 启动轮询 const indexing = data.knowledge?.some((k) => k.status === 'pending' || k.status === 'indexing'); if (indexing && !pollTimer.current) { pollTimer.current = window.setInterval(refresh, 2000); } else if (!indexing && pollTimer.current) { window.clearInterval(pollTimer.current); pollTimer.current = null; } }; useEffect(() => { TeamAPI.list() .then(setTeams) .catch(() => setTeams([])); ModelAPI.list() .then(setModels) .catch(() => setModels([])); if (isNew) { setInitModalOpen(true); setSelectedAvatar(DEFAULT_AVATAR); setAgentName(''); form.setFieldsValue({ name: '', description: '', prompt: 'You are a helpful AI assistant.', model: '', temperature: 0.7, visibility: 'private', teamId: null, }); } else { setInitModalOpen(false); refresh(); } return () => { if (pollTimer.current) { window.clearInterval(pollTimer.current); pollTimer.current = null; } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [id]); const handleInitConfirm = async () => { const values = await initForm.validateFields(); setSaving(true); try { const created = await AgentAPI.create({ ...values, avatar: selectedAvatar, prompt: 'You are a helpful AI assistant.', temperature: 0.7, visibility: 'private', }); message.success('初始化成功'); setInitModalOpen(false); navigate(`/agents/${created.id}`, { replace: true }); } catch (e) { message.error('创建失败'); } finally { setSaving(false); } }; const handleSave = async (silent = false) => { if (isNew) return; // 初始弹窗未确认前不触发自动保存 const values = await form.validateFields(); if (!silent) setSaving(true); setAutoSaveStatus('saving'); try { await AgentAPI.update(id!, values); if (!silent) message.success('已保存'); refresh(); setAutoSaveStatus('saved'); } catch (e) { setAutoSaveStatus('error'); if (!silent) message.error('保存失败'); } finally { if (!silent) setSaving(false); } }; const beforeUploadKnowledge = async (file: UploadFile) => { if (!id) { message.warning('请先保存智能体基础信息后再上传'); return Upload.LIST_IGNORE; } try { await AgentAPI.uploadKnowledge(id, [file as unknown as File]); message.success(`${file.name} 已上传,正在建索引…`); refresh(); } catch (e: any) { message.error('上传失败:' + (e?.message ?? e)); } return Upload.LIST_IGNORE; }; const uploadAvatar = async (file: File) => { setAvatarUploading(true); try { const res = await ImageAPI.upload([file]); const url = res.files?.[0]?.url; if (!url) { throw new Error('未获取到图片地址'); } return url; } finally { setAvatarUploading(false); } }; const beforeUploadInitAvatar = async (file: UploadFile) => { try { const url = await uploadAvatar(file as unknown as File); setSelectedAvatar(url); message.success('头像上传成功'); } catch (e: any) { message.error('头像上传失败:' + (e?.message ?? e)); } return Upload.LIST_IGNORE; }; const beforeUploadEditAvatar = async (file: UploadFile) => { if (!id) return Upload.LIST_IGNORE; try { const url = await uploadAvatar(file as unknown as File); await AgentAPI.update(id, { avatar: url }); message.success('头像已更新'); setAvatarSelectorOpen(false); refresh(); } catch (e: any) { message.error('头像上传失败:' + (e?.message ?? e)); } return Upload.LIST_IGNORE; }; const liveAgent = agent || (form.getFieldsValue() as Agent); const currentName = liveAgent?.name || agentName || '未命名智能体'; return ( <>
{/* Header */}
{autoSaveStatus === 'saving' ? '正在保存...' : '更改已自动保存'}
{/* Main Content */}
{/* Left Column: Personalization (System Prompt) */}

个性化

定义这个智能体的身份、语气、边界和输出规范,让它更像一个稳定的角色,而不是随机回复的模型。

System Prompt
提示词是这个智能体最重要的灵魂设定
可以从身份定位、擅长任务、回应风格、拒答边界和输出格式这五个维度去描述,让预览区的表现更稳定。
handleSave(true)} >
建议写清楚角色设定、目标用户、语气和回答结构。
提示:写得越具体,预览区里的回答风格越稳定。
{/* Middle Column: Capabilities & Basic Info */}

能力与设置

这里决定它用什么模型、拥有哪些知识和技能,以及它会以怎样的方式被别人看到。

Capability
handleSave(true)} >
setAvatarSelectorOpen(true)} > {isImageUrl(agent?.avatar || selectedAvatar) ? ( avatar ) : ( (agentName?.charAt(0) || '?').toUpperCase() )}
更换形象
{currentName}
{liveAgent?.description || '补充基础资料后,这里会更像一个完整可运营的产品角色。'}
基础设置
), children: (
名称、描述和可见性决定了这个智能体如何被理解和管理。
setAgentName(e.target.value)} style={{ borderRadius: 12, height: 42 }} />
({ value: t.id, label: t.name }))} />
), }, ]} />
模型设置
描述(选填) } >
{['客服助理', '内容创作', '数据分析', '私人教练'].map((label) => ( { // 给 initForm 赋值(因为第一步向导是 initForm,不是主页面的 form) initForm.setFieldsValue({ description: label }); }} style={{ padding: '6px 10px', borderRadius: 999, background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', fontSize: 12.5, cursor: 'pointer', transition: 'all 0.2s', }} onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--color-border)'; e.currentTarget.style.color = 'var(--color-text)'; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--color-surface-2)'; e.currentTarget.style.color = 'var(--color-text-secondary)'; }} > {label} ))}
选择你的智能体形象
默认会使用系统头像,你也可以上传自己的图片替换。
{[DEFAULT_AVATAR, ...PRESET_AVATARS].map((url) => (
setSelectedAvatar(url)} className={`relative aspect-square rounded-full cursor-pointer transition-all duration-300 overflow-hidden border-2 ${selectedAvatar === url ? 'scale-110 shadow-lg z-10' : 'border-transparent opacity-70 hover:opacity-100 hover:scale-105'}`} style={{ borderColor: selectedAvatar === url ? 'var(--color-brand)' : 'transparent' }} > preset {selectedAvatar === url && (
)}
))}
setAvatarSelectorOpen(false)} footer={null} width={520} centered >
点击即可更换
{/* 默认与内置图片 */} {[DEFAULT_AVATAR, ...PRESET_AVATARS].map((url) => (
{ await AgentAPI.update(id!, { avatar: url }); setAvatarSelectorOpen(false); refresh(); }} className={`relative aspect-square rounded-full cursor-pointer transition-all duration-300 overflow-hidden border-2 ${agent?.avatar === url ? 'scale-110 shadow-lg z-10' : 'border-transparent opacity-70 hover:opacity-100 hover:scale-105'}`} style={{ borderColor: agent?.avatar === url ? 'var(--color-brand)' : 'transparent' }} > preset {agent?.avatar === url && (
)}
))}
); }