import { useEffect, useMemo, useRef, useState } from 'react'; import { normalizeCustomWorldLandmarks } from '../../data/customWorldSceneGraph'; import { rpgCreationAssetClient } from '../../services/rpg-creation/rpgCreationAssetClient'; import type { CustomWorldLandmark, CustomWorldNpc, CustomWorldPlayableNpc, CustomWorldProfile, } from '../../types'; import type { ResultTab } from '../CustomWorldEntityCatalog'; import type { RpgCreationEditorTarget } from '../rpg-creation-editor/RpgCreationEntityEditorModal'; export type EntityGenerationKind = 'playable' | 'story' | 'landmark'; export type PendingGeneratedEntity = { id: string; kind: EntityGenerationKind; title: string; progress: number; phaseLabel: string; }; export type RecentGeneratedIds = Record; export type AgentEntityGenerationResult = { profile?: CustomWorldProfile | null; }; function getCreateTargetByTab( activeTab: ResultTab, ): RpgCreationEditorTarget | null { if (activeTab === 'playable') return { kind: 'playable', mode: 'create' }; if (activeTab === 'story') return { kind: 'story', mode: 'create' }; if (activeTab === 'landmarks') return { kind: 'landmark', mode: 'create' }; return null; } function getCreateLabelByTab(activeTab: ResultTab) { if (activeTab === 'playable') return '新增可扮演角色'; if (activeTab === 'story') return '新增场景角色'; if (activeTab === 'landmarks') return '新增场景'; return ''; } function createPendingGeneratedEntity( kind: EntityGenerationKind, ): PendingGeneratedEntity { return { id: `pending-${kind}-${Date.now()}`, kind, title: kind === 'playable' ? '新可扮演角色' : kind === 'story' ? '新场景角色' : '新场景', progress: 8, phaseLabel: '正在整理世界上下文', }; } function resolvePendingPhaseLabel( kind: EntityGenerationKind, progress: number, ) { if (progress < 28) { return '正在整理世界上下文'; } if (progress < 72) { return kind === 'landmark' ? '正在推理场景结构' : '正在推理角色结构'; } return '正在回写结果'; } function prependPlayableNpc( profile: CustomWorldProfile, npc: CustomWorldPlayableNpc, ) { return { ...profile, playableNpcs: [npc, ...profile.playableNpcs], } satisfies CustomWorldProfile; } function prependStoryNpc(profile: CustomWorldProfile, npc: CustomWorldNpc) { return { ...profile, storyNpcs: [npc, ...profile.storyNpcs], } satisfies CustomWorldProfile; } function prependLandmark( profile: CustomWorldProfile, landmark: CustomWorldLandmark, ) { return { ...profile, landmarks: normalizeCustomWorldLandmarks({ landmarks: [landmark, ...profile.landmarks], storyNpcs: profile.storyNpcs, }), } satisfies CustomWorldProfile; } function getEntityCountByKind( profile: CustomWorldProfile, kind: EntityGenerationKind, ) { if (kind === 'playable') return profile.playableNpcs.length; if (kind === 'story') return profile.storyNpcs.length; return profile.landmarks.length; } function removeStoryNpcsFromProfile( profile: CustomWorldProfile, ids: string[], ) { const idSet = new Set(ids); const nextStoryNpcs = profile.storyNpcs.filter((npc) => !idSet.has(npc.id)); return { ...profile, storyNpcs: nextStoryNpcs, landmarks: normalizeCustomWorldLandmarks({ landmarks: profile.landmarks.map((landmark) => ({ ...landmark, sceneNpcIds: landmark.sceneNpcIds.filter((npcId) => !idSet.has(npcId)), })), storyNpcs: nextStoryNpcs, }), } satisfies CustomWorldProfile; } function removeLandmarksFromProfile( profile: CustomWorldProfile, ids: string[], ) { const idSet = new Set(ids); const nextLandmarks = profile.landmarks.filter( (landmark) => !idSet.has(landmark.id), ); return { ...profile, landmarks: normalizeCustomWorldLandmarks({ landmarks: nextLandmarks.map((landmark) => ({ ...landmark, connections: landmark.connections.filter( (connection) => !idSet.has(connection.targetLandmarkId), ), })), storyNpcs: profile.storyNpcs, }), } satisfies CustomWorldProfile; } export function useRpgCreationResultActions(params: { activeTab: ResultTab; agentEntityGenerator?: | ((kind: EntityGenerationKind) => Promise) | undefined; isGenerating: boolean; onProfileChange: (profile: CustomWorldProfile) => void; profile: CustomWorldProfile; readOnly: boolean; triggerRegenerate?: () => void; }) { const { activeTab, agentEntityGenerator, isGenerating, onProfileChange, profile, readOnly, triggerRegenerate, } = params; const [editorTarget, setEditorTarget] = useState(null); const [pendingGeneratedEntity, setPendingGeneratedEntity] = useState(null); const [recentGeneratedIds, setRecentGeneratedIds] = useState( { playable: [], story: [], landmark: [], }, ); const [localGenerationError, setLocalGenerationError] = useState( null, ); const pendingProgressTimerRef = useRef(null); const createTarget = useMemo( () => getCreateTargetByTab(activeTab), [activeTab], ); const createLabel = useMemo( () => getCreateLabelByTab(activeTab), [activeTab], ); const stopPendingProgressTimer = () => { if (pendingProgressTimerRef.current !== null) { window.clearInterval(pendingProgressTimerRef.current); pendingProgressTimerRef.current = null; } }; useEffect(() => () => stopPendingProgressTimer(), []); const startPendingProgress = (kind: EntityGenerationKind) => { stopPendingProgressTimer(); setPendingGeneratedEntity(createPendingGeneratedEntity(kind)); pendingProgressTimerRef.current = window.setInterval(() => { setPendingGeneratedEntity((current) => { if (!current || current.kind !== kind) { return current; } const nextProgress = Math.min( current.progress + (current.progress < 56 ? 11 : 5), 88, ); return { ...current, progress: nextProgress, phaseLabel: resolvePendingPhaseLabel(kind, nextProgress), }; }); }, 520); }; const finishPendingProgress = () => { stopPendingProgressTimer(); setPendingGeneratedEntity(null); }; const markGeneratedAsRecent = ( kind: EntityGenerationKind, generatedId: string, ) => { setRecentGeneratedIds((current) => ({ ...current, [kind]: [ generatedId, ...current[kind].filter((id) => id !== generatedId), ].slice(0, 6), })); }; const handleGenerateEntity = async (kind: EntityGenerationKind) => { if (readOnly || isGenerating || pendingGeneratedEntity) { return; } setLocalGenerationError(null); startPendingProgress(kind); try { if (agentEntityGenerator) { const previousCount = getEntityCountByKind(profile, kind); const generationResult = await agentEntityGenerator(kind); const currentCount = generationResult?.profile ? getEntityCountByKind(generationResult.profile, kind) : previousCount; if (currentCount <= previousCount) { throw new Error('生成请求已完成,但结果页未收到新增内容,请返回创作页重新打开草稿后重试。'); } } else if (kind === 'playable') { const nextNpc = await rpgCreationAssetClient.generatePlayableNpc({ profile, }); onProfileChange(prependPlayableNpc(profile, nextNpc)); markGeneratedAsRecent('playable', nextNpc.id); } else if (kind === 'story') { const nextNpc = await rpgCreationAssetClient.generateStoryNpc({ profile, }); onProfileChange(prependStoryNpc(profile, nextNpc)); markGeneratedAsRecent('story', nextNpc.id); } else { const nextLandmark = await rpgCreationAssetClient.generateLandmark({ profile, }); onProfileChange(prependLandmark(profile, nextLandmark)); markGeneratedAsRecent('landmark', nextLandmark.id); } } catch (generationError) { setLocalGenerationError( generationError instanceof Error ? generationError.message : '生成失败,请稍后重试。', ); } finally { finishPendingProgress(); } }; const handleRegenerate = () => { if (isGenerating || !triggerRegenerate) { return; } const confirmed = window.confirm( `确认重新生成“${profile.name}”吗?\n\n重新生成会重新生成当前世界中的所有信息,包括你修改和新增的所有内容。`, ); if (!confirmed) { return; } triggerRegenerate(); }; const handleDeleteStoryNpcs = (ids: string[]) => { if (ids.length === 0) { return; } onProfileChange(removeStoryNpcsFromProfile(profile, ids)); }; const handleDeleteLandmarks = (ids: string[]) => { if (ids.length === 0) { return; } onProfileChange(removeLandmarksFromProfile(profile, ids)); }; return { createLabel, createTarget, editorTarget, handleDeleteLandmarks, handleDeleteStoryNpcs, handleGenerateEntity, handleRegenerate, localGenerationError, pendingGeneratedEntity, recentGeneratedIds, setEditorTarget, closeEditorTarget: () => setEditorTarget(null), }; }