| | |
| | | import React, { useState } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | | SearchInput, |
| | | TopToolbar, |
| | | SelectColumnsButton, |
| | | EditButton, |
| | | FilterButton, |
| | | BulkDeleteButton, |
| | | WrapperField, |
| | | TextField, |
| | | NumberField, |
| | | DateField, |
| | | BooleanField, |
| | | TextInput, |
| | | DateInput, |
| | | SelectInput, |
| | | useListContext, |
| | | Pagination, |
| | | EditButton, |
| | | DeleteButton, |
| | | } from 'react-admin'; |
| | | import { Box } from '@mui/material'; |
| | | import { styled } from '@mui/material/styles'; |
| | | import { Box, 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 AiParamCreate from "./AiParamCreate"; |
| | | |
| | | const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({ |
| | | '& .css-1vooibu-MuiSvgIcon-root': { |
| | | height: '.9em' |
| | | }, |
| | | '& .RaDatagrid-row': { |
| | | cursor: 'auto' |
| | | }, |
| | | '& .opt': { |
| | | width: 200 |
| | | }, |
| | | })); |
| | | import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout"; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | |
| | | { id: '0', name: 'common.enums.statusFalse' }, |
| | | ]} |
| | | />, |
| | | ] |
| | | ]; |
| | | |
| | | const providerLabel = (provider) => { |
| | | if (provider === 'openai') { |
| | | return 'OpenAI Compatible'; |
| | | } |
| | | if (provider === 'mock') { |
| | | return 'Mock'; |
| | | } |
| | | return provider || '未配置'; |
| | | }; |
| | | |
| | | const AiParamBoard = () => { |
| | | const { data, isLoading } = useListContext(); |
| | | const records = data || []; |
| | | const enabledCount = records.filter((item) => item.status === 1).length; |
| | | const defaultCount = records.filter((item) => item.defaultFlag === 1).length; |
| | | const openaiCount = records.filter((item) => item.provider === 'openai').length; |
| | | const mockCount = records.filter((item) => item.provider === 'mock').length; |
| | | |
| | | if (!isLoading && !records.length) { |
| | | return <EmptyData />; |
| | | } |
| | | |
| | | return ( |
| | | <AiConsoleLayout |
| | | title="AI参数" |
| | | subtitle="保持当前系统后台的白底卡片和轻边框风格,用卡片方式展示模型配置概况,方便和其他 AI 页面统一查看。" |
| | | stats={[ |
| | | { label: '参数总数', value: records.length }, |
| | | { label: '启用', value: enabledCount }, |
| | | { label: '默认模型', value: defaultCount }, |
| | | { label: 'OpenAI / Mock', value: `${openaiCount} / ${mockCount}` }, |
| | | ]} |
| | | > |
| | | <AiConsolePanel |
| | | title="模型配置" |
| | | subtitle="展示模型编码、供应商、上下文轮数和默认状态;创建、编辑、删除仍沿用原系统的弹窗与编辑页。" |
| | | minHeight={460} |
| | | > |
| | | <Box |
| | | sx={{ |
| | | display: 'grid', |
| | | gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', |
| | | gap: 2, |
| | | }} |
| | | > |
| | | {records.map((record) => ( |
| | | <Box key={record.id}> |
| | | <Box sx={aiCardSx(record.defaultFlag === 1)}> |
| | | <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}> |
| | | <Box> |
| | | <Typography variant="subtitle1" sx={{ fontWeight: 700 }}> |
| | | {record.name || record.modelCode || record.uuid} |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary"> |
| | | {record.modelCode || '未填写模型编码'} |
| | | </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.defaultFlag === 1 ? <Chip size="small" color="primary" label="默认" /> : null} |
| | | </Stack> |
| | | </Stack> |
| | | <Grid container spacing={1.25} sx={{ mt: 1 }}> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">供应商</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.25 }}>{providerLabel(record.provider)}</Typography> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">模型名</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.25 }}>{record.modelName || '-'}</Typography> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">上下文轮数</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.25 }}>{record.maxContextMessages || 0}</Typography> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">排序</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.25 }}>{record.sort || 0}</Typography> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <Typography variant="caption" color="text.secondary">聊天地址</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.25, wordBreak: 'break-all' }}> |
| | | {record.chatUrl || '未配置'} |
| | | </Typography> |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <Typography variant="caption" color="text.secondary">备注</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.25, minHeight: 42 }}> |
| | | {record.memo || '未填写备注'} |
| | | </Typography> |
| | | </Grid> |
| | | </Grid> |
| | | <Stack direction="row" spacing={1} sx={{ mt: 1.5 }}> |
| | | <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} /> |
| | | </Stack> |
| | | </Box> |
| | | </Box> |
| | | ))} |
| | | </Box> |
| | | <Box sx={{ mt: 2 }}> |
| | | <Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} /> |
| | | </Box> |
| | | </AiConsolePanel> |
| | | </AiConsoleLayout> |
| | | ); |
| | | }; |
| | | |
| | | const AiParamList = () => { |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | | <Box display="flex" sx={{ width: '100%' }}> |
| | | <List |
| | | sx={{ width: '100%', flexGrow: 1 }} |
| | | title={"menu.aiParam"} |
| | | empty={<EmptyData onClick={() => { setCreateDialog(true) }} />} |
| | | filters={filters} |
| | |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | | pagination={false} |
| | | > |
| | | <StyledDatagrid |
| | | preferenceKey='aiParam' |
| | | bulkActionButtons={() => <BulkDeleteButton mutationMode={OPERATE_MODE} />} |
| | | rowClick={false} |
| | | omit={['id', 'createTime', 'memo', 'statusBool', 'defaultFlagBool']} |
| | | > |
| | | <NumberField source="id" /> |
| | | <TextField source="uuid" label="table.field.aiParam.uuid" /> |
| | | <TextField source="name" label="table.field.aiParam.name" /> |
| | | <TextField source="modelCode" label="table.field.aiParam.modelCode" /> |
| | | <TextField source="provider" label="table.field.aiParam.provider" /> |
| | | <TextField source="modelName" label="table.field.aiParam.modelName" /> |
| | | <NumberField source="maxContextMessages" label="table.field.aiParam.maxContextMessages" /> |
| | | <NumberField source="sort" label="table.field.aiParam.sort" /> |
| | | <BooleanField source="defaultFlagBool" label="table.field.aiParam.defaultFlag" sortable={false} /> |
| | | <BooleanField source="statusBool" label="common.field.status" sortable={false} /> |
| | | <DateField source="updateTime" label="common.field.updateTime" showTime /> |
| | | <TextField source="memo" label="common.field.memo" sortable={false} /> |
| | | <WrapperField cellClassName="opt" label="common.field.opt"> |
| | | <EditButton sx={{ padding: '1px', fontSize: '.75rem' }} /> |
| | | <DeleteButton sx={{ padding: '1px', fontSize: '.75rem' }} mutationMode={OPERATE_MODE} /> |
| | | </WrapperField> |
| | | </StyledDatagrid> |
| | | <AiParamBoard /> |
| | | </List> |
| | | <AiParamCreate |
| | | open={createDialog} |