import React, { useState } from "react";
|
import {
|
List,
|
SearchInput,
|
TopToolbar,
|
SelectColumnsButton,
|
EditButton,
|
FilterButton,
|
TextInput,
|
DateInput,
|
SelectInput,
|
useNotify,
|
useRefresh,
|
useListContext,
|
Pagination,
|
DeleteButton,
|
} from 'react-admin';
|
import { Box, Button, Chip, Grid, Stack, Typography } from '@mui/material';
|
import EmptyData from "@/page/components/EmptyData";
|
import MyCreateButton from "@/page/components/MyCreateButton";
|
import MyExportButton from '@/page/components/MyExportButton';
|
import { OPERATE_MODE, DEFAULT_PAGE_SIZE } from '@/config/setting';
|
import AiRouteCreate from "./AiRouteCreate";
|
import request from "@/utils/request";
|
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
|
|
const routeChoices = [
|
{ id: 'general_chat', name: '通用对话' },
|
{ id: 'system_diagnose', name: '系统诊断' },
|
];
|
|
const filters = [
|
<SearchInput source="condition" alwaysOn />,
|
<DateInput label='common.time.after' source="timeStart" alwaysOn />,
|
<DateInput label='common.time.before' source="timeEnd" alwaysOn />,
|
<SelectInput source="routeCode" label="路由编码" choices={routeChoices} />,
|
<TextInput source="modelCode" label="模型编码" />,
|
<SelectInput label="common.field.status" source="status" choices={[
|
{ id: '1', name: 'common.enums.statusTrue' },
|
{ id: '0', name: 'common.enums.statusFalse' },
|
]} />,
|
];
|
|
const RouteCardActions = ({ record }) => {
|
const refresh = useRefresh();
|
const notify = useNotify();
|
|
const handleToggle = async () => {
|
try {
|
await request.post('/ai/route/toggle', { id: record.id, status: record.status === 1 ? 0 : 1 });
|
notify('common.response.success');
|
refresh();
|
} catch (error) {
|
notify(error.message || '操作失败', { type: 'error' });
|
}
|
};
|
|
const handleReset = async () => {
|
try {
|
await request.post('/ai/route/reset', { id: record.id });
|
notify('common.response.success');
|
refresh();
|
} catch (error) {
|
notify(error.message || '操作失败', { type: 'error' });
|
}
|
};
|
|
return (
|
<Stack direction="row" spacing={1} flexWrap="wrap">
|
<EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
|
<DeleteButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} mutationMode={OPERATE_MODE} />
|
<Button size="small" variant="outlined" onClick={handleToggle}>{record.status === 1 ? '停用' : '启用'}</Button>
|
<Button size="small" variant="outlined" onClick={handleReset}>重置</Button>
|
</Stack>
|
);
|
};
|
|
const RouteBoard = () => {
|
const { data, isLoading } = useListContext();
|
const records = data || [];
|
const enabledCount = records.filter((item) => item.status === 1).length;
|
const coolingCount = records.filter((item) => item.cooldownUntil).length;
|
const failSwitchCount = records.filter((item) => (item.failCount || 0) > 0).length;
|
const successCount = records.reduce((sum, item) => sum + (item.successCount || 0), 0);
|
|
if (!isLoading && !records.length) {
|
return <EmptyData />;
|
}
|
|
return (
|
<AiConsoleLayout
|
title="AI模型路由"
|
subtitle="参考 zy-wcs-master 的 LLM 控制台信息层次,突出路由状态、冷却与成功失败计数,但保留现有系统的按钮、筛选和表单风格。"
|
stats={[
|
{ label: '总路由', value: records.length },
|
{ label: '启用', value: enabledCount },
|
{ label: '冷却中', value: coolingCount },
|
{ label: '累计成功', value: successCount, helper: `已有失败记录 ${failSwitchCount} 条` },
|
]}
|
>
|
<AiConsolePanel
|
title="路由卡片"
|
subtitle="每张卡片代表一条模型候选,优先级越小越先命中;启停、重置会立即作用于后续请求。"
|
minHeight={460}
|
>
|
<Grid container spacing={2}>
|
{records.map((record) => (
|
<Grid item xs={12} md={6} xl={4} key={record.id}>
|
<Box sx={aiCardSx(record.status === 1)}>
|
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
|
<Box>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
|
{record.modelCode}
|
</Typography>
|
<Typography variant="caption" sx={{ color: '#8093a8' }}>
|
{record.routeCode} · 优先级 {record.priority}
|
</Typography>
|
</Box>
|
<Stack direction="row" spacing={0.75} flexWrap="wrap" justifyContent="flex-end">
|
<Chip size="small" color={record.status === 1 ? 'success' : 'default'} label={record.status === 1 ? '启用' : '停用'} />
|
{record.cooldownUntil ? <Chip size="small" color="warning" label="冷却中" /> : null}
|
</Stack>
|
</Stack>
|
<Grid container spacing={1.25} sx={{ mt: 1 }}>
|
<Grid item xs={6}>
|
<Typography variant="caption" sx={{ color: '#70839a' }}>失败次数</Typography>
|
<Typography variant="h6" sx={{ color: '#2f455c', mt: 0.25 }}>{record.failCount || 0}</Typography>
|
</Grid>
|
<Grid item xs={6}>
|
<Typography variant="caption" sx={{ color: '#70839a' }}>成功次数</Typography>
|
<Typography variant="h6" sx={{ color: '#2f455c', mt: 0.25 }}>{record.successCount || 0}</Typography>
|
</Grid>
|
<Grid item xs={12}>
|
<Typography variant="caption" sx={{ color: '#70839a' }}>冷却截止</Typography>
|
<Typography variant="body2" sx={{ mt: 0.25, color: '#31465d' }}>
|
{record.cooldownUntil$ || '未进入冷却'}
|
</Typography>
|
</Grid>
|
<Grid item xs={12}>
|
<Typography variant="caption" sx={{ color: '#70839a' }}>备注</Typography>
|
<Typography variant="body2" sx={{ mt: 0.25, color: '#31465d', minHeight: 42 }}>
|
{record.memo || '未填写备注'}
|
</Typography>
|
</Grid>
|
</Grid>
|
<Box sx={{ mt: 1.5 }}>
|
<RouteCardActions record={record} />
|
</Box>
|
</Box>
|
</Grid>
|
))}
|
</Grid>
|
<Box sx={{ mt: 2 }}>
|
<Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
|
</Box>
|
</AiConsolePanel>
|
</AiConsoleLayout>
|
);
|
};
|
|
const AiRouteList = () => {
|
const [createDialog, setCreateDialog] = useState(false);
|
|
return (
|
<Box display="flex" sx={{ width: '100%' }}>
|
<List
|
sx={{ width: '100%', flexGrow: 1 }}
|
title={"menu.aiRoute"}
|
empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
|
filters={filters}
|
sort={{ field: "priority", order: "asc" }}
|
actions={(
|
<TopToolbar>
|
<FilterButton />
|
<MyCreateButton onClick={() => { setCreateDialog(true) }} />
|
<SelectColumnsButton preferenceKey='aiRoute' />
|
<MyExportButton />
|
</TopToolbar>
|
)}
|
perPage={DEFAULT_PAGE_SIZE}
|
pagination={false}
|
>
|
<RouteBoard />
|
</List>
|
<AiRouteCreate open={createDialog} setOpen={setCreateDialog} />
|
</Box>
|
);
|
}
|
|
export default AiRouteList;
|