iconsulting/packages/admin-client/src/features/experience/presentation/pages/ExperiencePage.tsx

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>
);
}