update:1.更新根据分析建议重新生成章节内容
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user