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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user