| New file |
| | |
| | | import React, { useMemo, useState } from "react"; |
| | | import { |
| | | FilterButton, |
| | | List, |
| | | SearchInput, |
| | | SelectInput, |
| | | 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 AiPromptForm from "./AiPromptForm"; |
| | | import AiConfigDialog from "../aiShared/AiConfigDialog"; |
| | | |
| | | const filters = [ |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <TextInput source="code" label="编码" />, |
| | | <TextInput source="scene" label="场景" />, |
| | | <SelectInput |
| | | source="status" |
| | | label="状态" |
| | | choices={[ |
| | | { id: "1", name: "common.enums.statusTrue" }, |
| | | { id: "0", name: "common.enums.statusFalse" }, |
| | | ]} |
| | | />, |
| | | ]; |
| | | |
| | | const defaultValues = { |
| | | code: "home.default", |
| | | scene: "home", |
| | | status: 1, |
| | | }; |
| | | |
| | | const truncateText = (value, max = 120) => { |
| | | if (!value) { |
| | | return "--"; |
| | | } |
| | | return value.length > max ? `${value.slice(0, max)}...` : value; |
| | | }; |
| | | |
| | | const AiPromptCards = ({ 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">暂无 Prompt 配置</Typography> |
| | | <Typography variant="body2" color="text.secondary" mt={1}> |
| | | 新建一张 Prompt 卡片后,AI 对话会动态加载这里的内容。 |
| | | </Typography> |
| | | </Card> |
| | | </Box> |
| | | ); |
| | | } |
| | | |
| | | return ( |
| | | <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.code || "--"} |
| | | </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={`Scene: ${record.scene || "--"}`} /> |
| | | </Stack> |
| | | <Divider sx={{ my: 1.5 }} /> |
| | | <Typography variant="caption" color="text.secondary">System Prompt</Typography> |
| | | <Typography variant="body2" sx={{ mt: 0.5 }}> |
| | | {truncateText(record.systemPrompt)} |
| | | </Typography> |
| | | <Typography variant="caption" color="text.secondary" display="block" mt={1.5}> |
| | | User Prompt Template |
| | | </Typography> |
| | | <Typography variant="body2">{truncateText(record.userPromptTemplate, 100)}</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 AiPromptList = () => { |
| | | 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( |
| | | "aiPrompt", |
| | | { id: record.id }, |
| | | { |
| | | onSuccess: () => { |
| | | notify("删除成功"); |
| | | refresh(); |
| | | }, |
| | | onError: (error) => { |
| | | notify(error?.message || "删除失败", { type: "error" }); |
| | | }, |
| | | } |
| | | ); |
| | | }; |
| | | |
| | | const dialogTitle = { |
| | | create: "新建 Prompt", |
| | | edit: "编辑 Prompt", |
| | | show: "查看 Prompt 详情", |
| | | }[dialogState.mode]; |
| | | |
| | | return ( |
| | | <> |
| | | <List |
| | | title="menu.aiPrompt" |
| | | filters={filters} |
| | | sort={{ field: "create_time", order: "desc" }} |
| | | actions={( |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}> |
| | | 新建 |
| | | </Button> |
| | | <MyExportButton /> |
| | | </TopToolbar> |
| | | )} |
| | | > |
| | | <AiPromptCards |
| | | onView={(id) => openDialog("show", id)} |
| | | onEdit={(id) => openDialog("edit", id)} |
| | | onDelete={handleDelete} |
| | | deleting={deleting} |
| | | /> |
| | | </List> |
| | | <AiConfigDialog |
| | | open={dialogState.open} |
| | | mode={dialogState.mode} |
| | | title={dialogTitle} |
| | | resource="aiPrompt" |
| | | recordId={dialogState.recordId} |
| | | defaultValues={defaultValues} |
| | | maxWidth="lg" |
| | | onClose={closeDialog} |
| | | > |
| | | <AiPromptForm readOnly={dialogState.mode === "show"} /> |
| | | </AiConfigDialog> |
| | | </> |
| | | ); |
| | | }; |
| | | |
| | | export default AiPromptList; |