refactor: split AgentEditor into module files; remove redundant local images
|
|
@ -5,3 +5,4 @@ dist
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
tsconfig.tsbuildinfo
|
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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||