feature:新增大纲续写-智能生成组织功能,自动添加组织成员

This commit is contained in:
xiamuceer
2026-01-05 14:27:27 +08:00
parent 6e603ee1a9
commit ba7ee591b6
6 changed files with 1986 additions and 84 deletions
+424 -5
View File
@@ -33,6 +33,37 @@ interface CharacterConfirmationData {
chapter_range: string;
}
// 组织预测数据类型
interface PredictedOrganization {
name?: string;
organization_description: string;
organization_type: string;
importance: string;
appearance_chapter: number;
power_level: number;
plot_function: string;
location?: string;
motto?: string;
initial_members: Array<{
character_name: string;
position: string;
reason?: string;
}>;
relationship_suggestions: Array<{
target_organization: string;
relationship_type: string;
reason?: string;
}>;
}
interface OrganizationConfirmationData {
code: string;
message: string;
predicted_organizations: PredictedOrganization[];
reason: string;
chapter_range: string;
}
const { TextArea } = Input;
export default function Outline() {
@@ -56,6 +87,11 @@ export default function Outline() {
const [pendingGenerateData, setPendingGenerateData] = useState<any>(null);
const [selectedCharacterIndices, setSelectedCharacterIndices] = useState<number[]>([]);
// 组织确认相关状态
const [organizationConfirmData, setOrganizationConfirmData] = useState<OrganizationConfirmationData | null>(null);
const [organizationConfirmVisible, setOrganizationConfirmVisible] = useState(false);
const [selectedOrganizationIndices, setSelectedOrganizationIndices] = useState<number[]>([]);
// 缓存批量展开的规划数据,避免重复AI调用
const [cachedBatchExpansionResponse, setCachedBatchExpansionResponse] = useState<BatchOutlineExpansionResponse | null>(null);
@@ -124,6 +160,15 @@ export default function Outline() {
}
}, [characterConfirmData]);
// 当组织确认数据变化时,初始化选中状态(默认全选)
useEffect(() => {
if (organizationConfirmData) {
setSelectedOrganizationIndices(
organizationConfirmData.predicted_organizations.map((_, idx) => idx)
);
}
}, [organizationConfirmData]);
// 移除事件监听,避免无限循环
// Hook 内部已经更新了 store,不需要再次刷新
@@ -205,6 +250,9 @@ export default function Outline() {
plot_stage?: 'development' | 'climax' | 'ending';
keep_existing?: boolean;
enable_auto_characters?: boolean;
require_character_confirmation?: boolean;
enable_auto_organizations?: boolean;
require_organization_confirmation?: boolean;
}
const handleGenerate = async (values: GenerateFormValues) => {
@@ -237,7 +285,10 @@ export default function Outline() {
mode: values.mode || 'auto',
story_direction: values.story_direction,
plot_stage: values.plot_stage || 'development',
enable_auto_characters: values.enable_auto_characters !== undefined ? values.enable_auto_characters : true
enable_auto_characters: values.enable_auto_characters !== undefined ? values.enable_auto_characters : true,
require_character_confirmation: values.require_character_confirmation !== undefined ? values.require_character_confirmation : true,
enable_auto_organizations: values.enable_auto_organizations !== undefined ? values.enable_auto_organizations : true,
require_organization_confirmation: values.require_organization_confirmation !== undefined ? values.require_organization_confirmation : true
};
// 只有在用户选择了模型时才添加model参数
@@ -281,6 +332,20 @@ export default function Outline() {
setCharacterConfirmData(data);
setCharacterConfirmVisible(true);
},
onOrganizationConfirmation: (data: any) => {
// ✨ 新增:处理组织确认事件
console.log('收到组织确认请求:', data);
// 关闭SSE进度Modal
setSSEModalVisible(false);
setIsGenerating(false);
// 保存待处理的生成数据
setPendingGenerateData(requestData);
// 显示组织确认对话框
setOrganizationConfirmData(data);
setOrganizationConfirmVisible(true);
},
onError: (error: string) => {
// 现在只处理真正的错误
message.error(`生成失败: ${error}`);
@@ -359,6 +424,9 @@ export default function Outline() {
theme: currentProject.theme || '',
model: defaultModel, // 添加默认模型
enable_auto_characters: false, // 默认禁用自动角色引入
require_character_confirmation: true, // 默认需要用户确认
enable_auto_organizations: false, // 默认禁用自动组织引入
require_organization_confirmation: true, // 默认需要用户确认
}}
>
{hasOutlines && (
@@ -467,19 +535,94 @@ export default function Outline() {
<TextArea rows={2} placeholder="其他特殊要求(可选)" />
</Form.Item>
{/* 自动角色引入开关 - 仅在续写模式显示 */}
{isContinue && (
{/* 自动角色和组织引入开关 - 仅在续写模式显示 */}
{isContinue && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{/* 角色引入部分 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24, alignItems: 'flex-start' }}>
<Form.Item
label="智能角色引入"
name="enable_auto_characters"
tooltip="AI会根据剧情发展自动判断是否需要引入新角色,并自动创建角色卡片和建立关系"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
)}
{/* 角色确认选项 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.enable_auto_characters !== currentValues.enable_auto_characters
}
>
{({ getFieldValue }) => {
const enableAutoChars = getFieldValue('enable_auto_characters');
if (!enableAutoChars) return null;
return (
<Form.Item
label="新角色确认"
name="require_character_confirmation"
tooltip="启用后,AI预测到需要新角色时会先让您确认;禁用后,AI预测的角色将直接创建"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
</div>
{/* 组织引入部分 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 24, alignItems: 'flex-start' }}>
<Form.Item
label="智能组织引入"
name="enable_auto_organizations"
tooltip="AI会根据剧情发展自动判断是否需要引入新组织/势力,并自动创建设定和建立关系"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
{/* 组织确认选项 */}
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.enable_auto_organizations !== currentValues.enable_auto_organizations
}
>
{({ getFieldValue }) => {
const enableAutoOrgs = getFieldValue('enable_auto_organizations');
if (!enableAutoOrgs) return null;
return (
<Form.Item
label="新组织确认"
name="require_organization_confirmation"
tooltip="启用后,AI预测到需要新组织时会先让您确认;禁用后,AI预测的组织将直接创建"
style={{ marginBottom: 0 }}
>
<Radio.Group buttonStyle="solid">
<Radio.Button value={true}></Radio.Button>
<Radio.Button value={false}></Radio.Button>
</Radio.Group>
</Form.Item>
);
}}
</Form.Item>
</div>
</div>
)}
</>
);
}}
@@ -1640,6 +1783,15 @@ export default function Outline() {
setCharacterConfirmData(null);
// 刷新大纲列表
refreshOutlines();
},
onOrganizationConfirmation: (data: any) => {
// 处理可能的后续组织确认
console.log('收到组织确认请求:', data);
setSSEModalVisible(false);
setIsGenerating(false);
setPendingGenerateData(requestData);
setOrganizationConfirmData(data);
setOrganizationConfirmVisible(true);
}
});
@@ -1666,7 +1818,7 @@ export default function Outline() {
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('跳过角色创建,开始续写大纲...');
setSSEMessage('跳过角色创建,继续生成...');
setSSEModalVisible(true);
// 准备请求数据,禁用自动角色引入
@@ -1687,6 +1839,15 @@ export default function Outline() {
onResult: (data: any) => {
console.log('生成完成,结果:', data);
},
onOrganizationConfirmation: (data: any) => {
// 处理可能的后续组织确认
console.log('收到组织确认请求:', data);
setSSEModalVisible(false);
setIsGenerating(false);
setPendingGenerateData(requestData);
setOrganizationConfirmData(data);
setOrganizationConfirmVisible(true);
},
onError: (error: string) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
@@ -1714,6 +1875,128 @@ export default function Outline() {
}
};
// 处理组织确认 - 用户同意创建组织
const handleConfirmOrganizations = async (selectedOrganizations: PredictedOrganization[]) => {
if (!pendingGenerateData) {
message.error('生成数据丢失,请重新操作');
return;
}
try {
setOrganizationConfirmVisible(false);
setIsGenerating(true);
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('正在创建确认的组织...');
setSSEModalVisible(true);
// 准备请求数据,添加确认的组织
const requestData = {
...pendingGenerateData,
confirmed_organizations: selectedOrganizations
};
console.log('携带确认组织重新请求:', requestData);
// 重新发起SSE请求
const apiUrl = `/api/outlines/generate-stream`;
const client = new SSEPostClient(apiUrl, requestData, {
onProgress: (msg: string, progress: number) => {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
setIsGenerating(false);
},
onComplete: () => {
message.success('大纲生成完成!');
setSSEModalVisible(false);
setIsGenerating(false);
// 清理状态
setPendingGenerateData(null);
setOrganizationConfirmData(null);
// 刷新大纲列表
refreshOutlines();
}
});
client.connect();
} catch (error) {
console.error('确认组织失败:', error);
message.error('操作失败');
setSSEModalVisible(false);
setIsGenerating(false);
}
};
// 处理组织确认 - 用户拒绝创建组织
const handleRejectOrganizations = async () => {
if (!pendingGenerateData) {
message.error('生成数据丢失,请重新操作');
return;
}
try {
setOrganizationConfirmVisible(false);
setIsGenerating(true);
// 显示进度Modal
setSSEProgress(0);
setSSEMessage('跳过组织创建,继续生成...');
setSSEModalVisible(true);
// 准备请求数据,禁用自动组织引入
const requestData = {
...pendingGenerateData,
enable_auto_organizations: false // 禁用自动组织引入
};
console.log('跳过组织创建,重新请求:', requestData);
// 重新发起SSE请求
const apiUrl = `/api/outlines/generate-stream`;
const client = new SSEPostClient(apiUrl, requestData, {
onProgress: (msg: string, progress: number) => {
setSSEMessage(msg);
setSSEProgress(progress);
},
onResult: (data: any) => {
console.log('生成完成,结果:', data);
},
onError: (error: string) => {
message.error(`生成失败: ${error}`);
setSSEModalVisible(false);
setIsGenerating(false);
},
onComplete: () => {
message.success('大纲生成完成!');
setSSEModalVisible(false);
setIsGenerating(false);
// 清理状态
setPendingGenerateData(null);
setOrganizationConfirmData(null);
// 刷新大纲列表
refreshOutlines();
}
});
client.connect();
} catch (error) {
console.error('跳过组织创建失败:', error);
message.error('操作失败');
setSSEModalVisible(false);
setIsGenerating(false);
}
};
// 渲染角色确认对话框
const renderCharacterConfirmModal = () => {
if (!characterConfirmData) return null;
@@ -1853,10 +2136,146 @@ export default function Outline() {
);
};
// 渲染组织确认对话框
const renderOrganizationConfirmModal = () => {
if (!organizationConfirmData) return null;
return (
<Modal
title={
<Space>
<ExclamationCircleOutlined style={{ color: 'var(--color-warning)' }} />
<span></span>
</Space>
}
open={organizationConfirmVisible}
onOk={() => {
const selectedOrganizations = organizationConfirmData.predicted_organizations.filter(
(_, idx) => selectedOrganizationIndices.includes(idx)
);
handleConfirmOrganizations(selectedOrganizations);
}}
onCancel={() => {
modalApi.confirm({
title: '确认操作',
content: '是否跳过组织创建,直接续写大纲?',
okText: '跳过组织,继续续写',
cancelText: '返回选择',
onOk: handleRejectOrganizations
});
}}
width={800}
centered
okText={`确认创建选中的 ${selectedOrganizationIndices.length} 个组织`}
cancelText="跳过组织创建"
>
<div>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-warning-bg)', borderRadius: 4, border: '1px solid var(--color-warning-border)' }}>
<div style={{ fontWeight: 500, marginBottom: 8, color: '#d48806' }}>
AI
</div>
<div style={{ color: '#666', marginBottom: 8 }}>
{organizationConfirmData.reason}
</div>
<Tag color="blue">{organizationConfirmData.chapter_range}</Tag>
<Tag color="green">{organizationConfirmData.predicted_organizations.length} </Tag>
</div>
<div style={{ marginBottom: 12 }}>
<Space>
<Button
size="small"
onClick={() => setSelectedOrganizationIndices(
organizationConfirmData.predicted_organizations.map((_, idx) => idx)
)}
>
</Button>
<Button
size="small"
onClick={() => setSelectedOrganizationIndices([])}
>
</Button>
</Space>
</div>
<List
dataSource={organizationConfirmData.predicted_organizations}
renderItem={(org, index) => (
<List.Item
key={index}
style={{
background: selectedOrganizationIndices.includes(index) ? '#f0f5ff' : 'transparent',
padding: 12,
borderRadius: 4,
marginBottom: 8,
border: selectedOrganizationIndices.includes(index) ? '1px solid var(--color-primary)' : '1px solid var(--color-border-secondary)',
cursor: 'pointer'
}}
onClick={() => {
if (selectedOrganizationIndices.includes(index)) {
setSelectedOrganizationIndices(selectedOrganizationIndices.filter(i => i !== index));
} else {
setSelectedOrganizationIndices([...selectedOrganizationIndices, index]);
}
}}
>
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Space>
<input
type="checkbox"
checked={selectedOrganizationIndices.includes(index)}
onChange={() => { }}
style={{ cursor: 'pointer' }}
/>
<span style={{ fontWeight: 500, fontSize: 16 }}>
{org.name || org.organization_description}
</span>
<Tag color="blue">{org.organization_type}</Tag>
<Tag color="orange">: {org.power_level}</Tag>
</Space>
<Tag>{org.appearance_chapter}</Tag>
</div>
<div style={{ marginBottom: 8, color: '#666' }}>
<strong></strong>{org.plot_function}
</div>
{org.location && (
<div style={{ marginBottom: 8 }}>
<strong></strong>{org.location}
</div>
)}
{org.initial_members && org.initial_members.length > 0 && (
<div style={{ marginBottom: 8 }}>
<strong></strong>
<Space wrap style={{ marginLeft: 8 }}>
{org.initial_members.map((member, idx) => (
<Tag key={idx} color="purple">
{member.character_name} - {member.position}
</Tag>
))}
</Space>
</div>
)}
</div>
</List.Item>
)}
/>
</div>
</Modal>
);
};
return (
<>
{/* 角色确认对话框 */}
{renderCharacterConfirmModal()}
{/* 组织确认对话框 */}
{renderOrganizationConfirmModal()}
{/* 批量展开预览 Modal */}
<Modal
+8
View File
@@ -18,6 +18,7 @@ export interface SSEClientOptions {
onComplete?: () => void;
onConnectionError?: (error: Event) => void;
onCharacterConfirmation?: (data: any) => void; // 新增:角色确认回调
onOrganizationConfirmation?: (data: any) => void; // 新增:组织确认回调
}
export class SSEClient {
@@ -200,6 +201,13 @@ export class SSEPostClient {
}
currentEvent = ''; // 重置事件类型
return; // 暂停流程,等待用户确认
} else if (currentEvent === 'organization_confirmation_required') {
// 处理组织确认事件
if (this.options.onOrganizationConfirmation) {
this.options.onOrganizationConfirmation(data);
}
currentEvent = ''; // 重置事件类型
return; // 暂停流程,等待用户确认
} else {
// 标准消息处理
const message: SSEMessage = data;