394 lines
11 KiB
TypeScript
394 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Card,
|
|
Table,
|
|
Button,
|
|
Select,
|
|
Tag,
|
|
Space,
|
|
Modal,
|
|
message,
|
|
Tabs,
|
|
Typography,
|
|
Statistic,
|
|
Row,
|
|
Col,
|
|
} from 'antd';
|
|
import {
|
|
CheckOutlined,
|
|
CloseOutlined,
|
|
EyeOutlined,
|
|
PlayCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
import api from '../../../../shared/utils/api';
|
|
import { useAuth } from '../../../../shared/hooks/useAuth';
|
|
|
|
const { Title, Text, Paragraph } = Typography;
|
|
|
|
const EXPERIENCE_TYPES = [
|
|
{ value: 'COMMON_QUESTION', label: '常见问题' },
|
|
{ value: 'ANSWER_TEMPLATE', label: '回答模板' },
|
|
{ value: 'CLARIFICATION', label: '澄清方式' },
|
|
{ value: 'USER_PATTERN', label: '用户模式' },
|
|
{ value: 'CONVERSION_TRIGGER', label: '转化触发' },
|
|
{ value: 'KNOWLEDGE_GAP', label: '知识缺口' },
|
|
{ value: 'CONVERSATION_SKILL', label: '对话技巧' },
|
|
{ value: 'OBJECTION_HANDLING', label: '异议处理' },
|
|
];
|
|
|
|
interface Experience {
|
|
id: string;
|
|
experienceType: string;
|
|
content: string;
|
|
scenario: string;
|
|
confidence: number;
|
|
relatedCategory: string;
|
|
sourceConversationIds: string[];
|
|
verificationStatus: string;
|
|
usageCount: number;
|
|
positiveCount: number;
|
|
negativeCount: number;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
}
|
|
|
|
export function ExperiencePage() {
|
|
const [activeTab, setActiveTab] = useState('pending');
|
|
const [typeFilter, setTypeFilter] = useState<string>();
|
|
const [selectedExperience, setSelectedExperience] = useState<Experience | null>(null);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const queryClient = useQueryClient();
|
|
const admin = useAuth((state) => state.admin);
|
|
|
|
const { data: pendingData, isLoading: pendingLoading } = useQuery({
|
|
queryKey: ['pending-experiences', typeFilter],
|
|
queryFn: async () => {
|
|
const params = new URLSearchParams();
|
|
if (typeFilter) params.append('type', typeFilter);
|
|
const response = await api.get(`/memory/experience/pending?${params}`);
|
|
return response.data.data;
|
|
},
|
|
enabled: activeTab === 'pending',
|
|
});
|
|
|
|
const { data: stats } = useQuery({
|
|
queryKey: ['experience-stats'],
|
|
queryFn: async () => {
|
|
const response = await api.get('/memory/experience/statistics');
|
|
return response.data.data;
|
|
},
|
|
});
|
|
|
|
const approveMutation = useMutation({
|
|
mutationFn: (id: string) =>
|
|
api.post(`/memory/experience/${id}/approve`, { adminId: admin?.id }),
|
|
onSuccess: () => {
|
|
message.success('经验已批准');
|
|
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
},
|
|
});
|
|
|
|
const rejectMutation = useMutation({
|
|
mutationFn: (id: string) =>
|
|
api.post(`/memory/experience/${id}/reject`, { adminId: admin?.id }),
|
|
onSuccess: () => {
|
|
message.success('经验已拒绝');
|
|
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
},
|
|
});
|
|
|
|
const runEvolutionMutation = useMutation({
|
|
mutationFn: () => api.post('/evolution/run', { hoursBack: 24, limit: 50 }),
|
|
onSuccess: (response) => {
|
|
const result = response.data.data;
|
|
message.success(
|
|
`进化任务完成:分析了${result.conversationsAnalyzed}个对话,提取了${result.experiencesExtracted}条经验`
|
|
);
|
|
queryClient.invalidateQueries({ queryKey: ['pending-experiences'] });
|
|
queryClient.invalidateQueries({ queryKey: ['experience-stats'] });
|
|
},
|
|
});
|
|
|
|
const handleView = (exp: Experience) => {
|
|
setSelectedExperience(exp);
|
|
setIsModalOpen(true);
|
|
};
|
|
|
|
const getTypeLabel = (type: string) => {
|
|
return EXPERIENCE_TYPES.find((t) => t.value === type)?.label || type;
|
|
};
|
|
|
|
const getStatusTag = (status: string) => {
|
|
const statusMap: Record<string, { color: string; label: string }> = {
|
|
PENDING: { color: 'orange', label: '待审核' },
|
|
APPROVED: { color: 'green', label: '已通过' },
|
|
REJECTED: { color: 'red', label: '已拒绝' },
|
|
DEPRECATED: { color: 'default', label: '已弃用' },
|
|
};
|
|
const s = statusMap[status] || { color: 'default', label: status };
|
|
return <Tag color={s.color}>{s.label}</Tag>;
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '类型',
|
|
dataIndex: 'experienceType',
|
|
key: 'experienceType',
|
|
render: (type: string) => <Tag>{getTypeLabel(type)}</Tag>,
|
|
},
|
|
{
|
|
title: '场景',
|
|
dataIndex: 'scenario',
|
|
key: 'scenario',
|
|
ellipsis: true,
|
|
},
|
|
{
|
|
title: '内容',
|
|
dataIndex: 'content',
|
|
key: 'content',
|
|
ellipsis: true,
|
|
render: (text: string) => (
|
|
<Text ellipsis style={{ maxWidth: 200 }}>
|
|
{text}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
title: '置信度',
|
|
dataIndex: 'confidence',
|
|
key: 'confidence',
|
|
render: (confidence: number) => (
|
|
<span
|
|
className={
|
|
confidence >= 70
|
|
? 'text-green-600'
|
|
: confidence >= 40
|
|
? 'text-yellow-600'
|
|
: 'text-red-600'
|
|
}
|
|
>
|
|
{confidence}%
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
title: '来源对话',
|
|
dataIndex: 'sourceConversationIds',
|
|
key: 'sources',
|
|
render: (ids: string[]) => <span>{ids?.length || 0}个</span>,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'verificationStatus',
|
|
key: 'status',
|
|
render: getStatusTag,
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
render: (_: unknown, record: Experience) => (
|
|
<Space size="small">
|
|
<Button
|
|
type="text"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => handleView(record)}
|
|
/>
|
|
{record.verificationStatus === 'PENDING' && (
|
|
<>
|
|
<Button
|
|
type="text"
|
|
icon={<CheckOutlined />}
|
|
className="text-green-600"
|
|
onClick={() => approveMutation.mutate(record.id)}
|
|
loading={approveMutation.isPending}
|
|
/>
|
|
<Button
|
|
type="text"
|
|
icon={<CloseOutlined />}
|
|
danger
|
|
onClick={() => rejectMutation.mutate(record.id)}
|
|
loading={rejectMutation.isPending}
|
|
/>
|
|
</>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<Title level={4} className="mb-0">系统经验管理</Title>
|
|
<Button
|
|
type="primary"
|
|
icon={<PlayCircleOutlined />}
|
|
onClick={() => runEvolutionMutation.mutate()}
|
|
loading={runEvolutionMutation.isPending}
|
|
>
|
|
运行进化任务
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 统计卡片 */}
|
|
<Row gutter={[16, 16]} className="mb-4">
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic title="总经验" value={stats?.total || 0} />
|
|
</Card>
|
|
</Col>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="待审核"
|
|
value={stats?.byStatus?.PENDING || 0}
|
|
valueStyle={{ color: '#faad14' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="已通过"
|
|
value={stats?.byStatus?.APPROVED || 0}
|
|
valueStyle={{ color: '#52c41a' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={12} sm={6}>
|
|
<Card>
|
|
<Statistic
|
|
title="已拒绝"
|
|
value={stats?.byStatus?.REJECTED || 0}
|
|
valueStyle={{ color: '#ff4d4f' }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Card>
|
|
<Tabs
|
|
activeKey={activeTab}
|
|
onChange={setActiveTab}
|
|
items={[
|
|
{ key: 'pending', label: '待审核' },
|
|
{ key: 'approved', label: '已通过' },
|
|
{ key: 'rejected', label: '已拒绝' },
|
|
]}
|
|
/>
|
|
|
|
<div className="mb-4">
|
|
<Select
|
|
placeholder="筛选类型"
|
|
allowClear
|
|
style={{ width: 180 }}
|
|
value={typeFilter}
|
|
onChange={setTypeFilter}
|
|
options={EXPERIENCE_TYPES}
|
|
/>
|
|
</div>
|
|
|
|
<Table
|
|
columns={columns}
|
|
dataSource={pendingData?.items || []}
|
|
rowKey="id"
|
|
loading={pendingLoading}
|
|
pagination={{
|
|
total: pendingData?.total || 0,
|
|
pageSize: 20,
|
|
}}
|
|
/>
|
|
</Card>
|
|
|
|
{/* 详情弹窗 */}
|
|
<Modal
|
|
title="经验详情"
|
|
open={isModalOpen}
|
|
onCancel={() => {
|
|
setIsModalOpen(false);
|
|
setSelectedExperience(null);
|
|
}}
|
|
footer={
|
|
selectedExperience?.verificationStatus === 'PENDING'
|
|
? [
|
|
<Button
|
|
key="reject"
|
|
danger
|
|
onClick={() => {
|
|
rejectMutation.mutate(selectedExperience.id);
|
|
setIsModalOpen(false);
|
|
}}
|
|
>
|
|
拒绝
|
|
</Button>,
|
|
<Button
|
|
key="approve"
|
|
type="primary"
|
|
onClick={() => {
|
|
approveMutation.mutate(selectedExperience.id);
|
|
setIsModalOpen(false);
|
|
}}
|
|
>
|
|
批准
|
|
</Button>,
|
|
]
|
|
: null
|
|
}
|
|
width={600}
|
|
>
|
|
{selectedExperience && (
|
|
<div>
|
|
<div className="mb-4">
|
|
<Tag>{getTypeLabel(selectedExperience.experienceType)}</Tag>
|
|
{getStatusTag(selectedExperience.verificationStatus)}
|
|
{selectedExperience.isActive && <Tag color="blue">激活中</Tag>}
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<Text type="secondary">场景</Text>
|
|
<Paragraph>{selectedExperience.scenario}</Paragraph>
|
|
</div>
|
|
|
|
<div className="mb-4">
|
|
<Text type="secondary">内容</Text>
|
|
<Paragraph>{selectedExperience.content}</Paragraph>
|
|
</div>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={8}>
|
|
<Statistic
|
|
title="置信度"
|
|
value={selectedExperience.confidence}
|
|
suffix="%"
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Statistic
|
|
title="使用次数"
|
|
value={selectedExperience.usageCount}
|
|
/>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Statistic
|
|
title="来源对话"
|
|
value={selectedExperience.sourceConversationIds?.length || 0}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
{selectedExperience.relatedCategory && (
|
|
<div className="mt-4">
|
|
<Text type="secondary">相关类别: </Text>
|
|
<Tag color="blue">{selectedExperience.relatedCategory}</Tag>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|