This commit is contained in:
2026-04-16 15:45:00 +08:00
parent 6363267bca
commit 91b63675eb
43 changed files with 5652 additions and 853 deletions

View File

@@ -31,16 +31,17 @@ import {
getCustomWorldAgentSession,
sendCustomWorldAgentMessage,
} from '../../services/aiService';
import {
readCustomWorldAgentUiState,
writeCustomWorldAgentUiState,
} from '../../services/customWorldAgentUiState';
import { buildCustomWorldProfileFromAgentDraft } from '../../services/customWorldAgentDraftResult';
import {
buildAgentDraftFoundationGenerationProgress,
buildAgentDraftFoundationSettingText,
isDraftFoundationOperation,
isDraftFoundationOperationRunning,
} from '../../services/customWorldAgentGenerationProgress';
import {
readCustomWorldAgentUiState,
writeCustomWorldAgentUiState,
} from '../../services/customWorldAgentUiState';
import {
buildCustomWorldCreatorIntentDisplayText,
buildCustomWorldCreatorIntentGenerationText,
@@ -61,7 +62,7 @@ import {
type GameState,
} from '../../types';
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
import { type PlatformHomeTab,PlatformHomeView } from './PlatformHomeView';
import { type PlatformHomeTab, PlatformHomeView } from './PlatformHomeView';
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
const CustomWorldGenerationView = lazy(async () => {
@@ -106,6 +107,9 @@ type CustomWorldGenerationViewSource =
| 'agent-draft-foundation'
| null;
type CustomWorldResultViewSource = 'classic' | 'agent-draft' | null;
type CustomWorldAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
type PreGameSelectionFlowProps = {
selectionStage: SelectionStage;
setSelectionStage: (stage: SelectionStage) => void;
@@ -270,11 +274,21 @@ export function PreGameSelectionFlow({
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
const [customWorldProgress, setCustomWorldProgress] =
useState<CustomWorldGenerationProgress | null>(null);
const [customWorldAutoSaveState, setCustomWorldAutoSaveState] =
useState<CustomWorldAutoSaveState>('idle');
const [customWorldAutoSaveError, setCustomWorldAutoSaveError] = useState<
string | null
>(null);
const [customWorldGenerationViewSource, setCustomWorldGenerationViewSource] =
useState<CustomWorldGenerationViewSource>(null);
const [customWorldResultViewSource, setCustomWorldResultViewSource] =
useState<CustomWorldResultViewSource>(null);
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
useState<number | null>(null);
const customWorldAbortControllerRef = useRef<AbortController | null>(null);
const customWorldAutoSaveTimeoutRef = useRef<number | null>(null);
const lastAutoSavedProfileSignatureRef = useRef<string | null>(null);
const latestAutoSaveRequestIdRef = useRef(0);
const previewCustomWorldCharacters = useMemo(
() =>
@@ -307,34 +321,6 @@ export function PreGameSelectionFlow({
return nextSession;
}, []);
const refreshPlatformData = useCallback(async () => {
setIsLoadingPlatform(true);
setPlatformError(null);
try {
const [libraryEntries, galleryEntries] = await Promise.all([
listCustomWorldLibrary(),
listCustomWorldGallery(),
]);
setSavedCustomWorldEntries(libraryEntries);
setPublishedGalleryEntries(galleryEntries);
if (selectedDetailEntry) {
const nextOwnedEntry = libraryEntries.find(
(entry) =>
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
entry.profileId === selectedDetailEntry.profileId,
);
if (nextOwnedEntry) {
setSelectedDetailEntry(nextOwnedEntry);
}
}
} catch (error) {
setPlatformError(resolveErrorMessage(error, '读取平台数据失败。'));
} finally {
setIsLoadingPlatform(false);
}
}, [selectedDetailEntry]);
useEffect(() => {
if (hasAppliedInitialAgentWorkspaceRef.current) {
return;
@@ -397,6 +383,9 @@ export function PreGameSelectionFlow({
useEffect(
() => () => {
customWorldAbortControllerRef.current?.abort();
if (customWorldAutoSaveTimeoutRef.current !== null) {
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
}
},
[],
);
@@ -512,6 +501,72 @@ export function PreGameSelectionFlow({
syncAgentSessionSnapshot,
]);
useEffect(() => {
if (
!isDraftFoundationOperationRunning(agentOperation) ||
agentDraftGenerationStartedAt
) {
return;
}
setAgentDraftGenerationStartedAt(Date.now());
}, [agentDraftGenerationStartedAt, agentOperation]);
useEffect(() => {
if (
selectionStage !== 'custom-world-generating' ||
customWorldGenerationViewSource !== 'agent-draft-foundation' ||
!isDraftFoundationOperation(agentOperation) ||
agentOperation.status !== 'completed'
) {
return;
}
let cancelled = false;
const timeoutId = window.setTimeout(() => {
void (async () => {
const latestSession = activeAgentSessionId
? await syncAgentSessionSnapshot(activeAgentSessionId).catch(
() => null,
)
: agentSession;
if (cancelled) {
return;
}
const draftResultProfile = buildCustomWorldProfileFromAgentDraft(
latestSession ?? agentSession,
);
if (!draftResultProfile) {
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setSelectionStage('agent-workspace');
return;
}
setGeneratedCustomWorldProfile(draftResultProfile);
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource('agent-draft');
setSelectionStage('custom-world-result');
})();
}, 900);
return () => {
cancelled = true;
window.clearTimeout(timeoutId);
};
}, [
activeAgentSessionId,
agentOperation,
customWorldGenerationViewSource,
agentSession,
selectionStage,
setSelectionStage,
syncAgentSessionSnapshot,
]);
const customWorldSettingPreview = useMemo(() => {
if (customWorldCreatorIntent.sourceMode === 'freeform') {
return customWorldCreatorIntent.rawSettingText.trim();
@@ -531,6 +586,51 @@ export function PreGameSelectionFlow({
() => buildAgentDraftFoundationSettingText(agentSession),
[agentSession],
);
const agentDraftResultProfile = useMemo(
() => buildCustomWorldProfileFromAgentDraft(agentSession),
[agentSession],
);
const shouldAutoOpenAgentDraftResult = useMemo(
() =>
Boolean(
agentDraftResultProfile &&
agentSession &&
(agentSession.stage === 'object_refining' ||
agentSession.stage === 'visual_refining' ||
agentSession.stage === 'long_tail_review' ||
agentSession.stage === 'ready_to_publish' ||
agentSession.stage === 'published') &&
agentSession.draftCards.length > 0,
),
[agentDraftResultProfile, agentSession],
);
useEffect(() => {
if (!shouldAutoOpenAgentDraftResult || !agentDraftResultProfile) {
return;
}
if (selectionStage === 'agent-workspace') {
setGeneratedCustomWorldProfile(agentDraftResultProfile);
setCustomWorldResultViewSource('agent-draft');
setSelectionStage('custom-world-result');
return;
}
if (
selectionStage === 'custom-world-result' &&
!generatedCustomWorldProfile
) {
setGeneratedCustomWorldProfile(agentDraftResultProfile);
setCustomWorldResultViewSource('agent-draft');
}
}, [
agentDraftResultProfile,
generatedCustomWorldProfile,
selectionStage,
setSelectionStage,
shouldAutoOpenAgentDraftResult,
]);
const agentDraftGenerationProgress = useMemo(
() =>
@@ -543,25 +643,38 @@ export function PreGameSelectionFlow({
const isAgentDraftGenerationView =
customWorldGenerationViewSource === 'agent-draft-foundation';
const isAgentDraftResultView = customWorldResultViewSource === 'agent-draft';
const activeGenerationSettingText = isAgentDraftGenerationView
? agentDraftSettingPreview
: customWorldSettingPreview;
const activeGenerationProgress = isAgentDraftGenerationView
? agentDraftGenerationProgress
: customWorldProgress;
const isActiveGenerationRunning = isAgentDraftGenerationView
? isDraftFoundationOperationRunning(agentOperation)
: isGeneratingCustomWorld;
const activeGenerationError =
isAgentDraftGenerationView &&
isDraftFoundationOperation(agentOperation) &&
agentOperation.status === 'failed'
const activeGenerationError = isAgentDraftGenerationView
? isDraftFoundationOperation(agentOperation) &&
agentOperation.status === 'failed'
? agentOperation.error || agentOperation.phaseDetail
: customWorldError;
: null
: customWorldError;
const leaveCustomWorldResult = () => {
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
setCustomWorldResultViewSource(null);
setSelectionStage(
isAgentDraftResultView
? 'agent-workspace'
: selectedDetailEntry
? 'detail'
: 'platform',
);
};
const leaveCustomWorldGeneration = () => {
@@ -570,8 +683,11 @@ export function PreGameSelectionFlow({
}
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setSelectionStage('platform');
};
@@ -596,6 +712,12 @@ export function PreGameSelectionFlow({
const { session } = await createCustomWorldAgentSession({});
setAgentSession(session);
setAgentOperation(null);
setGeneratedCustomWorldProfile(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
persistAgentUiState(session.sessionId, null);
setShowCreationTypeModal(false);
setPlatformTab('create');
@@ -639,6 +761,20 @@ export function PreGameSelectionFlow({
return;
}
const isDraftFoundationAction = payload.action === 'draft_foundation';
if (isDraftFoundationAction) {
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource('agent-draft-foundation');
setCustomWorldResultViewSource(null);
setAgentDraftGenerationStartedAt(Date.now());
setSelectionStage('custom-world-generating');
}
try {
const { operation } = await executeCustomWorldAgentAction(
activeAgentSessionId,
@@ -665,10 +801,44 @@ export function PreGameSelectionFlow({
const leaveAgentWorkspace = () => {
setPlatformTab('create');
setAgentOperation(null);
setGeneratedCustomWorldProfile(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
persistAgentUiState(activeAgentSessionId, null);
setSelectionStage('platform');
};
const leaveAgentDraftGeneration = () => {
if (isDraftFoundationOperationRunning(agentOperation)) {
return;
}
setAgentDraftGenerationStartedAt(null);
setCustomWorldGenerationViewSource(null);
setSelectionStage('agent-workspace');
};
const leaveAgentDraftResult = () => {
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setPlatformTab('create');
setSelectionStage('platform');
};
const retryAgentDraftGeneration = () => {
void executeAgentAction({
action: 'draft_foundation',
});
};
const openCustomWorldCreator = () => {
if (isGeneratingCustomWorld) {
return;
@@ -683,7 +853,11 @@ export function PreGameSelectionFlow({
setPlatformError(null);
setDetailError(null);
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setCustomWorldCreatorIntent(
createEmptyCustomWorldCreatorIntent('freeform'),
);
@@ -710,7 +884,11 @@ export function PreGameSelectionFlow({
}
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource(null);
setShowCustomWorldModal(true);
};
@@ -740,26 +918,98 @@ export function PreGameSelectionFlow({
}
};
const saveGeneratedCustomWorld = async () => {
const saveGeneratedCustomWorld = useCallback(
async (profile = generatedCustomWorldProfile) => {
if (!profile) {
return null;
}
const profileSignature = JSON.stringify(profile);
const requestId = latestAutoSaveRequestIdRef.current + 1;
latestAutoSaveRequestIdRef.current = requestId;
setCustomWorldAutoSaveState('saving');
setCustomWorldAutoSaveError(null);
try {
const mutation = await upsertCustomWorldProfile(profile);
if (latestAutoSaveRequestIdRef.current !== requestId) {
return mutation;
}
lastAutoSavedProfileSignatureRef.current = profileSignature;
setSavedCustomWorldEntries(mutation.entries);
setSelectedDetailEntry((current) => {
if (!current || current.profileId === mutation.entry.profileId) {
return mutation.entry;
}
return current;
});
setCustomWorldAutoSaveState('saved');
setCustomWorldAutoSaveError(null);
return mutation;
} catch (error) {
if (latestAutoSaveRequestIdRef.current !== requestId) {
return null;
}
setCustomWorldAutoSaveState('error');
setCustomWorldAutoSaveError(
resolveErrorMessage(error, '保存自定义世界失败。'),
);
return null;
}
},
[generatedCustomWorldProfile],
);
useEffect(() => {
if (!generatedCustomWorldProfile) {
setCustomWorldAutoSaveState('idle');
setCustomWorldAutoSaveError(null);
lastAutoSavedProfileSignatureRef.current = null;
if (customWorldAutoSaveTimeoutRef.current !== null) {
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
customWorldAutoSaveTimeoutRef.current = null;
}
return;
}
try {
const mutation = await upsertCustomWorldProfile(
generatedCustomWorldProfile,
);
setSavedCustomWorldEntries(mutation.entries);
setSelectedDetailEntry(mutation.entry);
await refreshPlatformData();
setGeneratedCustomWorldProfile(null);
setCustomWorldError(null);
setCustomWorldProgress(null);
setSelectionStage('platform');
} catch (error) {
setCustomWorldError(resolveErrorMessage(error, '保存自定义世界失败。'));
if (
selectionStage !== 'custom-world-result' ||
isGeneratingCustomWorld
) {
return;
}
};
const nextSignature = JSON.stringify(generatedCustomWorldProfile);
if (nextSignature === lastAutoSavedProfileSignatureRef.current) {
return;
}
setCustomWorldAutoSaveState('saving');
if (customWorldAutoSaveTimeoutRef.current !== null) {
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
}
const profileToSave = generatedCustomWorldProfile;
customWorldAutoSaveTimeoutRef.current = window.setTimeout(() => {
void saveGeneratedCustomWorld(profileToSave);
customWorldAutoSaveTimeoutRef.current = null;
}, 600);
return () => {
if (customWorldAutoSaveTimeoutRef.current !== null) {
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
customWorldAutoSaveTimeoutRef.current = null;
}
};
}, [
generatedCustomWorldProfile,
isGeneratingCustomWorld,
saveGeneratedCustomWorld,
selectionStage,
]);
const openSavedCustomWorldEditor = (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
@@ -770,6 +1020,9 @@ export function PreGameSelectionFlow({
setSelectedDetailEntry(entry);
setGeneratedCustomWorldProfile(entry.profile);
lastAutoSavedProfileSignatureRef.current = JSON.stringify(entry.profile);
setCustomWorldAutoSaveState('saved');
setCustomWorldAutoSaveError(null);
setCustomWorldCreatorIntent(
entry.profile.creatorIntent ??
({
@@ -780,6 +1033,8 @@ export function PreGameSelectionFlow({
setCustomWorldGenerationMode(entry.profile.generationMode ?? 'full');
setCustomWorldError(null);
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource('classic');
setSelectionStage('custom-world-result');
};
@@ -844,6 +1099,9 @@ export function PreGameSelectionFlow({
...mergedProfile,
id: generatedCustomWorldProfile.id,
});
lastAutoSavedProfileSignatureRef.current = null;
setCustomWorldAutoSaveState('idle');
setCustomWorldAutoSaveError(null);
setCustomWorldProgress(null);
setCustomWorldError(null);
} catch (error) {
@@ -899,7 +1157,11 @@ export function PreGameSelectionFlow({
customWorldAbortControllerRef.current?.abort();
customWorldAbortControllerRef.current = abortController;
setCustomWorldError(null);
setCustomWorldAutoSaveError(null);
setCustomWorldAutoSaveState('idle');
setCustomWorldProgress(null);
setCustomWorldGenerationViewSource('classic');
setCustomWorldResultViewSource(null);
setShowCustomWorldModal(false);
setSelectionStage('custom-world-generating');
setIsGeneratingCustomWorld(true);
@@ -929,8 +1191,13 @@ export function PreGameSelectionFlow({
}
: profile,
);
lastAutoSavedProfileSignatureRef.current = null;
setCustomWorldAutoSaveState('idle');
setCustomWorldAutoSaveError(null);
setCustomWorldProgress(null);
setCustomWorldError(null);
setCustomWorldGenerationViewSource(null);
setCustomWorldResultViewSource('classic');
setSelectionStage('custom-world-result');
} catch (error) {
if (abortController.signal.aborted) {
@@ -1019,6 +1286,17 @@ export function PreGameSelectionFlow({
entry.profileId === selectedDetailEntry.profileId,
),
);
const resultViewSaveActionLabel =
customWorldAutoSaveState === 'saving'
? '自动保存中'
: customWorldAutoSaveState === 'saved'
? '已保存到我的作品'
: customWorldAutoSaveState === 'error'
? '重新保存到我的作品'
: '保存到我的作品';
const resultViewError =
customWorldAutoSaveError ??
(isAgentDraftResultView ? null : customWorldError);
return (
<>
@@ -1156,16 +1434,61 @@ export function PreGameSelectionFlow({
fallback={<LazyPanelFallback label="正在加载世界生成面板..." />}
>
<CustomWorldGenerationView
settingText={customWorldSettingPreview}
progress={customWorldProgress}
isGenerating={isGeneratingCustomWorld}
error={customWorldError}
onBack={leaveCustomWorldGeneration}
onEditSetting={editCustomWorldSetting}
onRetry={() => {
void createCustomWorld();
}}
onInterrupt={interruptCustomWorldGeneration}
settingText={activeGenerationSettingText}
progress={activeGenerationProgress}
isGenerating={isActiveGenerationRunning}
error={activeGenerationError}
onBack={
isAgentDraftGenerationView
? leaveAgentDraftGeneration
: leaveCustomWorldGeneration
}
onEditSetting={
isAgentDraftGenerationView
? leaveAgentDraftGeneration
: editCustomWorldSetting
}
onRetry={
isAgentDraftGenerationView
? retryAgentDraftGeneration
: () => {
void createCustomWorld();
}
}
onInterrupt={
isAgentDraftGenerationView
? undefined
: interruptCustomWorldGeneration
}
backLabel={
isAgentDraftGenerationView ? '返回工作区' : undefined
}
settingActionLabel={
isAgentDraftGenerationView ? '回到工作区' : undefined
}
retryLabel={
isAgentDraftGenerationView ? '重新生成草稿' : undefined
}
settingTitle={
isAgentDraftGenerationView ? '当前共创设定' : undefined
}
settingDescription={
isAgentDraftGenerationView
? '这批锚点会被整理成第一版世界底稿与草稿卡。'
: undefined
}
progressTitle={
isAgentDraftGenerationView ? '世界草稿生成进度' : undefined
}
activeBadgeLabel={
isAgentDraftGenerationView ? '草稿编译中' : undefined
}
pausedBadgeLabel={
isAgentDraftGenerationView ? '草稿生成已暂停' : undefined
}
idleBadgeLabel={
isAgentDraftGenerationView ? '等待返回工作区' : undefined
}
/>
</Suspense>
</motion.div>
@@ -1186,22 +1509,52 @@ export function PreGameSelectionFlow({
<CustomWorldResultView
profile={generatedCustomWorldProfile}
previewCharacters={previewCustomWorldCharacters}
isGenerating={isGeneratingCustomWorld}
progress={customWorldProgress?.overallProgress ?? 0}
progressLabel={customWorldProgress?.phaseLabel ?? ''}
error={customWorldError}
isGenerating={
isAgentDraftResultView ? false : isGeneratingCustomWorld
}
progress={
isAgentDraftResultView
? 0
: (customWorldProgress?.overallProgress ?? 0)
}
progressLabel={
isAgentDraftResultView
? ''
: (customWorldProgress?.phaseLabel ?? '')
}
error={resultViewError}
onProfileChange={setGeneratedCustomWorldProfile}
onBack={leaveCustomWorldResult}
onEditSetting={editCustomWorldSetting}
onRegenerate={() => {
void createCustomWorld();
}}
onContinueExpand={() => {
void continueExpandCustomWorld();
}}
onBack={
isAgentDraftResultView
? leaveAgentDraftResult
: leaveCustomWorldResult
}
onEditSetting={
isAgentDraftResultView ? undefined : editCustomWorldSetting
}
onRegenerate={
isAgentDraftResultView
? retryAgentDraftGeneration
: () => {
void createCustomWorld();
}
}
onContinueExpand={
isAgentDraftResultView
? undefined
: () => {
void continueExpandCustomWorld();
}
}
onSave={() => {
void saveGeneratedCustomWorld();
}}
readOnly={false}
backLabel={isAgentDraftResultView ? '返回创作' : undefined}
regenerateActionLabel={
isAgentDraftResultView ? '重新生成草稿' : undefined
}
saveActionLabel={resultViewSaveActionLabel}
/>
</Suspense>
</motion.div>