982 lines
40 KiB
TypeScript
982 lines
40 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
||
import { Button, Form, Input, InputNumber, Modal, Upload, App as AntApp, List, Popconfirm, Tag, Switch, Select, Collapse, Checkbox } 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 { DEFAULT_RH_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<KnowledgeStatus, { color: string; text: string }> = {
|
||
pending: { color: 'default', text: '待处理' },
|
||
indexing: { color: 'processing', text: '索引中…' },
|
||
ready: { color: 'success', text: '已就绪' },
|
||
failed: { color: 'error', text: '失败' },
|
||
};
|
||
|
||
const TYPE_TAG: Record<SkillType, { color: string; icon: string; label: string }> = {
|
||
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('/');
|
||
const parseModelSelections = (value?: string | string[]) =>
|
||
Array.isArray(value)
|
||
? value
|
||
: String(value || '')
|
||
.split(',')
|
||
.map((item) => item.trim())
|
||
.filter(Boolean);
|
||
|
||
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<Agent | null>(null);
|
||
const [saving, setSaving] = useState(false);
|
||
const [teams, setTeams] = useState<Team[]>([]);
|
||
const [models, setModels] = useState<AiModel[]>([]);
|
||
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<string | null>(null);
|
||
|
||
const pollTimer = useRef<number | null>(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 (
|
||
<>
|
||
<div
|
||
className="fixed inset-0 flex flex-col bg-white z-100 agent-editor-shell"
|
||
style={{ background: 'var(--color-bg)' }}
|
||
>
|
||
{/* Header */}
|
||
<header className="monica-header agent-editor-header">
|
||
<div className="flex items-center gap-4">
|
||
<Button
|
||
type="text"
|
||
icon={<ArrowLeftOutlined />}
|
||
onClick={() => navigate('/agents')}
|
||
className="hover:bg-gray-100"
|
||
style={{ borderRadius: 12, width: 40, height: 40 }}
|
||
/>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<RocketOutlined style={{ color: 'var(--color-brand)', fontSize: 18 }} />
|
||
<span
|
||
className="font-bold text-lg text-gray-800"
|
||
style={{ color: 'var(--color-text)' }}
|
||
>
|
||
{isNew ? '创建新智能体' : currentName}
|
||
</span>
|
||
</div>
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginTop: 2 }}>围绕个性化、能力配置与实时预览,搭建一个更完整的 AI 工作台。</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<span
|
||
className="text-xs text-gray-400"
|
||
style={{ color: 'var(--color-text-tertiary)' }}
|
||
>
|
||
{autoSaveStatus === 'saving' ? '正在保存...' : '更改已自动保存'}
|
||
</span>
|
||
<Button
|
||
icon={<FileTextOutlined />}
|
||
style={{ borderRadius: 12, height: 40 }}
|
||
>
|
||
文档
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
icon={<SaveOutlined />}
|
||
loading={saving}
|
||
onClick={() => handleSave()}
|
||
style={{ borderRadius: 12, height: 40, paddingInline: 16, fontWeight: 600 }}
|
||
>
|
||
保存
|
||
</Button>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content */}
|
||
<div
|
||
className="flex-1 overflow-hidden agent-editor-workbench"
|
||
style={{ background: 'var(--color-bg)' }}
|
||
>
|
||
{/* Left Column: Personalization (System Prompt) */}
|
||
<div className="flex-1 agent-editor-pane">
|
||
<div className="agent-editor-pane-body">
|
||
<div className="agent-editor-pane-header">
|
||
<div>
|
||
<h3 className="agent-editor-pane-title">个性化</h3>
|
||
<p className="agent-editor-pane-subtitle">定义这个智能体的身份、语气、边界和输出规范,让它更像一个稳定的角色,而不是随机回复的模型。</p>
|
||
</div>
|
||
<span className="agent-editor-badge">
|
||
<FileTextOutlined />
|
||
System Prompt
|
||
</span>
|
||
</div>
|
||
|
||
<div className="agent-editor-intro">
|
||
<div className="agent-editor-intro-title">提示词是这个智能体最重要的灵魂设定</div>
|
||
<div className="agent-editor-intro-text">可以从身份定位、擅长任务、回应风格、拒答边界和输出格式这五个维度去描述,让预览区的表现更稳定。</div>
|
||
</div>
|
||
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onValuesChange={() => handleSave(true)}
|
||
>
|
||
<div className="agent-editor-surface agent-editor-prompt-wrap">
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 10, paddingInline: 4 }}>建议写清楚角色设定、目标用户、语气和回答结构。</div>
|
||
<div className="agent-editor-prompt">
|
||
<Form.Item
|
||
name="prompt"
|
||
noStyle
|
||
>
|
||
<Input.TextArea
|
||
rows={30}
|
||
placeholder="在这里输入智能体的人设、技能、风格和输出规范..."
|
||
className="border-none focus:ring-0 bg-transparent p-4 font-mono text-sm leading-relaxed"
|
||
style={{ height: 'calc(100vh - 290px)', resize: 'none', borderRadius: 16 }}
|
||
/>
|
||
</Form.Item>
|
||
</div>
|
||
</div>
|
||
<div
|
||
className="text-[11px] text-gray-400 mt-2 px-2"
|
||
style={{ color: 'var(--color-text-tertiary)' }}
|
||
>
|
||
提示:写得越具体,预览区里的回答风格越稳定。
|
||
</div>
|
||
</Form>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Middle Column: Capabilities & Basic Info */}
|
||
<div className="flex-1 agent-editor-pane">
|
||
<div className="agent-editor-pane-body">
|
||
<div className="agent-editor-pane-header">
|
||
<div>
|
||
<h3 className="agent-editor-pane-title">能力与设置</h3>
|
||
<p className="agent-editor-pane-subtitle">这里决定它用什么模型、拥有哪些知识和技能,以及它会以怎样的方式被别人看到。</p>
|
||
</div>
|
||
<span className="agent-editor-badge">
|
||
<ToolOutlined />
|
||
Capability
|
||
</span>
|
||
</div>
|
||
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onValuesChange={() => handleSave(true)}
|
||
>
|
||
<div
|
||
className="agent-editor-intro"
|
||
style={{ marginBottom: 18 }}
|
||
>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||
<div
|
||
className="avatar-container"
|
||
style={{
|
||
width: 72,
|
||
height: 72,
|
||
borderRadius: '50%',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '24px',
|
||
color: 'white',
|
||
fontWeight: 'bold',
|
||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||
cursor: 'pointer',
|
||
overflow: 'hidden',
|
||
position: 'relative',
|
||
background: isImageUrl(agent?.avatar || selectedAvatar) ? '#f3f4f6' : agent?.avatar || selectedAvatar,
|
||
border: '3px solid rgba(255,255,255,0.92)',
|
||
}}
|
||
onClick={() => setAvatarSelectorOpen(true)}
|
||
>
|
||
{isImageUrl(agent?.avatar || selectedAvatar) ? (
|
||
<img
|
||
src={agent?.avatar || selectedAvatar}
|
||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||
alt="avatar"
|
||
/>
|
||
) : (
|
||
(agentName?.charAt(0) || '?').toUpperCase()
|
||
)}
|
||
<div className="avatar-overlay">
|
||
<span className="avatar-overlay-text">更换形象</span>
|
||
</div>
|
||
</div>
|
||
<div style={{ minWidth: 0 }}>
|
||
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}>{currentName}</div>
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 8 }}>{liveAgent?.description || '补充基础资料后,这里会更像一个完整可运营的产品角色。'}</div>
|
||
<Button
|
||
size="small"
|
||
type="default"
|
||
className="rounded-lg h-8"
|
||
onClick={() => setAvatarSelectorOpen(true)}
|
||
>
|
||
修改头像
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Collapse
|
||
ghost
|
||
expandIconPosition="end"
|
||
className="monica-collapse-bordered mb-4"
|
||
defaultActiveKey={['basic']}
|
||
items={[
|
||
{
|
||
key: 'basic',
|
||
label: (
|
||
<div
|
||
className="flex items-center gap-2 font-medium text-gray-700"
|
||
style={{ color: 'var(--color-text)' }}
|
||
>
|
||
<SettingOutlined style={{ color: 'var(--color-brand)' }} />
|
||
基础设置
|
||
</div>
|
||
),
|
||
children: (
|
||
<div className="space-y-4">
|
||
<div className="flex items-center gap-4 mb-2">
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-[11px] text-gray-400">名称、描述和可见性决定了这个智能体如何被理解和管理。</span>
|
||
</div>
|
||
</div>
|
||
<Form.Item
|
||
name="name"
|
||
label="名称"
|
||
rules={[{ required: true }]}
|
||
className="mb-3"
|
||
>
|
||
<Input
|
||
placeholder="智能体名称"
|
||
onChange={(e) => setAgentName(e.target.value)}
|
||
style={{ borderRadius: 12, height: 42 }}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="description"
|
||
label="描述"
|
||
className="mb-3"
|
||
>
|
||
<Input.TextArea
|
||
rows={3}
|
||
placeholder="简短描述这个智能体适合做什么..."
|
||
style={{ borderRadius: 12 }}
|
||
/>
|
||
</Form.Item>
|
||
<div className="flex gap-2">
|
||
<Form.Item
|
||
name="visibility"
|
||
label="可见性"
|
||
className="flex-1 mb-0"
|
||
>
|
||
<Select
|
||
size="middle"
|
||
style={{ minHeight: 42 }}
|
||
options={[
|
||
{ value: 'private', label: '🔒 私有' },
|
||
{ value: 'team', label: '👥 团队' },
|
||
{ value: 'public', label: '🌐 公开' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="teamId"
|
||
label="归属团队"
|
||
className="flex-1 mb-0"
|
||
tooltip="团队功能暂未开放"
|
||
>
|
||
<Select
|
||
size="middle"
|
||
placeholder="暂不开放"
|
||
disabled
|
||
allowClear
|
||
style={{ minHeight: 42 }}
|
||
options={teams.map((t) => ({ value: t.id, label: t.name }))}
|
||
/>
|
||
</Form.Item>
|
||
</div>
|
||
</div>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
|
||
<div
|
||
className="monica-card !mb-4"
|
||
style={{ borderRadius: 18 }}
|
||
>
|
||
<div className="flex items-center gap-2 mb-4 font-medium text-gray-700">
|
||
<SettingOutlined style={{ color: 'var(--color-brand)' }} />
|
||
模型设置
|
||
</div>
|
||
<div className="space-y-4">
|
||
<Form.Item
|
||
name="model"
|
||
label="模型"
|
||
className="mb-0"
|
||
getValueProps={(value) => ({ value: parseModelSelections(value) })}
|
||
normalize={(value) =>
|
||
Array.isArray(value) ? value.map((item) => String(item).trim()).filter(Boolean).join(', ') : ''
|
||
}
|
||
>
|
||
<Checkbox.Group className="agent-model-checkbox-group">
|
||
{models.map((m) => {
|
||
const inputPrice = 2 * m.model_ratio;
|
||
const outputPrice = inputPrice * m.completion_ratio;
|
||
return (
|
||
<Checkbox key={m.model_name} value={m.model_name} className="agent-model-checkbox-item">
|
||
<div className="agent-model-checkbox-content">
|
||
<div className="agent-model-checkbox-meta">
|
||
<img
|
||
src={m.icon || DEFAULT_RH_40X40_GRAY}
|
||
alt={m.model_name}
|
||
className="agent-model-checkbox-icon"
|
||
/>
|
||
<span className="agent-model-checkbox-name">{m.model_name}</span>
|
||
</div>
|
||
<div className="agent-model-checkbox-price">
|
||
<span>输入: ${inputPrice.toFixed(2)}/M</span>
|
||
<span>输出: ${outputPrice.toFixed(2)}/M</span>
|
||
</div>
|
||
</div>
|
||
</Checkbox>
|
||
);
|
||
})}
|
||
</Checkbox.Group>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="temperature"
|
||
label="Temperature"
|
||
className="mb-0"
|
||
hidden
|
||
>
|
||
<div className="flex items-center gap-4">
|
||
<InputNumber
|
||
min={0}
|
||
max={2}
|
||
step={0.1}
|
||
className="w-20"
|
||
style={{ borderRadius: 12, height: 42 }}
|
||
/>
|
||
<span className="text-[11px] text-gray-400">控制随机性,数值越高越发散</span>
|
||
</div>
|
||
</Form.Item>
|
||
</div>
|
||
</div>
|
||
|
||
<Collapse
|
||
ghost
|
||
expandIconPosition="end"
|
||
className="monica-collapse"
|
||
items={[
|
||
{
|
||
key: 'knowledge',
|
||
label: (
|
||
<div
|
||
className="flex items-center gap-2 font-medium text-gray-700"
|
||
style={{ color: 'var(--color-text)' }}
|
||
>
|
||
<DatabaseOutlined style={{ color: 'var(--color-brand)' }} />
|
||
知识库 ({agent?.knowledge?.length ?? 0})
|
||
</div>
|
||
),
|
||
children: (
|
||
<div className="px-1">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<span className="text-xs text-gray-500">上传文档以增强 AI 知识</span>
|
||
<Upload
|
||
multiple
|
||
beforeUpload={beforeUploadKnowledge as any}
|
||
showUploadList={false}
|
||
>
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
ghost
|
||
icon={<DatabaseOutlined />}
|
||
style={{ borderRadius: 10 }}
|
||
>
|
||
添加
|
||
</Button>
|
||
</Upload>
|
||
</div>
|
||
<List
|
||
size="small"
|
||
dataSource={agent?.knowledge ?? []}
|
||
renderItem={(item) => (
|
||
<List.Item
|
||
className="bg-white mb-2 rounded-lg border border-gray-100 p-2"
|
||
actions={[
|
||
<Popconfirm
|
||
key="del"
|
||
title="确认删除?"
|
||
onConfirm={() => AgentAPI.deleteKnowledge(id!, item.id).then(refresh)}
|
||
>
|
||
<Button
|
||
type="text"
|
||
danger
|
||
size="small"
|
||
style={{ borderRadius: 8 }}
|
||
>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>,
|
||
]}
|
||
>
|
||
<div className="flex flex-col gap-1 overflow-hidden">
|
||
<span className="text-sm font-medium truncate">{item.originalName}</span>
|
||
<span className="text-[10px] text-gray-400">
|
||
{(item.size / 1024).toFixed(1)} KB ·{' '}
|
||
<Tag
|
||
color={STATUS_TAG[item.status].color}
|
||
className="m-0 text-[10px] px-1"
|
||
>
|
||
{STATUS_TAG[item.status].text}
|
||
</Tag>
|
||
</span>
|
||
</div>
|
||
</List.Item>
|
||
)}
|
||
/>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'skills',
|
||
collapsible: 'disabled',
|
||
label: (
|
||
<div
|
||
className="flex items-center gap-2 font-medium text-gray-400 cursor-not-allowed"
|
||
title="技能功能开发中"
|
||
>
|
||
<ToolOutlined />
|
||
技能 & 工具 (开发中)
|
||
</div>
|
||
),
|
||
children: null,
|
||
}
|
||
]}
|
||
/>
|
||
|
||
<div
|
||
className="monica-card mt-6"
|
||
style={{ borderRadius: 18 }}
|
||
>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div
|
||
className="flex items-center gap-2 font-medium text-gray-700"
|
||
style={{ color: 'var(--color-text)' }}
|
||
>
|
||
<RocketOutlined style={{ color: 'var(--color-brand)' }} />
|
||
联网搜索
|
||
</div>
|
||
<Form.Item
|
||
name="webSearchEnabled"
|
||
valuePropName="checked"
|
||
className="mb-0"
|
||
>
|
||
<Switch size="small" />
|
||
</Form.Item>
|
||
</div>
|
||
<div className="text-[11px] text-gray-400">启用后,智能体可以在回答时通过 DuckDuckGo 搜索实时信息。</div>
|
||
</div>
|
||
</Form>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Column: Preview */}
|
||
<div className="flex-1 h-full agent-editor-pane">
|
||
<div className="agent-editor-pane-body agent-editor-preview-shell">
|
||
<div className="agent-editor-pane-header">
|
||
<div>
|
||
<h3 className="agent-editor-pane-title">预览</h3>
|
||
<p className="agent-editor-pane-subtitle">一边调整人设和能力,一边看对话气质是否符合你的预期。</p>
|
||
</div>
|
||
<span className="agent-editor-badge">
|
||
<RocketOutlined />
|
||
Live Preview
|
||
</span>
|
||
</div>
|
||
<div
|
||
className="agent-editor-intro"
|
||
style={{ marginBottom: 14 }}
|
||
>
|
||
<div className="agent-editor-intro-title">当前预览角色</div>
|
||
<div className="agent-editor-intro-text">{currentName} 会基于左侧 Prompt 与中栏设置立即更新。你可以直接在这里检查语气、头像和整体观感。</div>
|
||
</div>
|
||
<div
|
||
className="agent-editor-surface"
|
||
style={{ flex: 1, overflow: 'hidden' }}
|
||
>
|
||
<ChatPreview
|
||
agent={liveAgent}
|
||
agentId={id}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{!isNew && (
|
||
<SkillEditor
|
||
open={skillEditorOpen}
|
||
agentId={id!}
|
||
skillId={editingSkillId}
|
||
onClose={() => setSkillEditorOpen(false)}
|
||
onSaved={refresh}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<Modal
|
||
title={null}
|
||
open={initModalOpen}
|
||
onCancel={() => navigate('/marketplace')}
|
||
footer={null}
|
||
width={720}
|
||
centered
|
||
maskClosable={false}
|
||
destroyOnHidden
|
||
>
|
||
<div className="py-2">
|
||
<div style={{ marginBottom: 18 }}>
|
||
<div
|
||
style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '6px 12px',
|
||
borderRadius: 999,
|
||
background: 'rgba(8, 145, 178, 0.08)',
|
||
color: 'var(--color-brand)',
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<RocketOutlined />
|
||
第一步 · 定义智能体形象
|
||
</div>
|
||
<div style={{ fontSize: 28, fontWeight: 700, color: 'var(--color-text)', marginBottom: 8, letterSpacing: '-0.02em' }}>先给你的智能体一个更完整的开场</div>
|
||
<div style={{ fontSize: 14.5, color: 'var(--color-text-secondary)', lineHeight: 1.75 }}>先决定它的形象、名字和一句话定位。确认后会进入三栏工作台,继续完成个性化、能力配置和实时预览。</div>
|
||
</div>
|
||
|
||
<div className="agent-editor-modal-hero">
|
||
<div
|
||
className="agent-editor-modal-card"
|
||
style={{
|
||
padding: '22px 20px',
|
||
background: 'linear-gradient(180deg, rgba(236,253,245,0.96) 0%, rgba(240,249,255,0.96) 100%)',
|
||
}}
|
||
>
|
||
<div
|
||
className="rounded-full flex items-center justify-center text-3xl text-white font-bold shadow-xl relative transition-all duration-500 overflow-hidden ring-4 ring-white mx-auto"
|
||
style={{
|
||
width: 88,
|
||
height: 88,
|
||
background: isImageUrl(selectedAvatar) ? '#f3f4f6' : selectedAvatar,
|
||
}}
|
||
>
|
||
{isImageUrl(selectedAvatar) ? (
|
||
<img
|
||
src={selectedAvatar}
|
||
className="w-full h-full object-cover"
|
||
alt="avatar"
|
||
/>
|
||
) : (
|
||
(agentName?.charAt(0) || '?').toUpperCase()
|
||
)}
|
||
</div>
|
||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||
<div style={{ fontSize: 17, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}>{agentName || '你的新智能体'}</div>
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', lineHeight: 1.7 }}>这里会实时映射你输入的名字与选择的形象。</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Form
|
||
form={initForm}
|
||
layout="vertical"
|
||
onFinish={handleInitConfirm}
|
||
onValuesChange={(changed) => {
|
||
if (changed.name !== undefined) setAgentName(changed.name);
|
||
}}
|
||
className="agent-editor-modal-card"
|
||
style={{ padding: 20 }}
|
||
>
|
||
<Form.Item
|
||
name="name"
|
||
label={
|
||
<span
|
||
className="text-gray-500 font-medium"
|
||
style={{ color: 'var(--color-text-secondary)' }}
|
||
>
|
||
智能体名称
|
||
</span>
|
||
}
|
||
rules={[{ required: true, message: '请输入智能体名称' }]}
|
||
>
|
||
<Input
|
||
placeholder="给你的智能体起个名字"
|
||
size="large"
|
||
autoFocus
|
||
className="rounded-xl h-12 border-gray-200 focus:border-cyan-500"
|
||
/>
|
||
</Form.Item>
|
||
|
||
<Form.Item
|
||
name="description"
|
||
label={
|
||
<span
|
||
className="text-gray-500 font-medium"
|
||
style={{ color: 'var(--color-text-secondary)' }}
|
||
>
|
||
描述(选填)
|
||
</span>
|
||
}
|
||
>
|
||
<Input.TextArea
|
||
placeholder="介绍一下这个智能体是做什么的..."
|
||
rows={3}
|
||
className="rounded-xl border-gray-200 focus:border-cyan-500"
|
||
/>
|
||
</Form.Item>
|
||
|
||
<div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', marginBottom: 16 }}>
|
||
{['客服助理', '内容创作', '数据分析', '私人教练'].map((label) => (
|
||
<span
|
||
key={label}
|
||
onClick={() => {
|
||
// 给 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}
|
||
</span>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex gap-4 pt-2">
|
||
<Button
|
||
onClick={() => navigate('/marketplace')}
|
||
className="flex-1 h-12 rounded-xl border-gray-200 text-gray-500 font-semibold hover:bg-gray-50"
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
htmlType="submit"
|
||
loading={saving}
|
||
className="flex-1 h-12 rounded-xl border-none font-semibold"
|
||
>
|
||
确认创建
|
||
</Button>
|
||
</div>
|
||
</Form>
|
||
</div>
|
||
|
||
<div>
|
||
<div className="text-[11px] font-bold text-gray-400 uppercase tracking-widest mb-3 px-1">选择你的智能体形象</div>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, gap: 12 }}>
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)' }}>默认会使用系统头像,你也可以上传自己的图片替换。</div>
|
||
<Upload
|
||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||
showUploadList={false}
|
||
beforeUpload={beforeUploadInitAvatar as any}
|
||
>
|
||
<Button
|
||
icon={<UploadOutlined />}
|
||
loading={avatarUploading}
|
||
style={{ borderRadius: 10 }}
|
||
>
|
||
上传图片
|
||
</Button>
|
||
</Upload>
|
||
</div>
|
||
<div className="agent-editor-avatar-grid monica-scrollbar">
|
||
{[DEFAULT_AVATAR, ...PRESET_AVATARS].map((url) => (
|
||
<div
|
||
key={url}
|
||
onClick={() => 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' }}
|
||
>
|
||
<img
|
||
src={url}
|
||
className="w-full h-full object-cover"
|
||
alt="preset"
|
||
/>
|
||
{selectedAvatar === url && (
|
||
<div
|
||
className="absolute inset-0 flex items-center justify-center"
|
||
style={{ background: 'rgba(8, 145, 178, 0.10)' }}
|
||
>
|
||
<div className="bg-white rounded-full p-0.5 shadow-sm">
|
||
<div
|
||
className="w-2 h-2 rounded-full"
|
||
style={{ background: 'var(--color-brand)' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
<Modal
|
||
title="选择头像形象"
|
||
open={avatarSelectorOpen}
|
||
onCancel={() => setAvatarSelectorOpen(false)}
|
||
footer={null}
|
||
width={520}
|
||
centered
|
||
>
|
||
<div className="py-4">
|
||
<div className="flex items-center justify-between gap-3 mb-4">
|
||
<div className="text-[11px] font-bold text-gray-400 uppercase tracking-widest">点击即可更换</div>
|
||
<Upload
|
||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||
showUploadList={false}
|
||
beforeUpload={beforeUploadEditAvatar as any}
|
||
>
|
||
<Button
|
||
icon={<UploadOutlined />}
|
||
loading={avatarUploading}
|
||
style={{ borderRadius: 10 }}
|
||
>
|
||
上传图片
|
||
</Button>
|
||
</Upload>
|
||
</div>
|
||
<div className="grid grid-cols-6 gap-3 bg-gray-50 p-4 rounded-2xl max-h-[400px] overflow-y-auto monica-scrollbar">
|
||
{/* 默认与内置图片 */}
|
||
{[DEFAULT_AVATAR, ...PRESET_AVATARS].map((url) => (
|
||
<div
|
||
key={url}
|
||
onClick={async () => {
|
||
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' }}
|
||
>
|
||
<img
|
||
src={url}
|
||
className="w-full h-full object-cover"
|
||
alt="preset"
|
||
/>
|
||
{agent?.avatar === url && (
|
||
<div
|
||
className="absolute inset-0 flex items-center justify-center"
|
||
style={{ background: 'rgba(194, 84, 31, 0.08)' }}
|
||
>
|
||
<div className="bg-white rounded-full p-0.5 shadow-sm">
|
||
<div
|
||
className="w-2 h-2 rounded-full"
|
||
style={{ background: 'var(--color-brand)' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</>
|
||
);
|
||
}
|