Files
MuMuAINovel/frontend/src/pages/WorldSetting.tsx
T

519 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message, Space } from 'antd';
import { GlobalOutlined, EditOutlined, SyncOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { useStore } from '../store';
import { cardStyles } from '../components/CardStyles';
import { projectApi, wizardStreamApi } from '../services/api';
import { SSELoadingOverlay } from '../components/SSELoadingOverlay';
const { Title, Paragraph } = Typography;
const { TextArea } = Input;
export default function WorldSetting() {
const { currentProject, setCurrentProject } = useStore();
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [editForm] = Form.useForm();
const [isSaving, setIsSaving] = useState(false);
const [isRegenerating, setIsRegenerating] = useState(false);
const [regenerateProgress, setRegenerateProgress] = useState(0);
const [regenerateMessage, setRegenerateMessage] = useState('');
const [isPreviewModalVisible, setIsPreviewModalVisible] = useState(false);
const [newWorldData, setNewWorldData] = useState<{
time_period: string;
location: string;
atmosphere: string;
rules: string;
} | null>(null);
const [isSavingPreview, setIsSavingPreview] = useState(false);
const [modal, contextHolder] = Modal.useModal();
// AI重新生成世界观
const handleRegenerate = async () => {
if (!currentProject) return;
modal.confirm({
title: '确认重新生成',
content: '确定要使用AI重新生成世界观设定吗?这将替换当前的世界观内容。',
centered: true,
okText: '确认重新生成',
cancelText: '取消',
onOk: async () => {
setIsRegenerating(true);
setRegenerateProgress(0);
setRegenerateMessage('准备重新生成世界观...');
try {
await wizardStreamApi.regenerateWorldBuildingStream(
currentProject.id,
{},
{
onProgress: (msg: string, progress: number) => {
setRegenerateProgress(progress);
setRegenerateMessage(msg);
},
onChunk: (chunk: string) => {
// 可以在这里显示生成的内容片段(可选)
console.log('生成片段:', chunk);
},
onResult: (result: any) => {
// 保存新生成的数据
const newData = {
time_period: result.time_period,
location: result.location,
atmosphere: result.atmosphere,
rules: result.rules,
};
setNewWorldData(newData);
},
onError: (errorMsg: string) => {
console.error('重新生成失败:', errorMsg);
message.error(errorMsg || '重新生成失败,请重试');
},
onComplete: () => {
setIsRegenerating(false);
setRegenerateProgress(0);
setRegenerateMessage('');
// 显示预览对话框
setIsPreviewModalVisible(true);
}
}
);
} catch (error) {
console.error('重新生成出错:', error);
message.error('重新生成出错,请重试');
setIsRegenerating(false);
setRegenerateProgress(0);
setRegenerateMessage('');
}
}
});
};
// 确认保存重新生成的内容
const handleConfirmSave = async () => {
if (!currentProject || !newWorldData) return;
setIsSavingPreview(true);
try {
const updatedProject = await projectApi.updateProject(currentProject.id, {
world_time_period: newWorldData.time_period,
world_location: newWorldData.location,
world_atmosphere: newWorldData.atmosphere,
world_rules: newWorldData.rules,
});
setCurrentProject(updatedProject);
message.success('世界观已更新!');
setIsPreviewModalVisible(false);
setNewWorldData(null);
} catch (error) {
console.error('保存失败:', error);
message.error('保存失败,请重试');
} finally {
setIsSavingPreview(false);
}
};
// 取消保存,关闭预览
const handleCancelSave = () => {
setIsPreviewModalVisible(false);
setNewWorldData(null);
message.info('已取消,保持原有内容');
};
if (!currentProject) return null;
// 检查是否有世界设定信息
const hasWorldSetting = currentProject.world_time_period ||
currentProject.world_location ||
currentProject.world_atmosphere ||
currentProject.world_rules;
if (!hasWorldSetting) {
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* 固定头部 */}
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: '16px 0',
marginBottom: 16,
borderBottom: '1px solid var(--color-border-secondary)',
display: 'flex',
alignItems: 'center'
}}>
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
<h2 style={{ margin: 0 }}></h2>
</div>
{/* 可滚动内容区域 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
<Empty
description="暂无世界设定信息"
style={{ marginTop: 60 }}
>
<Paragraph type="secondary">
</Paragraph>
</Empty>
</div>
</div>
);
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{contextHolder}
{/* 固定头部 */}
<div style={{
position: 'sticky',
top: 0,
zIndex: 10,
backgroundColor: '#fff',
padding: '16px 0',
marginBottom: 24,
borderBottom: '1px solid #f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: 'var(--color-primary)' }} />
<h2 style={{ margin: 0 }}></h2>
</div>
<Space>
<Button
icon={<SyncOutlined />}
onClick={handleRegenerate}
disabled={isRegenerating}
>
AI重新生成
</Button>
<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>
</Space>
</div>
{/* 可滚动内容区域 */}
<div style={{ flex: 1, overflowY: 'auto' }}>
<Card
style={{
...cardStyles.base,
marginBottom: 16
}}
title={
<span style={{ fontSize: 18, fontWeight: 500 }}>
</span>
}
>
<Descriptions bordered column={1} styles={{ label: { width: 120, fontWeight: 500 } }}>
<Descriptions.Item label="小说名称">{currentProject.title}</Descriptions.Item>
{currentProject.description && (
<Descriptions.Item label="小说简介">{currentProject.description}</Descriptions.Item>
)}
<Descriptions.Item label="小说主题">{currentProject.theme || '未设定'}</Descriptions.Item>
<Descriptions.Item label="小说类型">{currentProject.genre || '未设定'}</Descriptions.Item>
<Descriptions.Item label="叙事视角">{currentProject.narrative_perspective || '未设定'}</Descriptions.Item>
<Descriptions.Item label="目标字数">
{currentProject.target_words ? `${currentProject.target_words.toLocaleString()}` : '未设定'}
</Descriptions.Item>
</Descriptions>
</Card>
<Card
style={{
...cardStyles.base,
marginBottom: 16
}}
title={
<span style={{ fontSize: 18, fontWeight: 500 }}>
<GlobalOutlined style={{ marginRight: 8 }} />
</span>
}
>
<div style={{ padding: '16px 0' }}>
{currentProject.world_time_period && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-primary)'
}}>
{currentProject.world_time_period}
</Paragraph>
</div>
)}
{currentProject.world_location && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-success)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-success)'
}}>
{currentProject.world_location}
</Paragraph>
</div>
)}
{currentProject.world_atmosphere && (
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-warning)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-warning)'
}}>
{currentProject.world_atmosphere}
</Paragraph>
</div>
)}
{currentProject.world_rules && (
<div style={{ marginBottom: 0 }}>
<Title level={5} style={{ color: 'var(--color-error)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
borderLeft: '4px solid var(--color-error)'
}}>
{currentProject.world_rules}
</Paragraph>
</div>
)}
</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>
{/* AI重新生成加载遮罩 */}
<SSELoadingOverlay
loading={isRegenerating}
progress={regenerateProgress}
message={regenerateMessage}
/>
{/* 预览重新生成的内容模态框 */}
<Modal
title="预览重新生成的世界观"
open={isPreviewModalVisible}
centered
width={900}
onOk={handleConfirmSave}
onCancel={handleCancelSave}
confirmLoading={isSavingPreview}
okText="确认替换"
cancelText="取消"
okButtonProps={{ danger: true }}
>
{newWorldData && (
<div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-warning-bg)', border: '1px solid var(--color-warning-border)', borderRadius: 8 }}>
<Typography.Text type="warning" strong>
"确认替换"
</Typography.Text>
</div>
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: 'var(--color-primary)', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #1890ff'
}}>
{newWorldData.time_period}
</Paragraph>
</div>
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#52c41a', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #52c41a'
}}>
{newWorldData.location}
</Paragraph>
</div>
<div style={{ marginBottom: 24 }}>
<Title level={5} style={{ color: '#faad14', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #faad14'
}}>
{newWorldData.atmosphere}
</Paragraph>
</div>
<div style={{ marginBottom: 0 }}>
<Title level={5} style={{ color: '#f5222d', marginBottom: 12 }}>
</Title>
<Paragraph style={{
fontSize: 15,
lineHeight: 1.8,
padding: 16,
background: '#f5f5f5',
borderRadius: 8,
borderLeft: '4px solid #f5222d'
}}>
{newWorldData.rules}
</Paragraph>
</div>
</div>
)}
</Modal>
</div>
);
}