aura-web/src/pages/AgentList.tsx

286 lines
12 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, useMemo, useState } from 'react';
import {
ArrowRightOutlined,
CompassOutlined,
DeleteOutlined,
EditOutlined,
MessageOutlined,
RobotOutlined
} from '@ant-design/icons';
import { Button, Col, Row, Empty, Popconfirm, App as AntApp, Tag, Space } from 'antd';
import { Link, useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import { Agent, AgentAPI } from '../api';
export default function AgentList() {
const [list, setList] = useState<Agent[]>([]);
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const { message } = AntApp.useApp();
const load = async () => {
setLoading(true);
try {
setList(await AgentAPI.list());
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
const handleDelete = async (id: string) => {
await AgentAPI.remove(id);
message.success('已删除');
load();
};
const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('/');
const publicCount = useMemo(() => list.filter((a) => a.visibility === 'public').length, [list]);
const teamCount = useMemo(() => list.filter((a) => a.visibility === 'team').length, [list]);
return (
<div className="page-container">
<div
style={{
borderRadius: 24,
padding: '30px 30px 26px',
background:
'linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 48%, rgba(239,246,255,0.96) 100%)',
border: '1px solid rgba(8, 145, 178, 0.12)',
boxShadow: '0 20px 48px rgba(15, 23, 42, 0.06)',
marginBottom: 24
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 20,
flexWrap: 'wrap',
marginBottom: 22
}}
>
<div style={{ maxWidth: 620 }}>
<div
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '6px 12px',
borderRadius: 999,
background: 'rgba(255,255,255,0.78)',
border: '1px solid rgba(8, 145, 178, 0.10)',
color: 'var(--color-text-secondary)',
fontSize: 12,
fontWeight: 600,
marginBottom: 16
}}
>
<RobotOutlined style={{ color: 'var(--color-brand)' }} />
Agent
</div>
<h2 className="page-title" style={{ marginBottom: 10 }}>
</h2>
<div className="page-subtitle" style={{ marginTop: 0, fontSize: 15, lineHeight: 1.75 }}>
AI 广
</div>
</div>
<Button
type="primary"
size="large"
icon={<CompassOutlined />}
onClick={() => navigate('/marketplace')}
style={{ borderRadius: 14, height: 46, padding: '0 18px', fontWeight: 600 }}
>
广
</Button>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 14 }}>
{[
{ label: '已创建智能体', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' },
{ label: '公开可分享', value: publicCount, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' },
{ label: '团队协作中', value: teamCount, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' }
].map((item) => (
<div
key={item.label}
style={{
borderRadius: 18,
padding: '16px 18px',
background: 'rgba(255,255,255,0.72)',
border: '1px solid rgba(255,255,255,0.7)'
}}
>
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 10 }}>{item.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
<span style={{ fontSize: 28, fontWeight: 700, color: 'var(--color-text)' }}>{item.value}</span>
<span
style={{
borderRadius: 999,
padding: '4px 8px',
background: item.tone,
color: item.color,
fontSize: 12,
fontWeight: 600
}}
>
</span>
</div>
</div>
))}
</div>
</div>
{!loading && list.length === 0 ? (
<div className="empty-state">
<Empty description="你还没有任何智能体">
<Button type="primary" onClick={() => navigate('/marketplace')} style={{ borderRadius: 10 }}>
广
</Button>
</Empty>
</div>
) : (
<Row gutter={[18, 18]}>
{list.map((a) => (
<Col xs={24} sm={12} md={8} lg={6} key={a.id}>
<div
className="agent-card"
style={{
borderRadius: 20,
padding: 20,
minHeight: 292,
background: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%)',
boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)'
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
<div
className="avatar"
style={{ background: a.avatar || 'var(--gradient-brand)', borderRadius: '50%', overflow: 'hidden', width: 54, height: 54 }}
>
{isImageUrl(a.avatar) ? (
<img src={a.avatar} className="w-full h-full object-cover" alt="avatar" />
) : (
(a.name?.charAt(0) || '?').toUpperCase()
)}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 700, fontSize: 17, color: 'var(--color-text)', marginBottom: 4 }}>{a.name}</div>
<div style={{ fontSize: 12.5, color: 'var(--color-text-tertiary)' }}>
{dayjs(a.updated_at).format('YYYY-MM-DD')}
</div>
</div>
</div>
<div
style={{
marginTop: 16,
padding: '16px 16px 18px',
borderRadius: 16,
background: 'linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%)',
border: '1px solid rgba(148, 163, 184, 0.14)'
}}
>
<div className="desc" style={{ minHeight: 66, fontSize: 13.5, lineHeight: 1.7 }}>
{a.description || '还没有填写描述,可以补充这个智能体适合解决什么问题。'}
</div>
</div>
<Space size={6} wrap style={{ marginTop: 14 }}>
{a.visibility === 'public' && (
<Tag bordered={false} style={{ background: 'var(--color-success-soft)', color: 'var(--color-success)', borderRadius: 999, margin: 0 }}>
</Tag>
)}
{a.visibility === 'team' && (
<Tag bordered={false} style={{ background: 'var(--color-info-soft)', color: 'var(--color-info)', borderRadius: 999, margin: 0 }}>
</Tag>
)}
{a.visibility === 'private' && (
<Tag bordered={false} style={{ background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', borderRadius: 999, margin: 0 }}>
</Tag>
)}
{a.model && (
<Tag bordered={false} style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, maxWidth: '100%' }}>
<span style={{ display: 'inline-block', maxWidth: 190, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{a.model}
</span>
</Tag>
)}
<Tag bordered={false} style={{ background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', borderRadius: 999, margin: 0 }}>
T={a.temperature}
</Tag>
{(a.fork_count ?? 0) > 0 && (
<Tag bordered={false} style={{ background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', borderRadius: 999, margin: 0 }}>
Fork {a.fork_count}
</Tag>
)}
</Space>
<div style={{ display: 'flex', gap: 8, marginTop: 'auto', paddingTop: 16, borderTop: '1px solid var(--color-border)' }}>
<Link to={`/agents/${a.id}/chat`} style={{ flex: 1 }}>
<Button type="primary" block icon={<MessageOutlined />} style={{ borderRadius: 12, height: 40, fontWeight: 600 }}>
</Button>
</Link>
<Link to={`/agents/${a.id}`} style={{ flex: 1 }}>
<Button block icon={<EditOutlined />} style={{ borderRadius: 12, height: 40 }}>
</Button>
</Link>
<Popconfirm
title="确定删除该智能体?"
description="将删除其知识库与对话记录"
onConfirm={() => handleDelete(a.id)}
okText="删除"
cancelText="取消"
>
<Button danger icon={<DeleteOutlined />} style={{ borderRadius: 12, width: 40, height: 40 }} />
</Popconfirm>
</div>
</div>
</Col>
))}
</Row>
)}
{list.length > 0 && (
<div
style={{
marginTop: 24,
borderRadius: 20,
padding: '18px 20px',
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 16,
flexWrap: 'wrap'
}}
>
<div>
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--color-text)', marginBottom: 4 }}>
</div>
<div style={{ fontSize: 13, color: 'var(--color-text-secondary)' }}>
广
</div>
</div>
<Button type="text" icon={<ArrowRightOutlined />} onClick={() => navigate('/marketplace')} style={{ color: 'var(--color-brand)', fontWeight: 600 }}>
广
</Button>
</div>
)}
</div>
);
}