update:1.开放系统内置提示词,支持用户自定义模板

This commit is contained in:
xiamuceer
2025-11-29 22:01:02 +08:00
parent e772676621
commit d102328b75
23 changed files with 2325 additions and 746 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "1.0.7",
"version": "1.0.8",
"type": "module",
"scripts": {
"dev": "vite",
+2
View File
@@ -17,6 +17,7 @@ import WritingStyles from './pages/WritingStyles';
import Settings from './pages/Settings';
import MCPPlugins from './pages/MCPPlugins';
import UserManagement from './pages/UserManagement';
import PromptTemplates from './pages/PromptTemplates';
// import Polish from './pages/Polish';
import Login from './pages/Login';
import AuthCallback from './pages/AuthCallback';
@@ -42,6 +43,7 @@ function App() {
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
<Route path="/inspiration" element={<ProtectedRoute><Inspiration /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Route path="/prompt-templates" element={<ProtectedRoute><><PromptTemplates /><AppFooter /></></ProtectedRoute>} />
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
<Route path="/user-management" element={<ProtectedRoute><UserManagement /></ProtectedRoute>} />
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
+2 -2
View File
@@ -351,9 +351,9 @@ const Inspiration: React.FC = () => {
type: 'ai',
content: `很好!现在请选择你想要的大纲模式:
📋 **一对一模式**:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
📚 **一对多模式**:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
请选择:`,
options: ['📋 一对一模式', '📚 一对多模式']
+13 -1
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch, Dropdown, Form, Input, InputNumber } from 'antd';
import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined, BulbOutlined, LoadingOutlined } from '@ant-design/icons';
import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined, BulbOutlined, LoadingOutlined, FileSearchOutlined } from '@ant-design/icons';
import { projectApi } from '../services/api';
import { useStore } from '../store';
import { useProjectSync } from '../store/hooks';
@@ -438,6 +438,12 @@ export default function ProjectList() {
{
type: 'divider'
},
{
key: 'prompt-templates',
label: '提示词管理',
icon: <FileSearchOutlined />,
onClick: () => navigate('/prompt-templates')
},
{
key: 'mcp',
label: 'MCP插件',
@@ -546,6 +552,12 @@ export default function ProjectList() {
{
type: 'divider'
},
{
key: 'prompt-templates',
label: '提示词管理',
icon: <FileSearchOutlined />,
onClick: () => navigate('/prompt-templates')
},
{
key: 'mcp',
label: 'MCP插件',
+491
View File
@@ -0,0 +1,491 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card,
Tabs,
Button,
Switch,
Modal,
Input,
Tag,
message,
Space,
Typography,
Row,
Col,
Alert,
Upload,
Spin,
Empty
} from 'antd';
import {
EditOutlined,
ReloadOutlined,
DownloadOutlined,
UploadOutlined,
CheckCircleOutlined,
FileSearchOutlined,
ArrowLeftOutlined,
InfoCircleOutlined
} from '@ant-design/icons';
import axios from 'axios';
import { cardStyles, cardHoverHandlers, gridConfig } from '../components/CardStyles';
const { TextArea } = Input;
const { Title, Text, Paragraph } = Typography;
interface PromptTemplate {
id: string;
user_id: string;
template_key: string;
template_name: string;
template_content: string;
description: string;
category: string;
parameters: string;
is_active: boolean;
is_system_default: boolean;
created_at: string;
updated_at: string;
}
interface CategoryGroup {
category: string;
count: number;
templates: PromptTemplate[];
}
export default function PromptTemplates() {
const navigate = useNavigate();
const [categories, setCategories] = useState<CategoryGroup[]>([]);
const [selectedCategory, setSelectedCategory] = useState<string>('0');
const [editingTemplate, setEditingTemplate] = useState<PromptTemplate | null>(null);
const [editorVisible, setEditorVisible] = useState(false);
const [loading, setLoading] = useState(false);
const isMobile = window.innerWidth <= 768;
// 加载模板数据
const loadTemplates = async () => {
try {
setLoading(true);
const response = await axios.get<CategoryGroup[]>('/api/prompt-templates/categories');
setCategories(response.data);
} catch (error: any) {
message.error(error.response?.data?.detail || '加载失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadTemplates();
}, []);
// 获取当前分类的模板
const getCurrentTemplates = (): PromptTemplate[] => {
const index = parseInt(selectedCategory);
if (index === 0) {
return categories.flatMap(cat => cat.templates);
}
return categories[index - 1]?.templates || [];
};
// 编辑模板
const handleEdit = (template: PromptTemplate) => {
setEditingTemplate({ ...template });
setEditorVisible(true);
};
// 保存模板
const handleSave = async () => {
if (!editingTemplate) return;
try {
setLoading(true);
await axios.post('/api/prompt-templates', {
template_key: editingTemplate.template_key,
template_name: editingTemplate.template_name,
template_content: editingTemplate.template_content,
description: editingTemplate.description,
category: editingTemplate.category,
parameters: editingTemplate.parameters,
is_active: editingTemplate.is_active
});
message.success('保存成功');
setEditorVisible(false);
loadTemplates();
} catch (error: any) {
message.error(error.response?.data?.detail || '保存失败');
} finally {
setLoading(false);
}
};
// 重置为系统默认
const handleReset = async (templateKey: string) => {
Modal.confirm({
title: '确认重置',
content: '确定要重置为系统默认模板吗?这将覆盖您的自定义内容。',
okText: '确定',
cancelText: '取消',
centered: true,
onOk: async () => {
try {
setLoading(true);
await axios.post(`/api/prompt-templates/${templateKey}/reset`);
message.success('已重置为系统默认');
loadTemplates();
} catch (error: any) {
message.error(error.response?.data?.detail || '重置失败');
} finally {
setLoading(false);
}
}
});
};
// 切换启用状态
const handleToggleActive = async (template: PromptTemplate, checked: boolean) => {
try {
await axios.put(`/api/prompt-templates/${template.template_key}`, {
is_active: checked
});
loadTemplates();
} catch (error: any) {
message.error(error.response?.data?.detail || '操作失败');
}
};
// 导出所有模板
const handleExport = async () => {
try {
const response = await axios.post('/api/prompt-templates/export');
const blob = new Blob([JSON.stringify(response.data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `prompt-templates-${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
message.success('导出成功');
} catch (error: any) {
message.error(error.response?.data?.detail || '导出失败');
}
};
// 导入模板
const handleImport = async (file: File) => {
try {
const text = await file.text();
const data = JSON.parse(text);
await axios.post('/api/prompt-templates/import', data);
message.success('导入成功');
loadTemplates();
} catch (error: any) {
message.error(error.response?.data?.detail || '导入失败');
}
return false; // 阻止默认上传行为
};
const currentTemplates = getCurrentTemplates();
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '20px 16px' : '40px 24px'
}}>
{/* 头部卡片 */}
<div style={{
maxWidth: 1400,
margin: '0 auto',
marginBottom: isMobile ? 20 : 40
}}>
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
}}
>
<Row align="middle" justify="space-between" gutter={[16, 16]}>
<Col xs={24} sm={12} md={14}>
<Space direction="vertical" size={4}>
<Space align="center">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/projects')}
size={isMobile ? 'small' : 'middle'}
/>
<Title level={isMobile ? 3 : 2} style={{ margin: 0 }}>
<FileSearchOutlined style={{ color: '#667eea', marginRight: 8 }} />
</Title>
</Space>
<Text type="secondary" style={{ fontSize: isMobile ? 12 : 14, marginLeft: isMobile ? 40 : 48 }}>
AI生成提示词
</Text>
</Space>
</Col>
<Col xs={24} sm={12} md={10}>
<Space wrap style={{ justifyContent: isMobile ? 'flex-start' : 'flex-end', width: '100%' }}>
<Button
icon={<DownloadOutlined />}
onClick={handleExport}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 8 }}
>
</Button>
<Upload
accept=".json"
showUploadList={false}
beforeUpload={handleImport}
>
<Button
icon={<UploadOutlined />}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 8 }}
>
</Button>
</Upload>
</Space>
</Col>
</Row>
{/* 使用提示 */}
<Alert
message={
<Space align="center">
<InfoCircleOutlined style={{ fontSize: 16, color: '#1890ff' }} />
<Text strong style={{ fontSize: isMobile ? 13 : 14 }}>使</Text>
</Space>
}
description={
<div>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', marginBottom: 8 }}>
<strong></strong>"编辑"
</Text>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block' }}>
<strong></strong>/使 <Text code>{'{variable_name}'}</Text> "重置"
</Text>
</div>
}
type="info"
showIcon={false}
style={{
marginTop: isMobile ? 16 : 24,
borderRadius: 12,
background: 'linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%)',
border: '1px solid #91d5ff'
}}
/>
</Card>
</div>
{/* 主内容区 */}
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
<Spin spinning={loading}>
{/* 分类标签 */}
{categories.length > 0 && (
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: isMobile ? 12 : 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
marginBottom: isMobile ? 16 : 24
}}
styles={{ body: { padding: isMobile ? '12px' : '16px' } }}
>
<Tabs
activeKey={selectedCategory}
onChange={setSelectedCategory}
items={[
{ key: '0', label: `全部 (${categories.reduce((sum, cat) => sum + cat.count, 0)})` },
...categories.map((cat, index) => ({
key: (index + 1).toString(),
label: `${cat.category} (${cat.count})`
}))
]}
/>
</Card>
)}
{/* 模板列表 */}
{currentTemplates.length === 0 ? (
<Card
variant="borderless"
style={{
background: 'rgba(255, 255, 255, 0.95)',
borderRadius: 16,
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
}}
>
<Empty
description="暂无模板数据"
style={{ padding: '80px 0' }}
/>
</Card>
) : (
<Row gutter={[16, 16]}>
{currentTemplates.map(template => (
<Col {...gridConfig} key={template.id}>
<Card
hoverable
variant="borderless"
style={cardStyles.project}
styles={{ body: { padding: 0, overflow: 'hidden' } }}
{...cardHoverHandlers}
>
{/* 头部 */}
<div style={{
background: template.is_system_default
? 'linear-gradient(135deg, #a8a8a8 0%, #636363 100%)'
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '16px' : '20px',
position: 'relative'
}}>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Title level={isMobile ? 5 : 4} style={{ margin: 0, color: '#fff', flex: 1 }} ellipsis>
{template.template_name}
</Title>
{!template.is_system_default && (
<Switch
checked={template.is_active}
onChange={(checked) => handleToggleActive(template, checked)}
size={isMobile ? 'small' : 'default'}
style={{ marginLeft: 8 }}
/>
)}
</div>
<Space wrap>
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
{template.category}
</Tag>
<Tag color="rgba(255,255,255,0.3)" style={{ color: '#fff', border: 'none' }}>
{template.is_system_default ? '系统默认' : '已自定义'}
</Tag>
</Space>
</Space>
</div>
{/* 内容 */}
<div style={{ padding: isMobile ? '16px' : '20px' }}>
<Paragraph
type="secondary"
ellipsis={{ rows: 3 }}
style={{ minHeight: 66, marginBottom: 16 }}
>
{template.description || '暂无描述'}
</Paragraph>
<Space wrap style={{ marginBottom: 16 }}>
<Tag
icon={<CheckCircleOutlined />}
color={template.is_system_default || template.is_active ? 'success' : 'default'}
>
{template.is_system_default ? '始终启用' : (template.is_active ? '已启用' : '已禁用')}
</Tag>
</Space>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 16 }}>
: {template.template_key}
</Text>
{/* 操作按钮 */}
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => handleEdit(template)}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 6 }}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={() => handleReset(template.template_key)}
size={isMobile ? 'small' : 'middle'}
style={{ borderRadius: 6 }}
>
</Button>
</Space>
</div>
</Card>
</Col>
))}
</Row>
)}
</Spin>
</div>
{/* 编辑对话框 */}
<Modal
title={`编辑模板: ${editingTemplate?.template_name}`}
open={editorVisible}
onCancel={() => setEditorVisible(false)}
onOk={handleSave}
width={isMobile ? '100%' : 900}
centered={!isMobile}
confirmLoading={loading}
okText="保存"
cancelText="取消"
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
styles={isMobile ? {
body: {
maxHeight: 'calc(100vh - 110px)',
overflowY: 'auto',
padding: '16px'
}
} : undefined}
>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}></label>
<Input
value={editingTemplate?.template_name || ''}
onChange={(e) => setEditingTemplate(prev => prev ? { ...prev, template_name: e.target.value } : null)}
placeholder="输入模板名称"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}></label>
<TextArea
value={editingTemplate?.description || ''}
onChange={(e) => setEditingTemplate(prev => prev ? { ...prev, description: e.target.value } : null)}
rows={2}
placeholder="简要描述模板用途"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 500 }}></label>
<TextArea
value={editingTemplate?.template_content || ''}
onChange={(e) => setEditingTemplate(prev => prev ? { ...prev, template_content: e.target.value } : null)}
rows={isMobile ? 15 : 20}
style={{ fontFamily: 'monospace', fontSize: '13px' }}
placeholder="输入提示词模板内容..."
/>
</div>
<Alert
message="提示:使用 {variable_name} 格式表示变量占位符"
type="info"
showIcon
style={{ borderRadius: 8 }}
/>
</Space>
</Modal>
</div>
);
}