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