update:更新自定义写作风格模块
This commit is contained in:
@@ -10,6 +10,7 @@ import Characters from './pages/Characters';
|
||||
import Relationships from './pages/Relationships';
|
||||
import Organizations from './pages/Organizations';
|
||||
import Chapters from './pages/Chapters';
|
||||
import WritingStyles from './pages/WritingStyles';
|
||||
import Settings from './pages/Settings';
|
||||
// import Polish from './pages/Polish';
|
||||
import Login from './pages/Login';
|
||||
@@ -41,6 +42,7 @@ function App() {
|
||||
<Route path="relationships" element={<Relationships />} />
|
||||
<Route path="organizations" element={<Organizations />} />
|
||||
<Route path="chapters" element={<Chapters />} />
|
||||
<Route path="writing-styles" element={<WritingStyles />} />
|
||||
{/* <Route path="polish" element={<Polish />} /> */}
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -3,8 +3,8 @@ import { List, Button, Modal, Form, Input, Select, message, Empty, Space, Badge,
|
||||
import { EditOutlined, FileTextOutlined, ThunderboltOutlined, LockOutlined, DownloadOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useChapterSync } from '../store/hooks';
|
||||
import { projectApi } from '../services/api';
|
||||
import type { Chapter, ChapterUpdate, ApiError } from '../types';
|
||||
import { projectApi, writingStyleApi } from '../services/api';
|
||||
import type { Chapter, ChapterUpdate, ApiError, WritingStyle } from '../types';
|
||||
import { cardStyles } from '../components/CardStyles';
|
||||
|
||||
const { TextArea } = Input;
|
||||
@@ -20,6 +20,8 @@ export default function Chapters() {
|
||||
const [editorForm] = Form.useForm();
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
|
||||
const contentTextAreaRef = useRef<any>(null);
|
||||
const [writingStyles, setWritingStyles] = useState<WritingStyle[]>([]);
|
||||
const [selectedStyleId, setSelectedStyleId] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
@@ -39,10 +41,29 @@ export default function Chapters() {
|
||||
useEffect(() => {
|
||||
if (currentProject?.id) {
|
||||
refreshChapters();
|
||||
loadWritingStyles();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentProject?.id]);
|
||||
|
||||
const loadWritingStyles = async () => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
try {
|
||||
const response = await writingStyleApi.getProjectStyles(currentProject.id);
|
||||
setWritingStyles(response.styles);
|
||||
|
||||
// 设置默认风格为初始选中
|
||||
const defaultStyle = response.styles.find(s => s.is_default);
|
||||
if (defaultStyle) {
|
||||
setSelectedStyleId(defaultStyle.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载写作风格失败:', error);
|
||||
message.error('加载写作风格失败');
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentProject) return null;
|
||||
|
||||
const canGenerateChapter = (chapter: Chapter): boolean => {
|
||||
@@ -146,7 +167,7 @@ export default function Chapters() {
|
||||
textArea.scrollTop = textArea.scrollHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, selectedStyleId);
|
||||
|
||||
message.success('AI创作成功');
|
||||
} catch (error) {
|
||||
@@ -163,6 +184,8 @@ export default function Chapters() {
|
||||
c => c.chapter_number < chapter.chapter_number
|
||||
).sort((a, b) => a.chapter_number - b.chapter_number);
|
||||
|
||||
const selectedStyle = writingStyles.find(s => s.id === selectedStyleId);
|
||||
|
||||
const modal = Modal.confirm({
|
||||
title: 'AI创作章节内容',
|
||||
width: 700,
|
||||
@@ -175,6 +198,9 @@ export default function Chapters() {
|
||||
<li>项目的世界观设定</li>
|
||||
<li>相关角色信息</li>
|
||||
<li><strong>前面已完成章节的内容(确保剧情连贯)</strong></li>
|
||||
{selectedStyle && (
|
||||
<li><strong>写作风格:{selectedStyle.name}</strong></li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{previousChapters.length > 0 && (
|
||||
@@ -219,6 +245,17 @@ export default function Chapters() {
|
||||
});
|
||||
|
||||
try {
|
||||
if (!selectedStyleId) {
|
||||
message.error('请先选择写作风格');
|
||||
modal.update({
|
||||
okButtonProps: { danger: true, loading: false },
|
||||
cancelButtonProps: { disabled: false },
|
||||
closable: true,
|
||||
maskClosable: true,
|
||||
keyboard: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await handleGenerate();
|
||||
modal.destroy();
|
||||
} catch (error) {
|
||||
@@ -526,6 +563,35 @@ export default function Chapters() {
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="写作风格"
|
||||
tooltip="选择AI创作时使用的写作风格,可在写作风格菜单中管理"
|
||||
required
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择写作风格"
|
||||
value={selectedStyleId}
|
||||
onChange={setSelectedStyleId}
|
||||
size="large"
|
||||
disabled={isGenerating}
|
||||
style={{ width: '100%' }}
|
||||
status={!selectedStyleId ? 'error' : undefined}
|
||||
>
|
||||
{writingStyles.map(style => (
|
||||
<Select.Option key={style.id} value={style.id}>
|
||||
{style.name}
|
||||
{style.is_default && ' (默认)'}
|
||||
{style.description && ` - ${style.description}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{!selectedStyleId && (
|
||||
<div style={{ color: '#ff4d4f', fontSize: 12, marginTop: 4 }}>
|
||||
请选择写作风格
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="章节内容" name="content">
|
||||
<TextArea
|
||||
ref={contentTextAreaRef}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MenuUnfoldOutlined,
|
||||
ApartmentOutlined,
|
||||
BankOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks';
|
||||
@@ -121,6 +122,11 @@ export default function ProjectDetail() {
|
||||
icon: <BookOutlined />,
|
||||
label: <Link to={`/project/${projectId}/chapters`}>章节管理</Link>,
|
||||
},
|
||||
{
|
||||
key: 'writing-styles',
|
||||
icon: <EditOutlined />,
|
||||
label: <Link to={`/project/${projectId}/writing-styles`}>写作风格</Link>,
|
||||
},
|
||||
// {
|
||||
// key: 'polish',
|
||||
// icon: <ToolOutlined />,
|
||||
@@ -137,6 +143,7 @@ export default function ProjectDetail() {
|
||||
if (path.includes('/outline')) return 'outline';
|
||||
if (path.includes('/characters')) return 'characters';
|
||||
if (path.includes('/chapters')) return 'chapters';
|
||||
if (path.includes('/writing-styles')) return 'writing-styles';
|
||||
// if (path.includes('/polish')) return 'polish';
|
||||
return 'world-setting'; // 默认选中世界设定
|
||||
}, [location.pathname]);
|
||||
|
||||
+169
-92
@@ -1,15 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, Alert } from 'antd';
|
||||
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Tooltip, Alert, Grid } from 'antd';
|
||||
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined} from '@ant-design/icons';
|
||||
import { settingsApi } from '../services/api';
|
||||
import type { SettingsUpdate } from '../types';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const screens = useBreakpoint();
|
||||
const isMobile = !screens.md; // md断点是768px
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [initialLoading, setInitialLoading] = useState(true);
|
||||
@@ -179,34 +182,62 @@ export default function SettingsPage() {
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
padding: window.innerWidth <= 768 ? '20px 16px' : '40px 24px'
|
||||
padding: isMobile ? '16px 12px' : '40px 24px'
|
||||
}}>
|
||||
<div style={{ maxWidth: 800, margin: '0 auto' }}>
|
||||
<div style={{
|
||||
maxWidth: isMobile ? '100%' : 800,
|
||||
margin: '0 auto'
|
||||
}}>
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
borderRadius: window.innerWidth <= 768 ? 12 : 16,
|
||||
borderRadius: isMobile ? 12 : 16,
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||
}}
|
||||
styles={{
|
||||
body: {
|
||||
padding: isMobile ? '16px' : '24px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size={isMobile ? 'middle' : 'large'} style={{ width: '100%' }}>
|
||||
{/* 标题栏 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<Space size={isMobile ? 'small' : 'middle'}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/')}
|
||||
type="text"
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
/>
|
||||
<Title level={window.innerWidth <= 768 ? 3 : 2} style={{ margin: 0 }}>
|
||||
<Title
|
||||
level={isMobile ? 4 : 2}
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: isMobile ? '18px' : undefined
|
||||
}}
|
||||
>
|
||||
<SettingOutlined style={{ marginRight: 8, color: '#667eea' }} />
|
||||
AI API 设置
|
||||
{isMobile ? 'API 设置' : 'AI API 设置'}
|
||||
</Title>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
fontSize: isMobile ? '13px' : '14px',
|
||||
lineHeight: isMobile ? '1.5' : '1.6'
|
||||
}}
|
||||
>
|
||||
配置你的AI API接口参数,这些设置将用于小说生成、角色创建等AI功能。
|
||||
</Paragraph>
|
||||
|
||||
@@ -215,7 +246,7 @@ export default function SettingsPage() {
|
||||
<Alert
|
||||
message="使用 .env 文件中的默认配置"
|
||||
description={
|
||||
<div>
|
||||
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<p style={{ margin: '8px 0' }}>
|
||||
当前显示的是从服务器 <code>.env</code> 文件读取的默认配置。
|
||||
</p>
|
||||
@@ -226,7 +257,7 @@ export default function SettingsPage() {
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
style={{ marginBottom: isMobile ? 12 : 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -236,7 +267,7 @@ export default function SettingsPage() {
|
||||
message="使用已保存的个人配置"
|
||||
type="success"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
style={{ marginBottom: isMobile ? 12 : 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -250,17 +281,17 @@ export default function SettingsPage() {
|
||||
>
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<span>API 提供商</span>
|
||||
<Tooltip title="选择你的AI服务提供商">
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
name="api_provider"
|
||||
rules={[{ required: true, message: '请选择API提供商' }]}
|
||||
>
|
||||
<Select size="large" onChange={handleProviderChange}>
|
||||
<Select size={isMobile ? 'middle' : 'large'} onChange={handleProviderChange}>
|
||||
{apiProviders.map(provider => (
|
||||
<Option key={provider.value} value={provider.value}>
|
||||
{provider.label}
|
||||
@@ -271,10 +302,10 @@ export default function SettingsPage() {
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<span>API 密钥</span>
|
||||
<Tooltip title="你的API密钥,将加密存储">
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
@@ -282,7 +313,7 @@ export default function SettingsPage() {
|
||||
rules={[{ required: true, message: '请输入API密钥' }]}
|
||||
>
|
||||
<Input.Password
|
||||
size="large"
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
placeholder="sk-..."
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
@@ -290,10 +321,10 @@ export default function SettingsPage() {
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<span>API 地址</span>
|
||||
<Tooltip title="API的基础URL地址">
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
@@ -304,17 +335,17 @@ export default function SettingsPage() {
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
size="large"
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
placeholder="https://api.openai.com/v1"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<span>模型名称</span>
|
||||
<Tooltip title="AI模型的名称,如 gpt-4, gpt-3.5-turbo">
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
@@ -322,9 +353,9 @@ export default function SettingsPage() {
|
||||
rules={[{ required: true, message: '请输入或选择模型名称' }]}
|
||||
>
|
||||
<Select
|
||||
size="large"
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
showSearch
|
||||
placeholder="输入模型名称或点击获取"
|
||||
placeholder={isMobile ? "选择模型" : "输入模型名称或点击获取"}
|
||||
optionFilterProp="label"
|
||||
loading={fetchingModels}
|
||||
onFocus={handleModelSelectFocus}
|
||||
@@ -336,17 +367,17 @@ export default function SettingsPage() {
|
||||
<>
|
||||
{menu}
|
||||
{fetchingModels && (
|
||||
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center' }}>
|
||||
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<Spin size="small" /> 正在获取模型列表...
|
||||
</div>
|
||||
)}
|
||||
{!fetchingModels && modelOptions.length === 0 && modelsFetched && (
|
||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center' }}>
|
||||
<div style={{ padding: '8px 12px', color: '#ff4d4f', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
未能获取到模型列表,请检查 API 配置
|
||||
</div>
|
||||
)}
|
||||
{!fetchingModels && modelOptions.length === 0 && !modelsFetched && (
|
||||
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center' }}>
|
||||
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
点击输入框自动获取模型列表
|
||||
</div>
|
||||
)}
|
||||
@@ -354,44 +385,46 @@ export default function SettingsPage() {
|
||||
)}
|
||||
notFoundContent={
|
||||
fetchingModels ? (
|
||||
<div style={{ padding: '8px 12px', textAlign: 'center' }}>
|
||||
<div style={{ padding: '8px 12px', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
<Spin size="small" /> 加载中...
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center' }}>
|
||||
<div style={{ padding: '8px 12px', color: '#8c8c8c', textAlign: 'center', fontSize: isMobile ? '12px' : '14px' }}>
|
||||
未找到匹配的模型
|
||||
</div>
|
||||
)
|
||||
}
|
||||
suffixIcon={
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!fetchingModels) {
|
||||
setModelsFetched(false);
|
||||
handleFetchModels(false);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: fetchingModels ? 'not-allowed' : 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 4px',
|
||||
height: '100%',
|
||||
marginRight: -8
|
||||
}}
|
||||
title="重新获取模型列表"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={fetchingModels}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
!isMobile ? (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!fetchingModels) {
|
||||
setModelsFetched(false);
|
||||
handleFetchModels(false);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: fetchingModels ? 'not-allowed' : 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 4px',
|
||||
height: '100%',
|
||||
marginRight: -8
|
||||
}}
|
||||
title="重新获取模型列表"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={fetchingModels}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
options={modelOptions.map(model => ({
|
||||
value: model.value,
|
||||
@@ -400,9 +433,9 @@ export default function SettingsPage() {
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{option.data.label}</div>
|
||||
<div style={{ fontWeight: 500, fontSize: isMobile ? '13px' : '14px' }}>{option.data.label}</div>
|
||||
{option.data.description && (
|
||||
<div style={{ fontSize: '12px', color: '#8c8c8c' }}>
|
||||
<div style={{ fontSize: isMobile ? '11px' : '12px', color: '#8c8c8c', marginTop: '2px' }}>
|
||||
{option.data.description}
|
||||
</div>
|
||||
)}
|
||||
@@ -413,10 +446,10 @@ export default function SettingsPage() {
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<span>温度参数</span>
|
||||
<Tooltip title="控制输出的随机性,值越高越随机(0.0-2.0)">
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
@@ -427,20 +460,20 @@ export default function SettingsPage() {
|
||||
max={2}
|
||||
step={0.1}
|
||||
marks={{
|
||||
0: '0.0',
|
||||
0.7: '0.7',
|
||||
1: '1.0',
|
||||
2: '2.0'
|
||||
0: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '0.0' },
|
||||
0.7: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '0.7' },
|
||||
1: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '1.0' },
|
||||
2: { style: { fontSize: isMobile ? '11px' : '12px' }, label: '2.0' }
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
<Space size={4}>
|
||||
<span>最大 Token 数</span>
|
||||
<Tooltip title="单次请求的最大token数量">
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c' }} />
|
||||
<InfoCircleOutlined style={{ color: '#8c8c8c', fontSize: isMobile ? '12px' : '14px' }} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
@@ -451,7 +484,7 @@ export default function SettingsPage() {
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
size="large"
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
style={{ width: '100%' }}
|
||||
min={1}
|
||||
max={32000}
|
||||
@@ -460,42 +493,86 @@ export default function SettingsPage() {
|
||||
</Form.Item>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Form.Item style={{ marginBottom: 0, marginTop: 32 }}>
|
||||
<Space size="middle" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<Form.Item style={{ marginBottom: 0, marginTop: isMobile ? 24 : 32 }}>
|
||||
{isMobile ? (
|
||||
// 移动端:垂直堆叠布局
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SaveOutlined />}
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none'
|
||||
border: 'none',
|
||||
height: '44px'
|
||||
}}
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Space size="middle" style={{ width: '100%' }}>
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
style={{ flex: 1, height: '44px' }}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{hasSettings && (
|
||||
<Button
|
||||
danger
|
||||
size="large"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDelete}
|
||||
loading={loading}
|
||||
style={{ flex: 1, height: '44px' }}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
{hasSettings && (
|
||||
<Button
|
||||
danger
|
||||
size="large"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDelete}
|
||||
loading={loading}
|
||||
>
|
||||
删除设置
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
// 桌面端:原有的水平布局
|
||||
<Space size="middle" style={{ width: '100%', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SaveOutlined />}
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
border: 'none'
|
||||
}}
|
||||
>
|
||||
保存设置
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Space>
|
||||
{hasSettings && (
|
||||
<Button
|
||||
danger
|
||||
size="large"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDelete}
|
||||
loading={loading}
|
||||
>
|
||||
删除设置
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
message,
|
||||
Card,
|
||||
Space,
|
||||
Tag,
|
||||
Popconfirm,
|
||||
Empty,
|
||||
Typography,
|
||||
Row,
|
||||
Col,
|
||||
Tooltip
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
StarOutlined,
|
||||
StarFilled
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { writingStyleApi } from '../services/api';
|
||||
import type { WritingStyle, WritingStyleCreate, WritingStyleUpdate } from '../types';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
export default function WritingStyles() {
|
||||
const { currentProject } = useStore();
|
||||
const [styles, setStyles] = useState<WritingStyle[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingStyle, setEditingStyle] = useState<WritingStyle | null>(null);
|
||||
const [createForm] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
|
||||
// 卡片网格配置
|
||||
const gridConfig = {
|
||||
gutter: isMobile ? 8 : 16, // 卡片之间的间距
|
||||
xs: 24,
|
||||
sm: 24,
|
||||
md: 12,
|
||||
lg: 8,
|
||||
xl: 6,
|
||||
};
|
||||
|
||||
// 加载项目风格
|
||||
useEffect(() => {
|
||||
if (currentProject?.id) {
|
||||
loadProjectStyles();
|
||||
}
|
||||
}, [currentProject?.id]);
|
||||
|
||||
const loadProjectStyles = async () => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await writingStyleApi.getProjectStyles(currentProject.id);
|
||||
// 对风格列表进行排序:默认风格优先,然后按原有顺序
|
||||
const sortedStyles = (response.styles || []).sort((a, b) => {
|
||||
// 默认风格排在前面
|
||||
if (a.is_default && !b.is_default) return -1;
|
||||
if (!a.is_default && b.is_default) return 1;
|
||||
return 0;
|
||||
});
|
||||
setStyles(sortedStyles);
|
||||
} catch {
|
||||
message.error('加载风格列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = async (values: { name: string; description?: string; prompt_content: string }) => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
try {
|
||||
const createData: WritingStyleCreate = {
|
||||
project_id: currentProject.id,
|
||||
name: values.name,
|
||||
style_type: 'custom',
|
||||
description: values.description,
|
||||
prompt_content: values.prompt_content,
|
||||
};
|
||||
|
||||
await writingStyleApi.createStyle(createData);
|
||||
message.success('创建成功');
|
||||
setIsCreateModalOpen(false);
|
||||
createForm.resetFields();
|
||||
await loadProjectStyles();
|
||||
} catch {
|
||||
message.error('创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (style: WritingStyle) => {
|
||||
setEditingStyle(style);
|
||||
editForm.setFieldsValue({
|
||||
name: style.name,
|
||||
description: style.description,
|
||||
prompt_content: style.prompt_content,
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleUpdate = async (values: WritingStyleUpdate) => {
|
||||
if (!editingStyle) return;
|
||||
|
||||
try {
|
||||
await writingStyleApi.updateStyle(editingStyle.id, values);
|
||||
message.success('更新成功');
|
||||
setIsEditModalOpen(false);
|
||||
editForm.resetFields();
|
||||
setEditingStyle(null);
|
||||
await loadProjectStyles();
|
||||
} catch {
|
||||
message.error('更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (styleId: number) => {
|
||||
try {
|
||||
await writingStyleApi.deleteStyle(styleId);
|
||||
message.success('删除成功');
|
||||
await loadProjectStyles();
|
||||
} catch {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefault = async (styleId: number) => {
|
||||
if (!currentProject?.id) return;
|
||||
|
||||
try {
|
||||
await writingStyleApi.setDefaultStyle(styleId, currentProject.id);
|
||||
message.success('设置默认风格成功');
|
||||
await loadProjectStyles();
|
||||
} catch {
|
||||
message.error('设置失败');
|
||||
}
|
||||
};
|
||||
|
||||
const showCreateModal = () => {
|
||||
createForm.resetFields();
|
||||
setIsCreateModalOpen(true);
|
||||
};
|
||||
|
||||
if (!currentProject) return null;
|
||||
|
||||
const getStyleTypeColor = (styleType: string) => {
|
||||
return styleType === 'preset' ? 'blue' : 'purple';
|
||||
};
|
||||
|
||||
const getStyleTypeLabel = (styleType: string) => {
|
||||
return styleType === 'preset' ? '预设' : '自定义';
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<div style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
backgroundColor: '#fff',
|
||||
padding: isMobile ? '12px 0' : '16px 0',
|
||||
marginBottom: isMobile ? 12 : 16,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
gap: isMobile ? 12 : 0,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: isMobile ? 'stretch' : 'center'
|
||||
}}>
|
||||
<h2 style={{ margin: 0, fontSize: isMobile ? 18 : 24 }}>写作风格管理</h2>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={showCreateModal}
|
||||
block={isMobile}
|
||||
>
|
||||
创建自定义风格
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
{styles.length === 0 ? (
|
||||
<Empty description="暂无风格数据" />
|
||||
) : (
|
||||
<Row
|
||||
gutter={[0, gridConfig.gutter]}
|
||||
style={{ marginLeft: 0, marginRight: 0 }}
|
||||
>
|
||||
{styles.map((style) => (
|
||||
<Col
|
||||
xs={gridConfig.xs}
|
||||
sm={gridConfig.sm}
|
||||
md={gridConfig.md}
|
||||
lg={gridConfig.lg}
|
||||
xl={gridConfig.xl}
|
||||
key={style.id}
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingRight: gridConfig.gutter / 2,
|
||||
marginBottom: gridConfig.gutter
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 12,
|
||||
border: style.is_default ? '2px solid #1890ff' : '1px solid #f0f0f0',
|
||||
}}
|
||||
bodyStyle={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '16px',
|
||||
}}
|
||||
actions={[
|
||||
<Tooltip key="default" title={style.is_default ? '当前默认' : '设为默认'}>
|
||||
<span
|
||||
onClick={() => !style.is_default && handleSetDefault(style.id)}
|
||||
style={{ cursor: style.is_default ? 'default' : 'pointer' }}
|
||||
>
|
||||
{style.is_default ? (
|
||||
<StarFilled style={{ color: '#faad14', fontSize: 18 }} />
|
||||
) : (
|
||||
<StarOutlined style={{ fontSize: 18 }} />
|
||||
)}
|
||||
</span>
|
||||
</Tooltip>,
|
||||
<Tooltip key="edit" title={style.project_id === null ? '预设风格不可编辑' : '编辑'}>
|
||||
<EditOutlined
|
||||
onClick={() => style.project_id !== null && handleEdit(style)}
|
||||
style={{
|
||||
fontSize: 18,
|
||||
cursor: style.project_id === null ? 'not-allowed' : 'pointer',
|
||||
color: style.project_id === null ? '#ccc' : undefined
|
||||
}}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Popconfirm
|
||||
key="delete"
|
||||
title="确定删除这个风格吗?"
|
||||
description={style.is_default ? '这是默认风格,删除后需要设置新的默认风格' : undefined}
|
||||
onConfirm={() => handleDelete(style.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
disabled={style.project_id === null || styles.length === 1}
|
||||
>
|
||||
<Tooltip title={
|
||||
style.project_id === null
|
||||
? '预设风格不可删除'
|
||||
: styles.length === 1
|
||||
? '至少保留一个风格'
|
||||
: '删除'
|
||||
}>
|
||||
<DeleteOutlined
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: (style.project_id === null || styles.length === 1) ? '#ccc' : undefined,
|
||||
cursor: (style.project_id === null || styles.length === 1) ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Space style={{ marginBottom: 12 }} wrap>
|
||||
<Text strong style={{ fontSize: 16 }}>{style.name}</Text>
|
||||
<Tag color={getStyleTypeColor(style.style_type)}>
|
||||
{getStyleTypeLabel(style.style_type)}
|
||||
</Tag>
|
||||
{style.is_default && <Tag color="gold">默认</Tag>}
|
||||
</Space>
|
||||
|
||||
{style.description && (
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
style={{ fontSize: 13, marginBottom: 12 }}
|
||||
ellipsis={{ rows: 2, tooltip: style.description }}
|
||||
>
|
||||
{style.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 0,
|
||||
backgroundColor: '#fafafa',
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
flex: 1,
|
||||
minHeight: 60,
|
||||
}}
|
||||
ellipsis={{ rows: 3, tooltip: style.prompt_content }}
|
||||
>
|
||||
{style.prompt_content}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 创建自定义风格 Modal */}
|
||||
<Modal
|
||||
title="创建自定义风格"
|
||||
open={isCreateModalOpen}
|
||||
onCancel={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
createForm.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
centered={!isMobile}
|
||||
width={isMobile ? '100%' : 600}
|
||||
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
|
||||
>
|
||||
<Form
|
||||
form={createForm}
|
||||
layout="vertical"
|
||||
onFinish={handleCreate}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="风格名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入风格名称' }]}
|
||||
>
|
||||
<Input placeholder="如:武侠风、科幻风" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="风格描述" name="description">
|
||||
<TextArea rows={2} placeholder="简要描述这个风格的特点..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="提示词内容"
|
||||
name="prompt_content"
|
||||
rules={[{ required: true, message: '请输入提示词内容' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={6}
|
||||
placeholder="输入风格的提示词,用于引导AI生成符合该风格的内容..."
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
createForm.resetFields();
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
创建
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑风格 Modal */}
|
||||
<Modal
|
||||
title="编辑写作风格"
|
||||
open={isEditModalOpen}
|
||||
onCancel={() => {
|
||||
setIsEditModalOpen(false);
|
||||
editForm.resetFields();
|
||||
setEditingStyle(null);
|
||||
}}
|
||||
footer={null}
|
||||
centered={!isMobile}
|
||||
width={isMobile ? '100%' : 600}
|
||||
style={isMobile ? { top: 0, paddingBottom: 0, maxWidth: '100vw' } : undefined}
|
||||
>
|
||||
<Form form={editForm} layout="vertical" onFinish={handleUpdate} style={{ marginTop: 16 }}>
|
||||
<Form.Item
|
||||
label="风格名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入风格名称' }]}
|
||||
>
|
||||
<Input placeholder="输入风格名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="风格描述" name="description">
|
||||
<TextArea rows={2} placeholder="简要描述这个风格的特点..." />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="提示词内容"
|
||||
name="prompt_content"
|
||||
rules={[{ required: true, message: '请输入提示词内容' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={6}
|
||||
placeholder="输入风格的提示词..."
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => {
|
||||
setIsEditModalOpen(false);
|
||||
editForm.resetFields();
|
||||
setEditingStyle(null);
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,6 +25,11 @@ import type {
|
||||
GenerateOutlineResponse,
|
||||
Settings,
|
||||
SettingsUpdate,
|
||||
WritingStyle,
|
||||
WritingStyleCreate,
|
||||
WritingStyleUpdate,
|
||||
PresetStyle,
|
||||
WritingStyleListResponse,
|
||||
} from '../types';
|
||||
|
||||
const api = axios.create({
|
||||
@@ -208,9 +213,36 @@ export const chapterApi = {
|
||||
|
||||
checkCanGenerate: (chapterId: string) =>
|
||||
api.get<unknown, import('../types').ChapterCanGenerateResponse>(`/chapters/${chapterId}/can-generate`),
|
||||
};
|
||||
|
||||
export const writingStyleApi = {
|
||||
// 获取预设风格列表
|
||||
getPresetStyles: () =>
|
||||
api.get<unknown, PresetStyle[]>('/writing-styles/presets/list'),
|
||||
|
||||
generateChapterContent: (chapterId: string) =>
|
||||
api.post<unknown, { content: string }>(`/chapters/${chapterId}/generate`, {}),
|
||||
// 获取项目的所有风格
|
||||
getProjectStyles: (projectId: string) =>
|
||||
api.get<unknown, WritingStyleListResponse>(`/writing-styles/project/${projectId}`),
|
||||
|
||||
// 创建新风格(基于预设或自定义)
|
||||
createStyle: (data: WritingStyleCreate) =>
|
||||
api.post<unknown, WritingStyle>('/writing-styles', data),
|
||||
|
||||
// 更新风格
|
||||
updateStyle: (styleId: number, data: WritingStyleUpdate) =>
|
||||
api.put<unknown, WritingStyle>(`/writing-styles/${styleId}`, data),
|
||||
|
||||
// 删除风格
|
||||
deleteStyle: (styleId: number) =>
|
||||
api.delete<unknown, { message: string }>(`/writing-styles/${styleId}`),
|
||||
|
||||
// 设置默认风格
|
||||
setDefaultStyle: (styleId: number, projectId: string) =>
|
||||
api.post<unknown, WritingStyle>(`/writing-styles/${styleId}/set-default`, { project_id: projectId }),
|
||||
|
||||
// 为项目初始化默认风格(如果没有任何风格)
|
||||
initializeDefaultStyles: (projectId: string) =>
|
||||
api.post<unknown, WritingStyleListResponse>(`/writing-styles/project/${projectId}/initialize`, {}),
|
||||
};
|
||||
|
||||
export const polishApi = {
|
||||
|
||||
@@ -298,24 +298,11 @@ export function useChapterSync() {
|
||||
}
|
||||
}, [removeChapter]);
|
||||
|
||||
// AI生成章节内容(带同步)
|
||||
const generateChapterContent = useCallback(async (chapterId: string) => {
|
||||
try {
|
||||
const result = await chapterApi.generateChapterContent(chapterId);
|
||||
// 直接调用 API 更新
|
||||
const updated = await chapterApi.updateChapter(chapterId, { content: result.content });
|
||||
updateChapter(chapterId, updated);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('AI生成章节内容失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [updateChapter]);
|
||||
|
||||
// AI流式生成章节内容(带同步)
|
||||
const generateChapterContentStream = useCallback(async (
|
||||
chapterId: string,
|
||||
onProgress?: (content: string) => void
|
||||
onProgress?: (content: string) => void,
|
||||
styleId?: number
|
||||
) => {
|
||||
try {
|
||||
// 使用fetch处理流式响应
|
||||
@@ -324,6 +311,7 @@ export function useChapterSync() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(styleId ? { style_id: styleId } : {}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -394,7 +382,6 @@ export function useChapterSync() {
|
||||
createChapter,
|
||||
updateChapter: updateChapterSync,
|
||||
deleteChapter,
|
||||
generateChapterContent,
|
||||
generateChapterContentStream,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -298,6 +298,50 @@ export interface ApiResponse<T> {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// 写作风格类型定义
|
||||
export interface WritingStyle {
|
||||
id: number;
|
||||
project_id: string;
|
||||
name: string;
|
||||
style_type: 'preset' | 'custom';
|
||||
preset_id?: string;
|
||||
description?: string;
|
||||
prompt_content: string;
|
||||
is_default: boolean;
|
||||
order_index: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface WritingStyleCreate {
|
||||
project_id: string;
|
||||
name: string;
|
||||
style_type: 'preset' | 'custom';
|
||||
preset_id?: string;
|
||||
description?: string;
|
||||
prompt_content: string;
|
||||
is_default?: boolean;
|
||||
}
|
||||
|
||||
export interface WritingStyleUpdate {
|
||||
name?: string;
|
||||
description?: string;
|
||||
prompt_content?: string;
|
||||
order_index?: number;
|
||||
}
|
||||
|
||||
export interface PresetStyle {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
prompt_content: string;
|
||||
}
|
||||
|
||||
export interface WritingStyleListResponse {
|
||||
styles: WritingStyle[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface PaginationResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
|
||||
Reference in New Issue
Block a user