update:1.开放系统内置提示词,支持用户自定义模板
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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>} />
|
||||
|
||||
@@ -351,9 +351,9 @@ const Inspiration: React.FC = () => {
|
||||
type: 'ai',
|
||||
content: `很好!现在请选择你想要的大纲模式:
|
||||
|
||||
📋 **一对一模式**:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
||||
📋 一对一模式:传统模式,一个大纲对应一个章节,适合结构清晰、章节独立的小说。
|
||||
|
||||
📚 **一对多模式**:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
||||
📚 一对多模式:细化模式,一个大纲可以展开成多个章节,适合需要详细展开情节的小说。
|
||||
|
||||
请选择:`,
|
||||
options: ['📋 一对一模式', '📚 一对多模式']
|
||||
|
||||
@@ -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插件',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user