update:1.更新根据分析建议重新生成章节内容
This commit is contained in:
@@ -10,9 +10,12 @@ import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ReloadOutlined
|
||||
ReloadOutlined,
|
||||
EditOutlined
|
||||
} from '@ant-design/icons';
|
||||
import type { AnalysisTask, ChapterAnalysisResponse } from '../types';
|
||||
import ChapterRegenerationModal from './ChapterRegenerationModal';
|
||||
import ChapterContentComparison from './ChapterContentComparison';
|
||||
|
||||
// 判断是否为移动设备
|
||||
const isMobileDevice = () => window.innerWidth < 768;
|
||||
@@ -29,6 +32,11 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isMobile, setIsMobile] = useState(isMobileDevice());
|
||||
const [regenerationModalVisible, setRegenerationModalVisible] = useState(false);
|
||||
const [comparisonModalVisible, setComparisonModalVisible] = useState(false);
|
||||
const [chapterInfo, setChapterInfo] = useState<{ title: string; chapter_number: number; content: string } | null>(null);
|
||||
const [newGeneratedContent, setNewGeneratedContent] = useState('');
|
||||
const [newContentWordCount, setNewContentWordCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && chapterId) {
|
||||
@@ -54,6 +62,17 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 同时获取章节信息
|
||||
const chapterResponse = await fetch(`/api/chapters/${chapterId}`);
|
||||
if (chapterResponse.ok) {
|
||||
const chapterData = await chapterResponse.json();
|
||||
setChapterInfo({
|
||||
title: chapterData.title,
|
||||
chapter_number: chapterData.chapter_number,
|
||||
content: chapterData.content || ''
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
|
||||
|
||||
if (response.status === 404) {
|
||||
@@ -199,6 +218,17 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
);
|
||||
};
|
||||
|
||||
// 将分析建议转换为重新生成组件需要的格式
|
||||
const convertSuggestionsForRegeneration = () => {
|
||||
if (!analysis?.analysis?.suggestions) return [];
|
||||
|
||||
return analysis.analysis.suggestions.map((suggestion, index) => ({
|
||||
category: '改进建议',
|
||||
content: suggestion,
|
||||
priority: index < 3 ? 'high' : 'medium'
|
||||
}));
|
||||
};
|
||||
|
||||
const renderAnalysisResult = () => {
|
||||
if (!analysis) return null;
|
||||
|
||||
@@ -215,6 +245,29 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
icon: <TrophyOutlined />,
|
||||
children: (
|
||||
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
|
||||
{/* 根据建议重新生成按钮 */}
|
||||
{analysis_data.suggestions && analysis_data.suggestions.length > 0 && (
|
||||
<Alert
|
||||
message="发现改进建议"
|
||||
description={
|
||||
<div>
|
||||
<p style={{ marginBottom: 12 }}>AI已分析出 {analysis_data.suggestions.length} 条改进建议,您可以根据这些建议重新生成章节内容。</p>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setRegenerationModalVisible(true)}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
>
|
||||
根据建议重新生成
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card title="整体评分" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
|
||||
<Row gutter={isMobile ? 8 : 16}>
|
||||
<Col span={isMobile ? 12 : 6}>
|
||||
@@ -560,6 +613,50 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
|
||||
{task && task.status !== 'completed' && renderProgress()}
|
||||
{task && task.status === 'completed' && analysis && renderAnalysisResult()}
|
||||
|
||||
{/* 重新生成Modal */}
|
||||
{chapterInfo && (
|
||||
<ChapterRegenerationModal
|
||||
visible={regenerationModalVisible}
|
||||
onCancel={() => setRegenerationModalVisible(false)}
|
||||
onSuccess={(newContent: string, wordCount: number) => {
|
||||
// 保存新生成的内容
|
||||
setNewGeneratedContent(newContent);
|
||||
setNewContentWordCount(wordCount);
|
||||
// 关闭重新生成对话框
|
||||
setRegenerationModalVisible(false);
|
||||
// 打开对比界面
|
||||
setComparisonModalVisible(true);
|
||||
}}
|
||||
chapterId={chapterId}
|
||||
chapterTitle={chapterInfo.title}
|
||||
chapterNumber={chapterInfo.chapter_number}
|
||||
suggestions={convertSuggestionsForRegeneration()}
|
||||
hasAnalysis={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 内容对比组件 */}
|
||||
{chapterInfo && comparisonModalVisible && (
|
||||
<ChapterContentComparison
|
||||
visible={comparisonModalVisible}
|
||||
onClose={() => setComparisonModalVisible(false)}
|
||||
chapterId={chapterId}
|
||||
chapterTitle={chapterInfo.title}
|
||||
originalContent={chapterInfo.content}
|
||||
newContent={newGeneratedContent}
|
||||
wordCount={newContentWordCount}
|
||||
onApply={() => {
|
||||
// 应用新内容后刷新章节信息
|
||||
fetchAnalysisStatus();
|
||||
}}
|
||||
onDiscard={() => {
|
||||
// 放弃新内容,清空状态
|
||||
setNewGeneratedContent('');
|
||||
setNewContentWordCount(0);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Button, Card, Statistic, Row, Col, message } from 'antd';
|
||||
import { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import ReactDiffViewer from 'react-diff-viewer-continued';
|
||||
|
||||
interface ChapterContentComparisonProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
chapterId: string;
|
||||
chapterTitle: string;
|
||||
originalContent: string;
|
||||
newContent: string;
|
||||
wordCount: number;
|
||||
onApply: () => void;
|
||||
onDiscard: () => void;
|
||||
}
|
||||
|
||||
const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
chapterId,
|
||||
chapterTitle,
|
||||
originalContent,
|
||||
newContent,
|
||||
wordCount,
|
||||
onApply,
|
||||
onDiscard
|
||||
}) => {
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
|
||||
|
||||
const originalWordCount = originalContent.length;
|
||||
const wordCountDiff = wordCount - originalWordCount;
|
||||
const wordCountDiffPercent = ((wordCountDiff / originalWordCount) * 100).toFixed(1);
|
||||
|
||||
const handleApply = async () => {
|
||||
setApplying(true);
|
||||
try {
|
||||
const response = await fetch(`/api/chapters/${chapterId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: newContent
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('应用新内容失败');
|
||||
}
|
||||
|
||||
message.success('新内容已应用!正在触发章节分析...');
|
||||
|
||||
// 触发章节分析
|
||||
try {
|
||||
const analysisResponse = await fetch(`/api/chapters/${chapterId}/analyze`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (analysisResponse.ok) {
|
||||
message.success('章节分析已开始,请稍后查看结果');
|
||||
} else {
|
||||
message.warning('章节分析触发失败,您可以手动触发分析');
|
||||
}
|
||||
} catch (analysisError) {
|
||||
console.error('触发分析失败:', analysisError);
|
||||
message.warning('章节分析触发失败,您可以手动触发分析');
|
||||
}
|
||||
|
||||
onApply();
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '应用失败');
|
||||
} finally {
|
||||
setApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDiscard = () => {
|
||||
Modal.confirm({
|
||||
title: '确认放弃',
|
||||
content: '确定要放弃新生成的内容吗?此操作不可恢复。',
|
||||
okText: '确定放弃',
|
||||
cancelText: '取消',
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => {
|
||||
onDiscard();
|
||||
onClose();
|
||||
message.info('已放弃新内容');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`内容对比 - ${chapterTitle}`}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width="95%"
|
||||
centered
|
||||
style={{ maxWidth: 1600 }}
|
||||
footer={[
|
||||
<Button
|
||||
key="discard"
|
||||
danger
|
||||
icon={<CloseOutlined />}
|
||||
onClick={handleDiscard}
|
||||
>
|
||||
放弃新内容
|
||||
</Button>,
|
||||
<Button
|
||||
key="toggle"
|
||||
icon={<SwapOutlined />}
|
||||
onClick={() => setViewMode(viewMode === 'split' ? 'unified' : 'split')}
|
||||
>
|
||||
切换视图
|
||||
</Button>,
|
||||
<Button
|
||||
key="apply"
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
loading={applying}
|
||||
onClick={handleApply}
|
||||
>
|
||||
应用新内容
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
{/* 统计信息 */}
|
||||
<Card size="small" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={16}>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="原内容字数"
|
||||
value={originalWordCount}
|
||||
suffix="字"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="新内容字数"
|
||||
value={wordCount}
|
||||
suffix="字"
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="字数变化"
|
||||
value={wordCountDiff}
|
||||
suffix="字"
|
||||
valueStyle={{ color: wordCountDiff > 0 ? '#3f8600' : '#cf1322' }}
|
||||
prefix={wordCountDiff > 0 ? '+' : ''}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Statistic
|
||||
title="变化比例"
|
||||
value={wordCountDiffPercent}
|
||||
suffix="%"
|
||||
valueStyle={{ color: Math.abs(parseFloat(wordCountDiffPercent)) < 10 ? '#1890ff' : '#faad14' }}
|
||||
prefix={wordCountDiff > 0 ? '+' : ''}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 内容对比 */}
|
||||
<div style={{
|
||||
maxHeight: 'calc(90vh - 300px)',
|
||||
overflow: 'auto',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: 4
|
||||
}}>
|
||||
<ReactDiffViewer
|
||||
oldValue={originalContent}
|
||||
newValue={newContent}
|
||||
splitView={viewMode === 'split'}
|
||||
leftTitle="原内容"
|
||||
rightTitle="新内容"
|
||||
showDiffOnly={false}
|
||||
useDarkTheme={false}
|
||||
styles={{
|
||||
variables: {
|
||||
light: {
|
||||
diffViewerBackground: '#fff',
|
||||
addedBackground: '#e6ffed',
|
||||
addedColor: '#24292e',
|
||||
removedBackground: '#ffeef0',
|
||||
removedColor: '#24292e',
|
||||
wordAddedBackground: '#acf2bd',
|
||||
wordRemovedBackground: '#fdb8c0',
|
||||
addedGutterBackground: '#cdffd8',
|
||||
removedGutterBackground: '#ffdce0',
|
||||
gutterBackground: '#f6f8fa',
|
||||
gutterBackgroundDark: '#f3f4f6',
|
||||
highlightBackground: '#fffbdd',
|
||||
highlightGutterBackground: '#fff5b1',
|
||||
},
|
||||
},
|
||||
line: {
|
||||
padding: '10px 2px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '20px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterContentComparison;
|
||||
@@ -0,0 +1,402 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Checkbox,
|
||||
InputNumber,
|
||||
Space,
|
||||
Alert,
|
||||
Divider,
|
||||
Progress,
|
||||
Tag,
|
||||
message,
|
||||
Collapse,
|
||||
Card,
|
||||
Radio
|
||||
} from 'antd';
|
||||
import {
|
||||
ReloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { ssePost } from '../utils/sseClient';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Panel } = Collapse;
|
||||
|
||||
interface Suggestion {
|
||||
category: string;
|
||||
content: string;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
interface ChapterRegenerationModalProps {
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onSuccess: (newContent: string, wordCount: number) => void;
|
||||
chapterId: string;
|
||||
chapterTitle: string;
|
||||
chapterNumber: number;
|
||||
suggestions?: Suggestion[];
|
||||
hasAnalysis: boolean;
|
||||
}
|
||||
|
||||
|
||||
const ChapterRegenerationModal: React.FC<ChapterRegenerationModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
chapterId,
|
||||
chapterTitle,
|
||||
chapterNumber,
|
||||
suggestions = [],
|
||||
hasAnalysis
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [status, setStatus] = useState<'idle' | 'generating' | 'success' | 'error'>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [wordCount, setWordCount] = useState(0);
|
||||
const [selectedSuggestions, setSelectedSuggestions] = useState<number[]>([]);
|
||||
const [modificationSource, setModificationSource] = useState<'custom' | 'analysis_suggestions' | 'mixed'>('custom');
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// 重置状态
|
||||
setStatus('idle');
|
||||
setProgress(0);
|
||||
setErrorMessage('');
|
||||
setWordCount(0);
|
||||
setSelectedSuggestions([]);
|
||||
|
||||
// 如果有分析建议,默认选择混合模式
|
||||
if (hasAnalysis && suggestions.length > 0) {
|
||||
setModificationSource('mixed');
|
||||
} else {
|
||||
setModificationSource('custom');
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
form.setFieldsValue({
|
||||
modification_source: hasAnalysis && suggestions.length > 0 ? 'mixed' : 'custom',
|
||||
target_word_count: 3000,
|
||||
preserve_structure: false,
|
||||
preserve_character_traits: true,
|
||||
focus_areas: []
|
||||
});
|
||||
}
|
||||
}, [visible, hasAnalysis, suggestions.length, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证至少提供一种修改指令
|
||||
if (values.modification_source === 'custom' && !values.custom_instructions?.trim()) {
|
||||
message.error('请输入自定义修改要求');
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.modification_source === 'analysis_suggestions' && selectedSuggestions.length === 0) {
|
||||
message.error('请选择至少一条分析建议');
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.modification_source === 'mixed' &&
|
||||
selectedSuggestions.length === 0 &&
|
||||
!values.custom_instructions?.trim()) {
|
||||
message.error('请至少选择一条建议或输入自定义要求');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setStatus('generating');
|
||||
setProgress(0);
|
||||
setWordCount(0);
|
||||
|
||||
// 构建请求数据
|
||||
const requestData: any = {
|
||||
modification_source: values.modification_source,
|
||||
custom_instructions: values.custom_instructions,
|
||||
selected_suggestion_indices: selectedSuggestions,
|
||||
preserve_elements: {
|
||||
preserve_structure: values.preserve_structure,
|
||||
preserve_dialogues: values.preserve_dialogues || [],
|
||||
preserve_plot_points: values.preserve_plot_points || [],
|
||||
preserve_character_traits: values.preserve_character_traits
|
||||
},
|
||||
style_id: values.style_id,
|
||||
target_word_count: values.target_word_count,
|
||||
focus_areas: values.focus_areas || []
|
||||
};
|
||||
|
||||
let accumulatedContent = '';
|
||||
let currentWordCount = 0;
|
||||
|
||||
// 使用SSE流式生成
|
||||
await ssePost(
|
||||
`/api/chapters/${chapterId}/regenerate-stream`,
|
||||
requestData,
|
||||
{
|
||||
onProgress: (_msg: string, prog: number, _status: string, wordCount?: number) => {
|
||||
// 后端发送的进度消息
|
||||
setProgress(prog);
|
||||
// 如果后端提供了word_count,使用它;否则使用累积的字数
|
||||
if (wordCount !== undefined) {
|
||||
setWordCount(wordCount);
|
||||
currentWordCount = wordCount;
|
||||
}
|
||||
},
|
||||
onChunk: (content: string) => {
|
||||
// 累积内容块
|
||||
accumulatedContent += content;
|
||||
// 仅作为备用字数统计
|
||||
currentWordCount = accumulatedContent.length;
|
||||
// 不再自己计算进度,完全依赖后端发送的progress消息
|
||||
},
|
||||
onResult: (data: any) => {
|
||||
// 生成完成,确保使用最新的累积内容
|
||||
setProgress(100);
|
||||
setStatus('success');
|
||||
const finalWordCount = data.word_count || currentWordCount;
|
||||
setWordCount(finalWordCount);
|
||||
message.success('重新生成完成!');
|
||||
|
||||
// 直接调用onSuccess打开对比界面,传递最终的累积内容
|
||||
setTimeout(() => {
|
||||
onSuccess(accumulatedContent, finalWordCount);
|
||||
}, 500);
|
||||
},
|
||||
onComplete: () => {
|
||||
// SSE完成
|
||||
},
|
||||
onError: (error: string, code?: number) => {
|
||||
console.error('SSE Error:', error, code);
|
||||
setStatus('error');
|
||||
setErrorMessage(error || '生成失败');
|
||||
message.error('重新生成失败: ' + (error || '未知错误'));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('提交失败:', error);
|
||||
setStatus('error');
|
||||
setErrorMessage(error.message || '提交失败');
|
||||
message.error('操作失败: ' + (error.message || '未知错误'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestionSelect = (index: number, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedSuggestions([...selectedSuggestions, index]);
|
||||
} else {
|
||||
setSelectedSuggestions(selectedSuggestions.filter(i => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (loading) {
|
||||
Modal.confirm({
|
||||
title: '确认取消',
|
||||
content: '生成正在进行中,确定要取消吗?',
|
||||
onOk: () => {
|
||||
setLoading(false);
|
||||
setStatus('idle');
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`重新生成章节 - 第${chapterNumber}章:${chapterTitle}`}
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
width={800}
|
||||
centered
|
||||
footer={
|
||||
status === 'success' ? null : (
|
||||
[
|
||||
<Button key="cancel" onClick={handleCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="submit"
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={loading}
|
||||
icon={<ReloadOutlined />}
|
||||
>
|
||||
开始重新生成
|
||||
</Button>
|
||||
]
|
||||
)
|
||||
}
|
||||
>
|
||||
{status === 'generating' && (
|
||||
<Alert
|
||||
message="正在重新生成中..."
|
||||
description={
|
||||
<div>
|
||||
<Progress percent={progress} status="active" />
|
||||
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
|
||||
已生成 {wordCount} 字
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<Alert
|
||||
message="重新生成成功!"
|
||||
description={`共生成 ${wordCount} 字`}
|
||||
type="success"
|
||||
showIcon
|
||||
icon={<CheckCircleOutlined />}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<Alert
|
||||
message="生成失败"
|
||||
description={errorMessage}
|
||||
type="error"
|
||||
showIcon
|
||||
icon={<CloseCircleOutlined />}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
disabled={loading || status === 'success'}
|
||||
>
|
||||
{/* 修改来源 */}
|
||||
<Form.Item
|
||||
name="modification_source"
|
||||
label="修改来源"
|
||||
rules={[{ required: true, message: '请选择修改来源' }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => setModificationSource(e.target.value)}>
|
||||
<Radio value="custom">仅自定义修改</Radio>
|
||||
{hasAnalysis && suggestions.length > 0 && (
|
||||
<>
|
||||
<Radio value="analysis_suggestions">仅分析建议</Radio>
|
||||
<Radio value="mixed">混合模式</Radio>
|
||||
</>
|
||||
)}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 分析建议选择 */}
|
||||
{hasAnalysis && suggestions.length > 0 &&
|
||||
(modificationSource === 'analysis_suggestions' || modificationSource === 'mixed') && (
|
||||
<Form.Item label={`选择分析建议 (${selectedSuggestions.length}/${suggestions.length})`}>
|
||||
<Card size="small" style={{ maxHeight: 300, overflow: 'auto' }}>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<Checkbox
|
||||
key={index}
|
||||
checked={selectedSuggestions.includes(index)}
|
||||
onChange={(e) => handleSuggestionSelect(index, e.target.checked)}
|
||||
>
|
||||
<Space>
|
||||
<Tag color={
|
||||
suggestion.priority === 'high' ? 'red' :
|
||||
suggestion.priority === 'medium' ? 'orange' : 'blue'
|
||||
}>
|
||||
{suggestion.category}
|
||||
</Tag>
|
||||
<span style={{ fontSize: 13 }}>{suggestion.content}</span>
|
||||
</Space>
|
||||
</Checkbox>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 自定义修改要求 */}
|
||||
{(modificationSource === 'custom' || modificationSource === 'mixed') && (
|
||||
<Form.Item
|
||||
name="custom_instructions"
|
||||
label="自定义修改要求"
|
||||
tooltip="描述你希望如何改进这个章节"
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="例如:增强情感渲染,让主角的内心戏更加细腻..."
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 高级选项 */}
|
||||
<Collapse ghost>
|
||||
<Panel header="高级选项" key="advanced">
|
||||
{/* 重点优化方向 */}
|
||||
<Form.Item
|
||||
name="focus_areas"
|
||||
label="重点优化方向"
|
||||
>
|
||||
<Checkbox.Group>
|
||||
<Space direction="vertical">
|
||||
<Checkbox value="pacing">节奏把控</Checkbox>
|
||||
<Checkbox value="emotion">情感渲染</Checkbox>
|
||||
<Checkbox value="description">场景描写</Checkbox>
|
||||
<Checkbox value="dialogue">对话质量</Checkbox>
|
||||
<Checkbox value="conflict">冲突强度</Checkbox>
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 保留元素 */}
|
||||
<Form.Item label="保留元素">
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Form.Item name="preserve_structure" valuePropName="checked" noStyle>
|
||||
<Checkbox>保留整体结构和情节框架</Checkbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="preserve_character_traits" valuePropName="checked" noStyle>
|
||||
<Checkbox>保持角色性格一致</Checkbox>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 生成参数 */}
|
||||
<Form.Item
|
||||
name="target_word_count"
|
||||
label="目标字数"
|
||||
tooltip="生成内容的目标字数,实际字数可能有±20%的浮动"
|
||||
>
|
||||
<InputNumber min={500} max={10000} step={500} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</Form>
|
||||
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChapterRegenerationModal;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination } from 'antd';
|
||||
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined } from '@ant-design/icons';
|
||||
import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination, Form, Input } from 'antd';
|
||||
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { authApi, userApi } from '../services/api';
|
||||
import type { User } from '../types';
|
||||
import type { MenuProps } from 'antd';
|
||||
@@ -10,10 +10,13 @@ const { Text } = Typography;
|
||||
export default function UserMenu() {
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [showUserManagement, setShowUserManagement] = useState(false);
|
||||
const [showChangePassword, setShowChangePassword] = useState(false);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [changePasswordForm] = Form.useForm();
|
||||
const [changingPassword, setChangingPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadCurrentUser();
|
||||
@@ -84,6 +87,21 @@ export default function UserMenu() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => {
|
||||
try {
|
||||
setChangingPassword(true);
|
||||
await authApi.setPassword(values.newPassword);
|
||||
message.success('密码修改成功');
|
||||
setShowChangePassword(false);
|
||||
changePasswordForm.resetFields();
|
||||
} catch (error: any) {
|
||||
console.error('修改密码失败:', error);
|
||||
message.error(error.response?.data?.detail || '修改密码失败');
|
||||
} finally {
|
||||
setChangingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'user-info',
|
||||
@@ -110,6 +128,15 @@ export default function UserMenu() {
|
||||
}, {
|
||||
type: 'divider' as const,
|
||||
}] : []),
|
||||
{
|
||||
key: 'change-password',
|
||||
icon: <LockOutlined />,
|
||||
label: '修改密码',
|
||||
onClick: () => setShowChangePassword(true),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
@@ -341,6 +368,77 @@ export default function UserMenu() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={showChangePassword}
|
||||
onCancel={() => {
|
||||
setShowChangePassword(false);
|
||||
changePasswordForm.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
width={480}
|
||||
centered
|
||||
>
|
||||
<Form
|
||||
form={changePasswordForm}
|
||||
layout="vertical"
|
||||
onFinish={handleChangePassword}
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item
|
||||
label="新密码"
|
||||
name="newPassword"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请输入新密码(至少6个字符)"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="确认密码"
|
||||
name="confirmPassword"
|
||||
dependencies={['newPassword']}
|
||||
rules={[
|
||||
{ required: true, message: '请确认新密码' },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
if (!value || getFieldValue('newPassword') === value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="请再次输入新密码"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button onClick={() => {
|
||||
setShowChangePassword(false);
|
||||
changePasswordForm.resetFields();
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" loading={changingPassword}>
|
||||
确认修改
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Spin, Result, Button } from 'antd';
|
||||
import { Spin, Result, Button, Modal, Input, message } from 'antd';
|
||||
import { authApi } from '../services/api';
|
||||
import AnnouncementModal from '../components/AnnouncementModal';
|
||||
|
||||
@@ -9,6 +9,11 @@ export default function AuthCallback() {
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const [passwordStatus, setPasswordStatus] = useState<any>(null);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [settingPassword, setSettingPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleCallback = async () => {
|
||||
@@ -17,8 +22,21 @@ export default function AuthCallback() {
|
||||
// 这里只需要验证登录状态
|
||||
await authApi.getCurrentUser();
|
||||
|
||||
// 检查密码状态
|
||||
const pwdStatus = await authApi.getPasswordStatus();
|
||||
setPasswordStatus(pwdStatus);
|
||||
|
||||
setStatus('success');
|
||||
|
||||
// 只有在用户完全没有密码时才显示密码设置提示
|
||||
// 如果已经有密码(无论是默认密码还是自定义密码),都不再提示
|
||||
if (!pwdStatus.has_password) {
|
||||
setTimeout(() => {
|
||||
setShowPasswordModal(true);
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 sessionStorage 获取重定向地址
|
||||
const redirect = sessionStorage.getItem('login_redirect') || '/';
|
||||
sessionStorage.removeItem('login_redirect');
|
||||
@@ -105,6 +123,70 @@ export default function AuthCallback() {
|
||||
localStorage.setItem('announcement_do_not_show_until', tomorrow.getTime().toString());
|
||||
};
|
||||
|
||||
const handleSetPassword = async () => {
|
||||
if (!newPassword) {
|
||||
message.error('请输入新密码');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
message.error('密码长度至少为6个字符');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
message.error('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingPassword(true);
|
||||
try {
|
||||
await authApi.setPassword(newPassword);
|
||||
message.success('密码设置成功');
|
||||
setShowPasswordModal(false);
|
||||
|
||||
// 继续后续流程
|
||||
const redirect = sessionStorage.getItem('login_redirect') || '/';
|
||||
sessionStorage.removeItem('login_redirect');
|
||||
|
||||
const doNotShowUntil = localStorage.getItem('announcement_do_not_show_until');
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (!doNotShowUntil || now > parseInt(doNotShowUntil)) {
|
||||
setTimeout(() => {
|
||||
setShowAnnouncement(true);
|
||||
}, 500);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
navigate(redirect);
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('密码设置失败,请重试');
|
||||
} finally {
|
||||
setSettingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkipPasswordSetting = () => {
|
||||
setShowPasswordModal(false);
|
||||
|
||||
// 继续后续流程
|
||||
const redirect = sessionStorage.getItem('login_redirect') || '/';
|
||||
sessionStorage.removeItem('login_redirect');
|
||||
|
||||
const doNotShowUntil = localStorage.getItem('announcement_do_not_show_until');
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (!doNotShowUntil || now > parseInt(doNotShowUntil)) {
|
||||
setTimeout(() => {
|
||||
setShowAnnouncement(true);
|
||||
}, 500);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
navigate(redirect);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnnouncementModal
|
||||
@@ -112,6 +194,62 @@ export default function AuthCallback() {
|
||||
onClose={handleAnnouncementClose}
|
||||
onDoNotShowToday={handleDoNotShowToday}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="设置账号密码"
|
||||
open={showPasswordModal}
|
||||
centered
|
||||
onOk={handleSetPassword}
|
||||
onCancel={handleSkipPasswordSetting}
|
||||
confirmLoading={settingPassword}
|
||||
okText="设置密码"
|
||||
cancelText="暂不设置"
|
||||
width={500}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<p>您已成功通过 Linux DO 授权登录!</p>
|
||||
<p>系统已为您自动生成默认密码,您可以选择设置自定义密码或继续使用默认密码。</p>
|
||||
{passwordStatus?.default_password && (
|
||||
<div style={{
|
||||
background: '#f0f2f5',
|
||||
padding: 12,
|
||||
borderRadius: 4,
|
||||
marginTop: 12
|
||||
}}>
|
||||
<strong>账号:</strong>{passwordStatus.username}<br/>
|
||||
<strong>默认密码:</strong><code style={{
|
||||
background: '#fff',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 3,
|
||||
color: '#1890ff',
|
||||
fontSize: 14
|
||||
}}>{passwordStatus.default_password}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label>新密码(至少6个字符):</label>
|
||||
<Input.Password
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="请输入新密码"
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>确认密码:</label>
|
||||
<Input.Password
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="请再次输入密码"
|
||||
style={{ marginTop: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -122,7 +260,7 @@ export default function AuthCallback() {
|
||||
<Result
|
||||
status="success"
|
||||
title="登录成功"
|
||||
subTitle={showAnnouncement ? "欢迎使用..." : "正在跳转..."}
|
||||
subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")}
|
||||
style={{ background: 'white', padding: 40, borderRadius: 8 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -600,6 +600,7 @@ export default function MCPPluginsPage() {
|
||||
<Modal
|
||||
title={editingPlugin ? '编辑插件' : '添加插件'}
|
||||
open={modalVisible}
|
||||
centered
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
|
||||
@@ -73,23 +73,8 @@ export default function ProjectList() {
|
||||
};
|
||||
|
||||
const handleEnterProject = (id: string) => {
|
||||
const project = projects.find(p => p.id === id);
|
||||
if (project) {
|
||||
console.log('项目信息:', {
|
||||
id: project.id,
|
||||
title: project.title,
|
||||
wizard_status: project.wizard_status,
|
||||
wizard_step: project.wizard_step
|
||||
});
|
||||
|
||||
if (project.wizard_status === 'incomplete' || !project.wizard_status) {
|
||||
console.log('向导未完成,跳转到向导页面');
|
||||
navigate(`/wizard?projectId=${id}&step=${project.wizard_step || 0}`);
|
||||
} else {
|
||||
console.log('向导已完成,进入项目管理界面');
|
||||
navigate(`/project/${id}`);
|
||||
}
|
||||
}
|
||||
// 简化后直接进入项目,不再检查向导状态
|
||||
navigate(`/project/${id}`);
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
@@ -207,8 +192,8 @@ export default function ProjectList() {
|
||||
setSelectedProjectIds([]);
|
||||
};
|
||||
|
||||
// 获取可导出的项目(过滤掉向导未完成的项目)
|
||||
const exportableProjects = projects.filter(p => p.wizard_status === 'completed');
|
||||
// 获取所有可导出的项目
|
||||
const exportableProjects = projects;
|
||||
|
||||
// 关闭导出对话框
|
||||
const handleCloseExportModal = () => {
|
||||
@@ -631,12 +616,11 @@ export default function ProjectList() {
|
||||
<Row gutter={[16, 16]}>
|
||||
{projects.map((project) => {
|
||||
const progress = getProgress(project.current_words, project.target_words || 0);
|
||||
const isWizardComplete = project.wizard_status === 'completed';
|
||||
|
||||
return (
|
||||
<Col {...gridConfig} key={project.id}>
|
||||
<Badge.Ribbon
|
||||
text={isWizardComplete ? getStatusTag(project.status) : <Tag color="orange" icon={<RocketOutlined />}>创建中</Tag>}
|
||||
text={getStatusTag(project.status)}
|
||||
color="transparent"
|
||||
style={{ top: 12, right: 12 }}
|
||||
>
|
||||
@@ -680,69 +664,50 @@ export default function ProjectList() {
|
||||
{project.description || '暂无描述'}
|
||||
</Paragraph>
|
||||
|
||||
{isWizardComplete ? (
|
||||
<>
|
||||
{project.target_words && project.target_words > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>完成进度</Text>
|
||||
<Text strong style={{ fontSize: 12 }}>{progress}%</Text>
|
||||
</div>
|
||||
<Progress
|
||||
percent={progress}
|
||||
strokeColor={getProgressColor(progress)}
|
||||
showInfo={false}
|
||||
size={{ height: 8 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px 0',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#1890ff' }}>
|
||||
{(project.current_words / 1000).toFixed(1)}K
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>已写字数</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px 0',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#52c41a' }}>
|
||||
{project.target_words ? (project.target_words / 1000).toFixed(0) + 'K' : '--'}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>目标字数</Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '24px 0',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<RocketOutlined style={{ fontSize: 32, color: '#faad14', marginBottom: 12 }} />
|
||||
<div style={{ color: '#faad14', fontWeight: 'bold', marginBottom: 4 }}>
|
||||
项目创建中
|
||||
{project.target_words && project.target_words > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>完成进度</Text>
|
||||
<Text strong style={{ fontSize: 12 }}>{progress}%</Text>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
点击继续创建向导
|
||||
</Text>
|
||||
<Progress
|
||||
percent={progress}
|
||||
strokeColor={getProgressColor(progress)}
|
||||
showInfo={false}
|
||||
size={{ height: 8 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Row gutter={12}>
|
||||
<Col span={12}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px 0',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#1890ff' }}>
|
||||
{(project.current_words / 1000).toFixed(1)}K
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>已写字数</Text>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
padding: '12px 0',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 8
|
||||
}}>
|
||||
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#52c41a' }}>
|
||||
{project.target_words ? (project.target_words / 1000).toFixed(0) + 'K' : '--'}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>目标字数</Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
paddingTop: 16,
|
||||
|
||||
+307
-1182
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,18 @@
|
||||
import { Card, Descriptions, Empty, Typography } from 'antd';
|
||||
import { GlobalOutlined } from '@ant-design/icons';
|
||||
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message } from 'antd';
|
||||
import { GlobalOutlined, EditOutlined } from '@ant-design/icons';
|
||||
import { useState } from 'react';
|
||||
import { useStore } from '../store';
|
||||
import { cardStyles } from '../components/CardStyles';
|
||||
import { projectApi } from '../services/api';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
export default function WorldSetting() {
|
||||
const { currentProject } = useStore();
|
||||
const { currentProject, setCurrentProject } = useStore();
|
||||
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
|
||||
const [editForm] = Form.useForm();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
if (!currentProject) return null;
|
||||
|
||||
@@ -62,10 +68,28 @@ export default function WorldSetting() {
|
||||
marginBottom: 24,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: '#1890ff' }} />
|
||||
<h2 style={{ margin: 0 }}>世界设定</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: '#1890ff' }} />
|
||||
<h2 style={{ margin: 0 }}>世界设定</h2>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
editForm.setFieldsValue({
|
||||
world_time_period: currentProject.world_time_period || '',
|
||||
world_location: currentProject.world_location || '',
|
||||
world_atmosphere: currentProject.world_atmosphere || '',
|
||||
world_rules: currentProject.world_rules || '',
|
||||
});
|
||||
setIsEditModalVisible(true);
|
||||
}}
|
||||
>
|
||||
编辑世界观
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
@@ -182,6 +206,102 @@ export default function WorldSetting() {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 编辑世界观模态框 */}
|
||||
<Modal
|
||||
title="编辑世界观"
|
||||
open={isEditModalVisible}
|
||||
centered
|
||||
onCancel={() => {
|
||||
setIsEditModalVisible(false);
|
||||
editForm.resetFields();
|
||||
}}
|
||||
onOk={async () => {
|
||||
try {
|
||||
const values = await editForm.validateFields();
|
||||
setIsSaving(true);
|
||||
|
||||
const updatedProject = await projectApi.updateProject(currentProject.id, {
|
||||
world_time_period: values.world_time_period,
|
||||
world_location: values.world_location,
|
||||
world_atmosphere: values.world_atmosphere,
|
||||
world_rules: values.world_rules,
|
||||
});
|
||||
|
||||
setCurrentProject(updatedProject);
|
||||
message.success('世界观更新成功');
|
||||
setIsEditModalVisible(false);
|
||||
editForm.resetFields();
|
||||
} catch (error) {
|
||||
console.error('更新世界观失败:', error);
|
||||
message.error('更新失败,请重试');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}}
|
||||
confirmLoading={isSaving}
|
||||
width={800}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form
|
||||
form={editForm}
|
||||
layout="vertical"
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
<Form.Item
|
||||
label="时间设定"
|
||||
name="world_time_period"
|
||||
rules={[{ required: true, message: '请输入时间设定' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="描述故事发生的时代背景..."
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="地点设定"
|
||||
name="world_location"
|
||||
rules={[{ required: true, message: '请输入地点设定' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="描述故事发生的地理位置和环境..."
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="氛围设定"
|
||||
name="world_atmosphere"
|
||||
rules={[{ required: true, message: '请输入氛围设定' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="描述故事的整体氛围和基调..."
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="规则设定"
|
||||
name="world_rules"
|
||||
rules={[{ required: true, message: '请输入规则设定' }]}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="描述这个世界的特殊规则和设定..."
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -123,10 +123,23 @@ export const authApi = {
|
||||
localLogin: (username: string, password: string) =>
|
||||
api.post<unknown, { success: boolean; message: string; user: User }>('/auth/local/login', { username, password }),
|
||||
|
||||
bindAccountLogin: (username: string, password: string) =>
|
||||
api.post<unknown, { success: boolean; message: string; user: User }>('/auth/bind/login', { username, password }),
|
||||
|
||||
getLinuxDOAuthUrl: () => api.get<unknown, AuthUrlResponse>('/auth/linuxdo/url'),
|
||||
|
||||
getCurrentUser: () => api.get<unknown, User>('/auth/user'),
|
||||
|
||||
getPasswordStatus: () => api.get<unknown, {
|
||||
has_password: boolean;
|
||||
has_custom_password: boolean;
|
||||
username: string | null;
|
||||
default_password: string | null;
|
||||
}>('/auth/password/status'),
|
||||
|
||||
setPassword: (password: string) =>
|
||||
api.post<unknown, { success: boolean; message: string }>('/auth/password/set', { password }),
|
||||
|
||||
refreshSession: () => api.post<unknown, { message: string; expire_at: number; remaining_minutes: number }>('/auth/refresh'),
|
||||
|
||||
logout: () => api.post('/auth/logout'),
|
||||
@@ -306,6 +319,23 @@ export const chapterApi = {
|
||||
|
||||
checkCanGenerate: (chapterId: string) =>
|
||||
api.get<unknown, import('../types').ChapterCanGenerateResponse>(`/chapters/${chapterId}/can-generate`),
|
||||
|
||||
// 章节重新生成相关
|
||||
getRegenerationTasks: (chapterId: string, limit?: number) =>
|
||||
api.get<unknown, {
|
||||
chapter_id: string;
|
||||
total: number;
|
||||
tasks: Array<{
|
||||
task_id: string;
|
||||
status: string;
|
||||
version_number: number | null;
|
||||
version_note: string | null;
|
||||
original_word_count: number | null;
|
||||
regenerated_word_count: number | null;
|
||||
created_at: string | null;
|
||||
completed_at: string | null;
|
||||
}>;
|
||||
}>(`/chapters/${chapterId}/regeneration/tasks`, { params: { limit } }),
|
||||
};
|
||||
|
||||
export const writingStyleApi = {
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface SSEMessage {
|
||||
type: 'progress' | 'chunk' | 'result' | 'error' | 'done';
|
||||
message?: string;
|
||||
progress?: number;
|
||||
word_count?: number;
|
||||
status?: 'processing' | 'success' | 'error' | 'warning';
|
||||
content?: string;
|
||||
data?: any;
|
||||
@@ -10,7 +11,7 @@ export interface SSEMessage {
|
||||
}
|
||||
|
||||
export interface SSEClientOptions {
|
||||
onProgress?: (message: string, progress: number, status: string) => void;
|
||||
onProgress?: (message: string, progress: number, status: string, wordCount?: number) => void;
|
||||
onChunk?: (content: string) => void;
|
||||
onResult?: (data: any) => void;
|
||||
onError?: (error: string, code?: number) => void;
|
||||
@@ -61,8 +62,13 @@ export class SSEClient {
|
||||
private handleMessage(message: SSEMessage, resolve: Function, reject: Function) {
|
||||
switch (message.type) {
|
||||
case 'progress':
|
||||
if (this.options.onProgress && message.message && message.progress !== undefined) {
|
||||
this.options.onProgress(message.message, message.progress, message.status || 'processing');
|
||||
if (this.options.onProgress && message.progress !== undefined) {
|
||||
this.options.onProgress(
|
||||
message.message || '',
|
||||
message.progress,
|
||||
message.status || 'processing',
|
||||
message.word_count
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -201,8 +207,13 @@ export class SSEPostClient {
|
||||
private async handleMessage(message: SSEMessage, resolve: Function, reject: Function) {
|
||||
switch (message.type) {
|
||||
case 'progress':
|
||||
if (this.options.onProgress && message.message && message.progress !== undefined) {
|
||||
this.options.onProgress(message.message, message.progress, message.status || 'processing');
|
||||
if (this.options.onProgress && message.progress !== undefined) {
|
||||
this.options.onProgress(
|
||||
message.message || '',
|
||||
message.progress,
|
||||
message.status || 'processing',
|
||||
message.word_count
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user