aura-web/src/pages/AgentEditor.tsx

1001 lines
41 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 } from '../api';
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('/');
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 [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([]));
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="rounded-full flex items-center justify-center text-2xl text-white font-bold shadow-md cursor-pointer overflow-hidden group relative"
style={{
width: 72,
height: 72,
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}
className="w-full h-full object-cover"
alt="avatar"
/>
) : (
(agentName?.charAt(0) || '?').toUpperCase()
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
<span className="text-[10px] text-white font-medium drop-shadow-md"></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"
>
<Select
size="middle"
placeholder="选择团队"
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"
>
<Input
placeholder="默认模型"
style={{ borderRadius: 12, height: 42 }}
/>
</Form.Item>
<Form.Item
name="temperature"
label="Temperature"
className="mb-0"
>
<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',
label: (
<div
className="flex items-center gap-2 font-medium text-gray-700"
style={{ color: 'var(--color-text)' }}
>
<ToolOutlined style={{ color: 'var(--color-brand)' }} />
& ({agent?.skills?.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>
<Button
type="primary"
size="small"
ghost
style={{ borderRadius: 10 }}
onClick={() => {
setEditingSkillId(null);
setSkillEditorOpen(true);
}}
>
</Button>
</div>
<List
size="small"
dataSource={agent?.skills ?? []}
renderItem={(item) => (
<List.Item
className="bg-white mb-2 rounded-lg border border-gray-100 p-2"
actions={[
<Switch
key="toggle"
size="small"
checked={!!item.enabled}
onChange={(v) => AgentAPI.updateSkill(id!, item.id, { enabled: v }).then(refresh)}
/>,
<Button
key="edit"
type="text"
size="small"
style={{ borderRadius: 8 }}
onClick={() => {
setEditingSkillId(item.id);
setSkillEditorOpen(true);
}}
>
</Button>,
]}
>
<div className="flex items-center gap-2 overflow-hidden">
<Tag
color={TYPE_TAG[item.type].color}
className="m-0 text-[10px]"
>
{TYPE_TAG[item.type].label}
</Tag>
<span className="text-sm font-medium truncate">{item.name}</span>
</div>
</List.Item>
)}
/>
</div>
),
},
{
key: 'mcp',
label: (
<div
className="flex items-center gap-2 font-medium text-gray-700"
style={{ color: 'var(--color-text)' }}
>
<RocketOutlined style={{ color: 'var(--color-brand)' }} />
MCP
</div>
),
children: id ? <McpPanel agentId={id} /> : <div className="text-center text-gray-400 py-4"> MCP</div>,
},
]}
/>
<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>
</>
);
}