365 lines
16 KiB
TypeScript
365 lines
16 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Plus, Sparkles, ArrowRight, MessageCircle, Bot, BriefcaseBusiness, Info, Pencil, Trash2, Eye, EyeOff, MessageCircleMore, InfoIcon, Edit } from 'lucide-react';
|
|
import { Dialog, Loading, MessagePlugin, Popconfirm, Switch } from 'tdesign-react';
|
|
import { useTheme } from '../../contexts/ThemeContext';
|
|
import { useExpertAuthBootstrap } from '../../hooks';
|
|
import { knowledgetoolUrl } from '@/hooks/useExpertAuthBootstrap';
|
|
|
|
interface ExpertAgentItem {
|
|
id: number;
|
|
name: string;
|
|
avatar_url: string | null;
|
|
description: string | null;
|
|
model_name: string;
|
|
is_draft: boolean;
|
|
user_id: string;
|
|
created_at: string;
|
|
}
|
|
|
|
type ExpertTab = 'mine' | 'published';
|
|
|
|
export default function AgentExpertCenterPage() {
|
|
const { theme } = useTheme();
|
|
const isDark = theme === 'dark';
|
|
const { llmList, currentUser, authedFetch } = useExpertAuthBootstrap();
|
|
const navigate = useNavigate();
|
|
|
|
const [activeTab, setActiveTab] = useState<ExpertTab>('mine');
|
|
const [agentList, setAgentList] = useState<ExpertAgentItem[]>([]);
|
|
const [isListLoading, setIsListLoading] = useState(false);
|
|
const [isStatusUpdating, setIsStatusUpdating] = useState(false);
|
|
const [detailVisible, setDetailVisible] = useState(false);
|
|
const [selectedAgent, setSelectedAgent] = useState<ExpertAgentItem | null>(null);
|
|
|
|
const fetchExpertList = useCallback(async () => {
|
|
setIsListLoading(true);
|
|
try {
|
|
const isDraft = activeTab === 'mine';
|
|
const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/list`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ is_draft: isDraft }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error('expert-agents/list接口调用失败:', res.status, res.statusText);
|
|
setAgentList([]);
|
|
return;
|
|
}
|
|
|
|
const data: unknown = await res.json();
|
|
const rawList = Array.isArray(data) ? (data as ExpertAgentItem[]) : [];
|
|
|
|
if (isDraft) {
|
|
const mineFromList = currentUser ? rawList.filter((item) => item.user_id === currentUser.id) : [];
|
|
|
|
const allRes = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents`, {
|
|
method: 'GET',
|
|
});
|
|
|
|
let mineFromAll: ExpertAgentItem[] = [];
|
|
if (allRes.ok) {
|
|
const allData: unknown = await allRes.json();
|
|
const allList = Array.isArray(allData) ? (allData as ExpertAgentItem[]) : [];
|
|
mineFromAll = currentUser ? allList.filter((item) => item.user_id === currentUser.id) : [];
|
|
} else {
|
|
console.error('expert-agents接口调用失败:', allRes.status, allRes.statusText);
|
|
}
|
|
|
|
const mergedMap = new Map<number, ExpertAgentItem>();
|
|
[...mineFromList, ...mineFromAll].forEach((item) => mergedMap.set(item.id, item));
|
|
setAgentList(Array.from(mergedMap.values()));
|
|
return;
|
|
}
|
|
|
|
setAgentList(rawList);
|
|
} catch (error) {
|
|
console.error('expert-agents/list接口异常:', error);
|
|
setAgentList([]);
|
|
} finally {
|
|
setIsListLoading(false);
|
|
}
|
|
}, [activeTab, authedFetch, currentUser]);
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'mine' && !currentUser) return;
|
|
void fetchExpertList();
|
|
}, [activeTab, currentUser, fetchExpertList]);
|
|
|
|
const handleStatusToggle = async (nextPublished: boolean) => {
|
|
if (!selectedAgent) return;
|
|
|
|
const nextIsDraft = !nextPublished;
|
|
setIsStatusUpdating(true);
|
|
try {
|
|
const res = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${selectedAgent.id}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ is_draft: nextIsDraft }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
console.error('更新专家状态失败:', res.status, res.statusText);
|
|
return;
|
|
}
|
|
|
|
setSelectedAgent({ ...selectedAgent, is_draft: nextIsDraft });
|
|
await fetchExpertList();
|
|
} catch (error) {
|
|
console.error('更新专家状态异常:', error);
|
|
} finally {
|
|
setIsStatusUpdating(false);
|
|
}
|
|
};
|
|
|
|
const addAgentSearch = async () => {
|
|
const parmy = {
|
|
model_name: 'Qwen3.5-27B',
|
|
name: '未命名专家',
|
|
system_prompt: '你是一个专业的助手,能够回答用户的问题。'
|
|
}
|
|
const res: any = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(parmy),
|
|
});
|
|
console.log('创建专家', res);
|
|
const data: any = await res.json()
|
|
navigate(`/agent-expert-edit?id=${data.id}`)
|
|
}
|
|
|
|
const delAgentSearch = async (id) => {
|
|
const res: any = await authedFetch(`${knowledgetoolUrl}/api/v1/expert-agents/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
MessagePlugin.success('操作成功')
|
|
await fetchExpertList();
|
|
}
|
|
|
|
const cards = useMemo(
|
|
() =>
|
|
agentList.map((item) => ({
|
|
id: String(item.id),
|
|
title: item.name || '未命名专家',
|
|
desc: item.description || '暂无描述',
|
|
status: item.is_draft ? '草稿' : '已发布',
|
|
avatarSvg: item.avatar_url,
|
|
raw: item,
|
|
})),
|
|
[agentList],
|
|
);
|
|
|
|
return (
|
|
<div className={`w-full min-h-[calc(100vh-140px)] ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>
|
|
<div className="max-w-[1100px] mx-auto">
|
|
<section className={`rounded-2xl border px-6 py-6 ${isDark ? 'bg-slate-900/60 border-slate-700' : 'bg-white border-gray-200'}`}>
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-fuchsia-500 to-violet-600 flex items-center justify-center text-white shadow">
|
|
<Sparkles className="w-5 h-5" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold tracking-tight">我的智能体</h1>
|
|
</div>
|
|
<p className={`mt-2 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>管理你的专属 AI 专家助手</p>
|
|
<p className={`mt-1 text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>已加载模型:{llmList.length}</p>
|
|
|
|
<div className="mt-4 inline-flex items-center rounded-lg border overflow-hidden">
|
|
<button
|
|
onClick={() => setActiveTab('mine')}
|
|
className={`px-3 py-1.5 text-xs ${activeTab === 'mine' ? 'bg-slate-900 text-white' : isDark ? 'bg-slate-800/60 text-slate-300' : 'bg-gray-50 text-gray-600'}`}
|
|
>
|
|
我的智能体
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('published')}
|
|
className={`px-3 py-1.5 text-xs ${activeTab === 'published' ? 'bg-slate-900 text-white' : isDark ? 'bg-slate-800/60 text-slate-300' : 'bg-gray-50 text-gray-600'}`}
|
|
>
|
|
发布的智能体
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<button className="inline-flex items-center gap-2 h-10 px-4 rounded-full bg-slate-900 text-white hover:bg-slate-800 transition" onClick={addAgentSearch}>
|
|
<Plus className="w-4 h-4" /> 创建智能体
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="mt-6">
|
|
{isListLoading ? (
|
|
<div className={`min-h-[280px] rounded-2xl border flex items-center justify-center ${isDark ? 'bg-slate-900/40 border-slate-700' : 'bg-white border-gray-200'}`}>
|
|
<Loading loading text="加载专家列表中..." />
|
|
</div>
|
|
) : cards.length === 0 ? (
|
|
<div className={`min-h-[280px] rounded-2xl border flex flex-col items-center justify-center text-center px-4 ${isDark ? 'bg-slate-900/40 border-slate-700' : 'bg-white border-gray-200'}`}>
|
|
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center mb-3 ${isDark ? 'bg-slate-800 text-slate-300' : 'bg-gray-100 text-gray-500'}`}>
|
|
<BriefcaseBusiness className="w-6 h-6" />
|
|
</div>
|
|
<h3 className={`text-base font-semibold ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>
|
|
{activeTab === 'mine' ? '暂无我的专家' : '暂无发布的专家'}
|
|
</h3>
|
|
<p className={`mt-2 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
|
|
创建你的第一个专家助手,开始高效工作
|
|
</p>
|
|
<button className="mt-4 inline-flex items-center gap-2 h-10 px-4 rounded-xl bg-slate-900 text-white hover:bg-slate-800 transition">
|
|
<Plus className="w-4 h-4" /> 创建第一个专家
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{cards.map((item, idx) => (
|
|
<article
|
|
key={item.id}
|
|
className={`rounded-2xl border p-5 transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5 ${isDark ? 'bg-slate-900/60 border-slate-700 hover:border-slate-500' : 'bg-white border-gray-200 hover:border-gray-300'}`}
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
{item.avatarSvg ? (
|
|
<img
|
|
src={`data:image/svg+xml;utf8,${encodeURIComponent(item.avatarSvg)}`}
|
|
alt={item.title}
|
|
className="w-10 h-10 rounded-xl object-cover flex-shrink-0"
|
|
/>
|
|
) : (
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center text-white shadow-sm flex-shrink-0">
|
|
{idx % 2 === 0 ? <MessageCircle className="w-5 h-5" /> : <Bot className="w-5 h-5" />}
|
|
</div>
|
|
)}
|
|
<div className="min-w-0">
|
|
<h3 className="font-semibold text-[15px] leading-6 truncate">{item.title}</h3>
|
|
<p className={`text-xs mt-1 line-clamp-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{item.desc}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{activeTab === 'mine' && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
title="查看详情"
|
|
className={`inline-flex items-center justify-center w-7 h-7 rounded-md border transition ${isDark ? 'border-slate-600 text-slate-300 hover:bg-slate-700/60' : 'border-gray-200 text-gray-500 hover:bg-gray-100'}`}
|
|
onClick={() => {
|
|
setSelectedAgent(item.raw);
|
|
setDetailVisible(true);
|
|
}}
|
|
>
|
|
<Info className="w-4 h-4 shrink-0" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
title="编辑"
|
|
className={`inline-flex items-center justify-center w-7 h-7 rounded-md border transition ${isDark ? 'border-slate-600 text-slate-300 hover:bg-slate-700/60' : 'border-gray-200 text-gray-500 hover:bg-gray-100'}`}
|
|
onClick={() => {
|
|
navigate(`/agent-expert-edit?id=${item.id}`);
|
|
}}
|
|
>
|
|
<Edit className="w-4 h-4 shrink-0" />
|
|
</button>
|
|
<Popconfirm
|
|
content="确认删除该条数据吗?"
|
|
onConfirm={() => delAgentSearch(item.id)}
|
|
>
|
|
<button
|
|
type="button"
|
|
title="删除"
|
|
className={`inline-flex items-center justify-center w-7 h-7 rounded-md border transition ${isDark ? 'border-slate-600 text-slate-300 hover:bg-rose-900/20 hover:text-rose-300' : 'border-gray-200 text-gray-500 hover:bg-rose-50 hover:text-rose-500'}`}
|
|
>
|
|
<Trash2 className="w-4 h-4 shrink-0" />
|
|
</button>
|
|
</Popconfirm>
|
|
</>
|
|
)}
|
|
|
|
<span className={`inline-flex items-center gap-1.5 text-xs px-2 py-1 rounded-full border ${isDark ? 'bg-emerald-950/50 border-emerald-800 text-emerald-300' : 'bg-emerald-50 border-emerald-200 text-emerald-700'}`}>
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
|
{item.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => navigate(`/agent-chat?id=${item.id}`)}
|
|
className={`mt-6 inline-flex items-center gap-1 text-sm ${isDark ? 'text-slate-300 hover:text-white' : 'text-gray-700 hover:text-gray-900'}`}
|
|
>
|
|
进行对话
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
<section className={`mt-6 rounded-2xl border px-6 py-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4 ${isDark ? 'bg-violet-950/30 border-violet-800/50' : 'bg-violet-50 border-violet-100'}`}>
|
|
<div>
|
|
<h3 className="text-xl font-semibold">不确定从哪个开始?</h3>
|
|
<p className={`mt-2 text-sm ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>直接开始对话,AI 会自动匹配最合适的专家助手</p>
|
|
</div>
|
|
|
|
<button className="inline-flex items-center justify-center gap-2 h-11 px-6 rounded-xl bg-gradient-to-r from-fuchsia-500 to-violet-600 text-white font-medium hover:opacity-95 transition">
|
|
开始对话
|
|
<ArrowRight className="w-4 h-4" />
|
|
</button>
|
|
</section>
|
|
|
|
<Dialog
|
|
visible={detailVisible}
|
|
onClose={() => setDetailVisible(false)}
|
|
confirmBtn={null}
|
|
cancelBtn={null}
|
|
header="专家详情"
|
|
width={520}
|
|
>
|
|
<div className="py-2">
|
|
<div className="flex flex-col items-center text-center">
|
|
{selectedAgent?.avatar_url ? (
|
|
<img
|
|
src={`data:image/svg+xml;utf8,${encodeURIComponent(selectedAgent.avatar_url)}`}
|
|
alt={selectedAgent?.name || '专家头像'}
|
|
className="w-20 h-20 rounded-2xl object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-20 h-20 rounded-2xl bg-gray-100" />
|
|
)}
|
|
|
|
<h4 className="mt-4 text-2xl font-semibold text-gray-900">{selectedAgent?.name || '未命名专家'}</h4>
|
|
<p className="mt-2 text-sm text-gray-400">{selectedAgent?.description || '暂无描述'}</p>
|
|
</div>
|
|
|
|
<div className="mt-5 rounded-xl bg-gray-50 px-3 py-3 flex items-center justify-between">
|
|
<div className="inline-flex items-center gap-2 text-sm text-gray-500">
|
|
{selectedAgent?.is_draft ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
{selectedAgent?.is_draft ? '草稿状态' : '已发布'}
|
|
</div>
|
|
<Switch
|
|
value={!selectedAgent?.is_draft}
|
|
loading={isStatusUpdating}
|
|
disabled={isStatusUpdating}
|
|
onChange={(value) => {
|
|
void handleStatusToggle(value as boolean);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<button className="mt-4 w-full h-10 rounded-lg bg-gradient-to-r from-fuchsia-500 to-violet-600 text-white inline-flex items-center justify-center gap-2">
|
|
<MessageCircleMore className="w-4 h-4" />
|
|
开始聊天
|
|
</button>
|
|
</div>
|
|
</Dialog>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|