refactor: split AgentEditor into module files; remove redundant local images
|
|
@ -5,3 +5,4 @@ dist
|
|||
.env
|
||||
.env.local
|
||||
tsconfig.tsbuildinfo
|
||||
.vscode
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
|
@ -0,0 +1,66 @@
|
|||
import { Modal, Button, Upload } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Agent, AgentAPI } from '../../../api';
|
||||
import { DEFAULT_AVATAR, PRESET_AVATARS } from '../constants';
|
||||
|
||||
interface AvatarSelectorProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
agent: Agent | null;
|
||||
avatarUploading: boolean;
|
||||
beforeUploadEditAvatar: (file: any) => Promise<boolean>;
|
||||
onAvatarChange: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function AvatarSelector({
|
||||
open,
|
||||
onCancel,
|
||||
agent,
|
||||
avatarUploading,
|
||||
beforeUploadEditAvatar,
|
||||
onAvatarChange,
|
||||
}: AvatarSelectorProps) {
|
||||
return (
|
||||
<Modal title="选择头像形象" open={open} onCancel={onCancel} 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}>
|
||||
<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 () => {
|
||||
if (!agent?.id) return;
|
||||
await AgentAPI.update(agent.id, { avatar: url });
|
||||
onCancel();
|
||||
await onAvatarChange();
|
||||
}}
|
||||
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(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>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
import { Form, Input, Select, Collapse, Button, List, Popconfirm, Tag, Switch, InputNumber } from 'antd';
|
||||
import { DatabaseOutlined, RocketOutlined, SettingOutlined, ToolOutlined } from '@ant-design/icons';
|
||||
import { Agent, Team, AgentAPI } from '../../../api';
|
||||
import { STATUS_TAG, isImageUrl, parseModelSelections } from '../constants';
|
||||
import ModelCheckboxDropdown from './ModelCheckboxDropdown';
|
||||
|
||||
interface CapabilitySettingsProps {
|
||||
form: any;
|
||||
agent: Agent | null;
|
||||
teams: Team[];
|
||||
models: any[];
|
||||
currentName: string;
|
||||
agentName: string;
|
||||
selectedAvatar: string;
|
||||
setAvatarSelectorOpen: (open: boolean) => void;
|
||||
beforeUploadKnowledge: (file: any) => Promise<boolean>;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
export default function CapabilitySettings({
|
||||
form,
|
||||
agent,
|
||||
teams,
|
||||
models,
|
||||
currentName,
|
||||
agentName,
|
||||
selectedAvatar,
|
||||
setAvatarSelectorOpen,
|
||||
beforeUploadKnowledge,
|
||||
markDirty,
|
||||
}: CapabilitySettingsProps) {
|
||||
return (
|
||||
<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={markDirty}>
|
||||
<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: 24,
|
||||
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 }}>
|
||||
{agent?.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="智能体名称" 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(', ') : ''
|
||||
}
|
||||
>
|
||||
<ModelCheckboxDropdown models={models} />
|
||||
</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>
|
||||
<Input
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
id="knowledge-upload"
|
||||
onChange={async (e) => {
|
||||
const files = e.target.files;
|
||||
if (files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await beforeUploadKnowledge(files[i]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
ghost
|
||||
icon={<DatabaseOutlined />}
|
||||
style={{ borderRadius: 10 }}
|
||||
onClick={() => document.getElementById('knowledge-upload')?.click()}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
</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={() => agent && AgentAPI.deleteKnowledge(agent.id, item.id)}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { Button } from 'antd';
|
||||
import { ArrowLeftOutlined, FileTextOutlined, SaveOutlined, RocketOutlined } from '@ant-design/icons';
|
||||
|
||||
interface HeaderProps {
|
||||
isNew: boolean;
|
||||
currentName: string;
|
||||
navigate: any;
|
||||
autoSaveStatus: 'saved' | 'dirty' | 'saving' | 'error';
|
||||
saving: boolean;
|
||||
onSave: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function Header({ isNew, currentName, navigate, autoSaveStatus, saving, onSave }: HeaderProps) {
|
||||
return (
|
||||
<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'
|
||||
? '正在保存...'
|
||||
: autoSaveStatus === 'dirty'
|
||||
? '有未保存更改'
|
||||
: autoSaveStatus === 'error'
|
||||
? '保存失败'
|
||||
: '已保存'}
|
||||
</span>
|
||||
<Button icon={<FileTextOutlined />} style={{ borderRadius: 12, height: 40 }}>
|
||||
文档
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={onSave}
|
||||
style={{ borderRadius: 12, height: 40, paddingInline: 16, fontWeight: 600 }}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
import { Form, Input, Modal, Button, Upload } from 'antd';
|
||||
import { RocketOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { DEFAULT_AVATAR, PRESET_AVATARS, isImageUrl } from '../constants';
|
||||
|
||||
interface InitModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: (values: any) => Promise<void>;
|
||||
avatarUploading: boolean;
|
||||
selectedAvatar: string;
|
||||
setSelectedAvatar: (url: string) => void;
|
||||
agentName: string;
|
||||
setAgentName: (name: string) => void;
|
||||
saving: boolean;
|
||||
beforeUploadInitAvatar: (file: any) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export default function InitModal({
|
||||
open,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
avatarUploading,
|
||||
selectedAvatar,
|
||||
setSelectedAvatar,
|
||||
agentName,
|
||||
setAgentName,
|
||||
saving,
|
||||
beforeUploadInitAvatar,
|
||||
}: InitModalProps) {
|
||||
const [initForm] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={null}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
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={onConfirm}
|
||||
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.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={onCancel}
|
||||
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}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, Checkbox, Dropdown } from 'antd';
|
||||
import { DownOutlined } from '@ant-design/icons';
|
||||
import { AiModel } from '../../../api';
|
||||
import { DEFAULT_RH_40X40_GRAY } from '../../../constants';
|
||||
|
||||
interface ModelCheckboxDropdownProps {
|
||||
value?: string[];
|
||||
onChange?: (value: string[]) => void;
|
||||
models: AiModel[];
|
||||
}
|
||||
|
||||
export default function ModelCheckboxDropdown({ value = [], onChange, models }: ModelCheckboxDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const summary = value.length ? `${value.length} 个已选` : '选择模型';
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
popupRender={() => (
|
||||
<div className="agent-model-dropdown-panel" onClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox.Group
|
||||
value={value}
|
||||
onChange={(checked) => onChange?.(checked.map((item) => String(item)))}
|
||||
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>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button type="text" block className="agent-model-dropdown-trigger">
|
||||
<span className="agent-model-dropdown-summary">{summary}</span>
|
||||
<span className="agent-model-dropdown-values">
|
||||
{value.length ? value.join(', ') : '未选择'}
|
||||
</span>
|
||||
<DownOutlined className="agent-model-dropdown-arrow" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { RocketOutlined } from '@ant-design/icons';
|
||||
import { Agent } from '../../../api';
|
||||
import ChatPreview from '../../../components/ChatPreview';
|
||||
|
||||
interface PreviewPaneProps {
|
||||
liveAgent: Agent;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export default function PreviewPane({ liveAgent, agentId }: PreviewPaneProps) {
|
||||
return (
|
||||
<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">
|
||||
{liveAgent?.name || '未命名智能体'} 会基于左侧 Prompt 与中栏设置立即更新。你可以直接在这里检查语气、头像和整体观感。
|
||||
</div>
|
||||
</div>
|
||||
<div className="agent-editor-surface" style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<ChatPreview agent={liveAgent} agentId={agentId} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Form, Input } from 'antd';
|
||||
import { FileTextOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PromptEditorProps {
|
||||
form: any;
|
||||
markDirty: () => void;
|
||||
}
|
||||
|
||||
export default function PromptEditor({ form, markDirty }: PromptEditorProps) {
|
||||
return (
|
||||
<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={markDirty}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { KnowledgeStatus, SkillType } from '../../api';
|
||||
|
||||
export const DEFAULT_AVATAR = 'https://static.svipdata.com/hoyidata/materials/B7lNeTYQM1_0/materials.jpg';
|
||||
|
||||
export const PRESET_AVATARS: string[] = [
|
||||
'https://static.svipdata.com/hoyidata/materials/PkM0iQCaAY_0/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/tmPDm2FhJY_1/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/hNCyP4RvJL_2/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/l2OMyoIVff_3/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/CINYRp0JJS_4/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/oUFVVgRpJa_5/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/WZccHWHK40_6/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/sLmZKx5GoI_7/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/ZPHkYQHOE0_8/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/kpMcS2ekGq_9/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/1vkwcmV5MC_10/materials.png',
|
||||
'https://static.svipdata.com/hoyidata/materials/nLLWLA0eLZ_11/materials.png',
|
||||
];
|
||||
|
||||
export 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: '失败' },
|
||||
};
|
||||
|
||||
export 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' },
|
||||
};
|
||||
|
||||
export const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('/');
|
||||
|
||||
export const parseModelSelections = (value?: string | string[]) =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FormInstance } from 'antd';
|
||||
import { Agent, AgentAPI, Team, TeamAPI, AiModel, ModelAPI, ImageAPI } from '../../../api';
|
||||
import { DEFAULT_AVATAR } from '../constants';
|
||||
|
||||
interface UseAgentEditorOptions {
|
||||
id?: string;
|
||||
isNew: boolean;
|
||||
form: FormInstance;
|
||||
message: any;
|
||||
navigate: any;
|
||||
}
|
||||
|
||||
export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentEditorOptions) {
|
||||
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' | 'dirty' | '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('');
|
||||
const [skillEditorOpen, setSkillEditorOpen] = useState(false);
|
||||
const [editingSkillId, setEditingSkillId] = useState<string | null>(null);
|
||||
|
||||
const pollTimer = useRef<number | null>(null);
|
||||
const hydratingRef = useRef(false);
|
||||
|
||||
const refresh = async (force = false) => {
|
||||
if (!id) return;
|
||||
const data = await AgentAPI.detail(id);
|
||||
setAgent(data);
|
||||
if (force || autoSaveStatus !== 'dirty') {
|
||||
hydratingRef.current = true;
|
||||
form.setFieldsValue(data);
|
||||
window.setTimeout(() => {
|
||||
hydratingRef.current = false;
|
||||
}, 0);
|
||||
setAutoSaveStatus('saved');
|
||||
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(true);
|
||||
}
|
||||
return () => {
|
||||
if (pollTimer.current) {
|
||||
window.clearInterval(pollTimer.current);
|
||||
pollTimer.current = null;
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
const handleInitConfirm = async (values: any) => {
|
||||
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('已保存');
|
||||
await refresh(true);
|
||||
setAutoSaveStatus('saved');
|
||||
} catch (e) {
|
||||
setAutoSaveStatus('error');
|
||||
if (!silent) message.error('保存失败');
|
||||
} finally {
|
||||
if (!silent) setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 beforeUploadKnowledge = async (file: any) => {
|
||||
if (!id) {
|
||||
message.warning('请先保存智能体基础信息后再上传');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await AgentAPI.uploadKnowledge(id, [file as File]);
|
||||
message.success(`${file.name} 已上传,正在建索引…`);
|
||||
refresh();
|
||||
} catch (e: any) {
|
||||
message.error('上传失败:' + (e?.message ?? e));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const beforeUploadInitAvatar = async (file: any) => {
|
||||
try {
|
||||
const url = await uploadAvatar(file as File);
|
||||
setSelectedAvatar(url);
|
||||
message.success('头像上传成功');
|
||||
} catch (e: any) {
|
||||
message.error('头像上传失败:' + (e?.message ?? e));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const beforeUploadEditAvatar = async (file: any) => {
|
||||
if (!id) return false;
|
||||
try {
|
||||
const url = await uploadAvatar(file as File);
|
||||
await AgentAPI.update(id, { avatar: url });
|
||||
message.success('头像已更新');
|
||||
setAvatarSelectorOpen(false);
|
||||
refresh();
|
||||
} catch (e: any) {
|
||||
message.error('头像上传失败:' + (e?.message ?? e));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const liveAgent = { ...(agent ?? {}), ...(form.getFieldsValue() as Agent) } as Agent;
|
||||
const currentName = liveAgent?.name || agentName || '未命名智能体';
|
||||
|
||||
const markDirty = () => {
|
||||
if (hydratingRef.current) return;
|
||||
setAutoSaveStatus('dirty');
|
||||
};
|
||||
|
||||
return {
|
||||
agent,
|
||||
saving,
|
||||
teams,
|
||||
models,
|
||||
autoSaveStatus,
|
||||
initModalOpen,
|
||||
setInitModalOpen,
|
||||
selectedAvatar,
|
||||
setSelectedAvatar,
|
||||
avatarSelectorOpen,
|
||||
setAvatarSelectorOpen,
|
||||
avatarUploading,
|
||||
agentName,
|
||||
setAgentName,
|
||||
skillEditorOpen,
|
||||
setSkillEditorOpen,
|
||||
editingSkillId,
|
||||
setEditingSkillId,
|
||||
hydratingRef,
|
||||
refresh,
|
||||
handleInitConfirm,
|
||||
handleSave,
|
||||
beforeUploadKnowledge,
|
||||
beforeUploadInitAvatar,
|
||||
beforeUploadEditAvatar,
|
||||
liveAgent,
|
||||
currentName,
|
||||
markDirty,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Form } from 'antd';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { App as AntApp } from 'antd';
|
||||
import SkillEditor from '../../components/SkillEditor';
|
||||
import { useAgentEditor } from './hooks/useAgentEditor';
|
||||
import Header from './components/Header';
|
||||
import PromptEditor from './components/PromptEditor';
|
||||
import CapabilitySettings from './components/CapabilitySettings';
|
||||
import PreviewPane from './components/PreviewPane';
|
||||
import InitModal from './components/InitModal';
|
||||
import AvatarSelector from './components/AvatarSelector';
|
||||
|
||||
export default function AgentEditor() {
|
||||
const { id } = useParams();
|
||||
const isNew = !id;
|
||||
const navigate = useNavigate();
|
||||
const { message } = AntApp.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const pollTimer = useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
agent,
|
||||
saving,
|
||||
teams,
|
||||
models,
|
||||
autoSaveStatus,
|
||||
initModalOpen,
|
||||
setInitModalOpen,
|
||||
selectedAvatar,
|
||||
setSelectedAvatar,
|
||||
avatarSelectorOpen,
|
||||
setAvatarSelectorOpen,
|
||||
avatarUploading,
|
||||
agentName,
|
||||
setAgentName,
|
||||
skillEditorOpen,
|
||||
setSkillEditorOpen,
|
||||
editingSkillId,
|
||||
setEditingSkillId,
|
||||
refresh,
|
||||
handleInitConfirm,
|
||||
handleSave,
|
||||
beforeUploadKnowledge,
|
||||
beforeUploadInitAvatar,
|
||||
beforeUploadEditAvatar,
|
||||
liveAgent,
|
||||
currentName,
|
||||
markDirty,
|
||||
} = useAgentEditor({ id, isNew, form, message, navigate });
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollTimer.current) {
|
||||
window.clearInterval(pollTimer.current);
|
||||
pollTimer.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 flex flex-col bg-white z-100 agent-editor-shell" style={{ background: 'var(--color-bg)' }}>
|
||||
<Header
|
||||
isNew={isNew}
|
||||
currentName={currentName}
|
||||
navigate={navigate}
|
||||
autoSaveStatus={autoSaveStatus}
|
||||
saving={saving}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-hidden agent-editor-workbench" style={{ background: 'var(--color-bg)' }}>
|
||||
<PromptEditor form={form} markDirty={markDirty} />
|
||||
<CapabilitySettings
|
||||
form={form}
|
||||
agent={agent}
|
||||
teams={teams}
|
||||
models={models}
|
||||
currentName={currentName}
|
||||
agentName={agentName}
|
||||
selectedAvatar={agent?.avatar || selectedAvatar}
|
||||
setAvatarSelectorOpen={setAvatarSelectorOpen}
|
||||
beforeUploadKnowledge={beforeUploadKnowledge}
|
||||
markDirty={markDirty}
|
||||
/>
|
||||
<PreviewPane liveAgent={liveAgent} agentId={id} />
|
||||
</div>
|
||||
|
||||
{!isNew && (
|
||||
<SkillEditor
|
||||
open={skillEditorOpen}
|
||||
agentId={id!}
|
||||
skillId={editingSkillId}
|
||||
onClose={() => setSkillEditorOpen(false)}
|
||||
onSaved={refresh}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<InitModal
|
||||
open={initModalOpen}
|
||||
onCancel={() => navigate('/agents')}
|
||||
onConfirm={handleInitConfirm}
|
||||
avatarUploading={avatarUploading}
|
||||
selectedAvatar={selectedAvatar}
|
||||
setSelectedAvatar={setSelectedAvatar}
|
||||
agentName={agentName}
|
||||
setAgentName={setAgentName}
|
||||
saving={saving}
|
||||
beforeUploadInitAvatar={beforeUploadInitAvatar}
|
||||
/>
|
||||
|
||||
<AvatarSelector
|
||||
open={avatarSelectorOpen}
|
||||
onCancel={() => setAvatarSelectorOpen(false)}
|
||||
agent={agent}
|
||||
avatarUploading={avatarUploading}
|
||||
beforeUploadEditAvatar={beforeUploadEditAvatar}
|
||||
onAvatarChange={refresh}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||