imetting/frontend/src/pages/admin/PromptManagement.jsx

247 lines
9.1 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import {
Layout, List, Button, Space, Typography, Card,
Input, Switch, App, Modal, Form, Badge, Tooltip, Divider, Checkbox, Empty, Select, Tag
} from 'antd';
import {
PlusOutlined,
LeftOutlined,
RightOutlined,
DeleteOutlined,
BookOutlined,
FileTextOutlined,
StarFilled,
StarOutlined,
SaveOutlined,
CheckOutlined,
EditOutlined,
CloseOutlined,
MessageOutlined,
DatabaseOutlined
} from '@ant-design/icons';
import apiClient from '../../utils/apiClient';
import { buildApiUrl, API_ENDPOINTS } from '../../config/api';
import ActionButton from '../../components/ActionButton';
import MarkdownEditor from '../../components/MarkdownEditor';
import StatusTag from '../../components/StatusTag';
const { Sider, Content } = Layout;
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
const TASK_TYPES = {
MEETING_TASK: { label: '总结模版', icon: <MessageOutlined /> },
KNOWLEDGE_TASK: { label: '知识库模版', icon: <DatabaseOutlined /> }
};
const PromptManagement = () => {
const { message, modal } = App.useApp();
const [form] = Form.useForm();
const [prompts, setPrompts] = useState([]);
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [editingPrompt, setEditingPrompt] = useState(null);
const [editingTitle, setEditingTitle] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
fetchPrompts();
}, []);
const fetchPrompts = async () => {
setLoading(true);
try {
const response = await apiClient.get(buildApiUrl(API_ENDPOINTS.PROMPTS.LIST));
const list = response.data.prompts || [];
setPrompts(list);
if (list.length > 0 && !selectedPrompt) {
setSelectedPrompt(list[0]);
setEditingPrompt({ ...list[0] });
}
} finally {
setLoading(false);
}
};
const handlePromptSelect = (prompt) => {
setSelectedPrompt(prompt);
setEditingPrompt({ ...prompt });
setEditingTitle(false);
};
const handleEditChange = (field, value) => {
setEditingPrompt(prev => ({ ...prev, [field]: value }));
};
const handleSave = async () => {
if (!editingPrompt) return;
setIsSaving(true);
try {
const payload = {
...editingPrompt,
is_default: editingPrompt.is_default ? 1 : 0,
is_active: editingPrompt.is_active ? 1 : 0
};
await apiClient.put(buildApiUrl(API_ENDPOINTS.PROMPTS.UPDATE(editingPrompt.id)), payload);
message.success('保存成功');
fetchPrompts();
} catch (e) {
message.error('保存失败');
} finally {
setIsSaving(false);
}
};
const handleDelete = (item) => {
if (item.is_default) {
message.warning('默认模版不允许删除');
return;
}
modal.confirm({
title: '删除模版',
content: `确定要删除模版 "${item.name}" 吗?`,
okText: '删除',
okType: 'danger',
onOk: async () => {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.PROMPTS.DELETE(item.id)));
message.success('删除成功');
setSelectedPrompt(null);
fetchPrompts();
}
});
};
const groupedPrompts = {
MEETING_TASK: prompts.filter(p => p.task_type === 'MEETING_TASK'),
KNOWLEDGE_TASK: prompts.filter(p => p.task_type === 'KNOWLEDGE_TASK')
};
return (
<Layout style={{ background: '#fff', borderRadius: 12, overflow: 'hidden', minHeight: 'calc(100vh - 120px)' }}>
<Sider
width={280}
theme="light"
collapsed={sidebarCollapsed}
collapsible
onCollapse={setSidebarCollapsed}
style={{ borderRight: '1px solid #f0f0f0' }}
>
<div style={{ padding: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{!sidebarCollapsed && <Title level={5} style={{ margin: 0 }}>提示词模版</Title>}
<Button type="primary" shape="circle" icon={<PlusOutlined />} onClick={() => setShowCreateModal(true)} />
</div>
<div style={{ overflowY: 'auto', height: 'calc(100vh - 200px)' }}>
{['MEETING_TASK', 'KNOWLEDGE_TASK'].map(type => (
<div key={type} style={{ marginBottom: 16 }}>
{!sidebarCollapsed && (
<div style={{ padding: '0 16px 8px', fontSize: 12, color: '#8c8c8c' }}>
{TASK_TYPES[type].icon} <span style={{ marginLeft: 8 }}>{TASK_TYPES[type].label}</span>
</div>
)}
<List
dataSource={groupedPrompts[type]}
renderItem={item => (
<div
onClick={() => handlePromptSelect(item)}
style={{
padding: '10px 16px',
cursor: 'pointer',
background: selectedPrompt?.id === item.id ? '#e6f4ff' : 'transparent',
borderLeft: selectedPrompt?.id === item.id ? '4px solid #1677ff' : '4px solid transparent',
transition: 'all 0.3s'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Text strong ellipsis style={{ width: '80%' }}>{item.name}</Text>
{item.is_default === 1 && <StarFilled style={{ color: '#faad14', fontSize: 12 }} />}
</div>
{!item.is_active && <StatusTag active={false} compact />}
</div>
)}
/>
</div>
))}
</div>
</Sider>
<Content style={{ padding: '24px', background: '#fff' }}>
{selectedPrompt && editingPrompt ? (
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
<Title level={3} style={{ margin: 0 }}>{selectedPrompt.name}</Title>
<ActionButton tone="edit" variant="iconSm" tooltip="编辑" icon={<EditOutlined />} onClick={() => {}} />
</div>
<Paragraph type="secondary">{selectedPrompt.desc || '暂无描述'}</Paragraph>
</div>
<Space>
<Button icon={<DeleteOutlined />} className="btn-soft-red" onClick={() => handleDelete(selectedPrompt)}>删除</Button>
<Button icon={<SaveOutlined />} type="primary" loading={isSaving} onClick={handleSave}>保存更改</Button>
</Space>
</div>
<Card size="small" style={{ background: '#f9f9f9' }}>
<Space split={<Divider type="vertical" />}>
<Space><Text type="secondary">分类:</Text> <Tag color="blue">{TASK_TYPES[selectedPrompt.task_type].label}</Tag></Space>
<Space>
<Text type="secondary">默认:</Text>
<Checkbox checked={editingPrompt.is_default === 1} onChange={e => handleEditChange('is_default', e.target.checked ? 1 : 0)}>
设为默认
</Checkbox>
</Space>
<Space>
<Text type="secondary">状态:</Text>
<Switch checked={editingPrompt.is_active === 1} onChange={v => handleEditChange('is_active', v ? 1 : 0)} />
</Space>
</Space>
</Card>
<MarkdownEditor
value={editingPrompt.content}
onChange={v => handleEditChange('content', v)}
height={600}
showImageUpload={false}
/>
</Space>
) : (
<Empty style={{ marginTop: 100 }} description="请从左侧选择一个模版进行编辑" />
)}
</Content>
<Modal
title="创建新模版"
open={showCreateModal}
onCancel={() => setShowCreateModal(false)}
onOk={() => form.submit()}
>
<Form form={form} layout="vertical" onFinish={async (v) => {
await apiClient.post(buildApiUrl(API_ENDPOINTS.PROMPTS.CREATE), v);
message.success('创建成功');
setShowCreateModal(false);
fetchPrompts();
}}>
<Form.Item name="name" label="模版名称" rules={[{ required: true }]}>
<Input placeholder="例如:精简纪要" />
</Form.Item>
<Form.Item name="task_type" label="任务类型" rules={[{ required: true }]} initialValue="MEETING_TASK">
<Select>
<Select.Option value="MEETING_TASK">总结模版</Select.Option>
<Select.Option value="KNOWLEDGE_TASK">知识库模版</Select.Option>
</Select>
</Form.Item>
<Form.Item name="desc" label="模版描述">
<TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</Layout>
);
};
export default PromptManagement;