import React, { useEffect, useMemo, useState } from "react";
|
import {
|
List,
|
SearchInput,
|
TopToolbar,
|
SelectColumnsButton,
|
FilterButton,
|
TextInput,
|
DateInput,
|
SelectInput,
|
useNotify,
|
useRefresh,
|
useListContext,
|
Pagination,
|
EditButton,
|
} from 'react-admin';
|
import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, Grid, InputLabel, MenuItem, Select, Stack, TextField as MuiTextField, Typography } from '@mui/material';
|
import EmptyData from "@/page/components/EmptyData";
|
import MyCreateButton from "@/page/components/MyCreateButton";
|
import MyExportButton from '@/page/components/MyExportButton';
|
import { DEFAULT_PAGE_SIZE } from '@/config/setting';
|
import AiPromptCreate from "./AiPromptCreate";
|
import request from "@/utils/request";
|
import { AiConsoleLayout, AiConsolePanel, aiCardSx } from "@/page/components/AiConsoleLayout";
|
|
const sceneChoices = [
|
{ id: 'general_chat', name: '通用对话' },
|
{ id: 'system_diagnose', name: '系统诊断' },
|
];
|
|
const sceneLabels = {
|
general_chat: '通用对话',
|
system_diagnose: '系统诊断',
|
};
|
|
const filters = [
|
<SearchInput source="condition" alwaysOn />,
|
<DateInput label='common.time.after' source="timeStart" alwaysOn />,
|
<DateInput label='common.time.before' source="timeEnd" alwaysOn />,
|
<SelectInput source="sceneCode" label="场景" choices={sceneChoices} />,
|
<TextInput source="templateName" label="模板名称" />,
|
<SelectInput
|
source="publishedFlag"
|
label="发布状态"
|
choices={[
|
{ id: '1', name: '已发布' },
|
{ id: '0', name: '草稿' },
|
]}
|
/>,
|
<SelectInput
|
label="common.field.status"
|
source="status"
|
choices={[
|
{ id: '1', name: 'common.enums.statusTrue' },
|
{ id: '0', name: 'common.enums.statusFalse' },
|
]}
|
/>,
|
];
|
|
const PromptBoard = ({ onCreateDraft }) => {
|
const { data, isLoading } = useListContext();
|
const records = data || [];
|
const refresh = useRefresh();
|
const notify = useNotify();
|
const [selectedScene, setSelectedScene] = useState('');
|
const [versionDialog, setVersionDialog] = useState(false);
|
const [activeRecord, setActiveRecord] = useState(null);
|
const [logs, setLogs] = useState([]);
|
const [compareId, setCompareId] = useState('');
|
const [compareData, setCompareData] = useState(null);
|
|
const groupedScenes = useMemo(() => {
|
const map = {};
|
records.forEach((item) => {
|
const code = item.sceneCode || 'unknown';
|
if (!map[code]) {
|
map[code] = [];
|
}
|
map[code].push(item);
|
});
|
return map;
|
}, [records]);
|
|
const sceneCodes = Object.keys(groupedScenes).sort();
|
const selectedRecords = groupedScenes[selectedScene] || [];
|
const publishedCount = records.filter((item) => item.publishedFlag === 1).length;
|
const draftCount = records.filter((item) => item.publishedFlag !== 1).length;
|
|
useEffect(() => {
|
if (!selectedScene && sceneCodes.length) {
|
setSelectedScene(sceneCodes[0]);
|
}
|
if (selectedScene && !groupedScenes[selectedScene] && sceneCodes.length) {
|
setSelectedScene(sceneCodes[0]);
|
}
|
}, [selectedScene, sceneCodes, groupedScenes]);
|
|
const openVersionDialog = async (record) => {
|
try {
|
const logRes = await request.get(`/ai/prompt/publish-log/list?sceneCode=${record.sceneCode}`);
|
setActiveRecord(record);
|
setLogs(logRes.data?.data || []);
|
setCompareId('');
|
setCompareData(null);
|
setVersionDialog(true);
|
} catch (error) {
|
notify(error.message || '加载版本日志失败', { type: 'error' });
|
}
|
};
|
|
const handlePublish = async (record) => {
|
try {
|
await request.post('/ai/prompt/publish', { id: record.id });
|
notify('common.response.success');
|
refresh();
|
} catch (error) {
|
notify(error.message || '操作失败', { type: 'error' });
|
}
|
};
|
|
const handleRollback = async (record) => {
|
try {
|
await request.post('/ai/prompt/rollback', { id: record.id });
|
notify('common.response.success');
|
refresh();
|
} catch (error) {
|
notify(error.message || '操作失败', { type: 'error' });
|
}
|
};
|
|
const handleCopy = async (record) => {
|
try {
|
await request.post('/ai/prompt/copy', { id: record.id });
|
notify('common.response.success');
|
refresh();
|
} catch (error) {
|
notify(error.message || '操作失败', { type: 'error' });
|
}
|
};
|
|
const handleCompare = async () => {
|
if (!activeRecord || !compareId) {
|
return;
|
}
|
try {
|
const res = await request.get(`/ai/prompt/compare?leftId=${activeRecord.id}&rightId=${compareId}`);
|
setCompareData(res.data?.data || null);
|
} catch (error) {
|
notify(error.message || '加载对比失败', { type: 'error' });
|
}
|
};
|
|
if (!isLoading && !records.length) {
|
return <EmptyData onClick={onCreateDraft} />;
|
}
|
|
return (
|
<>
|
<AiConsoleLayout
|
title="Prompt 配置中心"
|
subtitle="页面结构参考 zy-wcs-master 的 Prompt 中心,保留原系统的按钮、编辑页和权限模型,重点增强场景感知、版本阅读和运营动作展示。"
|
actions={[
|
<Button key="draft" variant="contained" onClick={onCreateDraft}>新建草稿</Button>,
|
]}
|
stats={[
|
{ label: '场景数', value: sceneCodes.length },
|
{ label: '版本总数', value: records.length },
|
{ label: '已发布', value: publishedCount },
|
{ label: '草稿', value: draftCount },
|
]}
|
>
|
<Grid container spacing={2}>
|
<Grid item xs={12} md={3}>
|
<AiConsolePanel
|
title="场景"
|
subtitle="每个场景只会有一个已发布版本,诊断和聊天运行时会直接读取它。"
|
minHeight={520}
|
>
|
<Stack spacing={1.5}>
|
{sceneCodes.map((code) => {
|
const list = groupedScenes[code] || [];
|
const published = list.find((item) => item.publishedFlag === 1);
|
const drafts = list.filter((item) => item.publishedFlag !== 1).length;
|
return (
|
<Box key={code} sx={{ ...aiCardSx(selectedScene === code), cursor: 'pointer' }} onClick={() => setSelectedScene(code)}>
|
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
|
<Box>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
|
{sceneLabels[code] || code}
|
</Typography>
|
<Typography variant="caption" sx={{ color: '#8aa0b7' }}>{code}</Typography>
|
</Box>
|
<Chip size="small" color={published ? 'success' : 'default'} label={published ? `v${published.versionNo}` : '未发布'} />
|
</Stack>
|
<Grid container spacing={1} sx={{ mt: 1 }}>
|
<Grid item xs={6}>
|
<Box sx={{ p: 1, borderRadius: 2, border: '1px solid #e7eef7', backgroundColor: '#fff' }}>
|
<Typography variant="caption" sx={{ color: '#7f92a8' }}>版本数</Typography>
|
<Typography variant="h6" sx={{ mt: 0.25, color: '#2a3e55' }}>{list.length}</Typography>
|
</Box>
|
</Grid>
|
<Grid item xs={6}>
|
<Box sx={{ p: 1, borderRadius: 2, border: '1px solid #e7eef7', backgroundColor: '#fff' }}>
|
<Typography variant="caption" sx={{ color: '#7f92a8' }}>草稿数</Typography>
|
<Typography variant="h6" sx={{ mt: 0.25, color: '#2a3e55' }}>{drafts}</Typography>
|
</Box>
|
</Grid>
|
</Grid>
|
</Box>
|
);
|
})}
|
</Stack>
|
</AiConsolePanel>
|
</Grid>
|
<Grid item xs={12} md={4}>
|
<AiConsolePanel
|
title="版本列表"
|
subtitle={`当前场景:${sceneLabels[selectedScene] || selectedScene || '未选择场景'}`}
|
action={<Button size="small" variant="outlined" onClick={onCreateDraft}>新建草稿</Button>}
|
minHeight={520}
|
>
|
<Stack spacing={1.5}>
|
{selectedRecords.map((record) => (
|
<Box key={record.id} sx={aiCardSx(record.publishedFlag === 1)}>
|
<Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
|
<Box>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>
|
{record.templateName || `${sceneLabels[record.sceneCode] || record.sceneCode} v${record.versionNo}`}
|
</Typography>
|
<Typography variant="caption" sx={{ color: '#8093a8' }}>
|
版本 v{record.versionNo} · 更新时间 {record.updateTime || '-'}
|
</Typography>
|
</Box>
|
<Stack direction="row" spacing={0.75}>
|
<Chip size="small" color={record.publishedFlag === 1 ? 'warning' : 'default'} label={record.publishedFlag === 1 ? '已发布' : '草稿'} />
|
<Chip size="small" color={record.status === 1 ? 'success' : 'default'} label={record.status === 1 ? '启用' : '停用'} />
|
</Stack>
|
</Stack>
|
<Typography variant="body2" sx={{ mt: 1.25, minHeight: 48, color: '#31465d' }}>
|
{record.memo || '未填写版本备注'}
|
</Typography>
|
<Stack direction="row" spacing={1} flexWrap="wrap" sx={{ mt: 1.5 }}>
|
<EditButton record={record} sx={{ px: 1.25, py: 0.5, minWidth: 64 }} />
|
<Button size="small" variant="outlined" onClick={() => openVersionDialog(record)}>版本</Button>
|
<Button size="small" variant="outlined" onClick={() => handleCopy(record)}>复制</Button>
|
<Button size="small" variant="outlined" onClick={() => handlePublish(record)}>发布</Button>
|
<Button size="small" variant="outlined" onClick={() => handleRollback(record)}>回滚</Button>
|
</Stack>
|
</Box>
|
))}
|
</Stack>
|
<Box sx={{ mt: 2 }}>
|
<Pagination rowsPerPageOptions={[DEFAULT_PAGE_SIZE, 25, 50]} />
|
</Box>
|
</AiConsolePanel>
|
</Grid>
|
<Grid item xs={12} md={5}>
|
<AiConsolePanel
|
title="运营提示"
|
subtitle="当前版本编辑仍进入原有编辑页,这里主要承接版本阅读、对比和发布日志预览。"
|
minHeight={520}
|
>
|
<Box sx={{ ...aiCardSx(false), minHeight: 140 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>使用建议</Typography>
|
<Typography variant="body2" sx={{ mt: 1, color: '#31465d', lineHeight: 1.8 }}>
|
先在左侧切换场景,再在中间挑选草稿或线上版本。需要深度查看发布轨迹、做两版 Prompt 文本对比时,点击“版本”进入运营弹窗。
|
</Typography>
|
</Box>
|
<Box sx={{ ...aiCardSx(true), mt: 1.5, minHeight: 230 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#284059' }}>当前场景摘要</Typography>
|
<Stack spacing={1} sx={{ mt: 1.25 }}>
|
<Typography variant="body2" sx={{ color: '#31465d' }}>
|
场景编码:{selectedScene || '-'}
|
</Typography>
|
<Typography variant="body2" sx={{ color: '#31465d' }}>
|
发布版本:{(selectedRecords.find((item) => item.publishedFlag === 1)?.versionNo) ? `v${selectedRecords.find((item) => item.publishedFlag === 1)?.versionNo}` : '暂无'}
|
</Typography>
|
<Typography variant="body2" sx={{ color: '#31465d' }}>
|
草稿数量:{selectedRecords.filter((item) => item.publishedFlag !== 1).length}
|
</Typography>
|
<Typography variant="body2" sx={{ color: '#31465d' }}>
|
启用版本:{selectedRecords.filter((item) => item.status === 1).length}
|
</Typography>
|
</Stack>
|
</Box>
|
</AiConsolePanel>
|
</Grid>
|
</Grid>
|
</AiConsoleLayout>
|
|
<Dialog open={versionDialog} onClose={() => setVersionDialog(false)} fullWidth maxWidth="lg">
|
<DialogTitle>Prompt 版本运营</DialogTitle>
|
<DialogContent>
|
<Stack spacing={3}>
|
<Typography variant="body2">
|
当前模板:{activeRecord?.templateName || '-'} / 版本 {activeRecord?.versionNo || '-'}
|
</Typography>
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
|
<Box flex={1}>
|
<Typography variant="subtitle1" gutterBottom>发布日志</Typography>
|
{logs.map((item) => (
|
<Box key={item.id} sx={{ borderBottom: '1px solid #eee', pb: 1, mb: 1 }}>
|
<Typography variant="body2">{item.actionType} / v{item.versionNo} / {item.templateName}</Typography>
|
<Typography variant="caption" color="text.secondary">{item.actionDesc} / {item.createTime}</Typography>
|
</Box>
|
))}
|
</Box>
|
<Box flex={1}>
|
<Typography variant="subtitle1" gutterBottom>文本对比</Typography>
|
<Stack spacing={1.5}>
|
<FormControl sx={{ minWidth: 220 }}>
|
<InputLabel id="prompt-compare-label">对比版本</InputLabel>
|
<Select
|
labelId="prompt-compare-label"
|
label="对比版本"
|
value={compareId}
|
onChange={(event) => setCompareId(event.target.value)}
|
>
|
{selectedRecords.filter((item) => item.id !== activeRecord?.id).map((item) => (
|
<MenuItem key={item.id} value={item.id}>v{item.versionNo} / {item.templateName}</MenuItem>
|
))}
|
</Select>
|
</FormControl>
|
<Button variant="outlined" onClick={handleCompare}>加载对比</Button>
|
</Stack>
|
</Box>
|
</Stack>
|
{compareData ? (
|
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2}>
|
<Box flex={1}>
|
<Typography variant="subtitle1" gutterBottom>当前版本</Typography>
|
<MuiTextField label="基础提示词" fullWidth multiline minRows={5} value={compareData.left?.basePrompt || ''} disabled margin="dense" />
|
<MuiTextField label="工具提示词" fullWidth multiline minRows={5} value={compareData.left?.toolPrompt || ''} disabled margin="dense" />
|
<MuiTextField label="输出提示词" fullWidth multiline minRows={5} value={compareData.left?.outputPrompt || ''} disabled margin="dense" />
|
</Box>
|
<Box flex={1}>
|
<Typography variant="subtitle1" gutterBottom>对比版本</Typography>
|
<MuiTextField label="基础提示词" fullWidth multiline minRows={5} value={compareData.right?.basePrompt || ''} disabled margin="dense" />
|
<MuiTextField label="工具提示词" fullWidth multiline minRows={5} value={compareData.right?.toolPrompt || ''} disabled margin="dense" />
|
<MuiTextField label="输出提示词" fullWidth multiline minRows={5} value={compareData.right?.outputPrompt || ''} disabled margin="dense" />
|
</Box>
|
</Stack>
|
) : null}
|
</Stack>
|
</DialogContent>
|
<DialogActions>
|
<Button onClick={() => setVersionDialog(false)}>关闭</Button>
|
</DialogActions>
|
</Dialog>
|
</>
|
);
|
};
|
|
const AiPromptList = () => {
|
const [createDialog, setCreateDialog] = useState(false);
|
|
return (
|
<Box display="flex" sx={{ width: '100%' }}>
|
<List
|
sx={{ width: '100%', flexGrow: 1 }}
|
title={"menu.aiPrompt"}
|
empty={<EmptyData onClick={() => { setCreateDialog(true) }} />}
|
filters={filters}
|
sort={{ field: "updateTime", order: "desc" }}
|
actions={(
|
<TopToolbar>
|
<FilterButton />
|
<MyCreateButton onClick={() => { setCreateDialog(true) }} />
|
<SelectColumnsButton preferenceKey='aiPrompt' />
|
<MyExportButton />
|
</TopToolbar>
|
)}
|
perPage={DEFAULT_PAGE_SIZE}
|
pagination={false}
|
>
|
<PromptBoard onCreateDraft={() => setCreateDialog(true)} />
|
</List>
|
<AiPromptCreate open={createDialog} setOpen={setCreateDialog} />
|
</Box>
|
)
|
}
|
|
export default AiPromptList;
|