| | |
| | | import React, { useState } from "react"; |
| | | import React, { useMemo, useState } from "react"; |
| | | import { |
| | | List, |
| | | DatagridConfigurable, |
| | | SearchInput, |
| | | TopToolbar, |
| | | SelectColumnsButton, |
| | | EditButton, |
| | | FilterButton, |
| | | BulkDeleteButton, |
| | | WrapperField, |
| | | TextField, |
| | | NumberField, |
| | | DateField, |
| | | BooleanField, |
| | | TextInput, |
| | | DateInput, |
| | | List, |
| | | SearchInput, |
| | | SelectInput, |
| | | DeleteButton, |
| | | } from 'react-admin'; |
| | | import { Box } from '@mui/material'; |
| | | import { styled } from '@mui/material/styles'; |
| | | 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 |
| | | }, |
| | | })); |
| | | TextInput, |
| | | TopToolbar, |
| | | useDelete, |
| | | useListContext, |
| | | useNotify, |
| | | useRefresh, |
| | | } from "react-admin"; |
| | | import { |
| | | Box, |
| | | Button, |
| | | Card, |
| | | CardActions, |
| | | CardContent, |
| | | Chip, |
| | | CircularProgress, |
| | | Divider, |
| | | Grid, |
| | | Stack, |
| | | Typography, |
| | | } from "@mui/material"; |
| | | import AddRoundedIcon from "@mui/icons-material/AddRounded"; |
| | | import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined"; |
| | | import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; |
| | | import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; |
| | | import MyExportButton from "@/page/components/MyExportButton"; |
| | | import AiParamForm from "./AiParamForm"; |
| | | import AiConfigDialog from "../aiShared/AiConfigDialog"; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <DateInput label='common.time.after' source="timeStart" alwaysOn />, |
| | | <DateInput label='common.time.before' source="timeEnd" alwaysOn />, |
| | | <TextInput source="name" label="table.field.aiParam.name" />, |
| | | <TextInput source="modelCode" label="table.field.aiParam.modelCode" />, |
| | | <TextInput source="provider" label="table.field.aiParam.provider" />, |
| | | <TextInput source="modelName" label="table.field.aiParam.modelName" />, |
| | | <TextInput source="providerType" label="提供方类型" />, |
| | | <TextInput source="model" label="模型" />, |
| | | <SelectInput |
| | | source="defaultFlag" |
| | | label="table.field.aiParam.defaultFlag" |
| | | choices={[ |
| | | { id: '1', name: 'common.enums.true' }, |
| | | { id: '0', name: 'common.enums.false' }, |
| | | ]} |
| | | />, |
| | | <TextInput label="common.field.memo" source="memo" />, |
| | | <SelectInput |
| | | label="common.field.status" |
| | | source="status" |
| | | label="状态" |
| | | choices={[ |
| | | { id: '1', name: 'common.enums.statusTrue' }, |
| | | { id: '0', name: 'common.enums.statusFalse' }, |
| | | { id: "1", name: "common.enums.statusTrue" }, |
| | | { id: "0", name: "common.enums.statusFalse" }, |
| | | ]} |
| | | />, |
| | | ] |
| | | ]; |
| | | |
| | | const AiParamList = () => { |
| | | const [createDialog, setCreateDialog] = useState(false); |
| | | const defaultValues = { |
| | | providerType: "OPENAI_COMPATIBLE", |
| | | temperature: 0.7, |
| | | topP: 1, |
| | | timeoutMs: 60000, |
| | | streamingEnabled: true, |
| | | status: 1, |
| | | }; |
| | | |
| | | const truncateText = (value, max = 84) => { |
| | | if (!value) { |
| | | return "--"; |
| | | } |
| | | return value.length > max ? `${value.slice(0, max)}...` : value; |
| | | }; |
| | | |
| | | const AiParamCards = ({ onView, onEdit, onDelete, deleting }) => { |
| | | const { data, isLoading } = useListContext(); |
| | | const records = useMemo(() => (Array.isArray(data) ? data : []), [data]); |
| | | |
| | | if (isLoading) { |
| | | return ( |
| | | <Box display="flex" justifyContent="center" py={8}> |
| | | <CircularProgress size={28} /> |
| | | </Box> |
| | | ); |
| | | } |
| | | |
| | | if (!records.length) { |
| | | return ( |
| | | <Box px={2} py={6}> |
| | | <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}> |
| | | <Typography variant="subtitle1">暂无 AI 参数配置</Typography> |
| | | <Typography variant="body2" color="text.secondary" mt={1}> |
| | | 可以先新建一个 OpenAI 兼容模型参数卡片。 |
| | | </Typography> |
| | | </Card> |
| | | </Box> |
| | | ); |
| | | } |
| | | |
| | | return ( |
| | | <Box display="flex"> |
| | | <Box px={2} py={2}> |
| | | <Grid container spacing={2}> |
| | | {records.map((record) => ( |
| | | <Grid item xs={12} md={6} xl={4} key={record.id}> |
| | | <Card |
| | | variant="outlined" |
| | | sx={{ |
| | | height: "100%", |
| | | borderRadius: 3, |
| | | boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)", |
| | | }} |
| | | > |
| | | <CardContent sx={{ pb: 1.5 }}> |
| | | <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}> |
| | | <Box> |
| | | <Typography variant="h6" sx={{ mb: 0.5 }}> |
| | | {record.name} |
| | | </Typography> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | {record.model || "--"} |
| | | </Typography> |
| | | </Box> |
| | | <Chip |
| | | size="small" |
| | | color={record.statusBool ? "success" : "default"} |
| | | label={record.statusBool ? "启用" : "停用"} |
| | | /> |
| | | </Stack> |
| | | <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}> |
| | | <Chip size="small" variant="outlined" label={record.providerType$ || "OPENAI_COMPATIBLE"} /> |
| | | <Chip |
| | | size="small" |
| | | variant="outlined" |
| | | color={record.streamingEnabled ? "info" : "default"} |
| | | label={record.streamingEnabled ? "流式响应" : "非流式"} |
| | | /> |
| | | </Stack> |
| | | <Divider sx={{ my: 1.5 }} /> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | Base URL |
| | | </Typography> |
| | | <Typography variant="body2" sx={{ mb: 1.5, wordBreak: "break-all" }}> |
| | | {truncateText(record.baseUrl, 120)} |
| | | </Typography> |
| | | <Grid container spacing={1}> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">Temperature</Typography> |
| | | <Typography variant="body2">{record.temperature ?? "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">Top P</Typography> |
| | | <Typography variant="body2">{record.topP ?? "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">Max Tokens</Typography> |
| | | <Typography variant="body2">{record.maxTokens ?? "--"}</Typography> |
| | | </Grid> |
| | | <Grid item xs={6}> |
| | | <Typography variant="caption" color="text.secondary">Timeout</Typography> |
| | | <Typography variant="body2">{record.timeoutMs ?? "--"} ms</Typography> |
| | | </Grid> |
| | | </Grid> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1.5}> |
| | | 备注 |
| | | </Typography> |
| | | <Typography variant="body2">{truncateText(record.memo)}</Typography> |
| | | </CardContent> |
| | | <CardActions sx={{ px: 2, pb: 2, pt: 0, justifyContent: "space-between" }}> |
| | | <Stack direction="row" spacing={1}> |
| | | <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record.id)}> |
| | | 详情 |
| | | </Button> |
| | | <Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}> |
| | | 编辑 |
| | | </Button> |
| | | </Stack> |
| | | <Button |
| | | size="small" |
| | | color="error" |
| | | startIcon={<DeleteOutlineOutlinedIcon />} |
| | | onClick={() => onDelete(record)} |
| | | disabled={deleting} |
| | | > |
| | | 删除 |
| | | </Button> |
| | | </CardActions> |
| | | </Card> |
| | | </Grid> |
| | | ))} |
| | | </Grid> |
| | | </Box> |
| | | ); |
| | | }; |
| | | |
| | | const AiParamList = () => { |
| | | const notify = useNotify(); |
| | | const refresh = useRefresh(); |
| | | const [deleteOne, { isPending: deleting }] = useDelete(); |
| | | const [dialogState, setDialogState] = useState({ open: false, mode: "create", recordId: null }); |
| | | |
| | | const openDialog = (mode, recordId = null) => setDialogState({ open: true, mode, recordId }); |
| | | const closeDialog = () => setDialogState({ open: false, mode: "create", recordId: null }); |
| | | |
| | | const handleDelete = (record) => { |
| | | if (!record?.id || !window.confirm(`确认删除“${record.name}”吗?`)) { |
| | | return; |
| | | } |
| | | deleteOne( |
| | | "aiParam", |
| | | { id: record.id }, |
| | | { |
| | | onSuccess: () => { |
| | | notify("删除成功"); |
| | | refresh(); |
| | | }, |
| | | onError: (error) => { |
| | | notify(error?.message || "删除失败", { type: "error" }); |
| | | }, |
| | | } |
| | | ); |
| | | }; |
| | | |
| | | const dialogTitle = { |
| | | create: "新建 AI 参数", |
| | | edit: "编辑 AI 参数", |
| | | show: "查看 AI 参数详情", |
| | | }[dialogState.mode]; |
| | | |
| | | return ( |
| | | <> |
| | | <List |
| | | title={"menu.aiParam"} |
| | | empty={<EmptyData onClick={() => { setCreateDialog(true) }} />} |
| | | title="menu.aiParam" |
| | | filters={filters} |
| | | sort={{ field: "sort", order: "asc" }} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <MyCreateButton onClick={() => { setCreateDialog(true) }} /> |
| | | <SelectColumnsButton preferenceKey='aiParam' /> |
| | | <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}> |
| | | 新建 |
| | | </Button> |
| | | <MyExportButton /> |
| | | </TopToolbar> |
| | | )} |
| | | perPage={DEFAULT_PAGE_SIZE} |
| | | > |
| | | <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> |
| | | <AiParamCards |
| | | onView={(id) => openDialog("show", id)} |
| | | onEdit={(id) => openDialog("edit", id)} |
| | | onDelete={handleDelete} |
| | | deleting={deleting} |
| | | /> |
| | | </List> |
| | | <AiParamCreate |
| | | open={createDialog} |
| | | setOpen={setCreateDialog} |
| | | /> |
| | | </Box> |
| | | ) |
| | | } |
| | | <AiConfigDialog |
| | | open={dialogState.open} |
| | | mode={dialogState.mode} |
| | | title={dialogTitle} |
| | | resource="aiParam" |
| | | recordId={dialogState.recordId} |
| | | defaultValues={defaultValues} |
| | | maxWidth="md" |
| | | onClose={closeDialog} |
| | | > |
| | | <AiParamForm readOnly={dialogState.mode === "show"} /> |
| | | </AiConfigDialog> |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | export default AiParamList; |