Files
Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
T
kdletters 8857e36e1a 调整创作主页与项目入口
新增陶泥儿创作主页和用户素材瀑布流

新增项目入口并复用画布中心封面

保留创作主页左侧导航和上方栏

修复玩家社区在创作页原地弹窗

更新路由、测试和项目记忆文档
2026-06-19 10:51:20 +08:00

17880 lines
585 KiB
TypeScript

import { Loader2 } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import {
type Dispatch,
lazy,
type SetStateAction,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
import type {
BarkBattleConfigEditorPayload,
BarkBattleDraftConfig,
BarkBattlePublishedConfig,
BarkBattleWorkSummary,
} from '../../../packages/shared/src/contracts/barkBattle';
import type {
BigFishRuntimeSnapshotResponse,
BigFishSessionSnapshotResponse,
ExecuteBigFishActionRequest,
SendBigFishMessageRequest,
SubmitBigFishInputRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type {
CreativeAgentSessionSnapshot,
CreativeAgentSseEvent,
CreativeDraftEditResult,
StreamCreativeAgentMessageRequest,
} from '../../../packages/shared/src/contracts/creativeAgent';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
import type {
BabyObjectMatchDraft,
CreateBabyObjectMatchDraftRequest,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type {
ExternalGenerationJobStatusRecord,
ExternalGenerationQueueOverview,
} from '../../../packages/shared/src/contracts/externalGeneration';
import type {
JumpHopJumpRequest,
JumpHopWorkSummaryResponse,
} from '../../../packages/shared/src/contracts/jumpHop';
import type {
CreateMatch3DSessionRequest,
ExecuteMatch3DActionRequest,
Match3DActionResponse,
Match3DAgentSessionSnapshot,
Match3DSessionResponse,
SendMatch3DMessageRequest,
} from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
import type {
Match3DWorkProfile,
Match3DWorkSummary,
} from '../../../packages/shared/src/contracts/match3dWorks';
import type {
PuzzleAgentActionRequest,
PuzzleAgentOperationRecord,
} from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
PuzzleDraftLevel,
PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { PuzzleCreativeTemplateSelection } from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
import type {
PuzzleRunSnapshot,
PuzzleRuntimePropKind,
SubmitPuzzleLeaderboardRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
ProfileDashboardSummary,
ProfilePlayedWorkSummary,
ProfilePlayStatsResponse,
ProfileSaveArchiveResumeResponse,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import type {
CreateSquareHoleSessionRequest,
ExecuteSquareHoleActionRequest,
SendSquareHoleMessageRequest,
SquareHoleActionResponse,
SquareHoleSessionResponse,
SquareHoleSessionSnapshot,
} from '../../../packages/shared/src/contracts/squareHoleAgent';
import type { SquareHoleRunSnapshot } from '../../../packages/shared/src/contracts/squareHoleRuntime';
import type {
SquareHoleWorkProfile,
SquareHoleWorkSummary,
} from '../../../packages/shared/src/contracts/squareHoleWorks';
import type {
CreateVisualNovelSessionRequest,
ExecuteVisualNovelAgentActionRequest,
SendVisualNovelMessageRequest,
VisualNovelAgentSessionSnapshot,
VisualNovelResultDraft,
VisualNovelRunSnapshot,
VisualNovelRuntimeActionRequest,
VisualNovelSessionResponse,
VisualNovelWorkDetail,
VisualNovelWorkSummary,
} from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
buildPublicWorkStagePath,
pushAppHistoryPath,
resolvePathForSelectionStage,
} from '../../routing/appPageRoutes';
import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFoundRecovery';
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
getStoredAccessToken,
} from '../../services/apiClient';
import {
ensureRuntimeGuestToken,
getPublicAuthUserByCode,
getPublicAuthUserById,
} from '../../services/authService';
import {
createBarkBattleDraft,
deleteBarkBattleWork,
listBarkBattleGallery,
listBarkBattleWorks,
publishBarkBattleWork,
updateBarkBattleDraftConfig,
} from '../../services/bark-battle-creation';
import { startBarkBattleRun } from '../../services/bark-battle-runtime';
import {
createBigFishCreationSession,
executeBigFishCreationAction,
getBigFishCreationSession,
streamBigFishCreationMessage,
} from '../../services/big-fish-creation';
import {
likeBigFishGalleryWork,
listBigFishGallery,
remixBigFishGalleryWork,
} from '../../services/big-fish-gallery';
import {
recordBigFishPlay,
startBigFishRun as startBigFishRuntimeRun,
submitBigFishInput as submitBigFishRuntimeInput,
} from '../../services/big-fish-runtime';
import {
deleteBigFishWork,
listBigFishWorks,
} from '../../services/big-fish-works';
import {
type CreationEntryConfig,
DEFAULT_UNIFIED_CREATION_MUD_POINT_COST,
fetchCreationEntryConfig,
} from '../../services/creationEntryConfigService';
import {
clearCreationUrlState,
readCreationUrlState,
writeCreationUrlState,
} from '../../services/creationUrlState';
import {
cancelCreativeAgentSession,
confirmCreativePuzzleTemplate,
createCreativeAgentSession,
streamCreativeAgentMessage,
streamCreativeDraftEdit,
} from '../../services/creative-agent';
import { getExternalGenerationQueueOverview } from '../../services/external-generation';
import {
readCustomWorldAgentUiState,
shouldRestoreCustomWorldAgentUiState,
} from '../../services/customWorldAgentUiState';
import {
createBabyObjectMatchDraft,
deleteLocalBabyObjectMatchDraft,
hasBabyObjectMatchPlaceholderAssets,
listLocalBabyObjectMatchDrafts,
publishBabyObjectMatchWork,
regenerateBabyObjectMatchDraftAssets,
saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import {
jumpHopClient,
type JumpHopGalleryCardResponse,
type JumpHopRunResponse,
type JumpHopRuntimeRequestOptions,
type JumpHopSessionResponse,
type JumpHopSessionSnapshotResponse,
JumpHopWorkProfileResponse,
JumpHopWorkspaceCreateRequest,
} from '../../services/jump-hop/jumpHopClient';
import { match3dCreationClient } from '../../services/match3d-creation';
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
import {
deleteMatch3DWork,
getMatch3DWorkDetail,
listMatch3DGallery,
listMatch3DWorks,
} from '../../services/match3d-works';
import {
mergeMatch3DGeneratedItemAssetsForRuntime,
normalizeMatch3DGeneratedItemAssetsForRuntime,
preloadMatch3DGeneratedRuntimeAssets,
} from '../../services/match3dGeneratedModelCache';
import {
buildBabyObjectMatchGenerationAnchorEntries,
buildBigFishGenerationAnchorEntries,
buildJumpHopGenerationAnchorEntries,
buildMatch3DGenerationAnchorEntries,
buildMiniGameDraftGenerationProgress,
buildPuzzleClearGenerationAnchorEntries,
buildPuzzleGenerationAnchorEntries,
buildSquareHoleGenerationAnchorEntries,
buildWoodenFishGenerationAnchorEntries,
createMiniGameDraftGenerationState,
type MiniGameDraftGenerationKind,
type MiniGameDraftGenerationState,
resolveMiniGameDraftGenerationStartedAtMs,
} from '../../services/miniGameDraftGenerationProgress';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import {
buildBabyObjectMatchPublicWorkCode,
buildBarkBattlePublicWorkCode,
buildBigFishPublicWorkCode,
buildJumpHopPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzleClearPublicWorkCode,
buildPuzzlePublicWorkCode,
buildSquareHolePublicWorkCode,
buildVisualNovelPublicWorkCode,
buildWoodenFishPublicWorkCode,
isSamePuzzleClearPublicWorkCode,
isSamePuzzlePublicWorkCode,
} from '../../services/publicWorkCode';
import {
createPuzzleAgentSession,
executePuzzleAgentAction,
getPuzzleAgentSession,
streamPuzzleAgentMessage,
} from '../../services/puzzle-agent';
import {
puzzleClearClient,
type PuzzleClearGalleryCardResponse,
type PuzzleClearRunResponse,
type PuzzleClearSessionResponse,
type PuzzleClearSessionSnapshotResponse,
type PuzzleClearWorkProfileResponse,
type PuzzleClearWorkspaceCreateRequest,
type PuzzleClearWorkSummaryResponse,
} from '../../services/puzzle-clear/puzzleClearClient';
import {
advancePuzzleClearLocalLevel,
createPuzzleClearLocalRuntimeSnapshot,
isPuzzleClearLocalRuntimeSnapshot,
markPuzzleClearLocalTimeUp,
retryPuzzleClearLocalLevel,
swapPuzzleClearLocalCards,
} from '../../services/puzzle-clear/puzzleClearLocalRuntime';
import {
getPuzzleGalleryDetail,
likePuzzleGalleryWork,
listPuzzleGallery,
remixPuzzleGalleryWork,
} from '../../services/puzzle-gallery';
import {
generatePuzzleOnboardingWork,
savePuzzleOnboardingWork,
} from '../../services/puzzle-onboarding';
import {
advancePuzzleNextLevel,
startPuzzleRun,
submitPuzzleLeaderboard,
} from '../../services/puzzle-runtime';
import {
advanceLocalPuzzleLevel,
applyLocalPuzzleFreezeTime,
dragLocalPuzzlePiece,
extendLocalPuzzleTime,
isLocalPuzzleRun,
refreshLocalPuzzleTimer,
resolvePuzzleRestartLevelId,
restartLocalPuzzleLevel,
setLocalPuzzlePaused,
startLocalPuzzleRun,
submitLocalPuzzleLeaderboard,
swapLocalPuzzlePieces,
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
import {
claimPuzzleWorkPointIncentive,
deletePuzzleWork,
listPuzzleWorks,
updatePuzzleWork,
} from '../../services/puzzle-works';
import {
clearPuzzleRuntimeUrlState,
type PuzzleRuntimeUrlState,
readPuzzleRuntimeUrlState,
writePuzzleRuntimeUrlState,
} from '../../services/puzzleRuntimeUrlState';
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetailByCode,
likeRpgEntryWorldGallery,
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import {
getRpgProfilePlayStats,
submitRpgProfileFeedback,
} from '../../services/rpg-entry/rpgProfileClient';
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
import { type RuntimeGuestRequestOptions } from '../../services/runtimeGuestAuth';
import { squareHoleCreationClient } from '../../services/square-hole-creation';
import {
dropSquareHoleShape,
finishSquareHoleTimeUp,
restartSquareHoleRun,
startSquareHoleRun,
stopSquareHoleRun,
} from '../../services/square-hole-runtime';
import {
deleteSquareHoleWork,
getSquareHoleWorkDetail,
listSquareHoleGallery,
listSquareHoleWorks,
} from '../../services/square-hole-works';
import {
compileVisualNovelWorkProfile,
createVisualNovelSession,
executeVisualNovelAction,
getVisualNovelSession,
streamVisualNovelMessage,
} from '../../services/visual-novel-creation';
import {
listVisualNovelGallery,
startVisualNovelRun,
streamVisualNovelRuntimeAction,
} from '../../services/visual-novel-runtime';
import {
deleteVisualNovelWork,
getVisualNovelWorkDetail,
listVisualNovelWorks,
publishVisualNovelWork,
updateVisualNovelWork,
} from '../../services/visual-novel-works';
import { requestGenerationResultSubscribePermission } from '../../services/wechatMiniProgramSubscribe';
import { postWechatMiniProgramShareTarget } from '../../services/wechatMiniProgramShareTarget';
import {
woodenFishClient,
type WoodenFishGalleryCardResponse,
type WoodenFishRunResponse,
type WoodenFishSessionResponse,
type WoodenFishSessionSnapshotResponse,
type WoodenFishWorkProfileResponse,
type WoodenFishWorkspaceCreateRequest,
} from '../../services/wooden-fish/woodenFishClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformAcknowledgeStatusDialog } from '../common/PlatformAcknowledgeStatusDialog';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformDangerConfirmDialog } from '../common/PlatformDangerConfirmDialog';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PublishShareModal } from '../common/PublishShareModal';
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
import { UnifiedModal } from '../common/UnifiedModal';
import { resolveCreativeAgentTargetSelectionStage } from '../creative-agent/creativeAgentViewModel';
import {
buildCreationWorkShelfItems,
type CreationWorkShelfItem,
isPersistedBarkBattleDraftGenerating,
} from '../custom-world-home/creationWorkShelf';
import {
buildPlatformRecommendFeedEntries,
selectAdjacentPlatformRecommendEntry,
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
import {
describePublicGalleryCardKind,
isBigFishGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isPuzzleClearGalleryEntry,
isPuzzleGalleryEntry,
mapPuzzleClearWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
resolvePlatformPublicWorkCode,
resolvePlatformWorldCoverImage,
resolvePlatformWorldFallbackCoverImage,
} from '../rpg-entry/rpgEntryWorldPresentation';
import { useRpgCreationAgentOperationPolling } from '../rpg-entry/useRpgCreationAgentOperationPolling';
import { useRpgCreationEnterWorld } from '../rpg-entry/useRpgCreationEnterWorld';
import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultAutosave';
import { useRpgCreationSessionController } from '../rpg-entry/useRpgCreationSessionController';
import { UnifiedCreationPage } from '../unified-creation/UnifiedCreationPage';
import {
getUnifiedCreationSpec,
type UnifiedCreationPlayId,
} from '../unified-creation/unifiedCreationSpecs';
import {
buildVisualNovelEntryGenerationAnchorEntries,
buildVisualNovelEntryGenerationProgress,
type VisualNovelEntryFormPayload,
} from '../visual-novel-creation/visualNovelEntryGeneration';
import { createMockVisualNovelRunFromDraft } from '../visual-novel-runtime/visualNovelMockData';
import {
buildBarkBattleDraftConfigFromWorkSummary,
buildBarkBattlePublishedConfigFromDraft,
buildBarkBattlePublishedConfigFromWork,
buildBarkBattlePublishSnapshot,
buildBarkBattleWorkSummaryFromDraft,
mergeBarkBattlePublishedConfigAssets,
mergeBarkBattleWorksByWorkId,
mergeBarkBattleWorkSummary,
resolveBarkBattleDraftGenerationStatus,
shouldPreserveLocalBarkBattleWorkOnRefresh,
} from './barkBattleWorkCache';
import {
type PlatformCreationLaunchTarget,
resolvePlatformCreationLaunchIntent,
} from './platformCreationLaunchModel';
import {
buildBabyObjectMatchCreationUrlState,
buildBarkBattleCreationUrlState,
buildBigFishCreationUrlState,
buildJumpHopCreationUrlState,
buildMatch3DCreationUrlState,
buildPuzzleClearCreationUrlState,
buildPuzzleCreationUrlState,
buildPuzzleDraftRuntimeUrlState,
buildPuzzlePublishedRuntimeUrlState,
buildPuzzleRuntimeUrlStateKey,
buildSquareHoleCreationUrlState,
buildVisualNovelCreationUrlState,
buildWoodenFishCreationUrlState,
hasPuzzleRuntimeUrlStateValue,
matchesBabyObjectMatchCreationUrlRestoreTarget,
matchesBarkBattleCreationUrlRestoreTarget,
matchesBigFishCreationUrlRestoreTarget,
matchesSessionProfileWorkCreationUrlRestoreTarget,
matchesVisualNovelCreationUrlRestoreTarget,
normalizeCreationUrlValue,
resolveCreationUrlRestoreTarget,
resolveInitialCreationUrlRestoreDecision,
resolveJumpHopCreationUrlRestoreStage,
resolveWoodenFishCreationUrlRestoreStage,
} from './platformCreationUrlStateModel';
import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow';
import { buildExternalGenerationQueueStatus } from './platformExternalGenerationQueueStatusModel';
import {
buildPlatformErrorDialogDismissKey,
buildPlatformTaskCompletionDialogDismissKey,
formatPlatformDialogSource,
isBackgroundGenerationStillRunningMessage,
normalizePlatformDialogMessage,
PLATFORM_TASK_COMPLETION_MESSAGE,
type PlatformDialogCandidate,
type PlatformErrorDialogState,
type PlatformTaskCompletionDialogState,
type PlatformTaskFailureDialogState,
resolveActivePlatformDialog,
resolvePlatformErrorDialog,
} from './platformDialogStateModel';
import {
buildCreationWorkShelfRuntimeState,
buildDraftCompletionDialogSource,
buildPendingBarkBattleWorks,
buildPendingBigFishWorks,
buildPendingJumpHopWorks,
buildPendingMatch3DWorks,
buildPendingPuzzleClearWorks,
buildPendingPuzzleWorks,
buildPendingSquareHoleWorks,
buildPendingVisualNovelWorks,
buildPendingWoodenFishWorks,
collectDraftNoticeKeys,
collectVisibleDraftNoticeKeys,
createPendingDraftShelfState,
type DraftGenerationNoticeMap,
type DraftGenerationNoticeStatus,
getGenerationNoticeShelfKeys,
hasUnreadDraftGenerationUpdates,
mergeBigFishWorkSummary,
mergePuzzleWorkSummary,
normalizeDraftNoticeId,
type PendingDraftShelfKind,
type PendingDraftShelfMap,
type PendingDraftShelfMetadata,
resolveBigFishDraftOpenIntent,
resolveJumpHopDraftOpenIntent,
resolveMatch3DDraftOpenIntent,
resolvePuzzleDraftOpenIntent,
resolveSquareHoleDraftOpenIntent,
resolveVisualNovelDraftOpenIntent,
resolveWoodenFishDraftOpenIntent,
} from './platformDraftGenerationShelfModel';
import {
canExposePublicWork,
EDUTAINMENT_HIDDEN_MESSAGE,
} from './platformEdutainmentVisibility';
import {
PlatformDraftGenerationPointNoticeDialog,
type DraftGenerationPointNotice,
} from './PlatformDraftGenerationPointNoticeDialog';
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
import {
derivePlatformCreationTypes,
isPlatformCreationTypeOpen,
isPlatformCreationTypeVisible,
} from './platformEntryCreationTypes';
import {
PuzzleOnboardingLoginOverlay,
type PuzzleOnboardingPhase,
PuzzleOnboardingView,
} from './PlatformEntryFlowShellImpl/PuzzleOnboardingView';
import { PuzzleRuntimeBlockingOverlay } from './PlatformEntryFlowShellImpl/PuzzleRuntimeBlockingOverlay';
import {
PlatformEntryHomeView,
type PlatformHomeTab,
} from './PlatformEntryHomeView';
import { PlatformProfileReferralModal } from './PlatformProfileReferralModal';
import { usePlatformDesktopLayout } from './platformEntryResponsive';
import {
buildCreationHubFallbackItems,
resolveRpgCreationErrorMessage,
} from './platformEntryShared';
import type {
PlatformEntryFlowShellProps,
SelectionStage,
} from './platformEntryTypes';
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
import { PlatformErrorDialog } from './PlatformErrorDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import { resolvePlatformGenerationProgressTickDecision } from './platformGenerationProgressTickModel';
import {
buildMatch3DProfileFromSession,
hasMatch3DRuntimeAsset,
hasMatch3DRuntimeBackgroundAsset,
mapMatch3DWorksForRuntimeUi,
mapMatch3DWorkToPublicWorkDetail,
mapPublicWorkDetailToMatch3DWork,
normalizeMatch3DWorkForRuntimeUi,
promoteMatch3DGeneratedBackgroundAsset,
resolveActiveMatch3DRuntimeProfile,
resolveMatch3DRuntimeBackgroundImageSrc,
resolveMatch3DRuntimeGeneratedBackgroundAsset,
resolveMatch3DRuntimeGeneratedItemAssets,
} from './platformMatch3DRuntimeProfile';
import {
createFailedMiniGameDraftGenerationStateForRestoredDraft,
createMiniGameDraftGenerationStateForRestoredDraft,
createPuzzleDraftGenerationStateFromPayload,
isMiniGameDraftGenerating,
isMiniGameDraftReady,
mergeMatch3DGeneratedAssetsIntoGenerationState,
mergePuzzleSessionProgressIntoGenerationState,
rebaseMiniGameDraftBackgroundCompileTaskForDisplay,
rebaseMiniGameDraftGenerationStateForDisplay,
resolveFinishedMiniGameDraftGenerationState,
} from './platformMiniGameDraftGenerationStateModel';
import {
buildJumpHopDraftActionPayload,
buildMatch3DFormPayloadFromSession,
buildMatch3DFormPayloadFromWork,
buildPendingMatch3DDraftMetadata,
buildPendingPuzzleDraftMetadata,
buildPuzzleCompileActionFromFormPayload,
buildPuzzleFormPayloadFromAction,
buildPuzzleFormPayloadFromSession,
buildPuzzleFormPayloadFromWork,
buildPuzzleWorkUpdatePayloadFromDraft,
buildWoodenFishDraftActionPayload,
isEmptyPuzzleFormOnlyDraft,
isPuzzleFormOnlyDraft,
} from './platformMiniGameDraftPayloadModel';
import {
buildJumpHopPendingSession,
buildPuzzleClearPendingSession,
buildPuzzleClearSessionFromWorkDetail,
buildPuzzleRuntimeWorkFromSession,
buildSquareHoleProfileFromSession,
buildVisualNovelSessionFromWorkDetail,
buildWoodenFishGeneratingWorkSummary,
buildWoodenFishPendingSession,
buildWoodenFishSessionFromWorkDetail,
} from './platformMiniGameSessionMappingModel';
import { resolvePlatformPlayedWorkOpenIntent } from './platformPlayedWorkOpenModel';
import {
adjustProfileDashboardWalletBalance,
reconcileProfileWalletLocalDeltaWithServerDashboard,
resolveProfileWalletBalance,
} from './platformProfileWalletDeltaModel';
import {
mapRpgPublicCodeSearchDetailToGalleryCard,
type PlatformPublicCodeSearchStep,
resolveBabyObjectMatchPublicCodeSearchMatch,
resolveBarkBattlePublicCodeSearchMatch,
resolveBigFishPublicCodeSearchMatch,
resolveJumpHopPublicCodeSearchMatch,
resolveMatch3DPublicCodeSearchMatch,
resolvePlatformPublicCodeSearchPlan,
resolvePuzzlePublicCodeSearchMatch,
resolveSquareHolePublicCodeSearchMatch,
resolveVisualNovelPublicCodeSearchMatch,
resolveWoodenFishPublicCodeSearchMatch,
} from './platformPublicCodeSearchModel';
import {
buildPlatformPublicGalleryFeeds,
getPlatformPublicGalleryEntryKey,
getPlatformRecommendRuntimeKind,
isPlatformRecommendRuntimeReadyForEntry,
isSamePlatformPublicGalleryEntry,
type RecommendRuntimeKind,
resolvePlatformRecommendRuntimeAutoStartDecision,
resolvePlatformRecommendRuntimeStartIntent,
} from './platformPublicGalleryFlow';
import {
mapBarkBattleWorkToPublicWorkDetail,
mapBigFishWorkToPublicWorkDetail,
mapJumpHopWorkToPublicWorkDetail,
mapPuzzleWorkToPublicWorkDetail,
mapRpgGalleryCardToPublicWorkDetail,
mapSquareHoleWorkToPublicWorkDetail,
mapVisualNovelWorkToPublicWorkDetail,
mapWoodenFishWorkToPublicWorkDetail,
resolveActivePlatformPublicWorkAuthorEntry,
resolvePlatformPublicWorkActionMode,
resolvePlatformPublicWorkDetailOpenDecision,
resolvePlatformPublicWorkDetailOpenStrategy,
resolvePlatformPublicWorkEditIntent,
resolvePlatformPublicWorkLikeIntent,
resolvePlatformPublicWorkRemixIntent,
resolvePlatformPublicWorkStartIntent,
resolveVisiblePuzzleDetailCoverCount,
} from './platformPublicWorkDetailFlow';
import {
hasRecoverableGeneratedPuzzleDraft,
normalizeRecoveredPuzzleDraftSession,
} from './platformPuzzleDraftRecoveryModel';
import {
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
} from './platformPuzzleIdentityModel';
import { mergePuzzleServiceRuntimeState } from './platformPuzzleRuntimeStateModel';
import { buildPlatformRecommendedEntries } from './platformRecommendation';
import {
type PlatformPuzzleRuntimeAuthMode,
resolvePlatformRecommendRuntimeAuthPlan,
} from './platformRecommendRuntimeAuthModel';
import {
buildPlatformRpgAgentResultPublishGateView,
resolvePlatformRpgAgentResultPreviewSourceLabel,
} from './platformRpgAgentResultPreviewModel';
import {
resolveSelectionStageAfterMissingCreationState,
resolveSelectionStageAfterProtectedDataLoss,
} from './platformSelectionStageModel';
import { PlatformTaskCompletionDialog } from './PlatformTaskCompletionDialog';
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
import { isPuzzleCompileActionReady } from './puzzleDraftGenerationState';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
type CreationWorkShelfKind = CreationWorkShelfItem['kind'];
type CreationFlowReturnTarget = 'create' | 'draft-shelf';
type Match3DBackgroundCompileTask = {
session: Match3DAgentSessionSnapshot;
payload: CreateMatch3DSessionRequest;
generationState: MiniGameDraftGenerationState;
error: string | null;
};
type PuzzleBackgroundCompileTask = {
session: PuzzleAgentSessionSnapshot;
payload: CreatePuzzleAgentSessionRequest;
generationState: MiniGameDraftGenerationState;
error: string | null;
};
type MiniGameGenerationProgressTickStateMap = Partial<
Record<MiniGameDraftGenerationKind, MiniGameDraftGenerationState | null>
>;
export function resolveMiniGameGenerationProgressTickState(
selectionStage: SelectionStage,
states: MiniGameGenerationProgressTickStateMap,
) {
const stageKindMap: Partial<
Record<SelectionStage, MiniGameDraftGenerationKind>
> = {
'puzzle-generating': 'puzzle',
'big-fish-generating': 'big-fish',
'square-hole-generating': 'square-hole',
'match3d-generating': 'match3d',
'baby-object-match-generating': 'baby-object-match',
'jump-hop-generating': 'jump-hop',
'puzzle-clear-generating': 'puzzle-clear',
'wooden-fish-generating': 'wooden-fish',
};
const kind = stageKindMap[selectionStage];
return kind ? (states[kind] ?? null) : null;
}
export function resolveMiniGameGenerationViewBusy(
isBusy: boolean,
state: MiniGameDraftGenerationState | null | undefined,
) {
return isBusy || isMiniGameDraftGenerating(state ?? null);
}
function isExternalGenerationQueueStage(selectionStage: SelectionStage) {
return (
selectionStage === 'puzzle-generating' ||
selectionStage === 'big-fish-generating' ||
selectionStage === 'square-hole-generating' ||
selectionStage === 'match3d-generating' ||
selectionStage === 'baby-object-match-generating' ||
selectionStage === 'jump-hop-generating' ||
selectionStage === 'puzzle-clear-generating' ||
selectionStage === 'wooden-fish-generating' ||
selectionStage === 'visual-novel-generating'
);
}
type PuzzleDetailReturnTarget = {
tab: PlatformHomeTab;
};
type PuzzleRuntimeReturnStage =
| 'puzzle-result'
| 'puzzle-gallery-detail'
| 'work-detail'
| 'platform';
type PuzzleRuntimeAuthMode = PlatformPuzzleRuntimeAuthMode;
type PuzzleOnboardingDraft = {
promptText: string;
item: PuzzleWorkSummary;
};
type BigFishRuntimeReturnStage = 'big-fish-result' | 'work-detail' | 'platform';
type BarkBattleRuntimeReturnStage =
| 'bark-battle-result'
| 'work-detail'
| 'platform';
type BigFishRuntimeSessionSource = 'draft' | 'work' | null;
type SquareHoleRuntimeReturnStage =
| 'square-hole-result'
| 'work-detail'
| 'platform';
type VisualNovelRuntimeReturnStage =
| 'visual-novel-result'
| 'visual-novel-gallery-detail'
| 'work-detail'
| 'platform';
type BabyObjectMatchRuntimeReturnStage =
| 'baby-object-match-result'
| 'work-detail'
| 'platform';
type JumpHopRuntimeReturnStage = 'jump-hop-result' | 'work-detail' | 'platform';
type PuzzleClearRuntimeReturnStage =
| 'puzzle-clear-result'
| 'work-detail'
| 'platform';
type WoodenFishRuntimeReturnStage =
| 'wooden-fish-result'
| 'work-detail'
| 'platform';
type VisualNovelEntryGenerationPhase = 'generating' | 'ready' | 'failed';
type BabyObjectMatchGenerationPhase = 'generating' | 'ready' | 'failed';
type PuzzleSaveArchiveState = {
runtimeKind?: unknown;
entryProfileId?: unknown;
currentProfileId?: unknown;
currentLevelId?: unknown;
};
type DeleteCreationWorkConfirmation = {
id: string;
title: string;
detail: string;
run: () => void;
};
type WorkNotFoundRecoveryDialogState = {
message: string;
nextPath: '/';
};
async function resumePuzzleProfileSaveArchiveRaw(worldKey: string) {
return requestRpgRuntimeJson<
ProfileSaveArchiveResumeResponse<PuzzleSaveArchiveState>
>(
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
{ method: 'POST' },
'恢复拼图存档失败',
);
}
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions =
BACKGROUND_AUTH_REQUEST_OPTIONS;
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS: JumpHopRuntimeRequestOptions =
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
type RecommendRuntimeAuthUi =
| { user?: { id?: string } | null }
| null
| undefined;
async function buildRecommendRuntimeGuestOptions() {
const { token } = await ensureRuntimeGuestToken();
return {
...RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
runtimeGuestToken: token,
};
}
function resolveCurrentRecommendRuntimeAuthPlan(
authUi: RecommendRuntimeAuthUi,
input: { embedded?: boolean; allowRuntimeGuestAuth?: boolean } = {},
) {
return resolvePlatformRecommendRuntimeAuthPlan({
embedded: input.embedded,
allowRuntimeGuestAuth: input.allowRuntimeGuestAuth,
authUserId: authUi?.user?.id ?? null,
hasStoredAccessToken: Boolean(getStoredAccessToken()),
});
}
function shouldUseRecommendRuntimeGuestAuth(authUi: RecommendRuntimeAuthUi) {
return (
resolveCurrentRecommendRuntimeAuthPlan(authUi, {
embedded: true,
allowRuntimeGuestAuth: true,
}).requestKind === 'runtime-guest'
);
}
async function buildRecommendRuntimeOptionsFromAuthPlan(
plan: ReturnType<typeof resolvePlatformRecommendRuntimeAuthPlan>,
) {
if (plan.requestKind === 'runtime-guest') {
return buildRecommendRuntimeGuestOptions();
}
if (plan.requestKind === 'background') {
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
}
return {};
}
async function buildRecommendRuntimeAuthOptions(
authUi: RecommendRuntimeAuthUi,
embedded?: boolean,
) {
return buildRecommendRuntimeOptionsFromAuthPlan(
resolveCurrentRecommendRuntimeAuthPlan(authUi, { embedded }),
);
}
function resolveRecommendEntryShareStage(
entry: PlatformPublicGalleryCard,
): PublishShareModalPayload['stage'] {
if (isBigFishGalleryEntry(entry)) {
return 'big-fish-runtime';
}
if (isPuzzleGalleryEntry(entry)) {
return 'puzzle-gallery-detail';
}
return 'work-detail';
}
function postRecommendEntryMiniProgramShareTarget(
entry: PlatformPublicGalleryCard | null | undefined,
) {
if (!entry) {
return false;
}
const publicWorkCode = resolvePlatformPublicWorkCode(entry)?.trim();
if (!publicWorkCode) {
return false;
}
return postWechatMiniProgramShareTarget({
targetPath: '/works/detail',
work: publicWorkCode,
title: entry.worldName,
});
}
function pushPuzzleResultHistoryEntry(
session: PuzzleAgentSessionSnapshot | null,
) {
pushAppHistoryPath('/creation/puzzle/result');
writeCreationUrlState(buildPuzzleCreationUrlState(session));
}
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
const PUZZLE_BACKGROUND_ACTION_POLL_INTERVAL_MS = 3000;
const PUZZLE_BACKGROUND_ACTION_MAX_POLL_ATTEMPTS = 160;
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
function isPuzzleBackgroundAction(payload: PuzzleAgentActionRequest) {
return (
payload.action === 'generate_puzzle_images' ||
payload.action === 'generate_puzzle_ui_background'
);
}
function findPuzzleActionLevel(
session: PuzzleAgentSessionSnapshot | null | undefined,
payload: PuzzleAgentActionRequest,
) {
const levels = session?.draft?.levels ?? [];
const targetLevelId =
'levelId' in payload ? payload.levelId?.trim() : undefined;
if (targetLevelId) {
return levels.find((level) => level.levelId === targetLevelId) ?? null;
}
return levels[0] ?? null;
}
function buildPuzzleGeneratedImageSignature(
level: PuzzleDraftLevel | null | undefined,
) {
if (!level) {
return '';
}
return [
level.levelName,
level.selectedCandidateId ?? '',
level.coverImageSrc ?? '',
level.coverAssetId ?? '',
level.levelSceneImageSrc ?? '',
level.levelSceneImageObjectKey ?? '',
level.uiSpritesheetImageSrc ?? '',
level.uiSpritesheetImageObjectKey ?? '',
level.levelBackgroundImageSrc ?? '',
level.levelBackgroundImageObjectKey ?? '',
(level.candidates ?? [])
.map((candidate) =>
[
candidate.candidateId,
candidate.imageSrc,
candidate.assetId,
String(candidate.selected),
].join(':'),
)
.join('|'),
].join('::');
}
function buildPuzzleUiBackgroundSignature(
level: PuzzleDraftLevel | null | undefined,
) {
if (!level) {
return '';
}
return [
level.uiBackgroundPrompt ?? '',
level.uiBackgroundImageSrc ?? '',
level.uiBackgroundImageObjectKey ?? '',
].join('::');
}
function buildPuzzleBackgroundActionSignature(
payload: PuzzleAgentActionRequest,
level: PuzzleDraftLevel | null | undefined,
) {
if (payload.action === 'generate_puzzle_ui_background') {
return buildPuzzleUiBackgroundSignature(level);
}
return buildPuzzleGeneratedImageSignature(level);
}
function hasPuzzleBackgroundActionAsset(
payload: PuzzleAgentActionRequest,
level: PuzzleDraftLevel | null | undefined,
) {
if (!level) {
return false;
}
if (payload.action === 'generate_puzzle_ui_background') {
return Boolean(level.uiBackgroundImageSrc?.trim());
}
return Boolean(
level.coverImageSrc?.trim() ||
level.candidates?.some((candidate) => candidate.imageSrc.trim()),
);
}
function isPuzzleBackgroundActionSettled(
payload: PuzzleAgentActionRequest,
baselineSession: PuzzleAgentSessionSnapshot,
latestSession: PuzzleAgentSessionSnapshot,
) {
const latestLevel = findPuzzleActionLevel(latestSession, payload);
if (!latestLevel) {
return false;
}
if (latestLevel.generationStatus === 'failed') {
return true;
}
if (latestLevel.generationStatus === 'generating') {
return false;
}
const baselineSignature = buildPuzzleBackgroundActionSignature(
payload,
findPuzzleActionLevel(baselineSession, payload),
);
const latestSignature = buildPuzzleBackgroundActionSignature(
payload,
latestLevel,
);
return (
hasPuzzleBackgroundActionAsset(payload, latestLevel) &&
latestSignature !== baselineSignature
);
}
function waitForPuzzleBackgroundActionPollTick() {
return new Promise<void>((resolve) => {
window.setTimeout(resolve, PUZZLE_BACKGROUND_ACTION_POLL_INTERVAL_MS);
});
}
/** 中文注释:首页公开数据降级时,入口关闭错误不弹窗;真实创作动作仍由对应工作台提示。 */
function isCreationEntryDisabledErrorMessage(
message: string | null | undefined,
) {
const normalized = normalizePlatformDialogMessage(message);
return Boolean(
normalized &&
(normalized.includes('creation_entry_disabled') ||
normalized.includes('该玩法入口暂不可用') ||
normalized.includes('创作入口已关闭')),
);
}
const PUZZLE_ONBOARDING_FIRST_VISIT_STORAGE_KEY =
'genarrative.puzzle-onboarding.first-visit.v1';
const PUZZLE_ONBOARDING_COPY = '待定待定待定';
const PUZZLE_ONBOARDING_CLEAR_COPY = '只差一步,就可以永久保留你的梦';
const PUZZLE_ONBOARDING_GENERATED_DELAY_MS = 700;
function isPuzzleOnboardingEnabled(): boolean {
// 中文注释:当前产品要求隐藏首访新手引导,旧流程暂时保留用于后续显式恢复。
return false;
}
function escapePuzzleOnboardingSvgText(value: string) {
return value
.replace(/&/gu, '&amp;')
.replace(/</gu, '&lt;')
.replace(/>/gu, '&gt;')
.replace(/"/gu, '&quot;');
}
function buildPuzzleOnboardingFallbackImage(promptText: string) {
const trimmedPrompt = promptText.trim();
const displayPrompt = escapePuzzleOnboardingSvgText(
trimmedPrompt.slice(0, 12) || '陶泥儿拼图',
);
return (
'data:image/svg+xml;utf8,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fde68a" />
<stop offset="0.48" stop-color="#38bdf8" />
<stop offset="1" stop-color="#312e81" />
</linearGradient>
<radialGradient id="glow" cx="34%" cy="30%" r="42%">
<stop offset="0" stop-color="#fff7ed" stop-opacity="0.86" />
<stop offset="1" stop-color="#fff7ed" stop-opacity="0" />
</radialGradient>
</defs>
<rect width="1024" height="1024" fill="url(#sky)" />
<circle cx="352" cy="292" r="248" fill="url(#glow)" />
<path d="M0 654 C156 570 296 680 452 610 C626 532 782 598 1024 500 V1024 H0 Z" fill="#0f172a" opacity="0.88" />
<path d="M0 802 C196 710 332 838 540 750 C704 682 846 724 1024 670 V1024 H0 Z" fill="#111827" opacity="0.72" />
<path d="M160 344 C242 268 340 282 430 350 C532 428 646 388 752 322 C844 266 918 302 958 362" fill="none" stroke="#fff7ed" stroke-width="18" stroke-linecap="round" opacity="0.72" />
<path d="M188 538 H836" stroke="#ffffff" stroke-width="16" stroke-linecap="round" opacity="0.26" />
<path d="M260 620 H764" stroke="#ffffff" stroke-width="12" stroke-linecap="round" opacity="0.2" />
<text x="512" y="868" text-anchor="middle" font-family="sans-serif" font-size="54" font-weight="800" fill="#ffffff" opacity="0.84">${displayPrompt}</text>
</svg>`)
);
}
function buildPuzzleOnboardingFallbackWork(
promptText: string,
): PuzzleWorkSummary {
const now = new Date().toISOString();
const seed = Date.now();
const coverImageSrc = buildPuzzleOnboardingFallbackImage(promptText);
const level: PuzzleDraftLevel = {
levelId: 'onboarding-local-level-1',
levelName: '梦境拼图',
pictureDescription: promptText,
pictureReference: null,
candidates: [
{
candidateId: 'onboarding-local-candidate-1',
imageSrc: coverImageSrc,
assetId: 'onboarding-local-asset-1',
prompt: promptText,
actualPrompt: promptText,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'onboarding-local-candidate-1',
coverImageSrc,
coverAssetId: 'onboarding-local-asset-1',
generationStatus: 'ready',
};
return {
workId: `onboarding-local-work-${seed}`,
profileId: `onboarding-local-profile-${seed}`,
ownerUserId: 'onboarding-guest',
sourceSessionId: null,
authorDisplayName: '陶泥儿主',
workTitle: '梦境拼图',
workDescription: promptText,
levelName: level.levelName,
summary: promptText,
themeTags: ['新手引导', '拼图'],
coverImageSrc,
coverAssetId: level.coverAssetId,
publicationStatus: 'draft',
updatedAt: now,
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: true,
levels: [level],
};
}
function shouldUseLocalPuzzleOnboardingFallback(error: unknown) {
return (
error instanceof ApiClientError &&
error.status === 404 &&
(error.code === 'NOT_FOUND' || error.message.includes('资源不存在'))
);
}
function isMissingPuzzleWorkError(error: unknown) {
return (
(error instanceof ApiClientError &&
error.status === 404 &&
(error.code === 'NOT_FOUND' ||
error.message.includes('资源不存在') ||
error.message.includes('未找到'))) ||
(error instanceof Error &&
(error.message.includes('资源不存在') ||
error.message.includes('未找到拼图作品')))
);
}
function resolveWorkNotFoundRecoveryDialogState(): WorkNotFoundRecoveryDialogState | null {
if (typeof window === 'undefined') {
return null;
}
const recoveryAction = resolveWorkNotFoundRecoveryAction(
window.location.pathname,
);
if (!recoveryAction) {
return null;
}
return {
message: '作品不存在或已下架,将返回首页。',
nextPath: recoveryAction.nextPath,
};
}
function hasSeenPuzzleOnboarding() {
if (!isPuzzleOnboardingEnabled()) {
return true;
}
if (typeof window === 'undefined') {
return true;
}
try {
return (
window.localStorage.getItem(PUZZLE_ONBOARDING_FIRST_VISIT_STORAGE_KEY) ===
'1'
);
} catch {
return false;
}
}
function markPuzzleOnboardingSeen() {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(PUZZLE_ONBOARDING_FIRST_VISIT_STORAGE_KEY, '1');
} catch {
// 中文注释:localStorage 不可写时只降级为本次会话展示,不影响主流程。
}
}
async function resolvePublicWorkAuthorSummary(
entry: PlatformPublicGalleryCard,
): Promise<PublicUserSummary | null> {
if ('authorPublicUserCode' in entry && entry.authorPublicUserCode?.trim()) {
try {
return await getPublicAuthUserByCode(entry.authorPublicUserCode);
} catch {
if (!entry.ownerUserId.trim()) {
return null;
}
}
}
if (entry.ownerUserId.trim()) {
return getPublicAuthUserById(entry.ownerUserId);
}
return null;
}
function openPuzzleRuntimeStage(
setSelectionStage: (stage: SelectionStage) => void,
state: PuzzleRuntimeUrlState,
) {
pushAppHistoryPath('/runtime/puzzle');
setSelectionStage('puzzle-runtime');
writePuzzleRuntimeUrlState(state);
}
const CustomWorldGenerationView = lazy(async () => {
const module = await import('../CustomWorldGenerationView');
return {
default: module.CustomWorldGenerationView,
};
});
const ImageCanvasEditorView = lazy(async () => {
const module = await import('../image-editor/ImageCanvasEditorView');
return {
default: module.ImageCanvasEditorView,
};
});
const ProjectGalleryView = lazy(async () => {
const module = await import('../project/ProjectGalleryView');
return {
default: module.ProjectGalleryView,
};
});
const CreationLandingView = lazy(async () => {
const module = await import('../creation-home/CreationLandingView');
return {
default: module.CreationLandingView,
};
});
const UnifiedCreationWorkspace = lazy(async () => {
const module = await import('../unified-creation/UnifiedCreationWorkspace');
return {
default: module.UnifiedCreationWorkspace,
};
});
const UnifiedGenerationPage = lazy(async () => {
const module = await import('../unified-creation/UnifiedGenerationPage');
return {
default: module.UnifiedGenerationPage,
};
});
const RpgCreationResultView = lazy(async () => {
const module = await import('../rpg-creation-result/RpgCreationResultView');
return {
default: module.RpgCreationResultView,
};
});
const CustomWorldAgentWorkspace = lazy(async () => {
const module = await import(
'../custom-world-agent/CustomWorldAgentWorkspace'
);
return {
default: module.CustomWorldAgentWorkspace,
};
});
const BigFishAgentWorkspace = lazy(async () => {
const module = await import('../big-fish-creation/BigFishAgentWorkspace');
return {
default: module.BigFishAgentWorkspace,
};
});
const BigFishResultView = lazy(async () => {
const module = await import('../big-fish-result/BigFishResultView');
return {
default: module.BigFishResultView,
};
});
const BigFishRuntimeShell = lazy(async () => {
const module = await import('../big-fish-runtime/BigFishRuntimeShell');
return {
default: module.BigFishRuntimeShell,
};
});
const Match3DResultView = lazy(async () => {
const module = await import('../match3d-result/Match3DResultView');
return {
default: module.Match3DResultView,
};
});
const Match3DRuntimeShell = lazy(async () => {
const module = await import('../match3d-runtime/Match3DRuntimeShell');
return {
default: module.Match3DRuntimeShell,
};
});
const SquareHoleAgentWorkspace = lazy(async () => {
const module = await import(
'../square-hole-creation/SquareHoleAgentWorkspace'
);
return {
default: module.SquareHoleAgentWorkspace,
};
});
const SquareHoleResultView = lazy(async () => {
const module = await import('../square-hole-result/SquareHoleResultView');
return {
default: module.SquareHoleResultView,
};
});
const SquareHoleRuntimeShell = lazy(async () => {
const module = await import('../square-hole-runtime/SquareHoleRuntimeShell');
return {
default: module.SquareHoleRuntimeShell,
};
});
const JumpHopResultView = lazy(async () => {
const module = await import('../jump-hop-result/JumpHopResultView');
return {
default: module.JumpHopResultView,
};
});
const JumpHopRuntimeShell = lazy(async () => {
const module = await import('../jump-hop-runtime/JumpHopRuntimeShell');
return {
default: module.JumpHopRuntimeShell,
};
});
const PuzzleClearWorkspace = lazy(async () => {
const module = await import('../puzzle-clear-creation/PuzzleClearWorkspace');
return {
default: module.PuzzleClearWorkspace,
};
});
const PuzzleClearResultView = lazy(async () => {
const module = await import('../puzzle-clear-result/PuzzleClearResultView');
return {
default: module.PuzzleClearResultView,
};
});
const PuzzleClearRuntimeShell = lazy(async () => {
const module = await import(
'../puzzle-clear-runtime/PuzzleClearRuntimeShell'
);
return {
default: module.PuzzleClearRuntimeShell,
};
});
const WoodenFishResultView = lazy(async () => {
const module = await import('../wooden-fish-result/WoodenFishResultView');
return {
default: module.WoodenFishResultView,
};
});
const WoodenFishRuntimeShell = lazy(async () => {
const module = await import('../wooden-fish-runtime/WoodenFishRuntimeShell');
return {
default: module.WoodenFishRuntimeShell,
};
});
const BarkBattleConfigEditor = lazy(async () => {
const module = await import('../bark-battle-creation/BarkBattleConfigEditor');
return {
default: module.BarkBattleConfigEditor,
};
});
const BarkBattleGeneratingView = lazy(async () => {
const module = await import(
'../bark-battle-creation/BarkBattleGeneratingView'
);
return {
default: module.BarkBattleGeneratingView,
};
});
const BarkBattleResultView = lazy(async () => {
const module = await import('../bark-battle-creation/BarkBattleResultView');
return {
default: module.BarkBattleResultView,
};
});
const BarkBattleRuntimeShell = lazy(async () => {
const module = await import(
'../../games/bark-battle/ui/BarkBattleRuntimeShell'
);
return {
default: module.BarkBattleRuntimeShell,
};
});
const CustomWorldCreationHub = lazy(async () => {
const module = await import('../custom-world-home/CustomWorldCreationHub');
return {
default: module.CustomWorldCreationHub,
};
});
const CreativeAgentWorkspace = lazy(async () => {
const module = await import('../creative-agent/CreativeAgentWorkspace');
return {
default: module.CreativeAgentWorkspace,
};
});
const VisualNovelAgentWorkspace = lazy(async () => {
const module = await import(
'../visual-novel-creation/VisualNovelAgentWorkspace'
);
return {
default: module.VisualNovelAgentWorkspace,
};
});
const BabyObjectMatchWorkspace = lazy(async () => {
const module = await import(
'../edutainment-creation/BabyObjectMatchWorkspace'
);
return {
default: module.BabyObjectMatchWorkspace,
};
});
const BabyObjectMatchResultView = lazy(async () => {
const module = await import(
'../edutainment-result/BabyObjectMatchResultView'
);
return {
default: module.BabyObjectMatchResultView,
};
});
const BabyObjectMatchRuntimeShell = lazy(async () => {
const module = await import(
'../edutainment-runtime/BabyObjectMatchRuntimeShell'
);
return {
default: module.BabyObjectMatchRuntimeShell,
};
});
const BabyLoveDrawingRuntimeShell = lazy(async () => {
const module = await import(
'../edutainment-runtime/BabyLoveDrawingRuntimeShell'
);
return {
default: module.BabyLoveDrawingRuntimeShell,
};
});
const VisualNovelResultView = lazy(async () => {
const module = await import('../visual-novel-result/VisualNovelResultView');
return {
default: module.VisualNovelResultView,
};
});
const VisualNovelRuntimeShell = lazy(async () => {
const module = await import(
'../visual-novel-runtime/VisualNovelRuntimeShell'
);
return {
default: module.VisualNovelRuntimeShell,
};
});
const PuzzleResultView = lazy(async () => {
const module = await import('../puzzle-result/PuzzleResultView');
return {
default: module.PuzzleResultView,
};
});
const PuzzleGalleryDetailView = lazy(async () => {
const module = await import('../puzzle-gallery/PuzzleGalleryDetailView');
return {
default: module.PuzzleGalleryDetailView,
};
});
const PuzzleRuntimeShell = lazy(async () => {
const module = await import('../puzzle-runtime/PuzzleRuntimeShell');
return {
default: module.PuzzleRuntimeShell,
};
});
function LazyPanelFallback({ label }: { label: string }) {
return (
<div className="flex h-full min-h-0 items-center justify-center">
<PlatformSubpanel
radius="sm"
padding="none"
className="px-5 py-4 text-sm text-[var(--platform-text-base)]"
>
{label}
</PlatformSubpanel>
</div>
);
}
function CreationResultRecoveryPanel({
title,
message,
actionLabel,
onAction,
}: {
title: string;
message: string;
actionLabel: string;
onAction: () => void;
}) {
return (
<div className="flex h-full min-h-0 items-center justify-center px-3 py-6">
<PlatformSubpanel
as="div"
radius="xl"
padding="none"
className="w-full max-w-sm p-5 text-center"
>
<div className="text-base font-black text-[var(--platform-text-strong)]">
{title}
</div>
<div className="mt-2 text-sm leading-6 text-[var(--platform-text-base)]">
{message}
</div>
<PlatformActionButton
onClick={onAction}
size="md"
className="mt-4 min-h-11"
>
{actionLabel}
</PlatformActionButton>
</PlatformSubpanel>
</div>
);
}
export function PlatformEntryFlowShellImpl({
selectionStage,
setSelectionStage,
hasSavedGame,
savedSnapshot,
handleContinueGame,
handleStartNewGame,
handleCustomWorldSelect,
initialPublicWorkCode,
}: PlatformEntryFlowShellProps) {
const authUi = useAuthUi();
const platformThemeClass =
authUi?.platformTheme === 'dark'
? 'platform-theme--dark'
: 'platform-theme--light';
const isDesktopLayout = usePlatformDesktopLayout();
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
const [isCreationCommunityOpen, setIsCreationCommunityOpen] =
useState(false);
const [draftGenerationPointNotice, setDraftGenerationPointNotice] =
useState<DraftGenerationPointNotice | null>(null);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [selectedPublicWorkDetail, setSelectedPublicWorkDetail] =
useState<PlatformPublicGalleryCard | null>(null);
const [selectedPublicWorkAuthor, setSelectedPublicWorkAuthor] =
useState<PublicUserSummary | null>(null);
const publicWorkAuthorRequestKeyRef = useRef(0);
const [publicWorkDetailError, setPublicWorkDetailError] = useState<
string | null
>(null);
const [isPublicWorkDetailBusy, setIsPublicWorkDetailBusy] = useState(false);
const [bigFishWorks, setBigFishWorks] = useState<BigFishWorkSummary[]>([]);
const [bigFishGalleryEntries, setBigFishGalleryEntries] = useState<
BigFishWorkSummary[]
>([]);
const [match3dWorks, setMatch3DWorks] = useState<Match3DWorkSummary[]>([]);
const [match3dGalleryEntries, setMatch3DGalleryEntries] = useState<
Match3DWorkSummary[]
>([]);
const [match3dProfile, setMatch3DProfile] =
useState<Match3DWorkProfile | null>(null);
const [match3dRuntimeProfile, setMatch3DRuntimeProfile] =
useState<Match3DWorkProfile | null>(null);
const [match3dRun, setMatch3DRun] = useState<Match3DRunSnapshot | null>(null);
const [match3dRuntimeReturnStage, setMatch3DRuntimeReturnStage] = useState<
'match3d-result' | 'work-detail'
>('match3d-result');
const [match3dFormDraftPayload, setMatch3DFormDraftPayload] =
useState<CreateMatch3DSessionRequest | null>(null);
const [match3dGenerationState, setMatch3DGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [match3dBackgroundCompileTasks, setMatch3DBackgroundCompileTasks] =
useState<Record<string, Match3DBackgroundCompileTask>>({});
const [isMatch3DLoadingLibrary, setIsMatch3DLoadingLibrary] = useState(false);
const [squareHoleWorks, setSquareHoleWorks] = useState<
SquareHoleWorkSummary[]
>([]);
const [squareHoleGalleryEntries, setSquareHoleGalleryEntries] = useState<
SquareHoleWorkSummary[]
>([]);
const [squareHoleProfile, setSquareHoleProfile] =
useState<SquareHoleWorkProfile | null>(null);
const [squareHoleRun, setSquareHoleRun] =
useState<SquareHoleRunSnapshot | null>(null);
const [squareHoleRuntimeReturnStage, setSquareHoleRuntimeReturnStage] =
useState<SquareHoleRuntimeReturnStage>('square-hole-result');
const [isSquareHoleLoadingLibrary, setIsSquareHoleLoadingLibrary] =
useState(false);
const [squareHoleGenerationState, setSquareHoleGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [jumpHopSession, setJumpHopSession] =
useState<JumpHopSessionSnapshotResponse | null>(null);
const [jumpHopRun, setJumpHopRun] = useState<
JumpHopRunResponse['run'] | null
>(null);
const [jumpHopRuntimeRequestOptions, setJumpHopRuntimeRequestOptions] =
useState<JumpHopRuntimeRequestOptions | null>(null);
const [jumpHopWork, setJumpHopWork] =
useState<JumpHopWorkProfileResponse | null>(null);
const [jumpHopGalleryEntries, setJumpHopGalleryEntries] = useState<
JumpHopGalleryCardResponse[]
>([]);
const [jumpHopWorks, setJumpHopWorks] = useState<
JumpHopWorkSummaryResponse[]
>([]);
const [jumpHopRuntimeReturnStage, setJumpHopRuntimeReturnStage] =
useState<JumpHopRuntimeReturnStage>('jump-hop-result');
const [jumpHopGenerationState, setJumpHopGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [jumpHopError, setJumpHopError] = useState<string | null>(null);
const [isJumpHopBusy, setIsJumpHopBusy] = useState(false);
const [barkBattleRuntimeRequestOptions, setBarkBattleRuntimeRequestOptions] =
useState<RuntimeGuestRequestOptions | null>(null);
const [puzzleClearSession, setPuzzleClearSession] =
useState<PuzzleClearSessionSnapshotResponse | null>(null);
const [puzzleClearRun, setPuzzleClearRun] = useState<
PuzzleClearRunResponse['run'] | null
>(null);
const [puzzleClearWork, setPuzzleClearWork] =
useState<PuzzleClearWorkProfileResponse | null>(null);
const [puzzleClearWorks, setPuzzleClearWorks] = useState<
PuzzleClearWorkSummaryResponse[]
>([]);
const [puzzleClearGalleryEntries, setPuzzleClearGalleryEntries] = useState<
PuzzleClearGalleryCardResponse[]
>([]);
const [puzzleClearRuntimeReturnStage, setPuzzleClearRuntimeReturnStage] =
useState<PuzzleClearRuntimeReturnStage>('puzzle-clear-result');
const [puzzleClearGenerationState, setPuzzleClearGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [puzzleClearError, setPuzzleClearError] = useState<string | null>(null);
const [isPuzzleClearBusy, setIsPuzzleClearBusy] = useState(false);
const [barkBattleWorks, setBarkBattleWorks] = useState<
BarkBattleWorkSummary[]
>([]);
const [barkBattleGalleryEntries, setBarkBattleGalleryEntries] = useState<
BarkBattleWorkSummary[]
>([]);
const [woodenFishSession, setWoodenFishSession] =
useState<WoodenFishSessionSnapshotResponse | null>(null);
const [woodenFishRun, setWoodenFishRun] = useState<
WoodenFishRunResponse['run'] | null
>(null);
const [woodenFishWork, setWoodenFishWork] =
useState<WoodenFishWorkProfileResponse | null>(null);
const [woodenFishWorks, setWoodenFishWorks] = useState<
WoodenFishWorkSummaryResponse[]
>([]);
const [woodenFishGalleryEntries, setWoodenFishGalleryEntries] = useState<
WoodenFishGalleryCardResponse[]
>([]);
const [woodenFishRuntimeReturnStage, setWoodenFishRuntimeReturnStage] =
useState<WoodenFishRuntimeReturnStage>('wooden-fish-result');
const [woodenFishGenerationState, setWoodenFishGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [woodenFishError, setWoodenFishError] = useState<string | null>(null);
const [isWoodenFishBusy, setIsWoodenFishBusy] = useState(false);
const [barkBattlePublishedConfig, setBarkBattlePublishedConfig] =
useState<BarkBattlePublishedConfig | null>(null);
const [barkBattleDraftConfig, setBarkBattleDraftConfig] =
useState<BarkBattleDraftConfig | null>(null);
const [barkBattleRuntimeMode, setBarkBattleRuntimeMode] = useState<
'draft' | 'published'
>('draft');
const [barkBattleRuntimeReturnStage, setBarkBattleRuntimeReturnStage] =
useState<BarkBattleRuntimeReturnStage>('platform');
const [barkBattleError, setBarkBattleError] = useState<string | null>(null);
const [
barkBattleGenerationPartialFailed,
setBarkBattleGenerationPartialFailed,
] = useState(false);
const [isBarkBattleBusy, setIsBarkBattleBusy] = useState(false);
const [bigFishRun, setBigFishRun] =
useState<BigFishRuntimeSnapshotResponse | null>(null);
const [bigFishRuntimeShare, setBigFishRuntimeShare] = useState<{
title: string;
publicWorkCode: string;
} | null>(null);
const [_bigFishRuntimeWork, setBigFishRuntimeWork] =
useState<BigFishWorkSummary | null>(null);
const [bigFishRuntimeStartedAt, setBigFishRuntimeStartedAt] = useState<
number | null
>(null);
const [_bigFishRuntimeSessionSource, setBigFishRuntimeSessionSource] =
useState<BigFishRuntimeSessionSource>(null);
const [bigFishRuntimeReturnStage, setBigFishRuntimeReturnStage] =
useState<BigFishRuntimeReturnStage>('platform');
const bigFishInputInFlightRef = useRef(false);
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
const [bigFishGenerationState, setBigFishGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [activeRecommendEntryKey, setActiveRecommendEntryKey] = useState<
string | null
>(null);
const [activeRecommendRuntimeKind, setActiveRecommendRuntimeKind] =
useState<RecommendRuntimeKind | null>(null);
const [activeRecommendRuntimeError, setActiveRecommendRuntimeError] =
useState<string | null>(null);
const [isStartingRecommendEntry, setIsStartingRecommendEntry] =
useState(false);
const recommendRuntimeStartRequestRef = useRef(0);
const [puzzleOperation, setPuzzleOperation] = useState<PuzzleAgentOperationRecord | null>(
null,
);
const [externalGenerationQueueOverview, setExternalGenerationQueueOverview] =
useState<ExternalGenerationQueueOverview | null>(null);
const [jumpHopQueueState, setJumpHopQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [puzzleClearQueueState, setPuzzleClearQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [woodenFishQueueState, setWoodenFishQueueState] =
useState<ExternalGenerationJobStatusRecord | null>(null);
const [puzzleWorks, setPuzzleWorks] = useState<PuzzleWorkSummary[]>([]);
const [puzzleGalleryEntries, setPuzzleGalleryEntries] = useState<
PuzzleWorkSummary[]
>([]);
const [selectedPuzzleDetail, setSelectedPuzzleDetail] =
useState<PuzzleWorkSummary | null>(null);
const [puzzleDetailReturnTarget, setPuzzleDetailReturnTarget] =
useState<PuzzleDetailReturnTarget | null>(null);
const [puzzleRuntimeReturnStage, setPuzzleRuntimeReturnStage] =
useState<PuzzleRuntimeReturnStage>('puzzle-gallery-detail');
const [puzzleRuntimeAuthMode, setPuzzleRuntimeAuthMode] =
useState<PuzzleRuntimeAuthMode>('default');
const buildRecommendRuntimeRequestOptions = useCallback(
async (
input: {
kind?: RecommendRuntimeKind;
embedded?: boolean;
forcePublicRuntime?: boolean;
} = {},
) => {
const shouldUseRuntimeOptions = Boolean(
input.forcePublicRuntime ||
input.embedded ||
(input.kind && activeRecommendRuntimeKind === input.kind),
);
return shouldUseRuntimeOptions
? buildRecommendRuntimeAuthOptions(authUi, true)
: {};
},
[activeRecommendRuntimeKind, authUi],
);
const buildPuzzleRuntimeRequestOptions = useCallback(
() =>
buildRecommendRuntimeRequestOptions({
kind: 'puzzle',
forcePublicRuntime: puzzleRuntimeAuthMode === 'isolated',
}),
[buildRecommendRuntimeRequestOptions, puzzleRuntimeAuthMode],
);
const [isPuzzleLeaderboardBusy, setIsPuzzleLeaderboardBusy] = useState(false);
const submittedPuzzleLeaderboardKeysRef = useRef(new Set<string>());
const puzzleStartInFlightKeyRef = useRef<string | null>(null);
const [puzzleRun, setPuzzleRun] = useState<PuzzleRunSnapshot | null>(null);
const puzzleRunRef = useRef<PuzzleRunSnapshot | null>(null);
const errorSetterRefNoop = useMemo(
() =>
((_: SetStateAction<string | null>) => undefined) as Dispatch<
SetStateAction<string | null>
>,
[],
);
const bigFishErrorSetterRef = useRef(errorSetterRefNoop);
const match3DErrorSetterRef = useRef(errorSetterRefNoop);
const squareHoleErrorSetterRef = useRef(errorSetterRefNoop);
const puzzleErrorSetterRef = useRef(errorSetterRefNoop);
const visualNovelErrorSetterRef = useRef(errorSetterRefNoop);
const [isPuzzleLoadingLibrary, setIsPuzzleLoadingLibrary] = useState(false);
const [puzzleShelfError, setPuzzleShelfError] = useState<string | null>(null);
const [puzzleCreationError, setPuzzleCreationError] = useState<string | null>(
null,
);
const [puzzleGenerationState, setPuzzleGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [puzzleBackgroundCompileTasks, setPuzzleBackgroundCompileTasks] =
useState<Record<string, PuzzleBackgroundCompileTask>>({});
const puzzleRuntimeReturnSessionRef =
useRef<PuzzleAgentSessionSnapshot | null>(null);
const [miniGameGenerationProgressNowMs, setMiniGameGenerationProgressNowMs] =
useState(() => Date.now());
const [puzzleFormDraftPayload, setPuzzleFormDraftPayload] =
useState<CreatePuzzleAgentSessionRequest | null>(null);
const [puzzleOnboardingPrompt, setPuzzleOnboardingPrompt] = useState('');
const [puzzleOnboardingPhase, setPuzzleOnboardingPhase] =
useState<PuzzleOnboardingPhase>('input');
const [puzzleOnboardingDraft, setPuzzleOnboardingDraft] =
useState<PuzzleOnboardingDraft | null>(null);
const [puzzleOnboardingError, setPuzzleOnboardingError] = useState<
string | null
>(null);
const [isPuzzleOnboardingSaving, setIsPuzzleOnboardingSaving] =
useState(false);
const [creativeAgentSession, setCreativeAgentSession] =
useState<CreativeAgentSessionSnapshot | null>(null);
const [creativeAgentError, setCreativeAgentError] = useState<string | null>(
null,
);
const [isCreativeAgentBusy, setIsCreativeAgentBusy] = useState(false);
const [isCreativeAgentStreaming, setIsCreativeAgentStreaming] =
useState(false);
const [creativeAgentEvents, setCreativeAgentEvents] = useState<
CreativeAgentSseEvent[]
>([]);
const [activeCreativeAgentSessionId, setActiveCreativeAgentSessionId] =
useState<string | null>(null);
const [creativeDraftEditError, setCreativeDraftEditError] = useState<
string | null
>(null);
const [isCreativeDraftEditBusy, setIsCreativeDraftEditBusy] = useState(false);
const [visualNovelWork, setVisualNovelWork] =
useState<VisualNovelWorkDetail | null>(null);
const [visualNovelWorks, setVisualNovelWorks] = useState<
VisualNovelWorkSummary[]
>([]);
const [visualNovelGalleryEntries, setVisualNovelGalleryEntries] = useState<
VisualNovelWorkSummary[]
>([]);
const [visualNovelRun, setVisualNovelRun] =
useState<VisualNovelRunSnapshot | null>(null);
const [visualNovelRuntimeReturnStage, setVisualNovelRuntimeReturnStage] =
useState<VisualNovelRuntimeReturnStage>('visual-novel-result');
const [visualNovelFormDraftPayload, setVisualNovelFormDraftPayload] =
useState<VisualNovelEntryFormPayload | null>(null);
const [
visualNovelGenerationStartedAtMs,
setVisualNovelGenerationStartedAtMs,
] = useState<number | null>(null);
const [visualNovelGenerationPhase, setVisualNovelGenerationPhase] =
useState<VisualNovelEntryGenerationPhase>('generating');
const [babyObjectMatchDraft, setBabyObjectMatchDraft] =
useState<BabyObjectMatchDraft | null>(null);
const [babyObjectMatchDrafts, setBabyObjectMatchDrafts] = useState<
BabyObjectMatchDraft[]
>([]);
const [babyObjectMatchFormPayload, setBabyObjectMatchFormPayload] =
useState<CreateBabyObjectMatchDraftRequest | null>(null);
const [babyObjectMatchGenerationState, setBabyObjectMatchGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [babyObjectMatchGenerationPhase, setBabyObjectMatchGenerationPhase] =
useState<BabyObjectMatchGenerationPhase>('generating');
const [
babyObjectMatchRuntimeReturnStage,
setBabyObjectMatchRuntimeReturnStage,
] = useState<BabyObjectMatchRuntimeReturnStage>('baby-object-match-result');
const [babyObjectMatchError, setBabyObjectMatchError] = useState<
string | null
>(null);
const [isBabyObjectMatchBusy, setIsBabyObjectMatchBusy] = useState(false);
const [isVisualNovelLoadingLibrary, setIsVisualNovelLoadingLibrary] =
useState(false);
const [isPuzzleNextLevelGenerating, setIsPuzzleNextLevelGenerating] =
useState(false);
const [isSearchingPublicCode, setIsSearchingPublicCode] = useState(false);
const [publicSearchError, setPublicSearchError] = useState<string | null>(
null,
);
const [searchedPublicUser, setSearchedPublicUser] =
useState<PublicUserSummary | null>(null);
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
string | null
>(null);
const [pendingDeleteCreationWork, setPendingDeleteCreationWork] =
useState<DeleteCreationWorkConfirmation | null>(null);
const [workNotFoundRecoveryDialog, setWorkNotFoundRecoveryDialog] =
useState<WorkNotFoundRecoveryDialogState | null>(null);
const [
claimingPuzzlePointIncentiveProfileId,
setClaimingPuzzlePointIncentiveProfileId,
] = useState<string | null>(null);
const [publishSharePayload, setPublishSharePayload] =
useState<PublishShareModalPayload | null>(null);
const [creationEntryConfig, setCreationEntryConfig] =
useState<CreationEntryConfig | null>(null);
const [creationEntryConfigError, setCreationEntryConfigError] = useState<
string | null
>(null);
const creationEntryTypes = useMemo(
() =>
creationEntryConfig
? derivePlatformCreationTypes(creationEntryConfig.creationTypes)
: [],
[creationEntryConfig],
);
const unifiedCreationConfigById = useMemo(() => {
const entries = creationEntryConfig?.creationTypes ?? [];
return new Map(entries.map((entry) => [entry.id, entry]));
}, [creationEntryConfig]);
const publicWorkInteractions = useMemo(
() => creationEntryConfig?.publicWorkInteractions ?? [],
[creationEntryConfig],
);
const getUnifiedSpec = useCallback(
(playId: UnifiedCreationPlayId) =>
getUnifiedCreationSpec(playId, unifiedCreationConfigById.get(playId)),
[unifiedCreationConfigById],
);
const isBigFishCreationVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'big-fish',
);
const isJumpHopCreationVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'jump-hop',
);
const isPuzzleClearCreationVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'puzzle-clear',
);
const isSquareHoleCreationVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'square-hole',
);
const isBabyObjectMatchVisible = isPlatformCreationTypeVisible(
creationEntryTypes,
'baby-object-match',
);
const isVisualNovelCreationOpen = isPlatformCreationTypeOpen(
creationEntryTypes,
'visual-novel',
);
const resolveCreationEntryMudPointCost = useCallback(
(id: PlatformCreationTypeId) =>
creationEntryTypes.find((item) => item.id === id)?.mudPointCost ??
DEFAULT_UNIFIED_CREATION_MUD_POINT_COST,
[creationEntryTypes],
);
const puzzleDraftGenerationPointCost =
resolveCreationEntryMudPointCost('puzzle');
const match3DDraftGenerationPointCost =
resolveCreationEntryMudPointCost('match3d');
const barkBattleDraftGenerationPointCost =
resolveCreationEntryMudPointCost('bark-battle');
const [profilePlayStats, setProfilePlayStats] =
useState<ProfilePlayStatsResponse | null>(null);
const [profilePlayStatsError, setProfilePlayStatsError] = useState<
string | null
>(null);
const [isProfilePlayStatsLoading, setIsProfilePlayStatsLoading] =
useState(false);
const [isProfilePlayStatsOpen, setIsProfilePlayStatsOpen] = useState(false);
const hadReadableProtectedDataRef = useRef(false);
const hasInitialAgentSession = Boolean(
readCustomWorldAgentUiState().activeSessionId &&
shouldRestoreCustomWorldAgentUiState(),
);
const handledInitialPublicWorkCodeRef = useRef<string | null>(null);
const handledJumpHopRuntimeRestoreRef = useRef<string | null>(null);
const selectionStageRef = useRef(selectionStage);
const creationFlowReturnTargetRef =
useRef<CreationFlowReturnTarget>('create');
const activeMatch3DGenerationSessionIdRef = useRef<string | null>(null);
const activePuzzleGenerationSessionIdRef = useRef<string | null>(null);
const [draftGenerationNotices, setDraftGenerationNotices] =
useState<DraftGenerationNoticeMap>({});
const [pendingDraftShelfItems, setPendingDraftShelfItems] =
useState<PendingDraftShelfMap>({});
const profileWalletLocalDeltaRef = useRef(0);
const lastProfileDashboardSnapshotRef =
useRef<ProfileDashboardSummary | null>(null);
const [
pendingPlatformTaskCompletionDialog,
setPendingPlatformTaskCompletionDialog,
] = useState<PlatformTaskCompletionDialogState | null>(null);
const [
pendingPlatformTaskFailureDialog,
setPendingPlatformTaskFailureDialog,
] = useState<PlatformTaskFailureDialogState | null>(null);
const [profileTaskRefreshKey, setProfileTaskRefreshKey] = useState(0);
const [initialCreationUrlState] = useState(() => readCreationUrlState());
const handledInitialCreationUrlStateRef = useRef(false);
const [initialPuzzleRuntimeUrlState] = useState(() =>
readPuzzleRuntimeUrlState(),
);
const handledPuzzleRuntimeUrlStateKeyRef = useRef<string | null>(null);
useEffect(() => {
selectionStageRef.current = selectionStage;
}, [selectionStage]);
const resetRecommendRuntimeSelection = useCallback(() => {
// 中文注释:推荐页嵌入运行态进入改造/创作后会清掉玩法 run;
// 同步清空推荐选择,避免返回推荐页时复用已失效的运行态占位。
recommendRuntimeStartRequestRef.current += 1;
setActiveRecommendEntryKey(null);
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError(null);
setIsStartingRecommendEntry(false);
}, []);
const updatePendingDraftShelfItem = useCallback(
(
kind: PendingDraftShelfKind,
id: string | null | undefined,
status: DraftGenerationNoticeStatus,
seen = false,
metadata?: PendingDraftShelfMetadata,
) => {
const normalizedId = normalizeDraftNoticeId(id);
if (!normalizedId) {
return;
}
setPendingDraftShelfItems((current) => ({
...current,
[kind]: (() => {
const currentItems = current[kind] ?? {};
return {
...currentItems,
[normalizedId]: createPendingDraftShelfState(
status,
seen,
new Date().toISOString(),
metadata ?? currentItems[normalizedId],
),
};
})(),
}));
},
[],
);
const clearPendingDraftShelfItem = useCallback(
(kind: PendingDraftShelfKind, id: string | null | undefined) => {
const normalizedId = normalizeDraftNoticeId(id);
if (!normalizedId) {
return;
}
setPendingDraftShelfItems((current) => {
const currentItems = current[kind];
if (!currentItems?.[normalizedId]) {
return current;
}
const nextItems = { ...currentItems };
delete nextItems[normalizedId];
return {
...current,
[kind]: nextItems,
};
});
},
[],
);
const updateDraftGenerationNotices = useCallback(
(
keys: string[],
status: DraftGenerationNoticeStatus,
seen = false,
message?: string | null,
) => {
const uniqueKeys = Array.from(new Set(keys.filter(Boolean)));
if (uniqueKeys.length === 0) {
return;
}
const completedAtMs = status === 'ready' ? Date.now() : undefined;
const normalizedMessage = message?.trim();
setDraftGenerationNotices((current) => {
const next = { ...current };
for (const key of uniqueKeys) {
next[key] =
completedAtMs === undefined
? {
status,
seen,
...(normalizedMessage ? { message: normalizedMessage } : {}),
}
: {
status,
seen,
completedAtMs,
...(normalizedMessage ? { message: normalizedMessage } : {}),
};
}
return next;
});
},
[],
);
const markDraftNoticeSeen = useCallback((keys: string[]) => {
const uniqueKeys = Array.from(new Set(keys.filter(Boolean)));
if (uniqueKeys.length === 0) {
return;
}
setDraftGenerationNotices((current) => {
let changed = false;
const next = { ...current };
for (const key of uniqueKeys) {
const notice = next[key];
if (notice?.status === 'ready' && !notice.seen) {
next[key] = { ...notice, seen: true };
changed = true;
}
}
return changed ? next : current;
});
}, []);
const markDraftGenerating = useCallback(
(kind: CreationWorkShelfKind, ids: Array<string | null | undefined>) => {
setPendingPlatformTaskCompletionDialog(null);
setPendingPlatformTaskFailureDialog(null);
updateDraftGenerationNotices(
collectDraftNoticeKeys(kind, ids),
'generating',
);
},
[
setPendingPlatformTaskCompletionDialog,
setPendingPlatformTaskFailureDialog,
updateDraftGenerationNotices,
],
);
const markDraftReady = useCallback(
(
kind: CreationWorkShelfKind,
ids: Array<string | null | undefined>,
viewedImmediately: boolean,
) => {
setPendingPlatformTaskFailureDialog(null);
updateDraftGenerationNotices(
collectDraftNoticeKeys(kind, ids),
'ready',
viewedImmediately,
);
setProfileTaskRefreshKey((current) => current + 1);
const completedAtMs = Date.now();
setPendingPlatformTaskCompletionDialog({
key: `${kind}:${collectDraftNoticeKeys(kind, ids).join('|')}:${completedAtMs}`,
source: buildDraftCompletionDialogSource(kind, ids),
message: PLATFORM_TASK_COMPLETION_MESSAGE,
completedAtMs,
});
},
[
setPendingPlatformTaskCompletionDialog,
setPendingPlatformTaskFailureDialog,
updateDraftGenerationNotices,
],
);
const markDraftFailed = useCallback(
(
kind: CreationWorkShelfKind,
ids: Array<string | null | undefined>,
errorMessage?: string | null,
showFailureDialog = true,
) => {
setPendingPlatformTaskCompletionDialog(null);
const noticeKeys = collectDraftNoticeKeys(kind, ids);
updateDraftGenerationNotices(noticeKeys, 'failed', false, errorMessage);
const normalizedErrorMessage = errorMessage?.trim();
if (normalizedErrorMessage && showFailureDialog) {
const failedAtMs = Date.now();
setPendingPlatformTaskFailureDialog({
key: `draft-failure:${kind}:${noticeKeys.join('|')}:${failedAtMs}`,
source: buildDraftCompletionDialogSource(kind, ids),
message: normalizedErrorMessage,
failedAtMs,
});
}
},
[
setPendingPlatformTaskCompletionDialog,
setPendingPlatformTaskFailureDialog,
updateDraftGenerationNotices,
],
);
const markPendingDraftGenerating = useCallback(
(
kind: PendingDraftShelfKind,
id: string | null | undefined,
metadata?: PendingDraftShelfMetadata,
) => {
setPendingPlatformTaskCompletionDialog(null);
setPendingPlatformTaskFailureDialog(null);
updatePendingDraftShelfItem(kind, id, 'generating', false, metadata);
},
[
setPendingPlatformTaskCompletionDialog,
setPendingPlatformTaskFailureDialog,
updatePendingDraftShelfItem,
],
);
const markPendingDraftReady = useCallback(
(
kind: PendingDraftShelfKind,
id: string | null | undefined,
viewedImmediately: boolean,
) => {
updatePendingDraftShelfItem(kind, id, 'ready', viewedImmediately);
},
[updatePendingDraftShelfItem],
);
const markPendingDraftFailed = useCallback(
(kind: PendingDraftShelfKind, id: string | null | undefined) => {
updatePendingDraftShelfItem(kind, id, 'failed', false);
},
[updatePendingDraftShelfItem],
);
const getMatch3DBackgroundCompileTask = useCallback(
(sessionId: string | null | undefined) => {
const normalizedSessionId = normalizeDraftNoticeId(sessionId);
return normalizedSessionId
? (match3dBackgroundCompileTasks[normalizedSessionId] ?? null)
: null;
},
[match3dBackgroundCompileTasks],
);
const getPuzzleBackgroundCompileTask = useCallback(
(sessionId: string | null | undefined) => {
const normalizedSessionId = normalizeDraftNoticeId(sessionId);
return normalizedSessionId
? (puzzleBackgroundCompileTasks[normalizedSessionId] ?? null)
: null;
},
[puzzleBackgroundCompileTasks],
);
useEffect(() => {
let cancelled = false;
setCreationEntryConfigError(null);
void fetchCreationEntryConfig()
.then((config) => {
if (!cancelled) {
setCreationEntryConfig(config);
}
})
.catch((error: unknown) => {
if (!cancelled) {
setCreationEntryConfigError(
error instanceof Error ? error.message : '读取创作入口配置失败。',
);
}
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
profileWalletLocalDeltaRef.current = 0;
lastProfileDashboardSnapshotRef.current = null;
}, [authUi?.user?.id]);
const applyProfileWalletLocalDeltaToDashboard = useCallback(
(latestDashboard: ProfileDashboardSummary | null) => {
const reconciledDelta =
reconcileProfileWalletLocalDeltaWithServerDashboard(
lastProfileDashboardSnapshotRef.current,
latestDashboard,
profileWalletLocalDeltaRef.current,
);
lastProfileDashboardSnapshotRef.current = latestDashboard;
profileWalletLocalDeltaRef.current = reconciledDelta;
return adjustProfileDashboardWalletBalance(
latestDashboard,
reconciledDelta,
);
},
[],
);
const getPlatformProfileDashboardWithLocalWalletDelta = useCallback(
async (options?: Parameters<typeof getPlatformProfileDashboard>[0]) => {
const latestDashboard = await getPlatformProfileDashboard(options);
return applyProfileWalletLocalDeltaToDashboard(latestDashboard);
},
[applyProfileWalletLocalDeltaToDashboard],
);
const platformBootstrap = usePlatformEntryBootstrap({
user: authUi?.user,
canAccessProtectedData: authUi?.canAccessProtectedData,
getProfileDashboard: getPlatformProfileDashboardWithLocalWalletDelta,
handleContinueGame,
hasInitialAgentSession,
});
const {
canReadProtectedData: canRefreshPlatformDashboard,
refreshProfileDashboard,
} = platformBootstrap;
const refreshPlatformDashboardSilently = useCallback(() => {
if (!canRefreshPlatformDashboard) {
return;
}
void refreshProfileDashboard().catch(() => undefined);
}, [canRefreshPlatformDashboard, refreshProfileDashboard]);
const adjustProfileWalletBalanceLocally = useCallback(
(delta: number) => {
if (!Number.isFinite(delta) || delta === 0) {
return;
}
profileWalletLocalDeltaRef.current += Math.trunc(delta);
platformBootstrap.setProfileDashboard((current) =>
adjustProfileDashboardWalletBalance(current, delta),
);
},
[platformBootstrap],
);
const entryNavigation = usePlatformEntryNavigation({
setSelectionStage,
setSelectedDetailEntry,
});
const { setPlatformTab } = platformBootstrap;
const returnPlatformHomeAfterMissingWork = useCallback(() => {
setPlatformTab('home');
setSelectionStage('platform');
const recoveryDialog = resolveWorkNotFoundRecoveryDialogState();
if (recoveryDialog) {
setWorkNotFoundRecoveryDialog(recoveryDialog);
return;
}
pushAppHistoryPath('/');
}, [setPlatformTab, setSelectionStage]);
const confirmWorkNotFoundRecovery = useCallback(() => {
const nextPath = workNotFoundRecoveryDialog?.nextPath ?? '/';
setWorkNotFoundRecoveryDialog(null);
setPlatformTab('home');
setSelectionStage('platform');
pushAppHistoryPath(nextPath);
}, [setPlatformTab, setSelectionStage, workNotFoundRecoveryDialog]);
useEffect(() => {
if (selectionStage === 'profile-feedback') {
setPlatformTab('profile');
}
}, [selectionStage, setPlatformTab]);
const openProfileFeedback = useCallback(() => {
if (!authUi?.user) {
authUi?.openLoginModal();
return;
}
setPlatformTab('profile');
setSelectionStage('profile-feedback');
}, [authUi, setPlatformTab, setSelectionStage]);
const enterCreateTab = useCallback(() => {
// 只依赖稳定的 setter,避免把 bootstrap 对象的 render 级引用变化
// 传导成 Agent session 恢复 effect 的重复触发。
setPlatformTab('create');
}, [setPlatformTab]);
const enterDraftTab = useCallback(() => {
setPlatformTab('saves');
}, [setPlatformTab]);
const markCreationFlowReturnToCreate = useCallback(() => {
creationFlowReturnTargetRef.current = 'create';
}, []);
const markCreationFlowReturnToDraftShelf = useCallback(() => {
creationFlowReturnTargetRef.current = 'draft-shelf';
}, []);
const returnToCreationFlowSource = useCallback(() => {
const returnTarget = creationFlowReturnTargetRef.current;
creationFlowReturnTargetRef.current = 'create';
clearCreationUrlState();
if (returnTarget === 'draft-shelf') {
enterDraftTab();
} else {
enterCreateTab();
}
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [enterCreateTab, enterDraftTab, setSelectionStage]);
const shouldReturnToDraftShelf = useCallback(
() => creationFlowReturnTargetRef.current === 'draft-shelf',
[],
);
const returnToCreationCenterFromGeneration = useCallback(() => {
markCreationFlowReturnToCreate();
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const isViewingMatch3DGeneration = useCallback((sessionId: string) => {
return (
selectionStageRef.current === 'match3d-generating' &&
activeMatch3DGenerationSessionIdRef.current === sessionId
);
}, []);
const isViewingPuzzleGeneration = useCallback((sessionId: string) => {
return (
selectionStageRef.current === 'puzzle-generating' &&
activePuzzleGenerationSessionIdRef.current === sessionId
);
}, []);
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
async (pointsCost: number) => {
try {
const latestDashboard = await getPlatformProfileDashboard(
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
);
const dashboardWithLocalDelta =
applyProfileWalletLocalDeltaToDashboard(latestDashboard);
platformBootstrap.setProfileDashboard(dashboardWithLocalDelta);
const walletBalance = resolveProfileWalletBalance(latestDashboard);
if (walletBalance >= pointsCost) {
setDraftGenerationPointNotice(null);
return true;
}
setDraftGenerationPointNotice({
kind: 'insufficient-points',
requiredPoints: pointsCost,
currentPoints: walletBalance,
});
return false;
} catch {
setDraftGenerationPointNotice({
kind: 'balance-load-failed',
});
return false;
}
},
[applyProfileWalletLocalDeltaToDashboard, platformBootstrap],
);
const resolveBigFishErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolvePuzzleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolveMatch3DErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolveSquareHoleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolveBarkBattleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const refreshBigFishShelf = useCallback(async () => {
setIsBigFishLoadingLibrary(true);
try {
const worksResponse = await listBigFishWorks();
setBigFishWorks(worksResponse.items);
bigFishErrorSetterRef.current(null);
} catch (error) {
bigFishErrorSetterRef.current(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
);
} finally {
setIsBigFishLoadingLibrary(false);
}
}, [resolveBigFishErrorMessage]);
const refreshBigFishGallery = useCallback(async () => {
try {
const galleryResponse = await listBigFishGallery();
setBigFishGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch (error) {
setBigFishGalleryEntries([]);
bigFishErrorSetterRef.current(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼广场失败。'),
);
return [];
}
}, [resolveBigFishErrorMessage]);
const refreshMatch3DShelf = useCallback(async () => {
setIsMatch3DLoadingLibrary(true);
try {
const worksResponse = await listMatch3DWorks();
setMatch3DWorks(mapMatch3DWorksForRuntimeUi(worksResponse.items));
match3DErrorSetterRef.current(null);
} catch (error) {
match3DErrorSetterRef.current(
resolveMatch3DErrorMessage(error, '读取抓大鹅作品列表失败。'),
);
} finally {
setIsMatch3DLoadingLibrary(false);
}
}, [resolveMatch3DErrorMessage]);
const refreshMatch3DGallery = useCallback(async () => {
try {
const galleryResponse = await listMatch3DGallery();
const items = mapMatch3DWorksForRuntimeUi(galleryResponse.items);
setMatch3DGalleryEntries(items);
return items;
} catch {
// 中文注释:公开广场是首页展示数据,失败时只降级为空列表;
// 不写入创作错误态,避免挡住抓大鹅共创入口。
setMatch3DGalleryEntries([]);
return [];
}
}, []);
const refreshSquareHoleShelf = useCallback(async () => {
setIsSquareHoleLoadingLibrary(true);
try {
const worksResponse = await listSquareHoleWorks();
setSquareHoleWorks(worksResponse.items);
squareHoleErrorSetterRef.current(null);
} catch (error) {
squareHoleErrorSetterRef.current(
resolveSquareHoleErrorMessage(error, '读取方洞挑战作品列表失败。'),
);
} finally {
setIsSquareHoleLoadingLibrary(false);
}
}, [resolveSquareHoleErrorMessage]);
const refreshSquareHoleGallery = useCallback(async () => {
try {
const galleryResponse = await listSquareHoleGallery();
setSquareHoleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch {
setSquareHoleGalleryEntries([]);
return [];
}
}, []);
const refreshJumpHopGallery = useCallback(async () => {
try {
const galleryResponse = await jumpHopClient.listGallery();
setJumpHopGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch {
setJumpHopGalleryEntries([]);
return [];
}
}, []);
const refreshJumpHopShelf = useCallback(async () => {
if (!isJumpHopCreationVisible) {
setJumpHopWorks([]);
return [];
}
try {
const worksResponse = await jumpHopClient.listWorks();
setJumpHopWorks(worksResponse.items);
return worksResponse.items;
} catch {
setJumpHopWorks([]);
return [];
}
}, [isJumpHopCreationVisible]);
const refreshPuzzleClearGallery = useCallback(async () => {
try {
const galleryResponse = await puzzleClearClient.listGallery();
setPuzzleClearGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch {
setPuzzleClearGalleryEntries([]);
return [];
}
}, []);
const refreshPuzzleClearShelf = useCallback(async () => {
if (!isPuzzleClearCreationVisible) {
setPuzzleClearWorks([]);
return [];
}
try {
const worksResponse = await puzzleClearClient.listWorks();
setPuzzleClearWorks(worksResponse.items);
return worksResponse.items;
} catch (error) {
setPuzzleClearWorks([]);
setPuzzleClearError(
resolvePuzzleErrorMessage(error, '读取拼消消作品列表失败。'),
);
return [];
}
}, [isPuzzleClearCreationVisible]);
const refreshWoodenFishGallery = useCallback(async () => {
try {
const galleryResponse = await woodenFishClient.listGallery();
setWoodenFishGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch {
setWoodenFishGalleryEntries([]);
return [];
}
}, []);
const refreshWoodenFishShelf = useCallback(async () => {
try {
const worksResponse = await woodenFishClient.listWorks();
setWoodenFishWorks(worksResponse.items);
return worksResponse.items;
} catch (error) {
setWoodenFishWorks([]);
setWoodenFishError(
resolvePuzzleErrorMessage(error, '读取敲木鱼作品列表失败。'),
);
return [];
}
}, [resolvePuzzleErrorMessage]);
const refreshPuzzleShelf = useCallback(async () => {
setIsPuzzleLoadingLibrary(true);
try {
const worksResponse = await listPuzzleWorks();
setPuzzleWorks(worksResponse.items);
setPuzzleShelfError(null);
} catch (error) {
setPuzzleShelfError(
resolvePuzzleErrorMessage(error, '读取拼图作品列表失败。'),
);
} finally {
setIsPuzzleLoadingLibrary(false);
}
}, [resolvePuzzleErrorMessage]);
const refreshPuzzleGallery = useCallback(async () => {
try {
const galleryResponse = await listPuzzleGallery();
setPuzzleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch (error) {
setPuzzleGalleryEntries([]);
puzzleErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '读取拼图广场失败。'),
);
return [];
}
}, [resolvePuzzleErrorMessage]);
const refreshVisualNovelShelf = useCallback(async () => {
if (!isVisualNovelCreationOpen) {
setVisualNovelWorks([]);
visualNovelErrorSetterRef.current(null);
return [];
}
setIsVisualNovelLoadingLibrary(true);
try {
const worksResponse = await listVisualNovelWorks();
setVisualNovelWorks(worksResponse.works);
visualNovelErrorSetterRef.current(null);
return worksResponse.works;
} catch (error) {
visualNovelErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '读取视觉小说作品列表失败。'),
);
return [];
} finally {
setIsVisualNovelLoadingLibrary(false);
}
}, [isVisualNovelCreationOpen, resolvePuzzleErrorMessage]);
const refreshVisualNovelGallery = useCallback(async () => {
if (!isVisualNovelCreationOpen) {
setVisualNovelGalleryEntries([]);
visualNovelErrorSetterRef.current(null);
return [];
}
try {
const galleryResponse = await listVisualNovelGallery();
setVisualNovelGalleryEntries(galleryResponse.works);
return galleryResponse.works;
} catch (error) {
setVisualNovelGalleryEntries([]);
visualNovelErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '读取视觉小说广场失败。'),
);
return [];
}
}, [isVisualNovelCreationOpen, resolvePuzzleErrorMessage]);
const refreshBarkBattleShelf = useCallback(async () => {
setIsBarkBattleBusy(true);
try {
const worksResponse = await listBarkBattleWorks();
const works = mergeBarkBattleWorksByWorkId(worksResponse.items);
setBarkBattleWorks((current) =>
mergeBarkBattleWorksByWorkId([
...works,
...current.filter((item) =>
shouldPreserveLocalBarkBattleWorkOnRefresh(item, works),
),
]),
);
setBarkBattleError(null);
return works;
} catch (error) {
setBarkBattleError(
resolvePuzzleErrorMessage(error, '读取汪汪声浪作品列表失败。'),
);
return [];
} finally {
setIsBarkBattleBusy(false);
}
}, [resolvePuzzleErrorMessage]);
const refreshBarkBattleGallery = useCallback(async () => {
try {
const galleryResponse = await listBarkBattleGallery();
setBarkBattleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch {
// 中文注释:公开广场读取失败只降级为空列表,避免阻断平台首页。
setBarkBattleGalleryEntries([]);
return [];
}
}, []);
const syncUpdatedPublicWorkDetail = useCallback(
(updatedEntry: PlatformPublicGalleryCard) => {
setSelectedPublicWorkDetail((current) =>
current && isSamePlatformPublicGalleryEntry(current, updatedEntry)
? updatedEntry
: current,
);
},
[],
);
const handleRpgDraftGenerationStarted = useCallback(
(sessionId: string) => {
markDraftGenerating('rpg', [sessionId, `draft:${sessionId}`]);
},
[markDraftGenerating],
);
const updateBarkBattleWorkCaches = useCallback(
(updated: BarkBattleWorkSummary) => {
setBarkBattleWorks((current) => {
const deduped = mergeBarkBattleWorksByWorkId(current);
const hasExisting = deduped.some(
(item) => item.workId === updated.workId,
);
if (hasExisting) {
return deduped.map((item) =>
mergeBarkBattleWorkSummary(item, updated),
);
}
return [updated, ...deduped];
});
setBarkBattleGalleryEntries((current) => {
const hasExisting = current.some(
(item) => item.workId === updated.workId,
);
if (updated.status !== 'published') {
return hasExisting
? current.map((item) => mergeBarkBattleWorkSummary(item, updated))
: current;
}
if (hasExisting) {
return current.map((item) =>
mergeBarkBattleWorkSummary(item, updated),
);
}
return [updated, ...current];
});
if (updated.status === 'published') {
syncUpdatedPublicWorkDetail(
mapBarkBattleWorkToPublicWorkDetail(updated),
);
}
},
[syncUpdatedPublicWorkDetail],
);
const handleRpgDraftGenerationCompleted = useCallback(
({
sessionId,
profileId,
viewedImmediately,
}: {
sessionId: string;
profileId: string | null;
viewedImmediately: boolean;
}) => {
markDraftReady(
'rpg',
[sessionId, `draft:${sessionId}`, profileId],
viewedImmediately,
);
void platformBootstrap.refreshCustomWorldWorks().catch(() => undefined);
},
[markDraftReady, platformBootstrap],
);
const refreshBabyObjectMatchShelf = useCallback(async () => {
setBabyObjectMatchDrafts(await listLocalBabyObjectMatchDrafts());
}, []);
useEffect(() => {
if (!platformBootstrap.canReadProtectedData) {
setBarkBattleWorks([]);
return;
}
if (
platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform'
) {
void refreshBarkBattleShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshBarkBattleShelf,
selectionStage,
]);
useEffect(() => {
if (!platformBootstrap.canReadProtectedData) {
setJumpHopWorks([]);
return;
}
if (
platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform'
) {
void refreshJumpHopShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshJumpHopShelf,
selectionStage,
]);
const sessionController = useRpgCreationSessionController({
userId: authUi?.user?.id,
openLoginModal: authUi?.openLoginModal,
selectionStage,
setSelectionStage,
enterCreateTab,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onDraftGenerationStarted: handleRpgDraftGenerationStarted,
onDraftGenerationCompleted: handleRpgDraftGenerationCompleted,
});
useRpgCreationAgentOperationPolling({
activeAgentSessionId: sessionController.activeAgentSessionId,
activeAgentOperationId: sessionController.activeAgentOperationId,
userId: authUi?.user?.id,
setAgentOperation: sessionController.setAgentOperation,
persistAgentUiState: sessionController.persistAgentUiState,
syncAgentSessionSnapshot: sessionController.syncAgentSessionSnapshot,
});
const autosaveCoordinator = useRpgCreationResultAutosave({
selectionStage,
activeAgentSessionId: sessionController.activeAgentSessionId,
generatedCustomWorldProfile: sessionController.generatedCustomWorldProfile,
isAgentDraftResultView: sessionController.isAgentDraftResultView,
userId: authUi?.user?.id,
setGeneratedCustomWorldProfile:
sessionController.setGeneratedCustomWorldProfile,
setAgentOperation: sessionController.setAgentOperation,
setSavedCustomWorldEntries: platformBootstrap.setSavedCustomWorldEntries,
setSelectedDetailEntry,
refreshCustomWorldWorks: platformBootstrap.refreshCustomWorldWorks,
persistAgentUiState: sessionController.persistAgentUiState,
syncAgentSessionSnapshot: sessionController.syncAgentSessionSnapshot,
syncAgentCreationResultView: sessionController.syncAgentCreationResultView,
buildDraftResultProfile: (view) =>
rpgCreationPreviewAdapter.buildPreviewFromResultView(view),
});
const detailNavigation = usePlatformEntryLibraryDetail({
userId: authUi?.user?.id,
selectedDetailEntry,
setSelectedDetailEntry,
savedCustomWorldEntries: platformBootstrap.savedCustomWorldEntries,
setSavedCustomWorldEntries: platformBootstrap.setSavedCustomWorldEntries,
setGeneratedCustomWorldProfile:
sessionController.setGeneratedCustomWorldProfile,
setCustomWorldError: sessionController.setCustomWorldError,
setCustomWorldAutoSaveError:
autosaveCoordinator.setCustomWorldAutoSaveError,
setCustomWorldAutoSaveState:
autosaveCoordinator.setCustomWorldAutoSaveState,
setCustomWorldGenerationViewSource:
sessionController.setCustomWorldGenerationViewSource,
setCustomWorldResultViewSource:
sessionController.setCustomWorldResultViewSource,
setSelectionStage,
setPlatformTabToCreate: enterCreateTab,
setPlatformTabToDraft: enterDraftTab,
setPlatformError: platformBootstrap.setPlatformError,
appendBrowseHistoryEntry: platformBootstrap.appendBrowseHistoryEntry,
refreshCustomWorldWorks: platformBootstrap.refreshCustomWorldWorks,
refreshPublishedGallery: platformBootstrap.refreshPublishedGallery,
persistAgentUiState: sessionController.persistAgentUiState,
syncAgentCreationResultView: sessionController.syncAgentCreationResultView,
buildDraftResultProfile: (view) =>
rpgCreationPreviewAdapter.buildPreviewFromResultView(view),
suppressAgentDraftResultAutoOpen:
sessionController.suppressAgentDraftResultAutoOpen,
releaseAgentDraftResultAutoOpenSuppression:
sessionController.releaseAgentDraftResultAutoOpenSuppression,
resetAutoSaveTrackingToIdle:
autosaveCoordinator.resetAutoSaveTrackingToIdle,
markAutoSavedProfile: autosaveCoordinator.markAutoSavedProfile,
});
const enterWorldCoordinator = useRpgCreationEnterWorld({
isAgentDraftResultView: sessionController.isAgentDraftResultView,
activeAgentSessionId: sessionController.activeAgentSessionId,
currentAgentSessionStage: sessionController.agentSession?.stage ?? null,
generatedCustomWorldProfile: sessionController.generatedCustomWorldProfile,
handleCustomWorldSelect,
syncAgentDraftResultProfile:
autosaveCoordinator.syncAgentDraftResultProfile,
executePublishWorld: async () => {
const latestSession = await autosaveCoordinator.executeAgentActionAndWait(
{
action: 'publish_world',
},
);
// 发布动作会在后端同步 gallery 投影;前端发布完成后立即刷新首页/分类页共用的公开作品列表。
await Promise.allSettled([
platformBootstrap.refreshPublishedGallery(),
platformBootstrap.refreshCustomWorldWorks(),
isBigFishCreationVisible
? refreshBigFishGallery()
: Promise.resolve([] as BigFishWorkSummary[]),
refreshMatch3DGallery(),
refreshPuzzleGallery(),
refreshBarkBattleGallery(),
]);
return latestSession;
},
syncAgentCreationResultView: sessionController.syncAgentCreationResultView,
setGeneratedCustomWorldProfile:
sessionController.setGeneratedCustomWorldProfile,
});
const previewCustomWorldCharacters = useMemo(
() =>
sessionController.generatedCustomWorldProfile
? buildCustomWorldPlayableCharacters(
sessionController.generatedCustomWorldProfile,
)
: [],
[sessionController.generatedCustomWorldProfile],
);
const agentResultPreview =
sessionController.agentSession?.resultPreview ?? null;
const agentResultPreviewBlockers = useMemo(
() => agentResultPreview?.blockers ?? [],
[agentResultPreview],
);
const agentResultPublishGateView = useMemo(
() =>
buildPlatformRpgAgentResultPublishGateView(
sessionController.generatedCustomWorldProfile,
agentResultPreviewBlockers,
Boolean(agentResultPreview?.publishReady),
),
[
agentResultPreview?.publishReady,
agentResultPreviewBlockers,
sessionController.generatedCustomWorldProfile,
],
);
const agentResultPreviewQualityFindings = useMemo(
() => agentResultPreview?.qualityFindings ?? [],
[agentResultPreview],
);
const openPublishShareModal = useCallback(
(payload: PublishShareModalPayload) => {
const publicWorkCode = payload.publicWorkCode.trim();
if (!publicWorkCode) {
return;
}
setPublishSharePayload({
...payload,
publicWorkCode,
title: payload.title.trim() || '我的作品',
});
},
[],
);
const openRecommendShareModal = useCallback(
(entry: PlatformPublicGalleryCard) => {
const publicWorkCode = resolvePlatformPublicWorkCode(entry)?.trim();
if (!publicWorkCode) {
return;
}
postRecommendEntryMiniProgramShareTarget(entry);
openPublishShareModal({
title: entry.worldName,
publicWorkCode,
stage: resolveRecommendEntryShareStage(entry),
workTypeLabel: describePublicGalleryCardKind(entry),
coverImageSrc: resolvePlatformWorldCoverImage(entry),
fallbackCoverImageSrc: resolvePlatformWorldFallbackCoverImage(entry),
});
},
[openPublishShareModal],
);
const openRpgPublishShareModal = useCallback(
async (profile: CustomWorldProfile | null | undefined) => {
const profileId = profile?.id?.trim();
if (!profileId) {
return;
}
const profileName = profile?.name?.trim() || '我的作品';
const galleryEntries = await platformBootstrap
.refreshPublishedGallery()
.catch(() => [] as CustomWorldGalleryCard[]);
const galleryEntry = galleryEntries.find(
(entry) => entry.profileId === profileId,
);
const publicWorkCode = galleryEntry?.publicWorkCode?.trim();
if (!publicWorkCode) {
return;
}
openPublishShareModal({
title: galleryEntry?.worldName || profileName,
publicWorkCode,
stage: 'work-detail',
});
},
[openPublishShareModal, platformBootstrap],
);
const agentResultPreviewSourceLabel = useMemo(() => {
return resolvePlatformRpgAgentResultPreviewSourceLabel(
agentResultPreview?.source,
);
}, [agentResultPreview]);
const publicGalleryFeeds = useMemo(
() =>
buildPlatformPublicGalleryFeeds({
rpgEntries: platformBootstrap.publishedGalleryEntries,
bigFishEntries: bigFishGalleryEntries,
match3dEntries: match3dGalleryEntries,
puzzleEntries: puzzleGalleryEntries,
puzzleClearEntries: puzzleClearGalleryEntries,
barkBattleGalleryEntries,
barkBattleWorks,
jumpHopEntries: jumpHopGalleryEntries,
woodenFishEntries: woodenFishGalleryEntries,
squareHoleEntries: squareHoleGalleryEntries,
visualNovelEntries: visualNovelGalleryEntries,
babyObjectMatchDrafts,
isBigFishCreationVisible,
isBabyObjectMatchVisible,
isVisualNovelCreationOpen,
}),
[
babyObjectMatchDrafts,
barkBattleGalleryEntries,
barkBattleWorks,
bigFishGalleryEntries,
isBabyObjectMatchVisible,
isBigFishCreationVisible,
isVisualNovelCreationOpen,
jumpHopGalleryEntries,
match3dGalleryEntries,
platformBootstrap.publishedGalleryEntries,
puzzleClearGalleryEntries,
puzzleGalleryEntries,
squareHoleGalleryEntries,
visualNovelGalleryEntries,
woodenFishGalleryEntries,
],
);
const {
featuredEntries: featuredGalleryEntries,
latestEntries: latestGalleryEntries,
} = publicGalleryFeeds;
const recommendRuntimeEntries = useMemo(
() =>
buildPlatformRecommendFeedEntries(
featuredGalleryEntries,
latestGalleryEntries,
),
[featuredGalleryEntries, latestGalleryEntries],
);
const creationHubItems = useMemo<CustomWorldWorkSummary[]>(
() =>
platformBootstrap.customWorldWorkEntries.length > 0
? platformBootstrap.customWorldWorkEntries
: buildCreationHubFallbackItems(
platformBootstrap.savedCustomWorldEntries,
),
[
platformBootstrap.customWorldWorkEntries,
platformBootstrap.savedCustomWorldEntries,
],
);
const bigFishShelfItems = useMemo(
() => [
...buildPendingBigFishWorks(
pendingDraftShelfItems['big-fish'],
bigFishWorks,
),
...bigFishWorks,
],
[bigFishWorks, pendingDraftShelfItems],
);
const jumpHopShelfItems = useMemo(
() => [
...buildPendingJumpHopWorks(
pendingDraftShelfItems['jump-hop'],
jumpHopWorks,
),
...jumpHopWorks,
],
[jumpHopWorks, pendingDraftShelfItems],
);
const puzzleClearShelfItems = useMemo(
() => [
...buildPendingPuzzleClearWorks(
pendingDraftShelfItems['puzzle-clear'],
puzzleClearWorks,
),
...puzzleClearWorks,
],
[pendingDraftShelfItems, puzzleClearWorks],
);
const woodenFishShelfItems = useMemo(
() => [
...buildPendingWoodenFishWorks(
pendingDraftShelfItems['wooden-fish'],
woodenFishWorks,
),
...woodenFishWorks,
],
[pendingDraftShelfItems, woodenFishWorks],
);
const match3dShelfItems = useMemo(
() => [
...buildPendingMatch3DWorks(pendingDraftShelfItems.match3d, match3dWorks),
...match3dWorks,
],
[match3dWorks, pendingDraftShelfItems],
);
const squareHoleShelfItems = useMemo(
() => [
...buildPendingSquareHoleWorks(
pendingDraftShelfItems['square-hole'],
squareHoleWorks,
),
...squareHoleWorks,
],
[pendingDraftShelfItems, squareHoleWorks],
);
const puzzleShelfItems = useMemo(
() => [
...buildPendingPuzzleWorks(pendingDraftShelfItems.puzzle, puzzleWorks),
...puzzleWorks,
],
[pendingDraftShelfItems, puzzleWorks],
);
const visualNovelShelfItems = useMemo(
() =>
isVisualNovelCreationOpen
? [
...buildPendingVisualNovelWorks(
pendingDraftShelfItems['visual-novel'],
visualNovelWorks,
),
...visualNovelWorks,
]
: [],
[isVisualNovelCreationOpen, pendingDraftShelfItems, visualNovelWorks],
);
const barkBattleShelfItems = useMemo(
() => [
...buildPendingBarkBattleWorks(
pendingDraftShelfItems['bark-battle'],
barkBattleWorks,
),
...barkBattleWorks,
],
[barkBattleWorks, pendingDraftShelfItems],
);
const getCreationWorkShelfState = useCallback(
(item: CreationWorkShelfItem) =>
buildCreationWorkShelfRuntimeState({
item,
notices: draftGenerationNotices,
pendingShelfItems: pendingDraftShelfItems,
}),
[draftGenerationNotices, pendingDraftShelfItems],
);
const visibleDraftNoticeKeys = useMemo(
() =>
collectVisibleDraftNoticeKeys({
rpgItems: creationHubItems,
bigFishItems: bigFishShelfItems,
jumpHopItems: jumpHopShelfItems,
woodenFishItems: woodenFishShelfItems,
match3dItems: match3dShelfItems,
squareHoleItems: isSquareHoleCreationVisible
? squareHoleShelfItems
: [],
puzzleItems: puzzleShelfItems,
visualNovelItems: visualNovelShelfItems,
barkBattleItems: barkBattleShelfItems,
babyObjectMatchItems: babyObjectMatchDrafts,
}),
[
babyObjectMatchDrafts,
barkBattleShelfItems,
bigFishShelfItems,
jumpHopShelfItems,
puzzleClearShelfItems,
woodenFishShelfItems,
creationHubItems,
isSquareHoleCreationVisible,
match3dShelfItems,
puzzleShelfItems,
squareHoleShelfItems,
visualNovelShelfItems,
],
);
const hasUnreadDraftUpdates = useMemo(
() =>
hasUnreadDraftGenerationUpdates(
draftGenerationNotices,
visibleDraftNoticeKeys,
),
[draftGenerationNotices, visibleDraftNoticeKeys],
);
const resultViewError =
autosaveCoordinator.customWorldAutoSaveError ??
sessionController.customWorldError;
const selectedPublicWorkActionMode = selectedPublicWorkDetail
? resolvePlatformPublicWorkActionMode(
selectedPublicWorkDetail,
authUi?.user?.id,
)
: 'remix';
const isSelectedPublicWorkOwned = selectedPublicWorkActionMode === 'edit';
useEffect(() => {
if (
selectionStage === 'custom-world-result' &&
!sessionController.generatedCustomWorldProfile
) {
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
}
}, [
selectedDetailEntry,
selectionStage,
sessionController.generatedCustomWorldProfile,
setSelectionStage,
]);
useEffect(() => {
const progressTickDecision = resolvePlatformGenerationProgressTickDecision({
selectionStage,
miniGameStates: {
puzzle: puzzleGenerationState,
match3d: match3dGenerationState,
'big-fish': bigFishGenerationState,
'square-hole': squareHoleGenerationState,
'jump-hop': jumpHopGenerationState,
'wooden-fish': woodenFishGenerationState,
'baby-object-match': babyObjectMatchGenerationState,
},
visualNovel: {
startedAtMs: visualNovelGenerationStartedAtMs,
phase: visualNovelGenerationPhase,
},
});
if (!progressTickDecision.shouldTick) {
return undefined;
}
setMiniGameGenerationProgressNowMs(Date.now());
const timerId = window.setInterval(() => {
setMiniGameGenerationProgressNowMs(Date.now());
}, 500);
return () => window.clearInterval(timerId);
}, [
babyObjectMatchGenerationState,
bigFishGenerationState,
jumpHopGenerationState,
match3dGenerationState,
puzzleGenerationState,
puzzleClearGenerationState,
squareHoleGenerationState,
woodenFishGenerationState,
selectionStage,
visualNovelGenerationPhase,
visualNovelGenerationStartedAtMs,
]);
const runProtectedAction = useCallback(
(action: () => void) => {
if (!authUi?.requireAuth) {
action();
return;
}
authUi.requireAuth(action);
},
[authUi],
);
const savePuzzleOnboardingDraft = useCallback(async () => {
if (!puzzleOnboardingDraft || isPuzzleOnboardingSaving) {
return;
}
setIsPuzzleOnboardingSaving(true);
setPuzzleOnboardingError(null);
try {
const response = await savePuzzleOnboardingWork({
promptText: puzzleOnboardingDraft.promptText,
item: puzzleOnboardingDraft.item,
});
setPuzzleWorks((current) => [response.item, ...current]);
setSelectedPuzzleDetail(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleOnboardingDraft(null);
setPuzzleOnboardingPrompt('');
setPuzzleOnboardingPhase('input');
platformBootstrap.setPlatformTab('home');
setSelectionStage('platform');
clearPuzzleRuntimeUrlState();
void refreshPuzzleShelf();
} catch (error) {
setPuzzleOnboardingError(
resolvePuzzleErrorMessage(error, '保存新手引导拼图失败。'),
);
} finally {
setIsPuzzleOnboardingSaving(false);
}
}, [
isPuzzleOnboardingSaving,
platformBootstrap,
puzzleOnboardingDraft,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
const requestPuzzleOnboardingLogin = useCallback(() => {
if (isPuzzleOnboardingSaving) {
return;
}
authUi?.openLoginModal(() => {
void savePuzzleOnboardingDraft();
});
}, [authUi, isPuzzleOnboardingSaving, savePuzzleOnboardingDraft]);
const skipPuzzleOnboarding = useCallback(() => {
markPuzzleOnboardingSeen();
setPuzzleOnboardingDraft(null);
setPuzzleOnboardingPrompt('');
setPuzzleOnboardingPhase('input');
setPuzzleOnboardingError(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setSelectedPuzzleDetail(null);
platformBootstrap.setPlatformTab(authUi?.user ? 'home' : 'category');
setSelectionStage('platform');
clearPuzzleRuntimeUrlState();
}, [authUi?.user, platformBootstrap, setSelectionStage]);
useEffect(() => {
if (
!authUi ||
authUi?.user ||
selectionStage !== 'platform' ||
hasSeenPuzzleOnboarding()
) {
return;
}
setPuzzleOnboardingPhase('input');
setPuzzleOnboardingError(null);
setSelectionStage('puzzle-onboarding');
}, [authUi, authUi?.user, selectionStage, setSelectionStage]);
const submitPuzzleOnboardingPrompt = useCallback(async () => {
const promptText = puzzleOnboardingPrompt.trim();
if (!promptText || puzzleOnboardingPhase === 'generating') {
return;
}
setPuzzleOnboardingPhase('generating');
setPuzzleOnboardingError(null);
try {
let item: PuzzleWorkSummary;
try {
const response = await generatePuzzleOnboardingWork({ promptText });
item = {
...response.item,
levels:
response.item.levels && response.item.levels.length > 0
? response.item.levels
: [response.level],
};
} catch (error) {
if (!shouldUseLocalPuzzleOnboardingFallback(error)) {
throw error;
}
// 中文注释:旧后端或资源路由尚未更新时,首访体验继续用前端临时图面兜底,不写入作品库。
item = buildPuzzleOnboardingFallbackWork(promptText);
}
setPuzzleOnboardingDraft({ promptText, item });
setSelectedPuzzleDetail(item);
setPuzzleOnboardingPhase('generated');
markPuzzleOnboardingSeen();
window.setTimeout(() => {
setPuzzleRun(startLocalPuzzleRun(item));
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('platform');
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzleDraftRuntimeUrlState(item, null),
);
}, PUZZLE_ONBOARDING_GENERATED_DELAY_MS);
} catch (error) {
setPuzzleOnboardingPhase('input');
setPuzzleOnboardingError(
resolvePuzzleErrorMessage(error, '生成新手引导拼图失败。'),
);
}
}, [
puzzleOnboardingPhase,
puzzleOnboardingPrompt,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
const requestDeleteCreationWork = useCallback(
(confirmation: DeleteCreationWorkConfirmation) => {
if (deletingCreationWorkId) {
return;
}
runProtectedAction(() => {
setPendingDeleteCreationWork(confirmation);
});
},
[deletingCreationWorkId, runProtectedAction],
);
const closeDeleteCreationWorkConfirmation = useCallback(() => {
if (deletingCreationWorkId) {
return;
}
setPendingDeleteCreationWork(null);
}, [deletingCreationWorkId]);
const confirmDeleteCreationWork = useCallback(() => {
const confirmation = pendingDeleteCreationWork;
if (!confirmation || deletingCreationWorkId) {
return;
}
setPendingDeleteCreationWork(null);
confirmation.run();
}, [deletingCreationWorkId, pendingDeleteCreationWork]);
const prepareCreationLaunch = useCallback(() => {
if (sessionController.isCreatingAgentSession) {
return false;
}
if (!hasSavedGame) {
handleStartNewGame();
}
markCreationFlowReturnToCreate();
sessionController.setCreationTypeError(null);
return true;
}, [
handleStartNewGame,
hasSavedGame,
markCreationFlowReturnToCreate,
sessionController,
]);
const openCreationTypePicker = useCallback(() => {
setSelectionStage('creation-home');
}, [setSelectionStage]);
const openProjectGallery = useCallback(() => {
setSelectionStage('project');
}, [setSelectionStage]);
const openCreationCommunity = useCallback(() => {
setIsCreationCommunityOpen(true);
}, []);
const openEditorProject = useCallback((projectId: string) => {
setSelectionStage('image-editor', {
path: `/editor/canvas?projectid=${encodeURIComponent(projectId)}`,
});
}, [setSelectionStage]);
const openCreationTypeModal = useCallback(() => {
if (!prepareCreationLaunch()) {
return;
}
setShowCreationTypeModal(true);
}, [prepareCreationLaunch]);
const bigFishFlow = usePlatformCreationAgentFlowController<
BigFishSessionSnapshotResponse,
Record<string, never>,
{ session: BigFishSessionSnapshotResponse },
SendBigFishMessageRequest,
ExecuteBigFishActionRequest,
{ session: BigFishSessionSnapshotResponse }
>({
client: {
createSession: createBigFishCreationSession,
getSession: getBigFishCreationSession,
streamMessage: streamBigFishCreationMessage,
executeAction: executeBigFishCreationAction,
selectSession: (response) => response.session,
},
createPayload: {},
workspaceStage: 'big-fish-agent-workspace',
resultStage: 'big-fish-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.action === 'big_fish_compile_draft',
resolveErrorMessage: resolveBigFishErrorMessage,
errorMessages: {
open: '开启大鱼吃小鱼共创工作台失败。',
restoreMissingSession: '这份大鱼吃小鱼草稿缺少会话信息,请重新开始创作。',
restore: '读取大鱼吃小鱼创作草稿失败。',
submit: '发送大鱼吃小鱼共创消息失败。',
execute: '执行大鱼吃小鱼操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onSessionChanged: (session) => {
writeCreationUrlState(buildBigFishCreationUrlState(session));
},
onActionComplete: ({ payload, response, setSession }) => {
setSession(response.session);
if (payload.action === 'big_fish_publish_game') {
void refreshBigFishShelf();
void refreshBigFishGallery();
openPublishShareModal({
title: response.session.draft?.title ?? '大鱼吃小鱼',
publicWorkCode: buildBigFishPublicWorkCode(
response.session.sessionId,
),
stage: 'big-fish-runtime',
});
}
if (payload.action !== 'big_fish_compile_draft') {
return;
}
setBigFishGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount: response.session.assetSlots.filter(
(slot) => slot.status === 'ready',
).length,
totalAssetCount: response.session.assetSlots.length,
})
: current,
);
const openResult = selectionStageRef.current === 'big-fish-generating';
const noticeIds = [
`big-fish-work-${response.session.sessionId}`,
response.session.sessionId,
];
markPendingDraftReady('big-fish', response.session.sessionId, openResult);
markDraftReady('big-fish', noticeIds, openResult);
void refreshBigFishShelf();
return { openResult };
},
beforeExecuteAction: ({ payload, session }) => {
if (payload.action !== 'big_fish_compile_draft') {
return;
}
markDraftGenerating('big-fish', [
`big-fish-work-${session.sessionId}`,
session.sessionId,
]);
markPendingDraftGenerating('big-fish', session.sessionId);
selectionStageRef.current = 'big-fish-generating';
setSelectionStage('big-fish-generating');
setBigFishGenerationState(createMiniGameDraftGenerationState('big-fish'));
},
onActionError: ({ payload, errorMessage, session }) => {
if (payload.action !== 'big_fish_compile_draft') {
return;
}
setBigFishGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'failed', {
error: errorMessage,
})
: current,
);
markPendingDraftFailed('big-fish', session.sessionId);
markDraftFailed(
'big-fish',
[`big-fish-work-${session.sessionId}`, session.sessionId],
errorMessage,
);
void refreshBigFishShelf();
},
});
const match3dRuntimeAdapter = useMemo(
() => createServerMatch3DRuntimeAdapter(),
[],
);
const resolveMatch3DRuntimeAdapter = useCallback(
(_profileId: string | null | undefined) => match3dRuntimeAdapter,
[match3dRuntimeAdapter],
);
const match3dFlow = usePlatformCreationAgentFlowController<
Match3DAgentSessionSnapshot,
CreateMatch3DSessionRequest,
Match3DSessionResponse,
SendMatch3DMessageRequest,
ExecuteMatch3DActionRequest,
Match3DActionResponse
>({
client: {
createSession: match3dCreationClient.createSession,
getSession: match3dCreationClient.getSession,
streamMessage: match3dCreationClient.streamMessage,
executeAction: match3dCreationClient.executeAction,
selectSession: (response) => response.session,
},
createPayload: {},
workspaceStage: 'match3d-agent-workspace',
resultStage: 'match3d-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.action === 'match3d_compile_draft',
resolveErrorMessage: resolveMatch3DErrorMessage,
errorMessages: {
open: '开启抓大鹅共创工作台失败。',
restoreMissingSession: '这份抓大鹅草稿缺少会话信息,请重新开始创作。',
restore: '读取抓大鹅创作草稿失败。',
submit: '发送抓大鹅共创消息失败。',
execute: '执行抓大鹅操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onSessionChanged: (session) => {
writeCreationUrlState(buildMatch3DCreationUrlState(session));
},
onActionComplete: async ({ payload, response, setSession }) => {
setSession(response.session);
if (payload.action !== 'match3d_compile_draft') {
return;
}
const openResult = selectionStageRef.current === 'match3d-generating';
setMatch3DGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
totalAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
})
: current,
);
const profileId = response.session.draft?.profileId;
if (!profileId) {
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
return { openResult };
}
let runtimeProfile: Match3DWorkProfile | null = null;
try {
const { item } = await getMatch3DWorkDetail(profileId);
runtimeProfile = normalizeMatch3DWorkForRuntimeUi({
...item,
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
response.session.draft?.generatedItemAssets,
item.generatedItemAssets,
),
});
setMatch3DProfile(runtimeProfile);
await refreshMatch3DShelf().catch(() => undefined);
} catch {
runtimeProfile = buildMatch3DProfileFromSession(response.session);
setMatch3DProfile(runtimeProfile);
}
markPendingDraftReady('match3d', response.session.sessionId, openResult);
markDraftReady(
'match3d',
[profileId, response.session.sessionId],
openResult,
);
if (openResult && runtimeProfile) {
try {
await preloadMatch3DGeneratedRuntimeAssets(
runtimeProfile.generatedItemAssets,
runtimeProfile.generatedBackgroundAsset,
{ expireSeconds: 300 },
);
const { run } = await match3dRuntimeAdapter.startRun(
runtimeProfile.profileId,
);
setMatch3DRuntimeProfile(runtimeProfile);
setMatch3DRun(run);
setMatch3DProfile(runtimeProfile);
setMatch3DRuntimeReturnStage('match3d-result');
setSelectionStage('match3d-runtime');
} catch (error) {
setMatch3DError(
resolveMatch3DErrorMessage(error, '启动抓大鹅玩法失败。'),
);
setSelectionStage('match3d-result');
}
return { openResult: false };
}
return { openResult };
},
beforeExecuteAction: ({ payload, session }) => {
if (payload.action !== 'match3d_compile_draft') {
return;
}
markDraftGenerating('match3d', [
session.draft?.profileId,
session.publishedProfileId,
session.sessionId,
]);
markPendingDraftGenerating('match3d', session.sessionId);
selectionStageRef.current = 'match3d-generating';
setSelectionStage('match3d-generating');
setMatch3DGenerationState(
createMiniGameDraftGenerationState(
'match3d',
resolveMiniGameDraftGenerationStartedAtMs(session.updatedAt),
),
);
},
onActionError: async ({ payload, errorMessage, session, setSession }) => {
if (payload.action !== 'match3d_compile_draft') {
return;
}
if (isBackgroundGenerationStillRunningMessage(errorMessage)) {
try {
const { session: latestSession } =
await match3dCreationClient.getSession(session.sessionId);
setSession(latestSession);
const profileId =
latestSession.draft?.profileId ?? latestSession.publishedProfileId;
if (profileId) {
const { item } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(item));
}
markPendingDraftGenerating('match3d', latestSession.sessionId);
markDraftGenerating('match3d', [
latestSession.draft?.profileId,
latestSession.publishedProfileId,
latestSession.sessionId,
]);
await refreshMatch3DShelf().catch(() => undefined);
return;
} catch {
await refreshMatch3DShelf().catch(() => undefined);
}
}
setMatch3DGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'failed', {
error: errorMessage,
})
: current,
);
markPendingDraftFailed('match3d', session.sessionId);
markDraftFailed(
'match3d',
[
session.draft?.profileId,
session.publishedProfileId,
session.sessionId,
],
errorMessage,
);
try {
const { session: latestSession } =
await match3dCreationClient.getSession(session.sessionId);
setSession(latestSession);
const profileId =
latestSession.draft?.profileId ?? latestSession.publishedProfileId;
if (profileId) {
const { item } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(item));
}
await refreshMatch3DShelf().catch(() => undefined);
} catch {
await refreshMatch3DShelf().catch(() => undefined);
}
},
});
const squareHoleFlow = usePlatformCreationAgentFlowController<
SquareHoleSessionSnapshot,
CreateSquareHoleSessionRequest,
SquareHoleSessionResponse,
SendSquareHoleMessageRequest,
ExecuteSquareHoleActionRequest,
SquareHoleActionResponse
>({
client: {
createSession: squareHoleCreationClient.createSession,
getSession: squareHoleCreationClient.getSession,
streamMessage: squareHoleCreationClient.streamMessage,
executeAction: squareHoleCreationClient.executeAction,
selectSession: (response) => response.session,
},
createPayload: {},
workspaceStage: 'square-hole-agent-workspace',
resultStage: 'square-hole-result',
platformStage: 'platform',
isCompileAction: () => false,
resolveErrorMessage: resolveSquareHoleErrorMessage,
errorMessages: {
open: '开启方洞挑战共创工作台失败。',
restoreMissingSession: '这份方洞挑战草稿缺少会话信息,请重新开始创作。',
restore: '读取方洞挑战创作草稿失败。',
submit: '发送方洞挑战共创消息失败。',
execute: '执行方洞挑战操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onSessionChanged: (session) => {
writeCreationUrlState(buildSquareHoleCreationUrlState(session));
},
beforeExecuteAction: ({ payload, session }) => {
if (payload.action === 'square_hole_compile_draft') {
markDraftGenerating('square-hole', [
session.draft?.profileId,
session.publishedProfileId,
session.sessionId,
]);
markPendingDraftGenerating('square-hole', session.sessionId);
setSquareHoleGenerationState(
createMiniGameDraftGenerationState('square-hole'),
);
selectionStageRef.current = 'square-hole-generating';
setSelectionStage('square-hole-generating');
}
if (payload.action === 'square_hole_generate_visual_assets') {
markDraftGenerating('square-hole', [
session.draft?.profileId,
session.publishedProfileId,
session.sessionId,
]);
markPendingDraftGenerating('square-hole', session.sessionId);
setSquareHoleGenerationState((current) => ({
...(current ?? createMiniGameDraftGenerationState('square-hole')),
phase: 'square-hole-cover',
completedAssetCount: 0,
totalAssetCount: 0,
error: null,
}));
selectionStageRef.current = 'square-hole-generating';
setSelectionStage('square-hole-generating');
}
},
onActionComplete: async ({ payload, response, setSession }) => {
setSession(response.session);
if (
payload.action !== 'square_hole_compile_draft' &&
payload.action !== 'square_hole_generate_visual_assets'
) {
return;
}
const shouldOpenSquareHoleResult = () =>
selectionStageRef.current === 'square-hole-generating';
const profileId = response.session.draft?.profileId;
if (!profileId) {
setSquareHoleProfile(null);
markPendingDraftReady(
'square-hole',
response.session.sessionId,
shouldOpenSquareHoleResult(),
);
markDraftReady(
'square-hole',
[response.session.sessionId, response.session.publishedProfileId],
shouldOpenSquareHoleResult(),
);
return;
}
if (payload.action === 'square_hole_compile_draft') {
try {
const assetResponse = await squareHoleCreationClient.executeAction(
response.session.sessionId,
{
action: 'square_hole_generate_visual_assets',
},
);
setSession(assetResponse.session);
const assetProfileId = assetResponse.session.draft?.profileId;
if (!assetProfileId) {
const shouldOpenResult = shouldOpenSquareHoleResult();
setSquareHoleProfile(
buildSquareHoleProfileFromSession(assetResponse.session),
);
markPendingDraftReady(
'square-hole',
assetResponse.session.sessionId,
shouldOpenResult,
);
markDraftReady(
'square-hole',
[
assetResponse.session.sessionId,
assetResponse.session.publishedProfileId,
],
shouldOpenResult,
);
if (shouldOpenResult) {
setSelectionStage('square-hole-result');
}
return;
}
const { item } = await getSquareHoleWorkDetail(assetProfileId);
const shouldOpenResult = shouldOpenSquareHoleResult();
setSquareHoleProfile(item);
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'ready',
{
completedAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
totalAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
error: null,
},
),
);
await refreshSquareHoleShelf().catch(() => undefined);
markPendingDraftReady(
'square-hole',
assetResponse.session.sessionId,
shouldOpenResult,
);
markDraftReady(
'square-hole',
[assetProfileId, assetResponse.session.sessionId],
shouldOpenResult,
);
if (shouldOpenResult) {
setSelectionStage('square-hole-result');
}
} catch (error) {
const shouldOpenResult = shouldOpenSquareHoleResult();
const errorMessage = resolveSquareHoleErrorMessage(
error,
'生成方洞挑战图片失败。',
);
setSquareHoleError(errorMessage);
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'failed',
{ error: errorMessage },
),
);
setSquareHoleProfile(
buildSquareHoleProfileFromSession(response.session),
);
markPendingDraftFailed('square-hole', response.session.sessionId);
markDraftFailed(
'square-hole',
[
response.session.draft?.profileId,
response.session.publishedProfileId,
response.session.sessionId,
],
errorMessage,
);
void refreshSquareHoleShelf().catch(() => undefined);
if (shouldOpenResult) {
setSelectionStage('square-hole-result');
}
}
return;
}
try {
const { item } = await getSquareHoleWorkDetail(profileId);
const shouldOpenResult = shouldOpenSquareHoleResult();
setSquareHoleProfile(item);
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'ready',
{
completedAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
totalAssetCount:
item.shapeOptions.length + item.holeOptions.length + 2,
error: null,
},
),
);
await refreshSquareHoleShelf().catch(() => undefined);
markPendingDraftReady(
'square-hole',
response.session.sessionId,
shouldOpenResult,
);
markDraftReady(
'square-hole',
[profileId, response.session.sessionId],
shouldOpenResult,
);
if (shouldOpenResult) {
setSelectionStage('square-hole-result');
}
} catch {
const shouldOpenResult = shouldOpenSquareHoleResult();
setSquareHoleProfile(
buildSquareHoleProfileFromSession(response.session),
);
markPendingDraftReady(
'square-hole',
response.session.sessionId,
shouldOpenResult,
);
markDraftReady(
'square-hole',
[profileId, response.session.sessionId],
shouldOpenResult,
);
if (shouldOpenResult) {
setSelectionStage('square-hole-result');
}
}
},
onActionError: ({ payload, errorMessage, session }) => {
if (
payload.action === 'square_hole_compile_draft' ||
payload.action === 'square_hole_generate_visual_assets'
) {
setSquareHoleGenerationState((current) =>
resolveFinishedMiniGameDraftGenerationState(
current ?? createMiniGameDraftGenerationState('square-hole'),
'failed',
{ error: errorMessage },
),
);
if (selectionStageRef.current === 'square-hole-generating') {
setSelectionStage('square-hole-generating');
}
markPendingDraftFailed('square-hole', session.sessionId);
markDraftFailed(
'square-hole',
[
session.draft?.profileId,
session.publishedProfileId,
session.sessionId,
],
errorMessage,
);
void refreshSquareHoleShelf().catch(() => undefined);
}
},
});
const puzzleFlow = usePlatformCreationAgentFlowController<
PuzzleAgentSessionSnapshot,
CreatePuzzleAgentSessionRequest,
{ session: PuzzleAgentSessionSnapshot },
SendPuzzleAgentMessageRequest,
PuzzleAgentActionRequest,
{
operation: PuzzleAgentOperationRecord;
session: PuzzleAgentSessionSnapshot;
}
>({
client: {
createSession: createPuzzleAgentSession,
getSession: getPuzzleAgentSession,
streamMessage: streamPuzzleAgentMessage,
executeAction: executePuzzleAgentAction,
selectSession: (response) => response.session,
},
createPayload: {},
workspaceStage: 'puzzle-agent-workspace',
resultStage: 'puzzle-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.action === 'compile_puzzle_draft',
resolveErrorMessage: resolvePuzzleErrorMessage,
errorMessages: {
open: '开启拼图共创工作台失败。',
restoreMissingSession: '这份拼图草稿缺少会话信息,请重新开始创作。',
restore: '读取拼图创作草稿失败。',
submit: '发送拼图共创消息失败。',
execute: '执行拼图操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
sessionController.setCreationTypeError(null);
setPuzzleCreationError(null);
setShowCreationTypeModal(false);
},
onSessionChanged: (session) => {
writeCreationUrlState(buildPuzzleCreationUrlState(session));
},
onOpenError: ({ errorMessage }) => {
sessionController.setCreationTypeError(errorMessage);
setPuzzleCreationError(errorMessage);
},
onActionComplete: async ({ payload, response, session, setSession }) => {
setPuzzleOperation(response.operation);
setSession(response.session);
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
setPuzzleFormDraftPayload(formPayload);
}
if (
payload.action === 'publish_puzzle_work' ||
payload.action === 'generate_puzzle_tags' ||
payload.action === 'compile_puzzle_draft' ||
payload.action === 'generate_puzzle_images' ||
payload.action === 'generate_puzzle_ui_background'
) {
refreshPlatformDashboardSilently();
await Promise.allSettled([
refreshPuzzleShelf(),
refreshPuzzleGallery(),
]);
}
if (payload.action === 'compile_puzzle_draft') {
const openResult = selectionStageRef.current === 'puzzle-generating';
if (!isPuzzleCompileActionReady(response.session)) {
const nextPayload =
formPayload ?? buildPuzzleFormPayloadFromSession(response.session);
const fallbackGenerationState =
createPuzzleDraftGenerationStateFromPayload(
nextPayload,
response.session,
);
const nextGenerationState =
mergePuzzleSessionProgressIntoGenerationState(
puzzleGenerationState ?? fallbackGenerationState,
response.session,
);
activePuzzleGenerationSessionIdRef.current =
response.session.sessionId;
setSelectionStage('puzzle-generating');
markDraftGenerating('puzzle', [
response.session.sessionId,
buildPuzzleResultWorkId(response.session.sessionId),
response.session.publishedProfileId,
buildPuzzleResultProfileId(response.session.sessionId),
]);
markPendingDraftGenerating(
'puzzle',
response.session.sessionId,
buildPendingPuzzleDraftMetadata(nextPayload),
);
setPuzzleGenerationState(nextGenerationState);
setPuzzleBackgroundCompileTasks((current) => {
const next = { ...current };
if (session.sessionId !== response.session.sessionId) {
delete next[session.sessionId];
}
next[response.session.sessionId] = {
session: response.session,
payload: nextPayload,
generationState: nextGenerationState,
error: null,
};
return next;
});
void refreshPuzzleShelf();
return { openResult: false };
}
setPuzzleGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount: 1,
totalAssetCount: 1,
})
: current,
);
const profileId =
response.session.publishedProfileId ??
buildPuzzleResultProfileId(response.session.sessionId);
markPendingDraftReady('puzzle', response.session.sessionId, openResult);
markDraftReady(
'puzzle',
[
response.session.sessionId,
buildPuzzleResultWorkId(response.session.sessionId),
profileId,
],
openResult,
);
void refreshPuzzleShelf();
if (openResult && response.session.draft) {
const draft = response.session.draft;
const draftProfileId =
response.session.publishedProfileId ??
buildPuzzleResultProfileId(response.session.sessionId);
if (!draft.coverImageSrc || !draftProfileId) {
setPuzzleError(
!draft.coverImageSrc
? '请先选择一张正式拼图图片。'
: '这份拼图草稿缺少会话信息,请重新开始创作。',
);
setSelectionStage('puzzle-result');
return { openResult: false };
}
try {
const { item } = await updatePuzzleWork(
draftProfileId,
buildPuzzleWorkUpdatePayloadFromDraft(draft),
);
const run = startLocalPuzzleRun(item);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('puzzle-result');
puzzleRuntimeReturnSessionRef.current = response.session;
pushPuzzleResultHistoryEntry(response.session);
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzleDraftRuntimeUrlState(item, null),
);
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'),
);
setSelectionStage('puzzle-result');
}
return { openResult: false };
}
return { openResult };
}
if (
payload.action === 'publish_puzzle_work' &&
response.session.publishedProfileId
) {
const galleryDetail = await getPuzzleGalleryDetail(
response.session.publishedProfileId,
);
setSelectedPuzzleDetail(galleryDetail.item);
const detailEntry = mapPuzzleWorkToPublicWorkDetail(galleryDetail.item);
setSelectedPublicWorkDetail(detailEntry);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
pushAppHistoryPath(
buildPublicWorkStagePath(
'work-detail',
buildPuzzlePublicWorkCode(galleryDetail.item.profileId),
),
);
openPublishShareModal({
title: galleryDetail.item.workTitle || galleryDetail.item.levelName,
publicWorkCode: buildPuzzlePublicWorkCode(
galleryDetail.item.profileId,
),
stage: 'puzzle-gallery-detail',
});
}
},
beforeExecuteAction: async ({ payload, session }) => {
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
setPuzzleFormDraftPayload(formPayload);
}
if (payload.action !== 'compile_puzzle_draft') {
return;
}
markDraftGenerating('puzzle', [
session.sessionId,
buildPuzzleResultWorkId(session.sessionId),
session.publishedProfileId,
buildPuzzleResultProfileId(session.sessionId),
]);
markPendingDraftGenerating(
'puzzle',
session.sessionId,
buildPendingPuzzleDraftMetadata(
formPayload ?? buildPuzzleFormPayloadFromSession(session),
),
);
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = session.sessionId;
setSelectionStage('puzzle-generating');
const nextGenerationState = createPuzzleDraftGenerationStateFromPayload(
formPayload ?? buildPuzzleFormPayloadFromSession(session),
session,
);
setPuzzleGenerationState(nextGenerationState);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[session.sessionId]: {
session,
payload: formPayload ?? buildPuzzleFormPayloadFromSession(session),
generationState: nextGenerationState,
error: null,
},
}));
void requestGenerationResultSubscribePermission();
},
onActionError: async ({ payload, errorMessage, session, setSession }) => {
if (payload.action !== 'compile_puzzle_draft') {
return;
}
refreshPlatformDashboardSilently();
const formPayload =
buildPuzzleFormPayloadFromAction(payload) ??
puzzleBackgroundCompileTasks[session.sessionId]?.payload ??
buildPuzzleFormPayloadFromSession(session);
const generationState =
puzzleBackgroundCompileTasks[session.sessionId]?.generationState ??
puzzleGenerationState ??
createPuzzleDraftGenerationStateFromPayload(formPayload, session);
const recovered = await recoverCompletedPuzzleDraftGeneration({
sessionId: session.sessionId,
payload: formPayload,
generationState,
setSession,
});
if (recovered) {
return;
}
const failedGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[session.sessionId]: {
session,
payload: formPayload,
generationState: failedGenerationState,
error: errorMessage,
},
}));
markPendingDraftFailed('puzzle', session.sessionId);
markDraftFailed(
'puzzle',
[
session.sessionId,
buildPuzzleResultWorkId(session.sessionId),
session.publishedProfileId,
buildPuzzleResultProfileId(session.sessionId),
],
errorMessage,
);
void refreshPuzzleShelf();
setPuzzleGenerationState((current) =>
current ? failedGenerationState : current,
);
},
});
const visualNovelFlow = usePlatformCreationAgentFlowController<
VisualNovelAgentSessionSnapshot,
CreateVisualNovelSessionRequest,
VisualNovelSessionResponse,
SendVisualNovelMessageRequest,
ExecuteVisualNovelAgentActionRequest,
VisualNovelSessionResponse
>({
client: {
createSession: createVisualNovelSession,
getSession: getVisualNovelSession,
streamMessage: streamVisualNovelMessage,
executeAction: executeVisualNovelAction,
selectSession: (response) => response.session,
},
createPayload: {
sourceMode: 'idea',
seedText: null,
sourceAssetIds: [],
},
workspaceStage: 'visual-novel-agent-workspace',
resultStage: 'visual-novel-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.kind === 'compile_work_profile',
resolveErrorMessage: resolvePuzzleErrorMessage,
errorMessages: {
open: '开启视觉小说共创工作台失败。',
restoreMissingSession: '这份视觉小说草稿缺少会话信息,请重新开始创作。',
restore: '读取视觉小说创作草稿失败。',
submit: '发送视觉小说共创消息失败。',
execute: '执行视觉小说操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onSessionChanged: (session) => {
writeCreationUrlState(buildVisualNovelCreationUrlState(session));
},
onActionComplete: ({ response, setSession }) => {
setSession(response.session);
const openResult =
selectionStageRef.current === 'visual-novel-generating';
markDraftReady(
'visual-novel',
[response.session.draft?.profileId, response.session.sessionId],
openResult,
);
void refreshVisualNovelShelf();
return { openResult };
},
});
const bigFishSession = bigFishFlow.session;
const bigFishError = bigFishFlow.error;
const setBigFishError = bigFishFlow.setError;
bigFishErrorSetterRef.current = setBigFishError;
const isBigFishBusy = bigFishFlow.isBusy;
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
const match3dSession = match3dFlow.session;
const match3dError = match3dFlow.error;
const setMatch3DSession = match3dFlow.setSession;
const setMatch3DError = match3dFlow.setError;
match3DErrorSetterRef.current = setMatch3DError;
const isMatch3DBusy = match3dFlow.isBusy;
const setStreamingMatch3DReplyText = match3dFlow.setStreamingReplyText;
const isStreamingMatch3DReply = match3dFlow.isStreamingReply;
const setIsStreamingMatch3DReply = match3dFlow.setIsStreamingReply;
const squareHoleSession = squareHoleFlow.session;
const squareHoleError = squareHoleFlow.error;
const setSquareHoleSession = squareHoleFlow.setSession;
const setSquareHoleError = squareHoleFlow.setError;
squareHoleErrorSetterRef.current = setSquareHoleError;
const isSquareHoleBusy = squareHoleFlow.isBusy;
const streamingSquareHoleReplyText = squareHoleFlow.streamingReplyText;
const setStreamingSquareHoleReplyText = squareHoleFlow.setStreamingReplyText;
const isStreamingSquareHoleReply = squareHoleFlow.isStreamingReply;
const setIsStreamingSquareHoleReply = squareHoleFlow.setIsStreamingReply;
const puzzleSession = puzzleFlow.session;
const puzzleError = puzzleFlow.error;
const setPuzzleSession = puzzleFlow.setSession;
const setPuzzleError = puzzleFlow.setError;
puzzleErrorSetterRef.current = setPuzzleError;
const isPuzzleBusy = puzzleFlow.isBusy;
const setIsPuzzleBusy = puzzleFlow.setIsBusy;
const isStreamingPuzzleReply = puzzleFlow.isStreamingReply;
const visualNovelSession = visualNovelFlow.session;
const visualNovelError = visualNovelFlow.error;
const setVisualNovelSession = visualNovelFlow.setSession;
const setVisualNovelError = visualNovelFlow.setError;
visualNovelErrorSetterRef.current = setVisualNovelError;
const isVisualNovelBusy = visualNovelFlow.isBusy;
const setIsVisualNovelBusy = visualNovelFlow.setIsBusy;
const isVisualNovelStreamingReply = visualNovelFlow.isStreamingReply;
const resetRpgSessionViewState = sessionController.resetSessionViewState;
const setRpgGeneratedCustomWorldProfile =
sessionController.setGeneratedCustomWorldProfile;
const setRpgCustomWorldError = sessionController.setCustomWorldError;
const persistRpgAgentUiState = sessionController.persistAgentUiState;
const resetAutoSaveTrackingToIdle =
autosaveCoordinator.resetAutoSaveTrackingToIdle;
const preflightPuzzleDraftGeneration = useCallback(async () => {
setPuzzleCreationError(null);
setPuzzleError(null);
return ensureEnoughDraftGenerationPointsFromServer(
puzzleDraftGenerationPointCost,
);
}, [
ensureEnoughDraftGenerationPointsFromServer,
puzzleDraftGenerationPointCost,
]);
const preflightMatch3DDraftGeneration = useCallback(async () => {
setMatch3DError(null);
return ensureEnoughDraftGenerationPointsFromServer(
match3DDraftGenerationPointCost,
);
}, [
ensureEnoughDraftGenerationPointsFromServer,
match3DDraftGenerationPointCost,
]);
const preflightBarkBattleDraftGeneration = useCallback(async () => {
setBarkBattleError(null);
return ensureEnoughDraftGenerationPointsFromServer(
barkBattleDraftGenerationPointCost,
);
}, [
barkBattleDraftGenerationPointCost,
ensureEnoughDraftGenerationPointsFromServer,
]);
const recoverCompletedPuzzleDraftGeneration = useCallback(
async ({
sessionId,
payload,
generationState,
setSession,
}: {
sessionId: string;
payload: CreatePuzzleAgentSessionRequest;
generationState: MiniGameDraftGenerationState;
setSession?: (session: PuzzleAgentSessionSnapshot) => void;
}) => {
let latestSession: PuzzleAgentSessionSnapshot;
try {
const response = await getPuzzleAgentSession(sessionId);
latestSession = normalizeRecoveredPuzzleDraftSession(response.session);
} catch {
return null;
}
if (!hasRecoverableGeneratedPuzzleDraft(latestSession)) {
return null;
}
const readyGenerationState = resolveFinishedMiniGameDraftGenerationState(
generationState,
'ready',
{
completedAssetCount: 1,
totalAssetCount: 1,
error: null,
},
);
const openResult = isViewingPuzzleGeneration(sessionId);
const profileId =
latestSession.publishedProfileId ??
buildPuzzleResultProfileId(latestSession.sessionId);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[sessionId]: {
session: latestSession,
payload,
generationState: readyGenerationState,
error: null,
},
}));
setSession?.(latestSession);
setPuzzleFormDraftPayload(payload);
setPuzzleOperation(null);
puzzleErrorSetterRef.current(null);
if (isViewingPuzzleGeneration(sessionId)) {
setPuzzleGenerationState(readyGenerationState);
}
markPendingDraftReady('puzzle', latestSession.sessionId, openResult);
markDraftReady(
'puzzle',
[
latestSession.sessionId,
buildPuzzleResultWorkId(latestSession.sessionId),
profileId,
],
openResult,
);
await refreshPuzzleShelf().catch(() => undefined);
if (!openResult) {
return { openResult };
}
const draft = latestSession.draft;
if (!draft?.coverImageSrc || !profileId) {
puzzleErrorSetterRef.current(
!draft?.coverImageSrc
? '请先选择一张正式拼图图片。'
: '这份拼图草稿缺少会话信息,请重新开始创作。',
);
setSelectionStage('puzzle-result');
return { openResult: false };
}
try {
const { item } = await updatePuzzleWork(
profileId,
buildPuzzleWorkUpdatePayloadFromDraft(draft),
);
const run = startLocalPuzzleRun(item);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('puzzle-result');
puzzleRuntimeReturnSessionRef.current = latestSession;
pushPuzzleResultHistoryEntry(latestSession);
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzleDraftRuntimeUrlState(item, null),
);
} catch (error) {
puzzleErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'),
);
setSelectionStage('puzzle-result');
}
return { openResult: false };
},
[
isViewingPuzzleGeneration,
markDraftReady,
markPendingDraftReady,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const activeMatch3DGenerationSessionId =
selectionStage === 'match3d-generating'
? (activeMatch3DGenerationSessionIdRef.current ??
match3dSession?.sessionId ??
null)
: null;
const activeMatch3DBackgroundCompileTask = getMatch3DBackgroundCompileTask(
activeMatch3DGenerationSessionId,
);
const match3dGenerationViewState =
activeMatch3DBackgroundCompileTask?.generationState ??
match3dGenerationState;
const match3dGenerationViewSession =
activeMatch3DBackgroundCompileTask?.session ?? match3dSession;
const match3dGenerationViewPayload =
activeMatch3DBackgroundCompileTask?.payload ?? match3dFormDraftPayload;
const match3dGenerationViewError =
activeMatch3DBackgroundCompileTask?.error ?? match3dError;
const isMatch3DGenerationViewBusy = resolveMiniGameGenerationViewBusy(
isMatch3DBusy,
match3dGenerationViewState,
);
const activePuzzleGenerationSessionId =
selectionStage === 'puzzle-generating'
? (activePuzzleGenerationSessionIdRef.current ??
puzzleSession?.sessionId ??
null)
: null;
const activePuzzleBackgroundCompileTask = getPuzzleBackgroundCompileTask(
activePuzzleGenerationSessionId,
);
const puzzleGenerationViewState =
activePuzzleBackgroundCompileTask?.generationState ?? puzzleGenerationState;
const puzzleGenerationViewSession =
activePuzzleBackgroundCompileTask?.session ?? puzzleSession;
const puzzleGenerationViewPayload =
activePuzzleBackgroundCompileTask?.payload ?? puzzleFormDraftPayload;
const puzzleGenerationViewStateRef = useRef(puzzleGenerationViewState);
const puzzleGenerationViewPayloadRef = useRef(puzzleGenerationViewPayload);
const setPuzzleSessionRef = useRef(puzzleFlow.setSession);
useEffect(() => {
puzzleGenerationViewStateRef.current = puzzleGenerationViewState;
}, [puzzleGenerationViewState]);
useEffect(() => {
puzzleGenerationViewPayloadRef.current = puzzleGenerationViewPayload;
}, [puzzleGenerationViewPayload]);
useEffect(() => {
setPuzzleSessionRef.current = puzzleFlow.setSession;
}, [puzzleFlow.setSession]);
const puzzleGenerationViewError =
activePuzzleBackgroundCompileTask?.error ?? puzzleError;
const isPuzzleGenerationViewBusy = resolveMiniGameGenerationViewBusy(
isPuzzleBusy,
puzzleGenerationViewState,
);
const isJumpHopGenerationViewBusy = resolveMiniGameGenerationViewBusy(
isJumpHopBusy,
jumpHopGenerationState,
);
const isPuzzleClearGenerationViewBusy = resolveMiniGameGenerationViewBusy(
isPuzzleClearBusy,
puzzleClearGenerationState,
);
const isWoodenFishGenerationViewBusy = resolveMiniGameGenerationViewBusy(
isWoodenFishBusy,
woodenFishGenerationState,
);
const shouldShowExternalGenerationQueueStatus =
isExternalGenerationQueueStage(selectionStage) ||
(selectionStage === 'platform' &&
platformBootstrap.platformTab === 'profile' &&
platformBootstrap.canReadProtectedData);
useEffect(() => {
if (!shouldShowExternalGenerationQueueStatus) {
setExternalGenerationQueueOverview(null);
return;
}
setExternalGenerationQueueOverview(null);
let disposed = false;
let controller: AbortController | null = null;
const refreshQueueOverview = () => {
controller?.abort();
controller = new AbortController();
getExternalGenerationQueueOverview(controller.signal)
.then((response) => {
if (!disposed) {
setExternalGenerationQueueOverview(response.overview);
}
})
.catch(() => {
if (!disposed) {
setExternalGenerationQueueOverview(null);
}
});
};
refreshQueueOverview();
const intervalId = window.setInterval(refreshQueueOverview, 4000);
return () => {
disposed = true;
controller?.abort();
window.clearInterval(intervalId);
};
}, [authUi?.user?.id, shouldShowExternalGenerationQueueStatus]);
const activeExternalGenerationJobState = useMemo(() => {
const candidates = [
puzzleOperation?.queueState ?? null,
jumpHopQueueState,
puzzleClearQueueState,
woodenFishQueueState,
].filter((candidate): candidate is ExternalGenerationJobStatusRecord =>
Boolean(candidate),
);
return (
candidates.find(
(candidate) =>
candidate.status === 'queued' || candidate.status === 'running',
) ??
candidates[0] ??
null
);
}, [
jumpHopQueueState,
puzzleClearQueueState,
puzzleOperation?.queueState,
woodenFishQueueState,
]);
const externalGenerationQueueStatus = useMemo(
() =>
buildExternalGenerationQueueStatus(
externalGenerationQueueOverview,
activeExternalGenerationJobState,
),
[activeExternalGenerationJobState, externalGenerationQueueOverview],
);
const profileExternalGenerationQueueStatus =
platformBootstrap.canReadProtectedData &&
platformBootstrap.platformTab === 'profile'
? externalGenerationQueueStatus
: null;
const platformBootstrapErrorForDisplay = isCreationEntryDisabledErrorMessage(
platformBootstrap.platformError,
)
? null
: platformBootstrap.platformError;
const [dismissedPlatformErrorDialogKey, setDismissedPlatformErrorDialogKey] =
useState<string | null>(null);
const [
dismissedPlatformTaskCompletionDialogKey,
setDismissedPlatformTaskCompletionDialogKey,
] = useState<string | null>(null);
const currentPlatformErrorDialog =
useMemo<PlatformErrorDialogState | null>(() => {
const candidates: PlatformDialogCandidate[] = [
{
key: pendingPlatformTaskFailureDialog?.key ?? 'draft-failure',
source: pendingPlatformTaskFailureDialog?.source ?? '创作草稿',
message: pendingPlatformTaskFailureDialog?.message,
},
{
key: 'creation-entry-config',
source: '创作入口配置',
message: creationEntryConfigError,
},
{
key: 'platform-bootstrap',
source: '平台首页',
message: platformBootstrapErrorForDisplay,
},
{
key: 'rpg-creation-type',
source: '创作入口',
message: sessionController.creationTypeError,
},
{
key: 'rpg-restore',
source: '创作作品架',
message: sessionController.agentWorkspaceRestoreError,
},
{
key: 'rpg-result',
source: formatPlatformDialogSource(
'RPG 草稿',
sessionController.agentSession?.sessionId ??
sessionController.generatedCustomWorldProfile?.id,
),
message: resultViewError,
},
{
key: 'public-work-detail',
source: formatPlatformDialogSource(
'作品详情',
selectedPublicWorkDetail
? resolvePlatformPublicWorkCode(selectedPublicWorkDetail)
: selectedDetailEntry?.profileId,
),
message: publicWorkDetailError ?? detailNavigation.detailError,
},
{
key: 'big-fish',
source: formatPlatformDialogSource(
selectionStage === 'big-fish-runtime'
? '大鱼吃小鱼游玩'
: '大鱼草稿',
bigFishRun?.runId ?? bigFishSession?.sessionId,
),
message: bigFishError,
},
{
key: 'match3d',
source: formatPlatformDialogSource(
selectionStage === 'match3d-runtime' ? '抓大鹅游玩' : '抓大鹅草稿',
match3dRun?.runId ??
match3dGenerationViewSession?.sessionId ??
match3dSession?.sessionId,
),
message: match3dGenerationViewError ?? match3dError,
},
{
key: 'square-hole',
source: formatPlatformDialogSource(
selectionStage === 'square-hole-runtime'
? '方洞挑战游玩'
: '方洞挑战草稿',
squareHoleRun?.runId ?? squareHoleSession?.sessionId,
),
message: squareHoleError,
},
{
key: 'jump-hop',
source: formatPlatformDialogSource(
selectionStage === 'jump-hop-runtime' ? '跳一跳游玩' : '跳一跳草稿',
jumpHopRun?.runId ?? jumpHopSession?.sessionId,
),
message: jumpHopError,
},
{
key: 'wooden-fish',
source: formatPlatformDialogSource(
selectionStage === 'wooden-fish-runtime'
? '敲木鱼游玩'
: '敲木鱼草稿',
woodenFishRun?.runId ?? woodenFishSession?.sessionId,
),
message: woodenFishError,
},
{
key: 'puzzle',
source: formatPlatformDialogSource(
selectionStage === 'puzzle-runtime' ? '拼图游玩' : '拼图草稿',
puzzleRun?.runId ??
puzzleGenerationViewSession?.sessionId ??
puzzleSession?.sessionId,
),
message:
puzzleGenerationViewError ?? puzzleCreationError ?? puzzleError,
},
{
key: 'puzzle-onboarding',
source: '拼图首次创作',
message: puzzleOnboardingError,
},
{
key: 'puzzle-shelf',
source: '拼图作品架',
message: puzzleShelfError,
},
{
key: 'visual-novel',
source: formatPlatformDialogSource(
selectionStage === 'visual-novel-runtime'
? '视觉小说游玩'
: '视觉小说草稿',
visualNovelRun?.runId ?? visualNovelSession?.sessionId,
),
message: visualNovelError,
},
{
key: 'baby-object-match',
source: formatPlatformDialogSource(
selectionStage === 'baby-object-match-runtime'
? '宝贝识物游玩'
: '宝贝识物草稿',
babyObjectMatchDraft?.profileId,
),
message: babyObjectMatchError,
},
{
key: 'bark-battle',
source: formatPlatformDialogSource(
selectionStage === 'bark-battle-runtime'
? '汪汪声浪游玩'
: '汪汪声浪草稿',
barkBattlePublishedConfig?.workId ?? barkBattleDraftConfig?.workId,
),
message: barkBattleError,
},
{
key: 'creative-agent',
source: formatPlatformDialogSource(
'智能创作 Agent',
creativeAgentSession?.sessionId,
),
message: creativeAgentError,
},
{
key: 'rpg-generation',
source: formatPlatformDialogSource(
'RPG 草稿生成',
sessionController.agentSession?.sessionId,
),
message: sessionController.activeGenerationError,
},
];
return resolvePlatformErrorDialog(candidates);
}, [
babyObjectMatchDraft?.profileId,
babyObjectMatchError,
barkBattleDraftConfig?.workId,
barkBattleError,
barkBattlePublishedConfig?.workId,
bigFishError,
bigFishRun?.runId,
bigFishSession?.sessionId,
creationEntryConfigError,
creativeAgentError,
creativeAgentSession?.sessionId,
detailNavigation.detailError,
jumpHopError,
jumpHopRun?.runId,
jumpHopSession?.sessionId,
match3dError,
match3dGenerationViewError,
match3dGenerationViewSession?.sessionId,
match3dRun?.runId,
match3dSession?.sessionId,
pendingPlatformTaskFailureDialog,
platformBootstrapErrorForDisplay,
publicWorkDetailError,
puzzleCreationError,
puzzleError,
puzzleGenerationViewError,
puzzleGenerationViewSession?.sessionId,
puzzleOnboardingError,
puzzleRun?.runId,
puzzleSession?.sessionId,
puzzleShelfError,
resultViewError,
selectedDetailEntry?.profileId,
selectedPublicWorkDetail,
selectionStage,
sessionController.activeGenerationError,
sessionController.agentSession?.sessionId,
sessionController.agentWorkspaceRestoreError,
sessionController.creationTypeError,
sessionController.generatedCustomWorldProfile?.id,
squareHoleError,
squareHoleRun?.runId,
squareHoleSession?.sessionId,
visualNovelError,
visualNovelRun?.runId,
visualNovelSession?.sessionId,
woodenFishError,
woodenFishRun?.runId,
woodenFishSession?.sessionId,
]);
const currentPlatformTaskCompletionDialog =
useMemo<PlatformTaskCompletionDialogState | null>(
() => pendingPlatformTaskCompletionDialog,
[pendingPlatformTaskCompletionDialog],
);
const activePlatformTaskCompletionDialog =
selectionStage === 'image-editor'
? null
: resolveActivePlatformDialog(
currentPlatformTaskCompletionDialog,
dismissedPlatformTaskCompletionDialogKey,
buildPlatformTaskCompletionDialogDismissKey,
);
const activePlatformErrorDialog =
selectionStage === 'image-editor'
? null
: resolveActivePlatformDialog(
currentPlatformErrorDialog,
dismissedPlatformErrorDialogKey,
buildPlatformErrorDialogDismissKey,
);
const closePlatformErrorDialog = useCallback(() => {
if (!currentPlatformErrorDialog) {
return;
}
const dismissKey = buildPlatformErrorDialogDismissKey(
currentPlatformErrorDialog,
);
if (dismissKey) {
setDismissedPlatformErrorDialogKey(dismissKey);
}
if (currentPlatformErrorDialog.key === 'creation-entry-config') {
setCreationEntryConfigError(null);
return;
}
if (currentPlatformErrorDialog.key === 'platform-bootstrap') {
platformBootstrap.setPlatformError(null);
return;
}
if (currentPlatformErrorDialog.key.startsWith('draft-failure:')) {
setPendingPlatformTaskFailureDialog(null);
return;
}
if (currentPlatformErrorDialog.key === 'rpg-creation-type') {
sessionController.setCreationTypeError(null);
return;
}
if (currentPlatformErrorDialog.key === 'rpg-restore') {
return;
}
if (
currentPlatformErrorDialog.key === 'rpg-result' ||
currentPlatformErrorDialog.key === 'rpg-generation'
) {
autosaveCoordinator.setCustomWorldAutoSaveError(null);
sessionController.setCustomWorldError(null);
return;
}
if (currentPlatformErrorDialog.key === 'public-work-detail') {
setPublicWorkDetailError(null);
detailNavigation.setDetailError(null);
return;
}
if (currentPlatformErrorDialog.key === 'big-fish') {
setBigFishError(null);
return;
}
if (currentPlatformErrorDialog.key === 'match3d') {
setMatch3DError(null);
return;
}
if (currentPlatformErrorDialog.key === 'square-hole') {
setSquareHoleError(null);
return;
}
if (currentPlatformErrorDialog.key === 'jump-hop') {
setJumpHopError(null);
return;
}
if (currentPlatformErrorDialog.key === 'puzzle-clear') {
setPuzzleClearError(null);
return;
}
if (currentPlatformErrorDialog.key === 'wooden-fish') {
setWoodenFishError(null);
return;
}
if (
currentPlatformErrorDialog.key === 'puzzle' ||
currentPlatformErrorDialog.key === 'puzzle-onboarding' ||
currentPlatformErrorDialog.key === 'puzzle-shelf'
) {
setPuzzleCreationError(null);
setPuzzleOnboardingError(null);
setPuzzleShelfError(null);
setPuzzleError(null);
return;
}
if (currentPlatformErrorDialog.key === 'visual-novel') {
setVisualNovelError(null);
return;
}
if (currentPlatformErrorDialog.key === 'baby-object-match') {
setBabyObjectMatchError(null);
return;
}
if (currentPlatformErrorDialog.key === 'bark-battle') {
setBarkBattleError(null);
return;
}
if (currentPlatformErrorDialog.key === 'creative-agent') {
setCreativeAgentError(null);
}
}, [
autosaveCoordinator,
currentPlatformErrorDialog,
detailNavigation,
platformBootstrap,
sessionController,
setBigFishError,
setMatch3DError,
setPendingPlatformTaskFailureDialog,
setPuzzleError,
setSquareHoleError,
setVisualNovelError,
]);
const closePlatformTaskCompletionDialog = useCallback(() => {
if (!currentPlatformTaskCompletionDialog) {
return;
}
const dismissKey = buildPlatformTaskCompletionDialogDismissKey(
currentPlatformTaskCompletionDialog,
);
if (dismissKey) {
setDismissedPlatformTaskCompletionDialogKey(dismissKey);
}
setPendingPlatformTaskCompletionDialog(null);
}, [currentPlatformTaskCompletionDialog]);
const shouldPollPuzzleGenerationSession =
selectionStage === 'puzzle-generating' &&
activePuzzleGenerationSessionId != null &&
isMiniGameDraftGenerating(puzzleGenerationViewState);
useEffect(() => {
if (
!shouldPollPuzzleGenerationSession ||
!activePuzzleGenerationSessionId
) {
return undefined;
}
let isDisposed = false;
const pollPuzzleDraftGenerationSession = async () => {
try {
const { session: latestSession } = await getPuzzleAgentSession(
activePuzzleGenerationSessionId,
);
if (isDisposed) {
return;
}
if (hasRecoverableGeneratedPuzzleDraft(latestSession)) {
const payload =
puzzleGenerationViewPayloadRef.current ??
buildPuzzleFormPayloadFromSession(latestSession);
const generationState =
puzzleGenerationViewStateRef.current ??
createPuzzleDraftGenerationStateFromPayload(payload, latestSession);
await recoverCompletedPuzzleDraftGeneration({
sessionId: latestSession.sessionId,
payload,
generationState,
setSession: setPuzzleSessionRef.current,
});
return;
}
setPuzzleSessionRef.current(latestSession);
setPuzzleBackgroundCompileTasks((current) => {
const task = current[activePuzzleGenerationSessionId];
if (!task) {
return current;
}
return {
...current,
[activePuzzleGenerationSessionId]: {
...task,
session: latestSession,
generationState: mergePuzzleSessionProgressIntoGenerationState(
task.generationState,
latestSession,
),
},
};
});
setPuzzleGenerationState((current) =>
current
? mergePuzzleSessionProgressIntoGenerationState(
current,
latestSession,
)
: current,
);
} catch {
// 中文注释:拼图长 action 仍以主请求结果为准;轮询失败只保留当前进度展示。
}
};
void pollPuzzleDraftGenerationSession();
const timerId = window.setInterval(() => {
void pollPuzzleDraftGenerationSession();
}, 3_000);
return () => {
isDisposed = true;
window.clearInterval(timerId);
};
}, [
activePuzzleGenerationSessionId,
recoverCompletedPuzzleDraftGeneration,
shouldPollPuzzleGenerationSession,
]);
const match3DGeneratingSessionId =
selectionStage === 'match3d-generating' ? match3dSession?.sessionId : null;
useEffect(() => {
puzzleRunRef.current = puzzleRun;
}, [puzzleRun]);
useEffect(() => {
if (
selectionStage !== 'match3d-generating' ||
!isMatch3DBusy ||
!match3DGeneratingSessionId
) {
return undefined;
}
let isDisposed = false;
const pollMatch3DDraftAssets = async () => {
try {
const { session: latestSession } =
await match3dCreationClient.getSession(match3DGeneratingSessionId);
if (isDisposed) {
return;
}
setMatch3DSession(latestSession);
const profileId =
latestSession.draft?.profileId ?? latestSession.publishedProfileId;
if (!profileId) {
return;
}
const { item } = await getMatch3DWorkDetail(profileId);
if (isDisposed) {
return;
}
const normalizedItem = normalizeMatch3DWorkForRuntimeUi(item);
setMatch3DProfile(normalizedItem);
setMatch3DGenerationState((current) =>
mergeMatch3DGeneratedAssetsIntoGenerationState(
current,
normalizedItem.generatedItemAssets,
),
);
} catch {
// 中文注释:生成长 action 仍在执行,轮询只是进度旁路;失败时保留主请求结果。
}
};
void pollMatch3DDraftAssets();
const timerId = window.setInterval(() => {
void pollMatch3DDraftAssets();
}, 3_000);
return () => {
isDisposed = true;
window.clearInterval(timerId);
};
}, [
isMatch3DBusy,
match3DGeneratingSessionId,
selectionStage,
setMatch3DSession,
setMatch3DProfile,
setMatch3DGenerationState,
]);
const openBigFishAgentWorkspace = useCallback(async () => {
markCreationFlowReturnToCreate();
setBigFishRun(null);
await bigFishFlow.openWorkspace();
}, [bigFishFlow, markCreationFlowReturnToCreate]);
const openSquareHoleAgentWorkspace = useCallback(async () => {
markCreationFlowReturnToCreate();
setSquareHoleSession(null);
setSquareHoleProfile(null);
setSquareHoleRun(null);
setSquareHoleError(null);
setSquareHoleGenerationState(null);
setSquareHoleRuntimeReturnStage('square-hole-result');
setStreamingSquareHoleReplyText('');
setIsStreamingSquareHoleReply(false);
await squareHoleFlow.openWorkspace();
}, [
setIsStreamingSquareHoleReply,
setSquareHoleError,
setSquareHoleProfile,
setSquareHoleRun,
setSquareHoleSession,
setStreamingSquareHoleReplyText,
squareHoleFlow,
markCreationFlowReturnToCreate,
]);
const openMatch3DWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setMatch3DRun(null);
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
setMatch3DGenerationState(null);
setMatch3DError(null);
setStreamingMatch3DReplyText('');
setIsStreamingMatch3DReply(false);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('match3d-agent-workspace');
}, [
enterCreateTab,
setIsStreamingMatch3DReply,
setMatch3DError,
setSelectionStage,
setStreamingMatch3DReplyText,
markCreationFlowReturnToCreate,
]);
const openJumpHopWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setJumpHopError(null);
setJumpHopSession(null);
setJumpHopWork(null);
setJumpHopRun(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopGenerationState(null);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('jump-hop-workspace');
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openPuzzleClearWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setPuzzleClearError(null);
setPuzzleClearSession(null);
setPuzzleClearWork(null);
setPuzzleClearRun(null);
setPuzzleClearGenerationState(null);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('puzzle-clear-workspace');
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openWoodenFishWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setWoodenFishError(null);
setWoodenFishSession(null);
setWoodenFishWork(null);
setWoodenFishRun(null);
setWoodenFishGenerationState(null);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('wooden-fish-workspace');
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openPuzzleWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setPuzzleSession(null);
setPuzzleFormDraftPayload(null);
setPuzzleOperation(null);
setPuzzleGenerationState(null);
setPuzzleRun(null);
setSelectedPuzzleDetail(null);
setPuzzleRuntimeAuthMode('default');
activePuzzleGenerationSessionIdRef.current = null;
enterCreateTab();
setShowCreationTypeModal(false);
setPuzzleCreationError(null);
setPuzzleError(null);
setSelectionStage('puzzle-agent-workspace');
}, [
enterCreateTab,
setPuzzleSession,
setPuzzleCreationError,
setPuzzleError,
setSelectionStage,
markCreationFlowReturnToCreate,
]);
const openBarkBattleWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
setBarkBattleDraftConfig(null);
setBarkBattlePublishedConfig(null);
setBarkBattleRuntimeMode('draft');
setBarkBattleRuntimeReturnStage('platform');
setBarkBattleError(null);
setBarkBattleGenerationPartialFailed(false);
setIsBarkBattleBusy(false);
enterCreateTab();
setShowCreationTypeModal(false);
selectionStageRef.current = 'bark-battle-workspace';
setSelectionStage('bark-battle-workspace');
}, [enterCreateTab, markCreationFlowReturnToCreate, setSelectionStage]);
const openVisualNovelWorkspace = useCallback(() => {
markCreationFlowReturnToCreate();
enterCreateTab();
setShowCreationTypeModal(false);
setVisualNovelError(null);
setSelectionStage('visual-novel-agent-workspace');
}, [
enterCreateTab,
markCreationFlowReturnToCreate,
setSelectionStage,
setVisualNovelError,
]);
const openBabyObjectMatchWorkspace = useCallback(() => {
if (!isBabyObjectMatchVisible) {
sessionController.setCreationTypeError(EDUTAINMENT_HIDDEN_MESSAGE);
return;
}
markCreationFlowReturnToCreate();
enterCreateTab();
setShowCreationTypeModal(false);
setBabyObjectMatchError(null);
setSelectionStage('baby-object-match-workspace');
}, [
enterCreateTab,
isBabyObjectMatchVisible,
markCreationFlowReturnToCreate,
sessionController,
setBabyObjectMatchError,
setSelectionStage,
]);
const leaveCreativeAgentWorkspace = useCallback(() => {
const sessionId = creativeAgentSession?.sessionId?.trim();
if (sessionId && creativeAgentSession?.stage !== 'target_ready') {
void cancelCreativeAgentSession(sessionId).catch(() => undefined);
}
setCreativeAgentSession(null);
setCreativeAgentEvents([]);
setActiveCreativeAgentSessionId(null);
setCreativeAgentError(null);
setIsCreativeAgentBusy(false);
setIsCreativeAgentStreaming(false);
setCreativeDraftEditError(null);
setIsCreativeDraftEditBusy(false);
markCreationFlowReturnToCreate();
enterCreateTab();
setSelectionStage('platform');
}, [
creativeAgentSession,
enterCreateTab,
markCreationFlowReturnToCreate,
setSelectionStage,
]);
const openCreativeAgentWorkspace = useCallback(async () => {
if (isCreativeAgentBusy || isCreativeAgentStreaming) {
return;
}
setPuzzleRun(null);
markCreationFlowReturnToCreate();
setPuzzleRuntimeAuthMode('default');
setPuzzleOperation(null);
setPuzzleGenerationState(null);
setPuzzleFormDraftPayload(null);
setCreativeAgentError(null);
setCreativeAgentEvents([]);
setIsCreativeAgentBusy(true);
try {
const response = await createCreativeAgentSession({
entryContext: 'creation_home',
});
setCreativeAgentSession(response.session);
setActiveCreativeAgentSessionId(response.session.sessionId);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('creative-agent-workspace');
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '开启智能创作工作区失败。'),
);
} finally {
setIsCreativeAgentBusy(false);
}
}, [
enterCreateTab,
isCreativeAgentBusy,
isCreativeAgentStreaming,
markCreationFlowReturnToCreate,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
const createPuzzleDraftFromForm = useCallback(
async (payload: CreatePuzzleAgentSessionRequest) => {
setPuzzleFormDraftPayload(payload);
setPuzzleCreationError(null);
setPuzzleError(null);
const shouldConsumePuzzleDraftPoints = payload.aiRedraw !== false;
if (
shouldConsumePuzzleDraftPoints &&
!(await preflightPuzzleDraftGeneration())
) {
return;
}
let nextSession: PuzzleAgentSessionSnapshot;
try {
const response = await createPuzzleAgentSession(payload);
nextSession = response.session;
} catch (error) {
const errorMessage = resolvePuzzleErrorMessage(
error,
'开启拼图创作工作台失败。',
);
setPuzzleCreationError(errorMessage);
setPuzzleError(errorMessage);
return;
}
const generationState = createPuzzleDraftGenerationStateFromPayload(
payload,
nextSession,
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: nextSession,
payload,
generationState,
error: null,
},
}));
puzzleFlow.setSession(nextSession);
setPuzzleGenerationState(generationState);
markDraftGenerating('puzzle', [
nextSession.sessionId,
buildPuzzleResultWorkId(nextSession.sessionId),
nextSession.publishedProfileId,
buildPuzzleResultProfileId(nextSession.sessionId),
]);
markPendingDraftGenerating(
'puzzle',
nextSession.sessionId,
buildPendingPuzzleDraftMetadata(payload),
);
if (shouldConsumePuzzleDraftPoints) {
adjustProfileWalletBalanceLocally(-PUZZLE_DRAFT_GENERATION_POINT_COST);
}
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = nextSession.sessionId;
setSelectionStage('puzzle-generating');
try {
const actionPayload = buildPuzzleCompileActionFromFormPayload(payload);
void requestGenerationResultSubscribePermission();
const response = await executePuzzleAgentAction(
nextSession.sessionId,
actionPayload,
);
setPuzzleOperation(response.operation);
const openResult = isViewingPuzzleGeneration(nextSession.sessionId);
const isCompileReady = isPuzzleCompileActionReady(response.session);
const nextGenerationState = isCompileReady
? resolveFinishedMiniGameDraftGenerationState(
generationState,
'ready',
{
completedAssetCount: 1,
totalAssetCount: 1,
},
)
: mergePuzzleSessionProgressIntoGenerationState(
generationState,
response.session,
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: response.session,
payload,
generationState: nextGenerationState,
error: null,
},
}));
if (isViewingPuzzleGeneration(nextSession.sessionId)) {
puzzleFlow.setSession(response.session);
setPuzzleGenerationState(nextGenerationState);
}
if (!isCompileReady) {
markDraftGenerating('puzzle', [
response.session.sessionId,
buildPuzzleResultWorkId(response.session.sessionId),
response.session.publishedProfileId,
buildPuzzleResultProfileId(response.session.sessionId),
]);
markPendingDraftGenerating(
'puzzle',
response.session.sessionId,
buildPendingPuzzleDraftMetadata(payload),
);
void refreshPuzzleShelf();
return;
}
const profileId =
response.session.publishedProfileId ??
buildPuzzleResultProfileId(response.session.sessionId);
markPendingDraftReady('puzzle', response.session.sessionId, openResult);
markDraftReady(
'puzzle',
[
response.session.sessionId,
buildPuzzleResultWorkId(response.session.sessionId),
profileId,
],
openResult,
);
void refreshPuzzleShelf();
if (openResult && response.session.draft) {
const draft = response.session.draft;
const draftProfileId =
response.session.publishedProfileId ??
buildPuzzleResultProfileId(response.session.sessionId);
if (!draft.coverImageSrc || !draftProfileId) {
setPuzzleError(
!draft.coverImageSrc
? '请先选择一张正式拼图图片。'
: '这份拼图草稿缺少会话信息,请重新开始创作。',
);
setSelectionStage('puzzle-result');
return;
}
try {
const { item } = await updatePuzzleWork(
draftProfileId,
buildPuzzleWorkUpdatePayloadFromDraft(draft),
);
const run = startLocalPuzzleRun(item);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('puzzle-result');
puzzleRuntimeReturnSessionRef.current = response.session;
pushPuzzleResultHistoryEntry(response.session);
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzleDraftRuntimeUrlState(item, null),
);
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'),
);
setSelectionStage('puzzle-result');
}
}
} catch (error) {
const errorMessage = resolvePuzzleErrorMessage(
error,
'执行拼图操作失败。',
);
const recovered = await recoverCompletedPuzzleDraftGeneration({
sessionId: nextSession.sessionId,
payload,
generationState,
setSession: puzzleFlow.setSession,
});
if (recovered) {
return;
}
if (shouldConsumePuzzleDraftPoints) {
adjustProfileWalletBalanceLocally(PUZZLE_DRAFT_GENERATION_POINT_COST);
}
const failedGenerationState =
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
);
const openFailure = isViewingPuzzleGeneration(nextSession.sessionId);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: nextSession,
payload,
generationState: failedGenerationState,
error: errorMessage,
},
}));
markPendingDraftFailed('puzzle', nextSession.sessionId);
markDraftFailed(
'puzzle',
[
nextSession.sessionId,
buildPuzzleResultWorkId(nextSession.sessionId),
nextSession.publishedProfileId,
buildPuzzleResultProfileId(nextSession.sessionId),
],
errorMessage,
!openFailure,
);
void refreshPuzzleShelf();
if (openFailure) {
setPuzzleError(errorMessage);
setPuzzleGenerationState(failedGenerationState);
}
} finally {
refreshPlatformDashboardSilently();
}
},
[
adjustProfileWalletBalanceLocally,
markDraftGenerating,
markDraftFailed,
markDraftReady,
markPendingDraftFailed,
markPendingDraftGenerating,
markPendingDraftReady,
isViewingPuzzleGeneration,
preflightPuzzleDraftGeneration,
puzzleFlow,
puzzleDraftGenerationPointCost,
refreshPuzzleShelf,
recoverCompletedPuzzleDraftGeneration,
refreshPlatformDashboardSilently,
resolvePuzzleErrorMessage,
setPuzzleError,
setSelectionStage,
],
);
const createMatch3DDraftFromForm = useCallback(
async (payload: CreateMatch3DSessionRequest) => {
setMatch3DFormDraftPayload(payload);
setMatch3DError(null);
setStreamingMatch3DReplyText('');
setIsStreamingMatch3DReply(false);
if (!(await preflightMatch3DDraftGeneration())) {
return;
}
let nextSession: Match3DAgentSessionSnapshot;
try {
const response = await match3dCreationClient.createSession(payload);
nextSession = response.session;
} catch (error) {
setMatch3DError(
resolveMatch3DErrorMessage(error, '开启抓大鹅共创工作台失败。'),
);
return;
}
const generationState = createMiniGameDraftGenerationState(
'match3d',
resolveMiniGameDraftGenerationStartedAtMs(nextSession.updatedAt),
);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: nextSession,
payload,
generationState,
error: null,
},
}));
setMatch3DSession(nextSession);
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
setMatch3DRun(null);
setMatch3DGenerationState(generationState);
markDraftGenerating('match3d', [
nextSession.draft?.profileId,
nextSession.publishedProfileId,
nextSession.sessionId,
]);
markPendingDraftGenerating(
'match3d',
nextSession.sessionId,
buildPendingMatch3DDraftMetadata(payload),
);
adjustProfileWalletBalanceLocally(-MATCH3D_DRAFT_GENERATION_POINT_COST);
selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = nextSession.sessionId;
setSelectionStage('match3d-generating');
try {
const response = await match3dCreationClient.executeAction(
nextSession.sessionId,
{
action: 'match3d_compile_draft',
generateClickSound: payload.generateClickSound,
},
);
const openResult = isViewingMatch3DGeneration(nextSession.sessionId);
const readyGenerationState =
resolveFinishedMiniGameDraftGenerationState(
generationState,
'ready',
{
completedAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
totalAssetCount:
response.session.draft?.generatedItemAssets?.length ?? 5,
},
);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: response.session,
payload,
generationState: readyGenerationState,
error: null,
},
}));
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
setMatch3DSession(response.session);
setMatch3DGenerationState(readyGenerationState);
}
const profileId = response.session.draft?.profileId;
if (!profileId) {
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
}
return;
}
let runtimeProfile: Match3DWorkProfile | null = null;
try {
const { item } = await getMatch3DWorkDetail(profileId);
runtimeProfile = normalizeMatch3DWorkForRuntimeUi({
...item,
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
response.session.draft?.generatedItemAssets,
item.generatedItemAssets,
),
});
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
setMatch3DProfile(runtimeProfile);
}
await refreshMatch3DShelf().catch(() => undefined);
} catch {
runtimeProfile = buildMatch3DProfileFromSession(response.session);
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
setMatch3DProfile(runtimeProfile);
}
}
markPendingDraftReady(
'match3d',
response.session.sessionId,
openResult,
);
markDraftReady(
'match3d',
[profileId, response.session.sessionId],
openResult,
);
if (openResult && runtimeProfile) {
try {
await preloadMatch3DGeneratedRuntimeAssets(
runtimeProfile.generatedItemAssets,
runtimeProfile.generatedBackgroundAsset,
{ expireSeconds: 300 },
);
const { run } = await match3dRuntimeAdapter.startRun(
runtimeProfile.profileId,
);
setMatch3DRuntimeProfile(runtimeProfile);
setMatch3DRun(run);
setMatch3DProfile(runtimeProfile);
setMatch3DRuntimeReturnStage('match3d-result');
setSelectionStage('match3d-runtime');
} catch (error) {
setMatch3DError(
resolveMatch3DErrorMessage(error, '启动抓大鹅玩法失败。'),
);
setSelectionStage('match3d-result');
}
}
} catch (error) {
const errorMessage = resolveMatch3DErrorMessage(
error,
'执行抓大鹅操作失败。',
);
if (isBackgroundGenerationStillRunningMessage(errorMessage)) {
try {
const { session: latestSession } =
await match3dCreationClient.getSession(nextSession.sessionId);
markPendingDraftGenerating('match3d', latestSession.sessionId);
markDraftGenerating('match3d', [
latestSession.draft?.profileId,
latestSession.publishedProfileId,
latestSession.sessionId,
]);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: latestSession,
payload,
generationState,
error: errorMessage,
},
}));
if (isViewingMatch3DGeneration(nextSession.sessionId)) {
setMatch3DError(errorMessage);
setMatch3DSession(latestSession);
setMatch3DGenerationState(generationState);
const profileId =
latestSession.draft?.profileId ??
latestSession.publishedProfileId;
if (profileId) {
const { item } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(item));
}
}
await refreshMatch3DShelf().catch(() => undefined);
return;
} catch {
await refreshMatch3DShelf().catch(() => undefined);
}
}
adjustProfileWalletBalanceLocally(MATCH3D_DRAFT_GENERATION_POINT_COST);
const failedGenerationState =
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
);
const openFailure = isViewingMatch3DGeneration(nextSession.sessionId);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: nextSession,
payload,
generationState: failedGenerationState,
error: errorMessage,
},
}));
markPendingDraftFailed('match3d', nextSession.sessionId);
markDraftFailed(
'match3d',
[
nextSession.draft?.profileId,
nextSession.publishedProfileId,
nextSession.sessionId,
],
errorMessage,
!openFailure,
);
if (openFailure) {
setMatch3DError(errorMessage);
setMatch3DGenerationState(failedGenerationState);
}
try {
const { session: latestSession } =
await match3dCreationClient.getSession(nextSession.sessionId);
markDraftFailed(
'match3d',
[
latestSession.draft?.profileId,
latestSession.publishedProfileId,
latestSession.sessionId,
],
errorMessage,
!openFailure,
);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[nextSession.sessionId]: {
session: latestSession,
payload,
generationState: failedGenerationState,
error: errorMessage,
},
}));
if (openFailure) {
setMatch3DSession(latestSession);
const profileId =
latestSession.draft?.profileId ??
latestSession.publishedProfileId;
if (profileId) {
const { item } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(item));
}
}
await refreshMatch3DShelf().catch(() => undefined);
} catch {
await refreshMatch3DShelf().catch(() => undefined);
}
} finally {
refreshPlatformDashboardSilently();
}
},
[
adjustProfileWalletBalanceLocally,
match3dRuntimeAdapter,
match3DDraftGenerationPointCost,
isViewingMatch3DGeneration,
markDraftGenerating,
markDraftFailed,
markDraftReady,
markPendingDraftFailed,
markPendingDraftGenerating,
markPendingDraftReady,
preflightMatch3DDraftGeneration,
refreshMatch3DShelf,
refreshPlatformDashboardSilently,
resolveMatch3DErrorMessage,
setIsStreamingMatch3DReply,
setMatch3DError,
setMatch3DProfile,
setMatch3DRun,
setMatch3DSession,
setSelectionStage,
setStreamingMatch3DReplyText,
],
);
const createVisualNovelDraftFromForm = useCallback(
async (payload: VisualNovelEntryFormPayload) => {
setVisualNovelFormDraftPayload(payload);
setVisualNovelGenerationStartedAtMs(Date.now());
setVisualNovelGenerationPhase('generating');
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setVisualNovelError(null);
setIsVisualNovelBusy(true);
setSelectionStage('visual-novel-generating');
try {
const createResponse = await createVisualNovelSession({
sourceMode: payload.sourceMode,
seedText: payload.seedText,
sourceAssetIds: payload.sourceAssetIds,
});
setVisualNovelSession(createResponse.session);
markDraftGenerating('visual-novel', [createResponse.session.sessionId]);
markPendingDraftGenerating(
'visual-novel',
createResponse.session.sessionId,
);
const nextSession = await streamVisualNovelMessage(
createResponse.session.sessionId,
{
clientMessageId: `visual-novel-entry-${Date.now().toString(36)}`,
text: payload.seedText,
},
);
setVisualNovelSession(nextSession);
setVisualNovelGenerationPhase('ready');
const openResult =
selectionStageRef.current === 'visual-novel-generating';
if (
nextSession.draft?.profileId &&
nextSession.draft.profileId !== nextSession.sessionId
) {
clearPendingDraftShelfItem('visual-novel', nextSession.sessionId);
}
markPendingDraftReady(
'visual-novel',
nextSession.draft?.profileId ?? nextSession.sessionId,
openResult,
);
markDraftReady(
'visual-novel',
[nextSession.draft?.profileId, nextSession.sessionId],
openResult,
);
void refreshVisualNovelShelf();
if (openResult) {
setSelectionStage('visual-novel-result');
}
} catch (error) {
setVisualNovelGenerationPhase('failed');
setVisualNovelError(
resolvePuzzleErrorMessage(error, '生成视觉小说草稿失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
markDraftReady,
markDraftGenerating,
markPendingDraftGenerating,
markPendingDraftReady,
clearPendingDraftShelfItem,
refreshVisualNovelShelf,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
setVisualNovelSession,
],
);
const createBabyObjectMatchDraftFromForm = useCallback(
async (payload: CreateBabyObjectMatchDraftRequest) => {
setBabyObjectMatchFormPayload(payload);
setBabyObjectMatchDraft(null);
setBabyObjectMatchError(null);
setIsBabyObjectMatchBusy(true);
setBabyObjectMatchGenerationPhase('generating');
setBabyObjectMatchGenerationState(
createMiniGameDraftGenerationState('baby-object-match'),
);
selectionStageRef.current = 'baby-object-match-generating';
setSelectionStage('baby-object-match-generating');
try {
const response = await createBabyObjectMatchDraft(payload);
setBabyObjectMatchDraft(response.draft);
writeCreationUrlState(
buildBabyObjectMatchCreationUrlState(response.draft),
);
void refreshBabyObjectMatchShelf();
setBabyObjectMatchGenerationPhase('ready');
setBabyObjectMatchGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'ready', {
completedAssetCount: response.draft.itemAssets.length,
totalAssetCount: response.draft.itemAssets.length,
})
: current,
);
const openResult =
selectionStageRef.current === 'baby-object-match-generating';
markDraftReady(
'baby-object-match',
[response.draft.profileId, response.draft.draftId],
openResult,
);
if (openResult) {
setSelectionStage('baby-object-match-result');
}
} catch (error) {
const errorMessage = resolvePuzzleErrorMessage(
error,
'生成宝贝识物草稿失败。',
);
setBabyObjectMatchGenerationPhase('failed');
setBabyObjectMatchError(errorMessage);
setBabyObjectMatchGenerationState((current) =>
current
? resolveFinishedMiniGameDraftGenerationState(current, 'failed', {
error: errorMessage,
})
: current,
);
} finally {
setIsBabyObjectMatchBusy(false);
}
},
[
markDraftReady,
refreshBabyObjectMatchShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const savePuzzleFormDraft = useCallback(
async (payload: CreatePuzzleAgentSessionRequest) => {
const session = puzzleFlow.session;
if (!session || session.stage !== 'collecting_anchors') {
return;
}
setPuzzleFormDraftPayload(payload);
try {
const response = await executePuzzleAgentAction(session.sessionId, {
action: 'save_puzzle_form_draft',
promptText: payload.pictureDescription ?? null,
workTitle: payload.workTitle,
workDescription:
payload.workDescription ?? payload.pictureDescription ?? '',
pictureDescription: payload.pictureDescription ?? '',
referenceImageSrc: payload.referenceImageSrc ?? null,
referenceImageSrcs: payload.referenceImageSrcs ?? [],
referenceImageAssetObjectId:
payload.referenceImageAssetObjectId ?? null,
referenceImageAssetObjectIds:
payload.referenceImageAssetObjectIds ?? [],
imageModel: payload.imageModel ?? null,
aiRedraw: payload.aiRedraw ?? true,
});
setPuzzleOperation(response.operation);
puzzleFlow.setSession(response.session);
setPuzzleError(null);
void refreshPuzzleShelf();
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '保存拼图表单草稿失败。'),
);
}
},
[puzzleFlow, refreshPuzzleShelf, resolvePuzzleErrorMessage, setPuzzleError],
);
useEffect(() => {
if (platformBootstrap.canReadProtectedData) {
hadReadableProtectedDataRef.current = true;
return;
}
if (authUi?.user || !hadReadableProtectedDataRef.current) {
return;
}
hadReadableProtectedDataRef.current = false;
// 创作中心只展示当前登录用户的私有作品。
// 一旦退出登录或鉴权上下文被收回,三类作品缓存必须同步清空,不能等刷新页面。
setShowCreationTypeModal(false);
setSelectedDetailEntry(null);
setSelectedPublicWorkDetail(null);
setPublicWorkDetailError(null);
setIsPublicWorkDetailBusy(false);
setBigFishWorks([]);
setBigFishRun(null);
setBigFishRuntimeShare(null);
setBigFishRuntimeWork(null);
setBigFishRuntimeStartedAt(null);
setBigFishRuntimeReturnStage('platform');
setBigFishGenerationState(null);
setBigFishError(null);
setMatch3DSession(null);
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
setMatch3DFormDraftPayload(null);
setMatch3DBackgroundCompileTasks({});
activeMatch3DGenerationSessionIdRef.current = null;
setMatch3DWorks([]);
setMatch3DGalleryEntries([]);
setMatch3DRun(null);
setMatch3DRuntimeReturnStage('match3d-result');
setMatch3DError(null);
setStreamingMatch3DReplyText('');
setIsStreamingMatch3DReply(false);
setSquareHoleSession(null);
setSquareHoleProfile(null);
setSquareHoleWorks([]);
setSquareHoleGalleryEntries([]);
setSquareHoleRun(null);
setSquareHoleRuntimeReturnStage('square-hole-result');
setSquareHoleGenerationState(null);
setSquareHoleError(null);
setStreamingSquareHoleReplyText('');
setIsStreamingSquareHoleReply(false);
setCreativeAgentSession(null);
setCreativeAgentEvents([]);
setActiveCreativeAgentSessionId(null);
setCreativeAgentError(null);
setIsCreativeAgentBusy(false);
setIsCreativeAgentStreaming(false);
setCreativeDraftEditError(null);
setIsCreativeDraftEditBusy(false);
setPuzzleOperation(null);
setPuzzleWorks([]);
setSelectedPuzzleDetail(null);
setPuzzleRuntimeReturnStage('puzzle-gallery-detail');
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleGenerationState(null);
setPuzzleBackgroundCompileTasks({});
activePuzzleGenerationSessionIdRef.current = null;
setIsPuzzleNextLevelGenerating(false);
setPuzzleShelfError(null);
setPuzzleCreationError(null);
setPuzzleError(null);
setPuzzleClearSession(null);
setPuzzleClearWork(null);
setPuzzleClearWorks([]);
setPuzzleClearGalleryEntries([]);
setPuzzleClearRun(null);
setPuzzleClearRuntimeReturnStage('puzzle-clear-result');
setPuzzleClearGenerationState(null);
setPuzzleClearError(null);
setIsPuzzleClearBusy(false);
setVisualNovelSession(null);
setVisualNovelWork(null);
setVisualNovelWorks([]);
setVisualNovelGalleryEntries([]);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setVisualNovelFormDraftPayload(null);
setVisualNovelGenerationStartedAtMs(null);
setVisualNovelGenerationPhase('generating');
setVisualNovelError(null);
setBabyObjectMatchDraft(null);
setBabyObjectMatchDrafts([]);
setBabyObjectMatchFormPayload(null);
setBabyObjectMatchGenerationState(null);
setBabyObjectMatchGenerationPhase('generating');
setBabyObjectMatchRuntimeReturnStage('baby-object-match-result');
setBabyObjectMatchError(null);
setIsBabyObjectMatchBusy(false);
setDeletingCreationWorkId(null);
setClaimingPuzzlePointIncentiveProfileId(null);
setPublishSharePayload(null);
setProfilePlayStats(null);
setProfilePlayStatsError(null);
setIsProfilePlayStatsOpen(false);
setDraftGenerationNotices({});
setPendingDraftShelfItems({});
setPendingPlatformTaskCompletionDialog(null);
setPendingPlatformTaskFailureDialog(null);
resetRpgSessionViewState();
setRpgGeneratedCustomWorldProfile(null);
setRpgCustomWorldError(null);
persistRpgAgentUiState(null, null);
resetAutoSaveTrackingToIdle();
const nextSelectionStage =
resolveSelectionStageAfterProtectedDataLoss(selectionStage);
if (nextSelectionStage !== selectionStage) {
setSelectionStage(nextSelectionStage);
}
}, [
authUi?.user,
platformBootstrap.canReadProtectedData,
persistRpgAgentUiState,
resetAutoSaveTrackingToIdle,
resetRpgSessionViewState,
selectionStage,
setBigFishError,
setIsStreamingMatch3DReply,
setIsStreamingSquareHoleReply,
setMatch3DError,
setMatch3DSession,
setPuzzleError,
setRpgCustomWorldError,
setRpgGeneratedCustomWorldProfile,
setSelectionStage,
setBabyObjectMatchError,
setSquareHoleError,
setSquareHoleSession,
setStreamingMatch3DReplyText,
setStreamingSquareHoleReplyText,
setVisualNovelError,
setVisualNovelSession,
]);
const handleCreationHubCreateType = useCallback(
(type: PlatformCreationTypeId) => {
const intent = resolvePlatformCreationLaunchIntent({
type,
isBabyObjectMatchVisible,
});
if (!intent.shouldPrepare) {
return;
}
if (!prepareCreationLaunch()) {
return;
}
if (intent.type === 'blocked') {
sessionController.setCreationTypeError(intent.message);
return;
}
if (intent.type !== 'launch') {
return;
}
const launchers = {
rpg: () => {
void sessionController.openRpgAgentWorkspace();
},
'big-fish': () => {
void openBigFishAgentWorkspace();
},
match3d: () => {
void openMatch3DWorkspace();
},
'square-hole': () => {
void openSquareHoleAgentWorkspace();
},
'jump-hop': () => {
void openJumpHopWorkspace();
},
'wooden-fish': () => {
void openWoodenFishWorkspace();
},
puzzle: () => {
void openPuzzleWorkspace();
},
'bark-battle': () => {
void openBarkBattleWorkspace();
},
'visual-novel': () => {
void openVisualNovelWorkspace();
},
'baby-object-match': () => {
void openBabyObjectMatchWorkspace();
},
} satisfies Record<PlatformCreationLaunchTarget, () => void>;
runProtectedAction(launchers[intent.target]);
},
[
isBabyObjectMatchVisible,
openBarkBattleWorkspace,
openBigFishAgentWorkspace,
openBabyObjectMatchWorkspace,
openJumpHopWorkspace,
openMatch3DWorkspace,
openPuzzleClearWorkspace,
prepareCreationLaunch,
openPuzzleWorkspace,
openSquareHoleAgentWorkspace,
openVisualNovelWorkspace,
openWoodenFishWorkspace,
sessionController,
runProtectedAction,
],
);
const leaveBigFishFlow = useCallback(() => {
setBigFishRun(null);
setBigFishRuntimeWork(null);
setBigFishRuntimeStartedAt(null);
setBigFishRuntimeReturnStage('platform');
setBigFishGenerationState(null);
bigFishFlow.leaveFlow();
returnToCreationFlowSource();
}, [bigFishFlow, returnToCreationFlowSource]);
const leaveMatch3DFlow = useCallback(() => {
setMatch3DRun(null);
setMatch3DRuntimeProfile(null);
setMatch3DFormDraftPayload(null);
setMatch3DGenerationState(null);
setMatch3DRuntimeReturnStage('match3d-result');
match3dFlow.leaveFlow();
returnToCreationFlowSource();
}, [match3dFlow, returnToCreationFlowSource, setMatch3DFormDraftPayload]);
const leaveSquareHoleFlow = useCallback(() => {
setSquareHoleRun(null);
setSquareHoleRuntimeReturnStage('square-hole-result');
setSquareHoleGenerationState(null);
squareHoleFlow.leaveFlow();
returnToCreationFlowSource();
}, [returnToCreationFlowSource, squareHoleFlow]);
const leaveJumpHopFlow = useCallback(() => {
setJumpHopRun(null);
setJumpHopWork(null);
setJumpHopRuntimeReturnStage('jump-hop-result');
setJumpHopGenerationState(null);
setJumpHopSession(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopError(null);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const leavePuzzleClearFlow = useCallback(() => {
setPuzzleClearRun(null);
setPuzzleClearWork(null);
setPuzzleClearRuntimeReturnStage('puzzle-clear-result');
setPuzzleClearGenerationState(null);
setPuzzleClearSession(null);
setPuzzleClearError(null);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const leaveWoodenFishFlow = useCallback(() => {
setWoodenFishRun(null);
setWoodenFishWork(null);
setWoodenFishRuntimeReturnStage('wooden-fish-result');
setWoodenFishGenerationState(null);
setWoodenFishSession(null);
setWoodenFishError(null);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const createReadyJumpHopGenerationState = useCallback(
(state: MiniGameDraftGenerationState) =>
resolveFinishedMiniGameDraftGenerationState(state, 'ready', {
completedAssetCount: 2,
totalAssetCount: 2,
}),
[],
);
const createReadyPuzzleClearGenerationState = useCallback(
(state: MiniGameDraftGenerationState) =>
resolveFinishedMiniGameDraftGenerationState(state, 'ready', {
completedAssetCount: 135,
totalAssetCount: 135,
}),
[],
);
const createReadyWoodenFishGenerationState = useCallback(
(state: MiniGameDraftGenerationState) =>
resolveFinishedMiniGameDraftGenerationState(state, 'ready', {
completedAssetCount: 2,
totalAssetCount: 2,
}),
[],
);
const leaveBarkBattleFlow = useCallback(() => {
setBarkBattleDraftConfig(null);
setBarkBattlePublishedConfig(null);
setBarkBattleRuntimeMode('draft');
setBarkBattleRuntimeReturnStage('platform');
setBarkBattleError(null);
setBarkBattleGenerationPartialFailed(false);
setIsBarkBattleBusy(false);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const createBarkBattleGeneratingDraft = useCallback(
async (payload: BarkBattleConfigEditorPayload) => {
setBarkBattleError(null);
const hasEnoughPoints = await preflightBarkBattleDraftGeneration();
if (!hasEnoughPoints) {
return;
}
setIsBarkBattleBusy(true);
setBarkBattleGenerationPartialFailed(false);
try {
const draft = await createBarkBattleDraft(payload);
setBarkBattleDraftConfig(draft);
writeCreationUrlState(buildBarkBattleCreationUrlState(draft));
setBarkBattlePublishedConfig(null);
markDraftGenerating('bark-battle', [draft.workId, draft.draftId]);
markPendingDraftGenerating(
'bark-battle',
draft.workId ?? draft.draftId,
);
updateBarkBattleWorkCaches(
buildBarkBattleWorkSummaryFromDraft(
draft,
authUi?.user,
'pending_assets',
),
);
void refreshBarkBattleShelf().catch(() => undefined);
selectionStageRef.current = 'bark-battle-generating';
setSelectionStage('bark-battle-generating');
} catch (error) {
setBarkBattleError(
resolvePuzzleErrorMessage(error, '生成汪汪声浪草稿失败。'),
);
} finally {
setIsBarkBattleBusy(false);
}
},
[
authUi?.user,
markDraftGenerating,
markPendingDraftGenerating,
preflightBarkBattleDraftGeneration,
refreshBarkBattleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
updateBarkBattleWorkCaches,
],
);
const handleBarkBattleGenerationComplete = useCallback(
(draft: BarkBattleDraftConfig, partialFailed: boolean) => {
setBarkBattleDraftConfig(draft);
writeCreationUrlState(buildBarkBattleCreationUrlState(draft));
setBarkBattlePublishedConfig(null);
setBarkBattleGenerationPartialFailed(partialFailed);
const generationStatus = resolveBarkBattleDraftGenerationStatus(
draft,
partialFailed,
);
updateBarkBattleWorkCaches(
buildBarkBattleWorkSummaryFromDraft(
draft,
authUi?.user,
generationStatus,
),
);
markPendingDraftReady('bark-battle', draft.workId ?? draft.draftId, true);
markDraftReady('bark-battle', [draft.workId, draft.draftId], true);
void refreshBarkBattleShelf().catch(() => undefined);
selectionStageRef.current = 'bark-battle-result';
setSelectionStage('bark-battle-result');
},
[
authUi?.user,
markDraftReady,
markPendingDraftReady,
refreshBarkBattleShelf,
setSelectionStage,
updateBarkBattleWorkCaches,
],
);
const handleBarkBattleDraftChange = useCallback(
async (draft: BarkBattleDraftConfig) => {
setBarkBattleError(null);
setBarkBattleGenerationPartialFailed(false);
setBarkBattleDraftConfig(draft);
if (!draft.draftId?.trim() || !draft.workId?.trim()) {
return;
}
setIsBarkBattleBusy(true);
try {
const persistedDraft = await updateBarkBattleDraftConfig({
draftId: draft.draftId,
workId: draft.workId,
configVersion: draft.configVersion,
rulesetVersion: draft.rulesetVersion,
title: draft.title,
description: draft.description,
themeDescription: draft.themeDescription,
playerImageDescription: draft.playerImageDescription,
opponentImageDescription: draft.opponentImageDescription,
onomatopoeia: draft.onomatopoeia,
playerCharacterImageSrc: draft.playerCharacterImageSrc,
opponentCharacterImageSrc: draft.opponentCharacterImageSrc,
uiBackgroundImageSrc: draft.uiBackgroundImageSrc,
difficultyPreset: draft.difficultyPreset,
});
const nextDraft = {
...persistedDraft,
playerCharacterImageSrc:
draft.playerCharacterImageSrc ??
persistedDraft.playerCharacterImageSrc,
opponentCharacterImageSrc:
draft.opponentCharacterImageSrc ??
persistedDraft.opponentCharacterImageSrc,
uiBackgroundImageSrc:
draft.uiBackgroundImageSrc ?? persistedDraft.uiBackgroundImageSrc,
};
setBarkBattleDraftConfig(nextDraft);
writeCreationUrlState(buildBarkBattleCreationUrlState(nextDraft));
updateBarkBattleWorkCaches(
buildBarkBattleWorkSummaryFromDraft(
nextDraft,
authUi?.user,
resolveBarkBattleDraftGenerationStatus(nextDraft, false),
),
);
void refreshBarkBattleShelf().catch(() => undefined);
} catch (error) {
setBarkBattleError(
resolvePuzzleErrorMessage(error, '保存汪汪声浪草稿素材失败。'),
);
} finally {
setIsBarkBattleBusy(false);
}
},
[
authUi?.user,
refreshBarkBattleShelf,
resolvePuzzleErrorMessage,
updateBarkBattleWorkCaches,
],
);
const testBarkBattleDraft = useCallback(
(draft: BarkBattleDraftConfig) => {
setBarkBattleError(null);
setBarkBattleRuntimeMode('draft');
setBarkBattleRuntimeReturnStage('bark-battle-result');
setBarkBattlePublishedConfig(
buildBarkBattlePublishedConfigFromDraft(draft),
);
selectionStageRef.current = 'bark-battle-runtime';
setSelectionStage('bark-battle-runtime');
},
[setSelectionStage],
);
const publishBarkBattleDraft = useCallback(
async (draft: BarkBattleDraftConfig) => {
setBarkBattleError(null);
const workId = draft.workId?.trim();
if (!workId) {
setBarkBattleError(
'这份汪汪声浪草稿缺少作品ID,请重新生成草稿后再发布。',
);
return;
}
setIsBarkBattleBusy(true);
try {
const published = await publishBarkBattleWork({
draftId: draft.draftId,
workId,
publishedSnapshot: buildBarkBattlePublishSnapshot(draft),
});
const publishedWithAssets = mergeBarkBattlePublishedConfigAssets(
published,
draft,
);
const publicWorkCode = buildBarkBattlePublicWorkCode(
publishedWithAssets.workId,
);
setBarkBattlePublishedConfig(publishedWithAssets);
setBarkBattleRuntimeReturnStage('work-detail');
const publishedSummary: BarkBattleWorkSummary = {
...publishedWithAssets,
ownerUserId: authUi?.user?.id ?? '',
authorDisplayName: authUi?.user?.displayName ?? '创作者',
summary: publishedWithAssets.description ?? '',
status: 'published',
generationStatus: 'ready',
publishReady: true,
playCount: 0,
publishedAt: publishedWithAssets.publishedAt,
};
updateBarkBattleWorkCaches(publishedSummary);
setSelectedPublicWorkDetail(
mapBarkBattleWorkToPublicWorkDetail(publishedSummary),
);
selectionStageRef.current = 'work-detail';
setSelectionStage('work-detail');
pushAppHistoryPath(
buildPublicWorkStagePath('work-detail', publicWorkCode),
);
void Promise.allSettled([
refreshBarkBattleShelf(),
refreshBarkBattleGallery(),
]);
openPublishShareModal({
title: publishedWithAssets.title,
publicWorkCode,
stage: 'work-detail',
});
} catch (error) {
setBarkBattleError(
resolvePuzzleErrorMessage(error, '发布汪汪声浪大作战作品失败。'),
);
} finally {
setIsBarkBattleBusy(false);
}
},
[
authUi?.user?.displayName,
authUi?.user?.id,
openPublishShareModal,
refreshBarkBattleGallery,
refreshBarkBattleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
updateBarkBattleWorkCaches,
],
);
const leavePuzzleFlow = useCallback(() => {
setPuzzleOperation(null);
puzzleRunRef.current = null;
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleGenerationState(null);
setIsPuzzleNextLevelGenerating(false);
setActiveCreativeAgentSessionId(null);
setCreativeDraftEditError(null);
resetRecommendRuntimeSelection();
clearPuzzleRuntimeUrlState();
puzzleFlow.leaveFlow();
returnToCreationFlowSource();
}, [puzzleFlow, resetRecommendRuntimeSelection, returnToCreationFlowSource]);
const leaveVisualNovelFlow = useCallback(() => {
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setVisualNovelFormDraftPayload(null);
setVisualNovelGenerationStartedAtMs(null);
setVisualNovelGenerationPhase('generating');
visualNovelFlow.leaveFlow();
returnToCreationFlowSource();
}, [returnToCreationFlowSource, visualNovelFlow]);
const leaveBabyObjectMatchFlow = useCallback(() => {
setBabyObjectMatchDraft(null);
setBabyObjectMatchFormPayload(null);
setBabyObjectMatchGenerationState(null);
setBabyObjectMatchGenerationPhase('generating');
setBabyObjectMatchError(null);
returnToCreationFlowSource();
}, [returnToCreationFlowSource]);
const saveBabyObjectMatchResultDraft = useCallback(
async (draft: BabyObjectMatchDraft) => {
setBabyObjectMatchError(null);
setIsBabyObjectMatchBusy(true);
try {
const response = await saveBabyObjectMatchDraft({ draft });
setBabyObjectMatchDraft(response.draft);
writeCreationUrlState(
buildBabyObjectMatchCreationUrlState(response.draft),
);
void refreshBabyObjectMatchShelf();
} catch (error) {
setBabyObjectMatchError(
resolvePuzzleErrorMessage(error, '保存宝贝识物草稿失败。'),
);
} finally {
setIsBabyObjectMatchBusy(false);
}
},
[refreshBabyObjectMatchShelf, resolvePuzzleErrorMessage],
);
const ensureBabyObjectMatchGeneratedAssets = useCallback(
async (draft: BabyObjectMatchDraft) => {
if (!hasBabyObjectMatchPlaceholderAssets(draft)) {
return draft;
}
const response = await regenerateBabyObjectMatchDraftAssets(draft);
setBabyObjectMatchDraft(response.draft);
void refreshBabyObjectMatchShelf();
return response.draft;
},
[refreshBabyObjectMatchShelf],
);
const regenerateBabyObjectMatchResultAssets = useCallback(
async (draft: BabyObjectMatchDraft) => {
setBabyObjectMatchError(null);
setIsBabyObjectMatchBusy(true);
try {
await ensureBabyObjectMatchGeneratedAssets(draft);
} catch (error) {
setBabyObjectMatchError(
resolvePuzzleErrorMessage(error, '重新生成宝贝识物素材失败。'),
);
} finally {
setIsBabyObjectMatchBusy(false);
}
},
[ensureBabyObjectMatchGeneratedAssets, resolvePuzzleErrorMessage],
);
const publishBabyObjectMatchResultDraft = useCallback(
async (draft: BabyObjectMatchDraft) => {
setBabyObjectMatchError(null);
setIsBabyObjectMatchBusy(true);
try {
const generatedDraft =
await ensureBabyObjectMatchGeneratedAssets(draft);
const response = await publishBabyObjectMatchWork({
draft: generatedDraft,
});
setBabyObjectMatchDraft(response.draft);
void refreshBabyObjectMatchShelf();
openPublishShareModal({
title: response.draft.workTitle,
publicWorkCode:
response.publicWorkCode ||
buildBabyObjectMatchPublicWorkCode(response.draft.profileId),
stage: 'work-detail',
});
} catch (error) {
setBabyObjectMatchError(
resolvePuzzleErrorMessage(
error,
'生成宝贝识物素材失败,请重试后再发布。',
),
);
} finally {
setIsBabyObjectMatchBusy(false);
}
},
[
ensureBabyObjectMatchGeneratedAssets,
openPublishShareModal,
refreshBabyObjectMatchShelf,
resolvePuzzleErrorMessage,
],
);
const startBabyObjectMatchRuntimeFromDraft = useCallback(
async (
draft: BabyObjectMatchDraft,
returnStage: BabyObjectMatchRuntimeReturnStage = 'baby-object-match-result',
options: { embedded?: boolean } = {},
) => {
setBabyObjectMatchError(null);
setIsBabyObjectMatchBusy(true);
try {
const generatedDraft =
await ensureBabyObjectMatchGeneratedAssets(draft);
setBabyObjectMatchDraft(generatedDraft);
setBabyObjectMatchFormPayload({
itemAName: generatedDraft.itemNames[0],
itemBName: generatedDraft.itemNames[1],
});
setBabyObjectMatchRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('baby-object-match-runtime');
const publicWorkCode =
generatedDraft.publicationStatus === 'published'
? buildBabyObjectMatchPublicWorkCode(generatedDraft.profileId)
: null;
if (publicWorkCode) {
pushAppHistoryPath(
buildPublicWorkStagePath(
'baby-object-match-runtime',
publicWorkCode,
),
);
}
}
return true;
} catch (error) {
const message = resolvePuzzleErrorMessage(
error,
'生成宝贝识物素材失败,请重试后再试玩。',
);
setBabyObjectMatchError(message);
if (options.embedded) {
setActiveRecommendRuntimeError(message);
}
return false;
} finally {
setIsBabyObjectMatchBusy(false);
}
},
[
ensureBabyObjectMatchGeneratedAssets,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const startBabyLoveDrawingRuntime = useCallback(() => {
setSelectionStage('baby-love-drawing-runtime');
pushAppHistoryPath('/runtime/baby-love-drawing');
}, [setSelectionStage]);
const startChildMotionDemo = useCallback(() => {
window.location.assign('/child-motion-demo');
}, []);
const resolveBabyObjectMatchRuntimeDraft = useCallback(
async (entry: PlatformPublicGalleryCard) => {
if (!isEdutainmentGalleryEntry(entry)) {
return null;
}
return (
babyObjectMatchDrafts.find(
(draft) => draft.profileId === entry.profileId,
) ??
(await listLocalBabyObjectMatchDrafts()).find(
(draft) => draft.profileId === entry.profileId,
) ??
null
);
},
[babyObjectMatchDrafts],
);
const startBabyObjectMatchRuntimeFromEntry = useCallback(
async (
entry: PlatformPublicGalleryCard,
returnStage: BabyObjectMatchRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
const draft = await resolveBabyObjectMatchRuntimeDraft(entry);
if (!draft) {
setPublicWorkDetailError(
'当前宝贝识物作品缺少本地草稿,暂时无法进入玩法。',
);
return false;
}
return await startBabyObjectMatchRuntimeFromDraft(
draft,
returnStage,
options,
);
},
[resolveBabyObjectMatchRuntimeDraft, startBabyObjectMatchRuntimeFromDraft],
);
const saveVisualNovelDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const currentSession = visualNovelSession;
if (!currentSession || !currentSession.sessionId.trim()) {
return;
}
setVisualNovelError(null);
const now = new Date().toISOString();
// 中文注释:VN-07 的保存草稿先保证结果页编辑不会丢失;
// 后端 work profile 编译仍由“编译草稿”显式触发,避免把本地保存伪装成正式持久化。
setVisualNovelSession({
...currentSession,
draft,
status: draft.publishReady ? 'ready' : currentSession.status,
updatedAt: now,
});
if (!visualNovelWork) {
return;
}
setIsVisualNovelBusy(true);
try {
const response = await updateVisualNovelWork(
visualNovelWork.summary.profileId,
{ draft },
);
setVisualNovelWork(response.work);
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '保存视觉小说草稿失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
setVisualNovelSession,
visualNovelSession,
visualNovelWork,
],
);
const compileVisualNovelDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const sessionId = visualNovelSession?.sessionId?.trim();
if (!sessionId) {
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const response = await compileVisualNovelWorkProfile(sessionId, {
draft,
});
setVisualNovelSession(response.session);
setVisualNovelWork(response.work);
clearPendingDraftShelfItem('visual-novel', sessionId);
setSelectionStage('visual-novel-result');
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '编译视觉小说草稿失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
clearPendingDraftShelfItem,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
setVisualNovelSession,
visualNovelSession?.sessionId,
],
);
const publishVisualNovelDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const profileId = draft.profileId?.trim();
if (!profileId) {
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const { work } = await updateVisualNovelWork(profileId, {
draft,
});
setVisualNovelWork(work);
const publishedResponse = await publishVisualNovelWork(profileId);
setVisualNovelWork(publishedResponse.work);
setSelectedPublicWorkDetail(
mapVisualNovelWorkToPublicWorkDetail(publishedResponse.work.summary),
);
await Promise.allSettled([
refreshVisualNovelShelf(),
refreshVisualNovelGallery(),
]);
openPublishShareModal({
title: publishedResponse.work.summary.title,
publicWorkCode: buildVisualNovelPublicWorkCode(
publishedResponse.work.summary.profileId,
),
stage: 'work-detail',
});
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '发布视觉小说作品失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
openPublishShareModal,
refreshVisualNovelGallery,
refreshVisualNovelShelf,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
],
);
const startVisualNovelTestRunFromDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const profileId =
visualNovelWork?.summary.profileId?.trim() ?? draft.profileId?.trim();
if (!profileId) {
const run = createMockVisualNovelRunFromDraft(draft);
setVisualNovelRun(run);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-runtime');
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const { run } = await startVisualNovelRun(profileId, {
profileId,
mode: 'test',
});
setVisualNovelRun(run);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-runtime');
} catch (error) {
// 中文注释:VN-07 只要求结果页进入测试运行时;真实 runtime 接口未就绪时,
// 使用当前 draft 生成本地 test run,不扩展到正式玩家运行链路。
setVisualNovelRun(createMockVisualNovelRunFromDraft(draft));
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-runtime');
setVisualNovelError(
resolvePuzzleErrorMessage(
error,
'已进入本地试玩,真实运行接口暂不可用。',
),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
setSelectionStage,
visualNovelWork?.summary.profileId,
],
);
const startVisualNovelRunFromProfile = useCallback(
async (
profileId: string,
returnStage: VisualNovelRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
const targetProfileId = profileId.trim();
if (!targetProfileId) {
return false;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
let workDetail =
visualNovelWork?.summary.profileId === targetProfileId
? visualNovelWork
: null;
if (!workDetail) {
const response = await getVisualNovelWorkDetail(
targetProfileId,
options.embedded ? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS : {},
);
workDetail = response.work;
}
const startRunPayload = {
profileId: targetProfileId,
mode: 'play' as const,
};
const runtimeRequestOptions = await buildRecommendRuntimeRequestOptions(
{
kind: 'visual-novel',
embedded: options.embedded,
},
);
const { run } = options.embedded
? await startVisualNovelRun(
targetProfileId,
startRunPayload,
runtimeRequestOptions,
)
: await startVisualNovelRun(targetProfileId, startRunPayload);
setVisualNovelWork(workDetail);
setVisualNovelRun(run);
setVisualNovelRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('visual-novel-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'visual-novel-runtime',
buildVisualNovelPublicWorkCode(targetProfileId),
),
);
}
return true;
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '启动视觉小说试玩失败。'),
);
return false;
} finally {
setIsVisualNovelBusy(false);
}
},
[
buildRecommendRuntimeRequestOptions,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
visualNovelWork,
],
);
const submitVisualNovelRuntimeAction = useCallback(
async (payload: VisualNovelRuntimeActionRequest) => {
if (!visualNovelRun || isVisualNovelBusy) {
return;
}
if (visualNovelRun.runId.startsWith('vn-run-local')) {
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'visual-novel',
});
const nextRun = await streamVisualNovelRuntimeAction(
visualNovelRun.runId,
payload,
runtimeRequestOptions,
);
setVisualNovelRun(nextRun);
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '推进视觉小说运行失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
buildRecommendRuntimeRequestOptions,
isVisualNovelBusy,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
visualNovelRun,
],
);
const streamCreativeAgentPayloadForSession = useCallback(
async (
session: CreativeAgentSessionSnapshot,
payload: StreamCreativeAgentMessageRequest,
) => {
setCreativeAgentError(null);
setIsCreativeAgentStreaming(true);
setCreativeAgentEvents([]);
setCreativeAgentSession((current) =>
current
? {
...current,
messages: [
...current.messages,
{
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.content
.map((part) =>
part.type === 'input_text' ? part.text.trim() : '参考图',
)
.filter(Boolean)
.join(' / '),
createdAt: new Date().toISOString(),
},
],
}
: current,
);
try {
const nextSession = await streamCreativeAgentMessage(
session.sessionId,
payload,
{
onEvent: (event) => {
setCreativeAgentEvents((current) => [...current, event]);
if (event.event === 'stage') {
setCreativeAgentSession((current) =>
current
? {
...current,
stage: event.data.stage,
updatedAt: new Date().toISOString(),
}
: current,
);
}
if (event.event === 'session') {
setCreativeAgentSession(event.data.session);
}
},
},
);
setCreativeAgentSession(nextSession);
if (nextSession.targetBinding) {
setActiveCreativeAgentSessionId(nextSession.sessionId);
}
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '发送智能创作消息失败。'),
);
} finally {
setIsCreativeAgentStreaming(false);
}
},
[resolvePuzzleErrorMessage],
);
const submitCreativeAgentMessage = useCallback(
async (payload: StreamCreativeAgentMessageRequest) => {
if (!creativeAgentSession || isCreativeAgentStreaming) {
return;
}
await streamCreativeAgentPayloadForSession(creativeAgentSession, payload);
},
[
creativeAgentSession,
isCreativeAgentStreaming,
streamCreativeAgentPayloadForSession,
],
);
const confirmCreativeTemplateSelection = useCallback(
async (selection: PuzzleCreativeTemplateSelection) => {
const sessionId = creativeAgentSession?.sessionId?.trim();
if (!sessionId || isCreativeAgentBusy || isCreativeAgentStreaming) {
return;
}
setIsCreativeAgentBusy(true);
setCreativeAgentError(null);
try {
const response = await confirmCreativePuzzleTemplate(sessionId, {
selection,
});
setCreativeAgentSession(response.session);
if (response.session.targetBinding) {
setActiveCreativeAgentSessionId(response.session.sessionId);
}
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '确认拼图模板失败。'),
);
} finally {
setIsCreativeAgentBusy(false);
}
},
[
creativeAgentSession?.sessionId,
isCreativeAgentBusy,
isCreativeAgentStreaming,
resolvePuzzleErrorMessage,
],
);
const submitBigFishMessage = bigFishFlow.submitMessage;
const submitSquareHoleMessage = squareHoleFlow.submitMessage;
const submitPuzzleMessage = puzzleFlow.submitMessage;
const executeBigFishAction = bigFishFlow.executeAction;
const executeMatch3DAction = match3dFlow.executeAction;
const executeSquareHoleAction = squareHoleFlow.executeAction;
const retryMatch3DDraftGeneration = useCallback(() => {
if (match3dSession?.sessionId) {
const retryPayload =
match3dFormDraftPayload ??
buildMatch3DFormPayloadFromSession(match3dSession);
void executeMatch3DAction({
action: 'match3d_compile_draft',
generateClickSound: retryPayload.generateClickSound,
});
return;
}
if (match3dFormDraftPayload) {
void createMatch3DDraftFromForm(match3dFormDraftPayload);
return;
}
}, [
createMatch3DDraftFromForm,
executeMatch3DAction,
match3dFormDraftPayload,
match3dSession,
]);
const retrySquareHoleAssetGeneration = useCallback(() => {
const session = squareHoleSession;
if (!session?.draft?.profileId) {
void executeSquareHoleAction({
action: 'square_hole_compile_draft',
});
return;
}
void executeSquareHoleAction({
action: 'square_hole_generate_visual_assets',
});
}, [executeSquareHoleAction, squareHoleSession]);
const compileJumpHopSession = useCallback(
async (
created: JumpHopSessionResponse,
payload?: JumpHopWorkspaceCreateRequest,
) => {
const generationState = createMiniGameDraftGenerationState(
'jump-hop',
resolveMiniGameDraftGenerationStartedAtMs(created.session.updatedAt),
);
setJumpHopError(null);
setJumpHopSession(created.session);
writeCreationUrlState(
buildJumpHopCreationUrlState({ session: created.session }),
);
setJumpHopWork(null);
setJumpHopRun(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopGenerationState(generationState);
setJumpHopQueueState(null);
setIsJumpHopBusy(true);
setSelectionStage('jump-hop-generating');
markDraftGenerating('jump-hop', [
created.session.sessionId,
created.session.draft?.profileId,
]);
markPendingDraftGenerating('jump-hop', created.session.sessionId);
try {
const response = await jumpHopClient.executeAction(
created.session.sessionId,
buildJumpHopDraftActionPayload('compile-draft', {
payload,
draft: created.session.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setJumpHopQueueState(response.queueState);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? null);
writeCreationUrlState(
buildJumpHopCreationUrlState({
session: response.session,
work: response.work,
}),
);
return;
}
setJumpHopQueueState(null);
const readyState = createReadyJumpHopGenerationState(generationState);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? null);
writeCreationUrlState(
buildJumpHopCreationUrlState({
session: response.session,
work: response.work,
}),
);
setJumpHopGenerationState(readyState);
if (response.work) {
setJumpHopWorks((current) => [
response.work!.summary,
...current.filter(
(item) => item.workId !== response.work!.summary.workId,
),
]);
markPendingDraftReady('jump-hop', created.session.sessionId, false);
markDraftReady(
'jump-hop',
[
created.session.sessionId,
response.work.summary.workId,
response.work.summary.profileId,
],
false,
);
void refreshJumpHopShelf().catch(() => undefined);
}
setSelectionStage('jump-hop-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
'生成跳一跳草稿失败。',
);
setJumpHopError(errorMessage);
setJumpHopQueueState(null);
setJumpHopGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
),
);
markPendingDraftFailed('jump-hop', created.session.sessionId);
markDraftFailed(
'jump-hop',
[created.session.sessionId, created.session.draft?.profileId],
errorMessage,
);
void refreshJumpHopShelf().catch(() => undefined);
try {
const latest = await jumpHopClient.getSession(
created.session.sessionId,
);
markDraftFailed(
'jump-hop',
[latest.session.sessionId, latest.session.draft?.profileId],
errorMessage,
);
setJumpHopSession(latest.session);
setJumpHopWork(null);
writeCreationUrlState(
buildJumpHopCreationUrlState({ session: latest.session }),
);
} catch {
setJumpHopSession(created.session);
setJumpHopWork(null);
writeCreationUrlState(
buildJumpHopCreationUrlState({ session: created.session }),
);
}
} finally {
setIsJumpHopBusy(false);
}
},
[
createReadyJumpHopGenerationState,
markDraftFailed,
markDraftGenerating,
markPendingDraftFailed,
markPendingDraftGenerating,
refreshJumpHopShelf,
setSelectionStage,
],
);
const retryJumpHopDraftGeneration = useCallback(() => {
if (!jumpHopSession) {
setSelectionStage('jump-hop-workspace');
return;
}
void compileJumpHopSession({ session: jumpHopSession });
}, [compileJumpHopSession, jumpHopSession, setSelectionStage]);
const regenerateJumpHopAsset = useCallback(
async (actionType: 'regenerate-tiles') => {
if (!jumpHopSession?.sessionId) {
setSelectionStage('jump-hop-workspace');
return;
}
const generationState = createMiniGameDraftGenerationState('jump-hop');
setJumpHopError(null);
setJumpHopGenerationState(generationState);
setJumpHopQueueState(null);
setIsJumpHopBusy(true);
setSelectionStage('jump-hop-generating');
try {
const response = await jumpHopClient.executeAction(
jumpHopSession.sessionId,
buildJumpHopDraftActionPayload(actionType, {
draft: jumpHopSession.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setJumpHopQueueState(response.queueState);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? jumpHopWork);
writeCreationUrlState(
buildJumpHopCreationUrlState({
session: response.session,
work: response.work ?? jumpHopWork,
}),
);
return;
}
setJumpHopQueueState(null);
setJumpHopSession(response.session);
setJumpHopWork(response.work ?? jumpHopWork);
writeCreationUrlState(
buildJumpHopCreationUrlState({
session: response.session,
work: response.work ?? jumpHopWork,
}),
);
setJumpHopGenerationState(
createReadyJumpHopGenerationState(generationState),
);
setSelectionStage('jump-hop-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
'重新生成跳一跳地块失败。',
);
setJumpHopError(errorMessage);
setJumpHopQueueState(null);
setJumpHopGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
),
);
} finally {
setIsJumpHopBusy(false);
}
},
[
createReadyJumpHopGenerationState,
jumpHopSession?.draft,
jumpHopSession?.sessionId,
jumpHopWork,
setSelectionStage,
],
);
const publishJumpHopDraft = useCallback(async () => {
const profileId = jumpHopWork?.summary.profileId?.trim();
if (!profileId) {
setJumpHopError('跳一跳草稿尚未生成可发布作品。');
setSelectionStage('jump-hop-result');
return;
}
setIsJumpHopBusy(true);
setJumpHopError(null);
try {
const response = await jumpHopClient.publishWork(profileId);
setJumpHopWork(response.item);
setJumpHopWorks((current) => [
response.item.summary,
...current.filter(
(item) => item.workId !== response.item.summary.workId,
),
]);
void refreshJumpHopShelf().catch(() => undefined);
openPublishShareModal({
title: response.item.summary.workTitle || '跳一跳',
publicWorkCode: buildJumpHopPublicWorkCode(
response.item.summary.profileId,
),
stage: 'work-detail',
});
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '发布跳一跳作品失败。'),
);
setSelectionStage('jump-hop-result');
} finally {
setIsJumpHopBusy(false);
}
}, [
jumpHopWork?.summary.profileId,
openPublishShareModal,
setSelectionStage,
]);
const startJumpHopTestRunFromProfile = useCallback(async () => {
const profileId = jumpHopWork?.summary.profileId?.trim();
if (!profileId) {
setJumpHopError('跳一跳草稿尚未生成可试玩作品。');
setSelectionStage('jump-hop-result');
return;
}
setIsJumpHopBusy(true);
setJumpHopError(null);
setJumpHopRuntimeReturnStage('jump-hop-result');
try {
const response = await jumpHopClient.startRun(profileId, {
runtimeMode: 'draft',
});
setJumpHopRun(response.run);
setSelectionStage('jump-hop-runtime');
} catch (error) {
setJumpHopError(
error instanceof Error ? error.message : '启动跳一跳试玩失败。',
);
setSelectionStage('jump-hop-result');
} finally {
setIsJumpHopBusy(false);
}
}, [jumpHopWork?.summary.profileId, setSelectionStage]);
const startJumpHopRunFromProfile = useCallback(
async (
profileId: string,
options: {
embedded?: boolean;
preloadedWork?: JumpHopWorkProfileResponse | null;
returnStage?: 'work-detail' | 'platform';
} = {},
) => {
const normalizedProfileId = profileId.trim();
if (!normalizedProfileId) {
setJumpHopError('当前跳一跳作品信息不完整,暂时无法进入玩法。');
return false;
}
setIsJumpHopBusy(true);
setJumpHopError(null);
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'jump-hop',
embedded: options.embedded,
forcePublicRuntime: shouldUseRecommendRuntimeGuestAuth(authUi),
});
setJumpHopRuntimeRequestOptions(
Object.keys(runtimeRequestOptions).length > 0
? runtimeRequestOptions
: null,
);
const [detail, runResponse] = await Promise.all([
options.preloadedWork
? Promise.resolve({ item: options.preloadedWork })
: jumpHopClient
.getWorkDetail(normalizedProfileId)
.catch(() => null),
jumpHopClient.startRun(normalizedProfileId, {
...runtimeRequestOptions,
runtimeMode: 'published',
}),
]);
if (detail?.item) {
setJumpHopWork(detail.item);
}
setJumpHopRun(runResponse.run);
if (!options.embedded) {
setSelectionStage('jump-hop-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'jump-hop-runtime',
buildJumpHopPublicWorkCode(normalizedProfileId),
),
);
}
return true;
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '启动跳一跳玩法失败。'),
);
return false;
} finally {
setIsJumpHopBusy(false);
}
},
[authUi, buildRecommendRuntimeRequestOptions, setSelectionStage],
);
useEffect(() => {
if (selectionStage !== 'jump-hop-runtime' || jumpHopRun) {
return;
}
const publicWorkCode = initialPublicWorkCode?.trim() ?? '';
const restoreKey = publicWorkCode || '__jump-hop-runtime-empty__';
if (handledJumpHopRuntimeRestoreRef.current === restoreKey) {
return;
}
handledJumpHopRuntimeRestoreRef.current = restoreKey;
if (!publicWorkCode) {
setJumpHopError(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopRuntimeReturnStage('platform');
setSelectionStage('platform');
pushAppHistoryPath('/');
return;
}
let cancelled = false;
const restoreJumpHopRuntime = async () => {
setIsJumpHopBusy(true);
setJumpHopError(null);
try {
const detail = await jumpHopClient.getGalleryDetail(publicWorkCode);
if (cancelled) {
return;
}
const profileId = detail.item.summary.profileId;
const started = await startJumpHopRunFromProfile(profileId, {
preloadedWork: detail.item,
returnStage: 'work-detail',
});
if (!started && !cancelled) {
setSelectionStage('platform');
pushAppHistoryPath('/');
}
} catch (error) {
if (cancelled) {
return;
}
setJumpHopError(
resolveRpgCreationErrorMessage(error, '恢复跳一跳玩法失败。'),
);
setJumpHopRun(null);
setJumpHopRuntimeRequestOptions(null);
setJumpHopRuntimeReturnStage('platform');
setSelectionStage('platform');
pushAppHistoryPath('/');
} finally {
if (!cancelled) {
setIsJumpHopBusy(false);
}
}
};
void restoreJumpHopRuntime();
return () => {
cancelled = true;
};
}, [
initialPublicWorkCode,
jumpHopRun,
selectionStage,
setSelectionStage,
startJumpHopRunFromProfile,
]);
const restartJumpHopRuntimeRun = useCallback(async () => {
const runId = jumpHopRun?.runId;
if (!runId) {
await startJumpHopTestRunFromProfile();
return;
}
setIsJumpHopBusy(true);
setJumpHopError(null);
try {
const response = await jumpHopClient.restartRun(
runId,
jumpHopRuntimeRequestOptions ?? undefined,
);
setJumpHopRun(response.run);
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '重新开始跳一跳失败。'),
);
} finally {
setIsJumpHopBusy(false);
}
}, [
jumpHopRun?.runId,
jumpHopRuntimeRequestOptions,
startJumpHopTestRunFromProfile,
]);
const submitJumpHopJumpAction = useCallback(
async (
payload: Pick<
JumpHopJumpRequest,
'dragDistance' | 'dragVectorX' | 'dragVectorY'
>,
) => {
const runId = jumpHopRun?.runId;
if (!runId) {
return;
}
try {
const response = await jumpHopClient.submitJump(
runId,
payload,
jumpHopRuntimeRequestOptions ?? undefined,
);
setJumpHopRun(response.run);
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '提交跳一跳失败。'),
);
}
},
[jumpHopRun?.runId, jumpHopRuntimeRequestOptions],
);
const compilePuzzleClearSession = useCallback(
async (
created: PuzzleClearSessionResponse,
payload?: PuzzleClearWorkspaceCreateRequest,
) => {
const generationState = createMiniGameDraftGenerationState(
'puzzle-clear',
resolveMiniGameDraftGenerationStartedAtMs(
created.session.updatedAt ?? created.session.createdAt,
),
);
setPuzzleClearError(null);
setPuzzleClearSession(created.session);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({ session: created.session }),
);
setPuzzleClearWork(null);
setPuzzleClearRun(null);
setPuzzleClearGenerationState(generationState);
setPuzzleClearQueueState(null);
setIsPuzzleClearBusy(true);
markDraftGenerating('puzzle-clear', [created.session.sessionId]);
markPendingDraftGenerating('puzzle-clear', created.session.sessionId);
selectionStageRef.current = 'puzzle-clear-generating';
setSelectionStage('puzzle-clear-generating');
try {
const response = await puzzleClearClient.executeAction(
created.session.sessionId,
{
actionType: 'compile-draft',
workTitle: payload?.workTitle ?? created.session.draft?.workTitle,
workDescription:
payload?.workDescription ??
created.session.draft?.workDescription,
themePrompt:
payload?.themePrompt ?? created.session.draft?.themePrompt,
boardBackgroundPrompt:
payload?.boardBackgroundPrompt ??
created.session.draft?.boardBackgroundPrompt,
generateBoardBackground:
payload?.generateBoardBackground ??
created.session.draft?.generateBoardBackground,
boardBackgroundAsset:
payload?.boardBackgroundAsset ??
created.session.draft?.boardBackgroundAsset,
},
);
if (response.queueState && response.session.status === 'generating') {
setPuzzleClearQueueState(response.queueState);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? null);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: response.session,
work: response.work,
}),
);
return;
}
setPuzzleClearQueueState(null);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? null);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: response.session,
work: response.work,
}),
);
setPuzzleClearGenerationState(
createReadyPuzzleClearGenerationState(generationState),
);
if (response.work) {
setPuzzleClearWorks((current) => [
response.work!.summary,
...current.filter(
(item) =>
item.workId !== response.work!.summary.workId &&
item.sourceSessionId !== response.work!.summary.sourceSessionId,
),
]);
markPendingDraftReady(
'puzzle-clear',
created.session.sessionId,
false,
);
markDraftReady(
'puzzle-clear',
[
created.session.sessionId,
response.work.summary.workId,
response.work.summary.profileId,
response.work.summary.sourceSessionId,
],
false,
);
void refreshPuzzleClearShelf().catch(() => undefined);
}
setSelectionStage('puzzle-clear-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
'生成拼消消草稿失败。',
);
setPuzzleClearError(errorMessage);
setPuzzleClearQueueState(null);
setPuzzleClearGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
),
);
markPendingDraftFailed('puzzle-clear', created.session.sessionId);
markDraftFailed(
'puzzle-clear',
[created.session.sessionId, created.session.draft?.profileId],
errorMessage,
);
setPuzzleClearWorks((current) =>
current.map((item) =>
item.sourceSessionId === created.session.sessionId
? {
...item,
generationStatus: 'failed',
updatedAt: new Date().toISOString(),
}
: item,
),
);
void refreshPuzzleClearShelf().catch(() => undefined);
try {
const latest = await puzzleClearClient.getSession(
created.session.sessionId,
);
markDraftFailed(
'puzzle-clear',
[latest.session.sessionId, latest.session.draft?.profileId],
errorMessage,
);
setPuzzleClearSession(latest.session);
setPuzzleClearWork(null);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({ session: latest.session }),
);
} catch {
setPuzzleClearSession(created.session);
setPuzzleClearWork(null);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({ session: created.session }),
);
}
} finally {
setIsPuzzleClearBusy(false);
}
},
[
createReadyPuzzleClearGenerationState,
markDraftFailed,
markDraftGenerating,
markDraftReady,
markPendingDraftFailed,
markPendingDraftGenerating,
markPendingDraftReady,
refreshPuzzleClearShelf,
setSelectionStage,
],
);
const retryPuzzleClearDraftGeneration = useCallback(() => {
if (!puzzleClearSession) {
setSelectionStage('puzzle-clear-workspace');
return;
}
void compilePuzzleClearSession({ session: puzzleClearSession });
}, [compilePuzzleClearSession, puzzleClearSession, setSelectionStage]);
const regeneratePuzzleClearAtlas = useCallback(async () => {
if (!puzzleClearSession?.sessionId) {
setSelectionStage('puzzle-clear-workspace');
return;
}
const generationState = createMiniGameDraftGenerationState('puzzle-clear');
setPuzzleClearError(null);
setPuzzleClearGenerationState(generationState);
setPuzzleClearQueueState(null);
setIsPuzzleClearBusy(true);
selectionStageRef.current = 'puzzle-clear-generating';
setSelectionStage('puzzle-clear-generating');
try {
const response = await puzzleClearClient.executeAction(
puzzleClearSession.sessionId,
{
actionType: 'regenerate-atlas',
profileId:
puzzleClearWork?.summary.profileId ??
puzzleClearSession.draft?.profileId,
workTitle: puzzleClearSession.draft?.workTitle,
workDescription: puzzleClearSession.draft?.workDescription,
themePrompt: puzzleClearSession.draft?.themePrompt,
boardBackgroundPrompt:
puzzleClearSession.draft?.boardBackgroundPrompt,
generateBoardBackground:
puzzleClearSession.draft?.generateBoardBackground,
boardBackgroundAsset: puzzleClearSession.draft?.boardBackgroundAsset,
},
);
if (response.queueState && response.session.status === 'generating') {
setPuzzleClearQueueState(response.queueState);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? puzzleClearWork);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: response.session,
work: response.work ?? puzzleClearWork,
}),
);
return;
}
setPuzzleClearQueueState(null);
setPuzzleClearSession(response.session);
setPuzzleClearWork(response.work ?? puzzleClearWork);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: response.session,
work: response.work ?? puzzleClearWork,
}),
);
setPuzzleClearGenerationState(
createReadyPuzzleClearGenerationState(generationState),
);
setSelectionStage('puzzle-clear-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
'重新生成拼消消图集失败。',
);
setPuzzleClearError(errorMessage);
setPuzzleClearQueueState(null);
setPuzzleClearGenerationState(
resolveFinishedMiniGameDraftGenerationState(generationState, 'failed', {
error: errorMessage,
}),
);
} finally {
setIsPuzzleClearBusy(false);
}
}, [
createReadyPuzzleClearGenerationState,
puzzleClearSession,
puzzleClearWork,
setSelectionStage,
]);
const publishPuzzleClearDraft = useCallback(async () => {
const profileId = puzzleClearWork?.summary.profileId?.trim();
if (!profileId) {
setPuzzleClearError('拼消消草稿尚未生成可发布作品。');
setSelectionStage('puzzle-clear-result');
return;
}
setIsPuzzleClearBusy(true);
setPuzzleClearError(null);
try {
const response = await puzzleClearClient.publishWork(profileId);
setPuzzleClearWork(response.item);
setPuzzleClearWorks((current) => [
response.item.summary,
...current.filter(
(item) => item.workId !== response.item.summary.workId,
),
]);
void refreshPuzzleClearShelf();
void refreshPuzzleClearGallery();
const publicWorkCode = buildPuzzleClearPublicWorkCode(
response.item.summary.profileId,
);
setSelectedPublicWorkDetail(
mapPuzzleClearWorkToPlatformGalleryCard(response.item),
);
setPublicWorkDetailError(null);
selectionStageRef.current = 'work-detail';
setSelectionStage('work-detail');
pushAppHistoryPath(
buildPublicWorkStagePath('work-detail', publicWorkCode),
);
openPublishShareModal({
title: response.item.summary.workTitle || '拼消消',
publicWorkCode,
stage: 'work-detail',
});
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '发布拼消消作品失败。'),
);
setSelectionStage('puzzle-clear-result');
} finally {
setIsPuzzleClearBusy(false);
}
}, [
openPublishShareModal,
puzzleClearWork?.summary.profileId,
refreshPuzzleClearGallery,
refreshPuzzleClearShelf,
setSelectionStage,
]);
const startPuzzleClearTestRunFromProfile = useCallback(async () => {
if (!puzzleClearWork) {
setPuzzleClearError('拼消消草稿尚未生成可试玩作品。');
setSelectionStage('puzzle-clear-result');
return;
}
setPuzzleClearError(null);
setPuzzleClearRuntimeReturnStage('puzzle-clear-result');
setPuzzleClearRun(createPuzzleClearLocalRuntimeSnapshot(puzzleClearWork));
setSelectionStage('puzzle-clear-runtime');
}, [puzzleClearWork, setSelectionStage]);
const startPuzzleClearRunFromProfile = useCallback(
async (
profileId: string,
options: {
embedded?: boolean;
returnStage?: 'work-detail' | 'platform';
} = {},
) => {
const normalizedProfileId = profileId.trim();
if (!normalizedProfileId) {
setPuzzleClearError('当前拼消消作品信息不完整,暂时无法进入玩法。');
return false;
}
setIsPuzzleClearBusy(true);
setPuzzleClearError(null);
setPuzzleClearRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
embedded: options.embedded,
});
const [detail, runResponse] = await Promise.all([
puzzleClearClient
.getRuntimeWorkDetail(normalizedProfileId)
.catch(() => null),
puzzleClearClient.startRun(normalizedProfileId, runtimeRequestOptions),
]);
if (detail?.item) {
setPuzzleClearWork(detail.item);
}
setPuzzleClearRun(runResponse.run);
if (!options.embedded) {
setSelectionStage('puzzle-clear-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'puzzle-clear-runtime',
buildPuzzleClearPublicWorkCode(normalizedProfileId),
),
);
}
return true;
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '启动拼消消玩法失败。'),
);
return false;
} finally {
setIsPuzzleClearBusy(false);
}
},
[buildRecommendRuntimeRequestOptions, setSelectionStage],
);
const retryPuzzleClearLevelRun = useCallback(async () => {
const runId = puzzleClearRun?.runId;
if (!runId) {
await startPuzzleClearTestRunFromProfile();
return;
}
if (isPuzzleClearLocalRuntimeSnapshot(puzzleClearRun)) {
if (!puzzleClearWork) {
setPuzzleClearError('拼消消草稿尚未生成可重试作品。');
setSelectionStage('puzzle-clear-result');
return;
}
setPuzzleClearError(null);
setPuzzleClearRun(
retryPuzzleClearLocalLevel(puzzleClearRun, puzzleClearWork),
);
return;
}
setIsPuzzleClearBusy(true);
setPuzzleClearError(null);
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.retryLevel(
runId,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '重试拼消消关卡失败。'),
);
} finally {
setIsPuzzleClearBusy(false);
}
}, [
puzzleClearRun,
puzzleClearWork,
buildRecommendRuntimeRequestOptions,
setSelectionStage,
startPuzzleClearTestRunFromProfile,
]);
const advancePuzzleClearLevelRun = useCallback(async () => {
const runId = puzzleClearRun?.runId;
if (!runId) {
return;
}
if (isPuzzleClearLocalRuntimeSnapshot(puzzleClearRun)) {
if (!puzzleClearWork) {
setPuzzleClearError('拼消消草稿尚未生成可继续作品。');
setSelectionStage('puzzle-clear-result');
return;
}
setPuzzleClearError(null);
setPuzzleClearRun(
advancePuzzleClearLocalLevel(puzzleClearRun, puzzleClearWork),
);
return;
}
setIsPuzzleClearBusy(true);
setPuzzleClearError(null);
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.advanceNextLevel(
runId,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '进入拼消消下一关失败。'),
);
} finally {
setIsPuzzleClearBusy(false);
}
}, [
puzzleClearRun,
puzzleClearWork,
buildRecommendRuntimeRequestOptions,
setSelectionStage,
]);
const markPuzzleClearLevelTimeUp = useCallback(async () => {
const runId = puzzleClearRun?.runId;
if (!runId) {
return;
}
if (isPuzzleClearLocalRuntimeSnapshot(puzzleClearRun)) {
setPuzzleClearError(null);
setPuzzleClearRun(markPuzzleClearLocalTimeUp(puzzleClearRun));
return;
}
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.markTimeUp(
runId,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '同步拼消消倒计时失败。'),
);
}
}, [puzzleClearRun, buildRecommendRuntimeRequestOptions]);
const swapPuzzleClearCardsInRun = useCallback(
async (payload: {
fromRow: number;
fromCol: number;
toRow: number;
toCol: number;
}) => {
const runId = puzzleClearRun?.runId;
if (!runId) {
return;
}
if (isPuzzleClearLocalRuntimeSnapshot(puzzleClearRun)) {
setPuzzleClearError(null);
setPuzzleClearRun(swapPuzzleClearLocalCards(puzzleClearRun, payload));
return;
}
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'puzzle-clear',
});
const response = await puzzleClearClient.swapCards(
runId,
payload,
runtimeRequestOptions,
);
setPuzzleClearRun(response.run);
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '交换拼消消卡片失败。'),
);
}
},
[puzzleClearRun, buildRecommendRuntimeRequestOptions],
);
const compileWoodenFishSession = useCallback(
async (
created: WoodenFishSessionResponse,
payload?: WoodenFishWorkspaceCreateRequest,
) => {
const generationState = createMiniGameDraftGenerationState(
'wooden-fish',
resolveMiniGameDraftGenerationStartedAtMs(
created.session.updatedAt ?? created.session.createdAt,
),
);
setWoodenFishError(null);
setWoodenFishSession(created.session);
writeCreationUrlState(
buildWoodenFishCreationUrlState({ session: created.session }),
);
setWoodenFishWork(null);
setWoodenFishRun(null);
setWoodenFishGenerationState(generationState);
setWoodenFishQueueState(null);
setIsWoodenFishBusy(true);
setSelectionStage('wooden-fish-generating');
markDraftGenerating('wooden-fish', [created.session.sessionId]);
markPendingDraftGenerating('wooden-fish', created.session.sessionId);
setWoodenFishWorks((current) => [
buildWoodenFishGeneratingWorkSummary(created.session, payload),
...current.filter(
(item) => item.sourceSessionId !== created.session.sessionId,
),
]);
try {
const response = await woodenFishClient.executeAction(
created.session.sessionId,
buildWoodenFishDraftActionPayload('compile-draft', {
payload,
draft: created.session.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setWoodenFishQueueState(response.queueState);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? null);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work,
}),
);
return;
}
setWoodenFishQueueState(null);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? null);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work,
}),
);
setWoodenFishGenerationState(
createReadyWoodenFishGenerationState(generationState),
);
if (response.work) {
setWoodenFishWorks((current) => [
response.work!.summary,
...current.filter(
(item) =>
item.workId !== response.work!.summary.workId &&
item.sourceSessionId !== response.work!.summary.sourceSessionId,
),
]);
markPendingDraftReady(
'wooden-fish',
created.session.sessionId,
false,
);
markDraftReady(
'wooden-fish',
[
created.session.sessionId,
response.work.summary.workId,
response.work.summary.profileId,
response.work.summary.sourceSessionId,
],
false,
);
void refreshWoodenFishShelf().catch(() => undefined);
}
setSelectionStage('wooden-fish-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
'生成敲木鱼草稿失败。',
);
setWoodenFishError(errorMessage);
setWoodenFishQueueState(null);
setWoodenFishGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
),
);
markPendingDraftFailed('wooden-fish', created.session.sessionId);
markDraftFailed(
'wooden-fish',
[created.session.sessionId, created.session.draft?.profileId],
errorMessage,
);
setWoodenFishWorks((current) =>
current.map((item) =>
item.sourceSessionId === created.session.sessionId
? {
...item,
generationStatus: 'failed',
updatedAt: new Date().toISOString(),
}
: item,
),
);
void refreshWoodenFishShelf().catch(() => undefined);
try {
const latest = await woodenFishClient.getSession(
created.session.sessionId,
);
markDraftFailed(
'wooden-fish',
[latest.session.sessionId, latest.session.draft?.profileId],
errorMessage,
);
setWoodenFishSession(latest.session);
setWoodenFishWork(null);
writeCreationUrlState(
buildWoodenFishCreationUrlState({ session: latest.session }),
);
} catch {
setWoodenFishSession(created.session);
setWoodenFishWork(null);
writeCreationUrlState(
buildWoodenFishCreationUrlState({ session: created.session }),
);
}
} finally {
setIsWoodenFishBusy(false);
}
},
[
createReadyWoodenFishGenerationState,
markDraftFailed,
markDraftGenerating,
markDraftReady,
markPendingDraftFailed,
markPendingDraftGenerating,
markPendingDraftReady,
refreshWoodenFishShelf,
setSelectionStage,
],
);
const retryWoodenFishDraftGeneration = useCallback(() => {
if (!woodenFishSession) {
setSelectionStage('wooden-fish-workspace');
return;
}
void compileWoodenFishSession({ session: woodenFishSession });
}, [compileWoodenFishSession, setSelectionStage, woodenFishSession]);
const regenerateWoodenFishAsset = useCallback(
async (actionType: 'regenerate-hit-object') => {
if (!woodenFishSession?.sessionId) {
setSelectionStage('wooden-fish-workspace');
return;
}
const generationState = createMiniGameDraftGenerationState(
'wooden-fish',
resolveMiniGameDraftGenerationStartedAtMs(woodenFishSession.updatedAt),
);
setWoodenFishError(null);
setWoodenFishGenerationState(generationState);
setWoodenFishQueueState(null);
setIsWoodenFishBusy(true);
setSelectionStage('wooden-fish-generating');
try {
const response = await woodenFishClient.executeAction(
woodenFishSession.sessionId,
buildWoodenFishDraftActionPayload(actionType, {
draft: woodenFishSession.draft,
}),
);
if (response.queueState && response.session.status === 'generating') {
setWoodenFishQueueState(response.queueState);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? woodenFishWork);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work ?? woodenFishWork,
}),
);
return;
}
setWoodenFishQueueState(null);
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? woodenFishWork);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work ?? woodenFishWork,
}),
);
setWoodenFishGenerationState(
createReadyWoodenFishGenerationState(generationState),
);
setSelectionStage('wooden-fish-result');
} catch (error) {
const errorMessage = resolveRpgCreationErrorMessage(
error,
'重新生成敲击物图案失败。',
);
setWoodenFishError(errorMessage);
setWoodenFishQueueState(null);
setWoodenFishGenerationState(
resolveFinishedMiniGameDraftGenerationState(
generationState,
'failed',
{ error: errorMessage },
),
);
} finally {
setIsWoodenFishBusy(false);
}
},
[
createReadyWoodenFishGenerationState,
setSelectionStage,
woodenFishSession?.draft,
woodenFishSession?.sessionId,
woodenFishWork,
],
);
const updateWoodenFishWorkMeta = useCallback(
async (payload: {
workTitle: string;
workDescription: string;
themeTags: string[];
}) => {
const sessionId = woodenFishSession?.sessionId?.trim();
const profileId =
woodenFishWork?.summary.profileId?.trim() ||
woodenFishSession?.draft?.profileId?.trim() ||
'';
if (!sessionId || !profileId) {
setWoodenFishError('敲木鱼草稿尚未生成可保存作品信息。');
setSelectionStage('wooden-fish-result');
return false;
}
setIsWoodenFishBusy(true);
setWoodenFishError(null);
try {
const response = await woodenFishClient.executeAction(sessionId, {
actionType: 'update-work-meta',
profileId,
workTitle: payload.workTitle,
workDescription: payload.workDescription,
themeTags: payload.themeTags,
});
setWoodenFishSession(response.session);
setWoodenFishWork(response.work ?? woodenFishWork);
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: response.session,
work: response.work ?? woodenFishWork,
}),
);
return true;
} catch (error) {
setWoodenFishError(
resolveRpgCreationErrorMessage(error, '保存敲木鱼作品信息失败。'),
);
setSelectionStage('wooden-fish-result');
return false;
} finally {
setIsWoodenFishBusy(false);
}
},
[
setSelectionStage,
woodenFishSession?.draft?.profileId,
woodenFishSession?.sessionId,
woodenFishWork,
],
);
const publishWoodenFishDraft = useCallback(async () => {
const profileId = woodenFishWork?.summary.profileId?.trim();
if (!profileId) {
setWoodenFishError('敲木鱼草稿尚未生成可发布作品。');
setSelectionStage('wooden-fish-result');
return;
}
setIsWoodenFishBusy(true);
setWoodenFishError(null);
try {
const response = await woodenFishClient.publishWork(profileId);
setWoodenFishWork(response.item);
void refreshWoodenFishShelf();
void refreshWoodenFishGallery();
openPublishShareModal({
title: response.item.summary.workTitle || '敲木鱼',
publicWorkCode: buildWoodenFishPublicWorkCode(
response.item.summary.profileId,
),
stage: 'work-detail',
});
} catch (error) {
setWoodenFishError(
resolveRpgCreationErrorMessage(error, '发布敲木鱼作品失败。'),
);
setSelectionStage('wooden-fish-result');
} finally {
setIsWoodenFishBusy(false);
}
}, [
openPublishShareModal,
refreshWoodenFishGallery,
refreshWoodenFishShelf,
setSelectionStage,
woodenFishWork?.summary.profileId,
]);
const startWoodenFishTestRunFromProfile = useCallback(async () => {
const profileId = woodenFishWork?.summary.profileId?.trim();
if (!profileId) {
setWoodenFishError('敲木鱼草稿尚未生成可试玩作品。');
setSelectionStage('wooden-fish-result');
return;
}
setIsWoodenFishBusy(true);
setWoodenFishError(null);
setWoodenFishRuntimeReturnStage('wooden-fish-result');
try {
const response = await woodenFishClient.startRun(profileId);
setWoodenFishRun(response.run);
setSelectionStage('wooden-fish-runtime');
} catch (error) {
setWoodenFishError(
error instanceof Error ? error.message : '启动敲木鱼试玩失败。',
);
setSelectionStage('wooden-fish-result');
} finally {
setIsWoodenFishBusy(false);
}
}, [setSelectionStage, woodenFishWork?.summary.profileId]);
const startWoodenFishRunFromProfile = useCallback(
async (
profileId: string,
options: {
embedded?: boolean;
returnStage?: 'work-detail' | 'platform';
} = {},
) => {
const normalizedProfileId = profileId.trim();
if (!normalizedProfileId) {
setWoodenFishError('当前敲木鱼作品信息不完整,暂时无法进入玩法。');
return false;
}
setIsWoodenFishBusy(true);
setWoodenFishError(null);
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'wooden-fish',
embedded: options.embedded,
});
const [detail, runResponse] = await Promise.all([
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
options.embedded
? woodenFishClient.startRun(
normalizedProfileId,
runtimeRequestOptions,
)
: woodenFishClient.startRun(normalizedProfileId),
]);
if (detail?.item) {
setWoodenFishWork(detail.item);
}
setWoodenFishRun(runResponse.run);
if (!options.embedded) {
setSelectionStage('wooden-fish-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'wooden-fish-runtime',
buildWoodenFishPublicWorkCode(normalizedProfileId),
),
);
}
return true;
} catch (error) {
setWoodenFishError(
resolveRpgCreationErrorMessage(error, '启动敲木鱼玩法失败。'),
);
return false;
} finally {
setIsWoodenFishBusy(false);
}
},
[buildRecommendRuntimeRequestOptions, setSelectionStage],
);
const checkpointWoodenFishRuntimeRun = useCallback(
async (payload: {
totalTapCount: number;
wordCounters: WoodenFishRunResponse['run']['wordCounters'];
}) => {
const runId = woodenFishRun?.runId;
if (!runId) {
return;
}
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'wooden-fish',
});
const response = await woodenFishClient.checkpointRun(
runId,
payload,
runtimeRequestOptions,
);
setWoodenFishRun(response.run);
},
[buildRecommendRuntimeRequestOptions, woodenFishRun?.runId],
);
const executePuzzleAction = puzzleFlow.executeAction;
const pollPuzzleBackgroundActionUntilSettled = useCallback(
(
payload: PuzzleAgentActionRequest,
baselineSession: PuzzleAgentSessionSnapshot,
) => {
if (!isPuzzleBackgroundAction(payload)) {
return;
}
void (async () => {
for (
let attempt = 0;
attempt < PUZZLE_BACKGROUND_ACTION_MAX_POLL_ATTEMPTS;
attempt += 1
) {
await waitForPuzzleBackgroundActionPollTick();
try {
const response = await getPuzzleAgentSession(
baselineSession.sessionId,
);
puzzleFlow.setSession(response.session);
if (
!isPuzzleBackgroundActionSettled(
payload,
baselineSession,
response.session,
)
) {
continue;
}
const latestLevel = findPuzzleActionLevel(response.session, payload);
if (latestLevel?.generationStatus === 'failed') {
const errorMessage = '拼图图片生成失败,请调整描述后重试。';
const failedGenerationState =
resolveFinishedMiniGameDraftGenerationState(
createPuzzleDraftGenerationStateFromPayload(
buildPuzzleFormPayloadFromSession(response.session),
response.session,
),
'failed',
{ error: errorMessage },
);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[response.session.sessionId]: {
session: response.session,
payload: buildPuzzleFormPayloadFromSession(response.session),
generationState: failedGenerationState,
error: errorMessage,
},
}));
markPendingDraftFailed('puzzle', response.session.sessionId);
markDraftFailed(
'puzzle',
[
response.session.sessionId,
buildPuzzleResultWorkId(response.session.sessionId),
response.session.publishedProfileId,
buildPuzzleResultProfileId(response.session.sessionId),
],
errorMessage,
!isViewingPuzzleGeneration(response.session.sessionId),
);
if (isViewingPuzzleGeneration(response.session.sessionId)) {
setPuzzleGenerationState(failedGenerationState);
}
setPuzzleError(errorMessage);
} else {
setPuzzleError(null);
}
refreshPlatformDashboardSilently();
await Promise.allSettled([
refreshPuzzleShelf(),
refreshPuzzleGallery(),
]);
return;
} catch (pollError) {
if (attempt >= 2) {
setPuzzleError(
resolvePuzzleErrorMessage(
pollError,
'刷新拼图图片生成结果失败。',
),
);
return;
}
}
}
setPuzzleError('拼图图片仍在后台生成,请稍后刷新草稿查看结果。');
})();
},
[
isViewingPuzzleGeneration,
markDraftFailed,
markPendingDraftFailed,
puzzleFlow,
refreshPlatformDashboardSilently,
refreshPuzzleGallery,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setPuzzleError,
],
);
const executePuzzleBackgroundAction = useCallback(
async (payload: PuzzleAgentActionRequest) => {
const targetSession = puzzleSession;
if (!targetSession) {
return;
}
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
setPuzzleFormDraftPayload(formPayload);
}
setPuzzleError(null);
try {
const response = await executePuzzleAgentAction(
targetSession.sessionId,
payload,
);
setPuzzleOperation(response.operation);
puzzleFlow.setSession(response.session);
if (
isPuzzleBackgroundAction(payload) &&
(response.operation.status === 'queued' ||
response.operation.status === 'running')
) {
pollPuzzleBackgroundActionUntilSettled(payload, targetSession);
}
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '执行拼图操作失败。'));
} finally {
if (
payload.action === 'generate_puzzle_images' ||
payload.action === 'generate_puzzle_ui_background'
) {
refreshPlatformDashboardSilently();
}
}
},
[
puzzleFlow,
puzzleSession,
pollPuzzleBackgroundActionUntilSettled,
refreshPlatformDashboardSilently,
resolvePuzzleErrorMessage,
setPuzzleError,
],
);
const retryPuzzleDraftGeneration = useCallback(() => {
if (puzzleSession?.sessionId) {
const retryPayload =
puzzleFormDraftPayload ??
buildPuzzleFormPayloadFromSession(puzzleSession);
void executePuzzleAction(
buildPuzzleCompileActionFromFormPayload(retryPayload),
);
return;
}
if (puzzleFormDraftPayload) {
void createPuzzleDraftFromForm(puzzleFormDraftPayload);
}
}, [
createPuzzleDraftFromForm,
executePuzzleAction,
puzzleFormDraftPayload,
puzzleSession,
]);
const retryVisualNovelDraftGeneration = useCallback(() => {
if (!visualNovelFormDraftPayload) {
setSelectionStage('visual-novel-agent-workspace');
return;
}
void createVisualNovelDraftFromForm(visualNovelFormDraftPayload);
}, [
createVisualNovelDraftFromForm,
setSelectionStage,
visualNovelFormDraftPayload,
]);
const retryBabyObjectMatchDraftGeneration = useCallback(() => {
if (!babyObjectMatchFormPayload) {
setSelectionStage('baby-object-match-workspace');
return;
}
void createBabyObjectMatchDraftFromForm(babyObjectMatchFormPayload);
}, [
babyObjectMatchFormPayload,
createBabyObjectMatchDraftFromForm,
setSelectionStage,
]);
const executePuzzleWorkspaceAction = useCallback(
(payload: PuzzleAgentActionRequest) => {
if (
payload.action === 'compile_puzzle_draft' &&
isEmptyPuzzleFormOnlyDraft(puzzleSession)
) {
const formPayload = buildPuzzleFormPayloadFromAction(payload);
if (formPayload) {
void createPuzzleDraftFromForm(formPayload);
return;
}
}
if (
payload.action === 'generate_puzzle_images' ||
payload.action === 'generate_puzzle_ui_background'
) {
void executePuzzleBackgroundAction(payload);
return;
}
void executePuzzleAction(payload);
},
[
createPuzzleDraftFromForm,
executePuzzleAction,
executePuzzleBackgroundAction,
puzzleSession,
],
);
const openCreativeAgentTarget = useCallback(async () => {
const binding = creativeAgentSession?.targetBinding;
if (!binding || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const response = await getPuzzleAgentSession(binding.targetSessionId);
puzzleFlow.setSession(response.session);
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
enterCreateTab();
setActiveCreativeAgentSessionId(creativeAgentSession.sessionId);
setCreativeDraftEditError(null);
setSelectionStage(
resolveCreativeAgentTargetSelectionStage(binding.targetStage),
);
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '打开拼图草稿失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
}, [
creativeAgentSession,
enterCreateTab,
isPuzzleBusy,
puzzleFlow,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
]);
const submitCreativeDraftEdit = useCallback(
async ({
instruction,
currentDraft,
}: {
instruction: string;
currentDraft: PuzzleResultDraft;
}): Promise<CreativeDraftEditResult | null> => {
const creativeSessionId =
activeCreativeAgentSessionId ?? creativeAgentSession?.sessionId ?? null;
const targetPuzzleSessionId = puzzleSession?.sessionId?.trim();
if (
!creativeSessionId ||
!targetPuzzleSessionId ||
isCreativeDraftEditBusy
) {
return null;
}
setIsCreativeDraftEditBusy(true);
setCreativeDraftEditError(null);
try {
const result = await streamCreativeDraftEdit(creativeSessionId, {
clientMessageId: `creative-edit-${Date.now().toString(36)}`,
instruction,
targetPuzzleSessionId,
currentDraft,
});
if ('puzzleSession' in result) {
puzzleFlow.setSession(result.puzzleSession);
setCreativeAgentSession(result.session);
setActiveCreativeAgentSessionId(result.session.sessionId);
return result;
}
setCreativeAgentSession(result);
setActiveCreativeAgentSessionId(result.sessionId);
const response = await getPuzzleAgentSession(targetPuzzleSessionId);
puzzleFlow.setSession(response.session);
return null;
} catch (error) {
setCreativeDraftEditError(
resolvePuzzleErrorMessage(error, '智能修订拼图草稿失败。'),
);
return null;
} finally {
setIsCreativeDraftEditBusy(false);
}
},
[
activeCreativeAgentSessionId,
creativeAgentSession?.sessionId,
isCreativeDraftEditBusy,
puzzleFlow,
puzzleSession?.sessionId,
resolvePuzzleErrorMessage,
],
);
useEffect(() => {
const nextSelectionStage = resolveSelectionStageAfterMissingCreationState({
stage: selectionStage,
bigFish: {
hasSession: Boolean(bigFishSession),
hasSessionDraft: Boolean(bigFishSession?.draft),
hasRun: Boolean(bigFishRun),
},
match3d: {
hasSession: Boolean(match3dSession),
hasSessionDraft: Boolean(match3dSession?.draft),
hasRun: Boolean(match3dRun),
},
squareHole: {
hasSession: Boolean(squareHoleSession),
hasSessionDraft: Boolean(squareHoleSession?.draft),
hasRun: Boolean(squareHoleRun),
},
visualNovel: {
hasSession: Boolean(visualNovelSession),
hasSessionDraft: Boolean(visualNovelSession?.draft),
hasWork: Boolean(visualNovelWork),
hasWorkDraft: Boolean(visualNovelWork?.draft),
hasRun: Boolean(visualNovelRun),
},
babyObjectMatch: {
hasDraft: Boolean(babyObjectMatchDraft),
hasFormPayload: Boolean(babyObjectMatchFormPayload),
},
});
if (nextSelectionStage !== selectionStage) {
setSelectionStage(nextSelectionStage);
}
}, [
babyObjectMatchDraft,
babyObjectMatchFormPayload,
bigFishRun,
bigFishSession,
match3dRun,
match3dSession,
selectionStage,
setSelectionStage,
squareHoleRun,
squareHoleSession,
visualNovelRun,
visualNovelSession,
visualNovelWork,
]);
const startBigFishRun = useCallback(async () => {
if (!bigFishSession) {
return;
}
const sessionId = bigFishSession.sessionId;
setBigFishError(null);
setBigFishRuntimeShare(null);
setBigFishRuntimeWork(null);
setBigFishRuntimeStartedAt(null);
setBigFishRun(null);
try {
const { run } = await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRuntimeSessionSource('draft');
setBigFishRuntimeReturnStage('big-fish-result');
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
);
});
void refreshBigFishShelf();
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
}
}, [
bigFishSession,
refreshBigFishShelf,
resolveBigFishErrorMessage,
setBigFishError,
setSelectionStage,
]);
const restartBigFishRun = useCallback(async () => {
if (!bigFishSession && !bigFishRun) {
return;
}
const sessionId = bigFishSession?.sessionId ?? bigFishRun?.sessionId;
if (!sessionId) {
return;
}
setBigFishError(null);
if (bigFishSession) {
setBigFishRuntimeShare(null);
setBigFishRuntimeReturnStage('big-fish-result');
}
setBigFishRuntimeStartedAt(null);
setBigFishRun(null);
try {
const { run } = await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRuntimeSessionSource(bigFishSession ? 'draft' : 'work');
setBigFishRuntimeReturnStage(
bigFishSession ? 'big-fish-result' : bigFishRuntimeReturnStage,
);
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
);
});
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
}
}, [
bigFishRun,
bigFishRuntimeReturnStage,
bigFishSession,
resolveBigFishErrorMessage,
setBigFishError,
setSelectionStage,
]);
const startPuzzleRunFromProfile = useCallback(
async (
profileId: string,
returnStage: PuzzleRuntimeReturnStage = 'work-detail',
detailItem?: PuzzleWorkSummary,
mirrorErrorToPublicDetail = false,
levelId?: string | null,
options: { embedded?: boolean; authMode?: PuzzleRuntimeAuthMode } = {},
) => {
const normalizedLevelId = levelId?.trim() ?? '';
const startKey = `${profileId}:${normalizedLevelId}`;
if (isPuzzleBusy || puzzleStartInFlightKeyRef.current === startKey) {
return false;
}
puzzleStartInFlightKeyRef.current = startKey;
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const item =
detailItem ?? (await getPuzzleGalleryDetail(profileId)).item;
const startRunPayload = {
profileId: item.profileId,
levelId: normalizedLevelId || null,
};
const authPlan = resolveCurrentRecommendRuntimeAuthPlan(authUi, {
embedded: options.embedded,
allowRuntimeGuestAuth:
options.embedded || options.authMode === 'isolated',
});
const runtimeGuestOptions =
authPlan.requestKind === 'runtime-guest'
? await buildRecommendRuntimeGuestOptions()
: {};
const authMode = authPlan.puzzleRuntimeAuthMode;
const runtimeAuthOptions =
authPlan.requestKind === 'runtime-guest'
? runtimeGuestOptions
: authPlan.requestKind === 'background'
? RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS
: {};
const { run } =
authMode === 'isolated'
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
: await startPuzzleRun(startRunPayload, runtimeAuthOptions);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode(authMode);
setPuzzleRuntimeReturnStage(returnStage);
if (!options.embedded) {
openPuzzleRuntimeStage(
setSelectionStage,
authMode === 'isolated'
? buildPuzzlePublishedRuntimeUrlState(item, levelId)
: buildPuzzleDraftRuntimeUrlState(item, levelId),
);
}
return true;
} catch (error) {
if (isMissingPuzzleWorkError(error)) {
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleGalleryEntries((current) =>
current.filter((entry) => entry.profileId !== profileId),
);
setPuzzleError(null);
setPublicWorkDetailError(null);
returnPlatformHomeAfterMissingWork();
return false;
}
const message = resolvePuzzleErrorMessage(error, '启动拼图玩法失败。');
setPuzzleError(message);
if (mirrorErrorToPublicDetail) {
setPublicWorkDetailError(message);
}
return false;
} finally {
if (puzzleStartInFlightKeyRef.current === startKey) {
puzzleStartInFlightKeyRef.current = null;
}
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
authUi,
resolvePuzzleErrorMessage,
returnPlatformHomeAfterMissingWork,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
],
);
const startMatch3DRunFromProfile = useCallback(
async (
profile: Match3DWorkProfile | Match3DWorkSummary,
returnStage: 'match3d-result' | 'work-detail' = 'match3d-result',
mirrorErrorToPublicDetail = false,
options: {
embedded?: boolean;
authMode?: PuzzleRuntimeAuthMode;
itemTypeCountOverride?: number;
} = {},
) => {
if (isMatch3DBusy) {
return false;
}
match3dFlow.setIsBusy(true);
setMatch3DError(null);
try {
let runtimeProfile: Match3DWorkProfile | Match3DWorkSummary = profile;
const canReadProtectedMatch3DDetail =
!options.embedded || !shouldUseRecommendRuntimeGuestAuth(authUi);
if (
canReadProtectedMatch3DDetail &&
(!hasMatch3DRuntimeAsset(profile.generatedItemAssets) ||
!hasMatch3DRuntimeBackgroundAsset(profile))
) {
try {
const { item } = await getMatch3DWorkDetail(profile.profileId);
runtimeProfile = normalizeMatch3DWorkForRuntimeUi({
...item,
generatedItemAssets: mergeMatch3DGeneratedItemAssetsForRuntime(
item.generatedItemAssets,
profile.generatedItemAssets,
),
});
} catch {
// 中文注释:详情补读只为拿完整生成素材;失败时继续按摘要开局,避免推荐流卡死。
}
}
runtimeProfile = promoteMatch3DGeneratedBackgroundAsset({
...runtimeProfile,
generatedItemAssets: normalizeMatch3DGeneratedItemAssetsForRuntime(
runtimeProfile.generatedItemAssets,
),
});
await preloadMatch3DGeneratedRuntimeAssets(
runtimeProfile.generatedItemAssets,
runtimeProfile.generatedBackgroundAsset,
{ expireSeconds: 300 },
);
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'match3d',
embedded: options.embedded,
forcePublicRuntime: options.authMode === 'isolated',
});
const runtimeOptions = {
...runtimeRequestOptions,
...(typeof options.itemTypeCountOverride === 'number'
? { itemTypeCountOverride: options.itemTypeCountOverride }
: {}),
};
const activeRuntimeAdapter = resolveMatch3DRuntimeAdapter(
runtimeProfile.profileId,
);
const { run } = await activeRuntimeAdapter.startRun(
runtimeProfile.profileId,
runtimeOptions,
);
// 中文注释:运行态必须锁定本次启动时的完整 profile,避免首次切屏渲染读到旧草稿 state。
setMatch3DRuntimeProfile(runtimeProfile);
setMatch3DRun(run);
setMatch3DProfile(runtimeProfile);
setMatch3DRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('match3d-runtime');
}
if (
!options.embedded &&
runtimeProfile.publicationStatus === 'published'
) {
pushAppHistoryPath(
buildPublicWorkStagePath(
'work-detail',
buildMatch3DPublicWorkCode(runtimeProfile.profileId),
),
);
}
return true;
} catch (error) {
const message = resolveMatch3DErrorMessage(
error,
'启动抓大鹅玩法失败。',
);
setMatch3DError(message);
if (mirrorErrorToPublicDetail) {
setPublicWorkDetailError(message);
}
return false;
} finally {
match3dFlow.setIsBusy(false);
}
},
[
isMatch3DBusy,
authUi,
buildRecommendRuntimeRequestOptions,
match3dFlow,
resolveMatch3DErrorMessage,
resolveMatch3DRuntimeAdapter,
setMatch3DError,
setSelectionStage,
],
);
const startSquareHoleRunFromProfile = useCallback(
async (
profile: SquareHoleWorkProfile | SquareHoleWorkSummary,
returnStage: SquareHoleRuntimeReturnStage = 'square-hole-result',
mirrorErrorToPublicDetail = false,
options: { embedded?: boolean } = {},
) => {
if (isSquareHoleBusy) {
return false;
}
squareHoleFlow.setIsBusy(true);
setSquareHoleError(null);
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
embedded: options.embedded,
});
const { run } = options.embedded
? await startSquareHoleRun(profile.profileId, runtimeRequestOptions)
: await startSquareHoleRun(profile.profileId);
setSquareHoleRun(run);
setSquareHoleRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('square-hole-runtime');
}
if (!options.embedded && profile.publicationStatus === 'published') {
pushAppHistoryPath(
buildPublicWorkStagePath(
'work-detail',
buildSquareHolePublicWorkCode(profile.profileId),
),
);
}
return true;
} catch (error) {
const message = resolveSquareHoleErrorMessage(
error,
'启动方洞挑战玩法失败。',
);
setSquareHoleError(message);
if (mirrorErrorToPublicDetail) {
setPublicWorkDetailError(message);
}
return false;
} finally {
squareHoleFlow.setIsBusy(false);
}
},
[
isSquareHoleBusy,
buildRecommendRuntimeRequestOptions,
resolveSquareHoleErrorMessage,
setSelectionStage,
setSquareHoleError,
squareHoleFlow,
],
);
const _buildPuzzleTestWork = useCallback(
(draft: PuzzleResultDraft) => {
const profileId =
puzzleSession?.publishedProfileId ??
`draft-${puzzleSession?.sessionId ?? 'puzzle'}-test`;
const now = new Date().toISOString();
return {
workId: `test-${profileId}`,
profileId,
ownerUserId: authUi?.user?.id ?? 'current-user',
sourceSessionId: puzzleSession?.sessionId ?? null,
authorDisplayName: authUi?.user?.displayName ?? '玩家',
workTitle: draft.workTitle || draft.levelName,
workDescription: draft.workDescription || draft.summary,
levelName: draft.levelName,
summary: draft.summary,
themeTags: draft.themeTags,
coverImageSrc: draft.coverImageSrc,
coverAssetId: draft.coverAssetId,
publicationStatus: 'draft',
updatedAt: now,
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
pointIncentiveTotalHalfPoints: 0,
pointIncentiveClaimedPoints: 0,
pointIncentiveTotalPoints: 0,
pointIncentiveClaimablePoints: 0,
publishReady: Boolean(puzzleSession?.resultPreview?.publishReady),
levels: draft.levels,
} satisfies PuzzleWorkSummary;
},
[
authUi?.user?.displayName,
authUi?.user?.id,
puzzleSession?.publishedProfileId,
puzzleSession?.resultPreview?.publishReady,
puzzleSession?.sessionId,
],
);
const startPuzzleTestRunFromDraft = useCallback(
async (
draft: PuzzleResultDraft,
options: { levelId?: string | null } = {},
) => {
if (isPuzzleBusy) {
return false;
}
if (!draft.coverImageSrc) {
setPuzzleError('请先选择一张正式拼图图片。');
return false;
}
const profileId =
puzzleSession?.publishedProfileId ??
buildPuzzleResultProfileId(puzzleSession?.sessionId);
if (!profileId) {
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
return false;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { item } = await updatePuzzleWork(
profileId,
buildPuzzleWorkUpdatePayloadFromDraft(draft),
);
const run = startLocalPuzzleRun(item, options.levelId ?? null);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('puzzle-result');
puzzleRuntimeReturnSessionRef.current =
puzzleSession?.draft && !isPuzzleFormOnlyDraft(puzzleSession)
? puzzleSession
: null;
pushPuzzleResultHistoryEntry(puzzleSession);
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzleDraftRuntimeUrlState(item, options.levelId ?? null),
);
return true;
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'));
return false;
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleSession,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
],
);
const returnFromPuzzleRuntime = useCallback(() => {
const targetStage = puzzleRuntimeReturnStage;
let sessionForResultUrl = puzzleSession;
if (
targetStage === 'puzzle-result' &&
(!puzzleSession?.draft || isPuzzleFormOnlyDraft(puzzleSession)) &&
puzzleRuntimeReturnSessionRef.current
) {
sessionForResultUrl = puzzleRuntimeReturnSessionRef.current;
puzzleFlow.setSession(puzzleRuntimeReturnSessionRef.current);
}
clearPuzzleRuntimeUrlState();
pushAppHistoryPath(resolvePathForSelectionStage(targetStage));
if (targetStage === 'puzzle-result') {
writeCreationUrlState(buildPuzzleCreationUrlState(sessionForResultUrl));
}
selectionStageRef.current = targetStage;
setSelectionStage(targetStage);
}, [puzzleFlow, puzzleRuntimeReturnStage, puzzleSession, setSelectionStage]);
const submitBigFishInput = useCallback(
async (payload: SubmitBigFishInputRequest) => {
if (
!bigFishRun ||
bigFishRun.status !== 'running' ||
bigFishInputInFlightRef.current
) {
return;
}
bigFishInputInFlightRef.current = true;
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'big-fish',
});
const { run } = await submitBigFishRuntimeInput(
bigFishRun.runId,
payload,
runtimeRequestOptions,
);
setBigFishRun(run);
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '同步大鱼吃小鱼输入失败。'),
);
} finally {
bigFishInputInFlightRef.current = false;
}
},
[
buildRecommendRuntimeRequestOptions,
bigFishRun,
resolveBigFishErrorMessage,
setBigFishError,
],
);
const reportBigFishObservedPlayTime = useCallback(() => {
const sessionId = bigFishRun?.sessionId?.trim();
if (!sessionId || !bigFishRuntimeStartedAt) {
return;
}
const elapsedMs = Math.max(1_000, Date.now() - bigFishRuntimeStartedAt);
setBigFishRuntimeStartedAt(null);
const reportPromise = buildRecommendRuntimeRequestOptions({
kind: 'big-fish',
}).then((runtimeRequestOptions) =>
recordBigFishPlay(sessionId, { elapsedMs }, runtimeRequestOptions),
);
void reportPromise.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
);
});
}, [
buildRecommendRuntimeRequestOptions,
bigFishRun?.sessionId,
bigFishRuntimeStartedAt,
resolveBigFishErrorMessage,
setBigFishError,
]);
const swapPuzzlePiecesInRun = useCallback(
async (payload: { firstPieceId: string; secondPieceId: string }) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
setPuzzleRun(
swapLocalPuzzlePieces(
puzzleRun,
payload,
isLocalPuzzleRun(puzzleRun) ? selectedPuzzleDetail : null,
),
);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '交换拼图块失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleRun,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
],
);
const dragPuzzlePiece = useCallback(
async (payload: {
pieceId: string;
targetRow: number;
targetCol: number;
}) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
setPuzzleRun(
dragLocalPuzzlePiece(
puzzleRun,
payload,
isLocalPuzzleRun(puzzleRun) ? selectedPuzzleDetail : null,
),
);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '拖动拼图块失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleRun,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
],
);
useEffect(() => {
if (selectionStage !== 'puzzle-runtime' || !puzzleRun?.currentLevel) {
return;
}
if (puzzleRun.currentLevel.status !== 'playing') {
return;
}
const timerId = window.setInterval(() => {
// 中文注释:拼图运行态的棋盘交互和倒计时展示都在前端即时裁决,超时后只刷新本地失败态。
setPuzzleRun((currentRun) =>
currentRun ? refreshLocalPuzzleTimer(currentRun) : currentRun,
);
}, 250);
return () => window.clearInterval(timerId);
}, [puzzleRun, selectionStage]);
useEffect(() => {
if (selectionStage !== 'puzzle-runtime' || puzzleRun) {
return;
}
if (!hasPuzzleRuntimeUrlStateValue(initialPuzzleRuntimeUrlState)) {
return;
}
const runtimeUrlStateKey = buildPuzzleRuntimeUrlStateKey(
initialPuzzleRuntimeUrlState,
);
if (
handledPuzzleRuntimeUrlStateKeyRef.current === runtimeUrlStateKey ||
isPuzzleBusy
) {
return;
}
handledPuzzleRuntimeUrlStateKeyRef.current = runtimeUrlStateKey;
let cancelled = false;
const restorePuzzleRuntime = async () => {
const runtimeProfileId = normalizeCreationUrlValue(
initialPuzzleRuntimeUrlState.runtimeProfileId,
);
const runtimeSessionId = normalizeCreationUrlValue(
initialPuzzleRuntimeUrlState.runtimeSessionId,
);
const runtimeLevelId = normalizeCreationUrlValue(
initialPuzzleRuntimeUrlState.runtimeLevelId,
);
const publicWorkCode = normalizeCreationUrlValue(
initialPuzzleRuntimeUrlState.publicWorkCode,
);
const runtimeMode = initialPuzzleRuntimeUrlState.mode ?? null;
const isPublishedRuntime = runtimeMode === 'published';
if (
!isPublishedRuntime &&
(platformBootstrap.isLoadingPlatform ||
!platformBootstrap.canReadProtectedData)
) {
handledPuzzleRuntimeUrlStateKeyRef.current = null;
return;
}
const fallbackStage: PuzzleRuntimeReturnStage =
runtimeMode === 'published' ? 'work-detail' : 'puzzle-result';
const candidateItems = isPublishedRuntime
? puzzleGalleryEntries.length > 0
? puzzleGalleryEntries
: await refreshPuzzleGallery()
: puzzleWorks.length > 0
? puzzleWorks
: (await listPuzzleWorks().catch(() => ({ items: [] }))).items;
const targetItem =
runtimeProfileId || runtimeSessionId || publicWorkCode
? (candidateItems.find(
(item) =>
item.profileId === runtimeProfileId ||
item.sourceSessionId === runtimeSessionId ||
(publicWorkCode
? isSamePuzzlePublicWorkCode(publicWorkCode, item.profileId)
: false),
) ?? null)
: null;
if (!targetItem) {
if (runtimeSessionId) {
try {
const { session } = await getPuzzleAgentSession(runtimeSessionId);
const restoredWork = buildPuzzleRuntimeWorkFromSession(session, {
userId: authUi?.user?.id ?? 'current-user',
displayName: authUi?.user?.displayName ?? '玩家',
});
if (restoredWork && !cancelled) {
setSelectedPuzzleDetail(restoredWork);
setPuzzleRun(
startLocalPuzzleRun(restoredWork, runtimeLevelId ?? null),
);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage(fallbackStage);
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzleDraftRuntimeUrlState(restoredWork, runtimeLevelId),
);
return;
}
} catch {
// 读不到草稿时继续回退到平台页,避免卡在空运行态。
}
}
if (!cancelled) {
setPuzzleError(
runtimeMode === 'published'
? '这份拼图运行态缺少可恢复作品,请返回作品详情重新进入。'
: '这份拼图试玩缺少可恢复草稿,请返回结果页重新进入。',
);
setPuzzleRuntimeAuthMode('default');
setPuzzleRun(null);
setSelectionStage('platform');
pushAppHistoryPath('/');
}
return;
}
if (cancelled) {
return;
}
setSelectedPuzzleDetail(targetItem);
setPuzzleRun(startLocalPuzzleRun(targetItem, runtimeLevelId ?? null));
setPuzzleRuntimeAuthMode(isPublishedRuntime ? 'isolated' : 'default');
setPuzzleRuntimeReturnStage(fallbackStage);
openPuzzleRuntimeStage(
setSelectionStage,
isPublishedRuntime
? buildPuzzlePublishedRuntimeUrlState(targetItem, runtimeLevelId)
: buildPuzzleDraftRuntimeUrlState(targetItem, runtimeLevelId),
);
};
void restorePuzzleRuntime();
return () => {
cancelled = true;
};
}, [
authUi?.user?.displayName,
authUi?.user?.id,
initialPuzzleRuntimeUrlState,
isPuzzleBusy,
platformBootstrap.canReadProtectedData,
platformBootstrap.isLoadingPlatform,
puzzleGalleryEntries,
puzzleRun,
puzzleRuntimeReturnStage,
puzzleWorks,
refreshPuzzleGallery,
selectionStage,
setSelectionStage,
]);
const setPuzzleRuntimePaused = useCallback(
async (paused: boolean) => {
if (!puzzleRun?.currentLevel) {
return;
}
setPuzzleRun((currentRun) =>
currentRun ? setLocalPuzzlePaused(currentRun, paused) : currentRun,
);
},
[puzzleRun],
);
const syncPuzzleRuntimeTimeout = useCallback(async () => {
if (
!puzzleRun?.currentLevel ||
puzzleRun.currentLevel.status === 'cleared'
) {
return;
}
setPuzzleRun((currentRun) =>
currentRun ? refreshLocalPuzzleTimer(currentRun) : currentRun,
);
}, [puzzleRun]);
const usePuzzleProp = useCallback(
async (propKind: PuzzleRuntimePropKind) => {
if (!puzzleRun?.currentLevel) {
return null;
}
const canUseProp =
propKind === 'extendTime'
? puzzleRun.currentLevel.status !== 'cleared'
: puzzleRun.currentLevel.status === 'playing';
if (!canUseProp) {
return null;
}
const currentRun = puzzleRunRef.current ?? puzzleRun;
if (!currentRun.currentLevel) {
return null;
}
const nextRun =
propKind === 'extendTime'
? extendLocalPuzzleTime(currentRun)
: propKind === 'freezeTime'
? applyLocalPuzzleFreezeTime(currentRun)
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return nextRun;
},
[puzzleRun],
);
const restartPuzzleCurrentLevel = useCallback(async () => {
const currentRun = puzzleRunRef.current ?? puzzleRun;
const currentLevel = currentRun?.currentLevel ?? null;
if (!currentRun || !currentLevel || isPuzzleBusy) {
return;
}
setPuzzleError(null);
setIsPuzzleBusy(true);
try {
if (isLocalPuzzleRun(currentRun)) {
const nextRun = restartLocalPuzzleLevel(currentRun);
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return;
}
const detailItem =
selectedPuzzleDetail?.profileId === currentLevel.profileId
? selectedPuzzleDetail
: await getPuzzleGalleryDetail(currentLevel.profileId).then(
(response) => response.item,
);
const startRunPayload = {
profileId: currentLevel.profileId,
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
};
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
const { run } = await startPuzzleRun(
startRunPayload,
runtimeRequestOptions,
);
setSelectedPuzzleDetail(detailItem);
puzzleRunRef.current = run;
setPuzzleRun(run);
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '重新开始拼图关卡失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
}, [
isPuzzleBusy,
puzzleRun,
buildPuzzleRuntimeRequestOptions,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
]);
const resumePuzzleSaveArchive = useCallback(
async (entry: ProfileSaveArchiveSummary) => {
if (isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
platformBootstrap.setSaveError(null);
try {
const resumedArchive = await resumePuzzleProfileSaveArchiveRaw(
entry.worldKey,
);
platformBootstrap.setSaveEntries((currentEntries) =>
currentEntries.map((currentEntry) =>
currentEntry.worldKey === resumedArchive.entry.worldKey
? resumedArchive.entry
: currentEntry,
),
);
const gameState = resumedArchive.snapshot.gameState;
const profileId =
typeof gameState.currentProfileId === 'string' &&
gameState.currentProfileId.trim()
? gameState.currentProfileId
: typeof gameState.entryProfileId === 'string' &&
gameState.entryProfileId.trim()
? gameState.entryProfileId
: (entry.profileId ?? entry.worldKey.replace(/^puzzle:/u, ''));
const levelId =
typeof gameState.currentLevelId === 'string' &&
gameState.currentLevelId.trim()
? gameState.currentLevelId
: null;
const item =
selectedPuzzleDetail?.profileId === profileId
? selectedPuzzleDetail
: await getPuzzleGalleryDetail(profileId).then(
(response) => response.item,
);
await startPuzzleRunFromProfile(
item.profileId,
'platform',
item,
false,
levelId,
);
} catch (error) {
platformBootstrap.setSaveError(
resolvePuzzleErrorMessage(error, '恢复拼图存档失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
platformBootstrap,
selectedPuzzleDetail,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
startPuzzleRunFromProfile,
],
);
useEffect(() => {
const currentLevel = puzzleRun?.currentLevel ?? null;
if (!puzzleRun || !currentLevel || currentLevel.status !== 'cleared') {
return;
}
if (currentLevel.elapsedMs === null) {
return;
}
if ((currentLevel.leaderboardEntries ?? []).length > 0) {
return;
}
const submitKey = [
puzzleRun.runId,
currentLevel.profileId,
currentLevel.levelId ?? currentLevel.levelIndex,
currentLevel.gridSize,
currentLevel.elapsedMs,
].join(':');
if (submittedPuzzleLeaderboardKeysRef.current.has(submitKey)) {
return;
}
submittedPuzzleLeaderboardKeysRef.current.add(submitKey);
setIsPuzzleLeaderboardBusy(true);
const payload: SubmitPuzzleLeaderboardRequest = {
profileId: currentLevel.profileId,
gridSize: currentLevel.gridSize,
elapsedMs: currentLevel.elapsedMs,
nickname: authUi?.user?.displayName?.trim() || '玩家',
};
if (isLocalPuzzleRun(puzzleRun)) {
setPuzzleRun(submitLocalPuzzleLeaderboard(puzzleRun, payload.nickname));
setIsPuzzleLeaderboardBusy(false);
return;
}
const submitLeaderboardPromise = buildPuzzleRuntimeRequestOptions().then(
(runtimeRequestOptions) =>
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeRequestOptions),
);
void submitLeaderboardPromise
.then(({ run }) => {
setPuzzleRun((currentRun) => {
if (!currentRun) {
return currentRun;
}
return mergePuzzleServiceRuntimeState(currentRun, run);
});
void platformBootstrap.refreshSaveArchives();
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '提交拼图排行榜失败。'),
);
})
.finally(() => {
setIsPuzzleLeaderboardBusy(false);
});
}, [
authUi?.user?.displayName,
platformBootstrap,
puzzleRun,
buildPuzzleRuntimeRequestOptions,
resolvePuzzleErrorMessage,
setPuzzleError,
]);
const advancePuzzleLevel = useCallback(
async (_target?: { profileId?: string; levelId?: string | null }) => {
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
return;
}
const currentLevel = puzzleRun.currentLevel;
if (!currentLevel || currentLevel.status !== 'cleared') {
return;
}
setIsPuzzleBusy(true);
setIsPuzzleNextLevelGenerating(true);
setPuzzleError(null);
try {
if (isLocalPuzzleRun(puzzleRun)) {
const nextRun = advanceLocalPuzzleLevel(
puzzleRun,
selectedPuzzleDetail,
_target,
);
setPuzzleRun(nextRun);
return;
}
const runtimeRequestOptions = await buildPuzzleRuntimeRequestOptions();
const targetProfileId = _target?.profileId?.trim() ?? '';
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
const itemPromise =
selectedPuzzleDetail?.profileId === targetProfileId
? Promise.resolve(selectedPuzzleDetail)
: getPuzzleGalleryDetail(targetProfileId).then(
(response) => response.item,
);
const advancePromise = advancePuzzleNextLevel(
puzzleRun.runId,
{
targetProfileId,
},
runtimeRequestOptions,
);
const [{ run }, item] = await Promise.all([
advancePromise,
itemPromise,
]);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
openPuzzleRuntimeStage(
setSelectionStage,
buildPuzzlePublishedRuntimeUrlState(
item,
run.currentLevel?.levelId ?? null,
),
);
return;
}
const { run } = await advancePuzzleNextLevel(
puzzleRun.runId,
{},
runtimeRequestOptions,
);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {
setIsPuzzleNextLevelGenerating(false);
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
isPuzzleLeaderboardBusy,
puzzleRun,
buildPuzzleRuntimeRequestOptions,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
],
);
const remodelCurrentPuzzleRuntimeWork = useCallback(
(profileId: string) => {
const targetProfileId = profileId.trim();
if (!targetProfileId || isPublicWorkDetailBusy || isPuzzleBusy) {
return;
}
runProtectedAction(async () => {
setIsPublicWorkDetailBusy(true);
setIsPuzzleBusy(true);
setPuzzleError(null);
setPublicWorkDetailError(null);
void remixPuzzleGalleryWork(targetProfileId)
.then((response) => {
resetRecommendRuntimeSelection();
puzzleFlow.setSession(response.session);
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
enterCreateTab();
setSelectionStage('puzzle-result');
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '改造拼图作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
setIsPuzzleBusy(false);
});
});
},
[
enterCreateTab,
isPublicWorkDetailBusy,
isPuzzleBusy,
puzzleFlow,
resolvePuzzleErrorMessage,
resetRecommendRuntimeSelection,
runProtectedAction,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
],
);
const saveAndExitRecommendPuzzleRuntime = useCallback(async () => {
if (activeRecommendRuntimeKind !== 'puzzle') {
return;
}
const currentRun = puzzleRunRef.current;
if (!currentRun) {
setActiveRecommendRuntimeKind(null);
return;
}
// 中文注释:推荐页嵌入拼图的“保存并退出”沿用现有运行态语义;
// 正式 run 的每次交换/拖动/通关已写回后端,退出时只收口暂停和本地快照。
const closedRun = currentRun.currentLevel
? setLocalPuzzlePaused(currentRun, false)
: currentRun;
puzzleRunRef.current = null;
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setActiveRecommendRuntimeKind(null);
if (closedRun.currentLevel) {
setPuzzleError(null);
}
}, [activeRecommendRuntimeKind, setPuzzleError]);
const leaveAgentWorkspace = useCallback(() => {
sessionController.resetSessionViewState();
sessionController.setGeneratedCustomWorldProfile(null);
autosaveCoordinator.resetAutoSaveTrackingToIdle();
sessionController.persistAgentUiState(
sessionController.activeAgentSessionId,
null,
);
returnToCreationFlowSource();
}, [autosaveCoordinator, returnToCreationFlowSource, sessionController]);
const leaveAgentDraftGeneration = useCallback(() => {
if (sessionController.isActiveGenerationRunning) {
return;
}
sessionController.setAgentDraftGenerationStartedAt(null);
sessionController.setCustomWorldGenerationViewSource(null);
setSelectionStage('agent-workspace');
}, [sessionController, setSelectionStage]);
const leaveAgentDraftResult = useCallback(() => {
sessionController.suppressAgentDraftResultAutoOpen();
sessionController.setGeneratedCustomWorldProfile(null);
sessionController.setCustomWorldError(null);
autosaveCoordinator.resetAutoSaveTrackingToIdle();
sessionController.setCustomWorldGenerationViewSource(null);
sessionController.setCustomWorldResultViewSource(null);
returnToCreationFlowSource();
}, [autosaveCoordinator, returnToCreationFlowSource, sessionController]);
const leaveCustomWorldResult = useCallback(() => {
sessionController.setGeneratedCustomWorldProfile(null);
sessionController.setCustomWorldError(null);
autosaveCoordinator.resetAutoSaveTrackingToIdle();
sessionController.setCustomWorldGenerationViewSource(null);
sessionController.setCustomWorldResultViewSource(null);
if (shouldReturnToDraftShelf()) {
returnToCreationFlowSource();
return;
}
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
}, [
autosaveCoordinator,
returnToCreationFlowSource,
selectedDetailEntry,
sessionController,
setSelectionStage,
shouldReturnToDraftShelf,
]);
const handleStartSelectedWorld = useCallback(() => {
if (!selectedDetailEntry) {
return;
}
runProtectedAction(() => {
handleCustomWorldSelect(selectedDetailEntry.profile);
});
}, [handleCustomWorldSelect, runProtectedAction, selectedDetailEntry]);
const handleDeleteLibraryEntry = useCallback(
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
if (!entry.profileId || deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'rpg-library',
entry,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
platformBootstrap.setPlatformError(null);
void deleteRpgEntryWorldProfile(entry.profileId)
.then(async (entries) => {
platformBootstrap.setSavedCustomWorldEntries(entries);
await platformBootstrap.refreshCustomWorldWorks().catch(() => []);
await platformBootstrap.refreshPublishedGallery().catch(() => []);
})
.catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '删除自定义世界失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[deletingCreationWorkId, platformBootstrap, requestDeleteCreationWork],
);
const handleDeletePublishedWork = useCallback(
(work: (typeof creationHubItems)[number]) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'rpg',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
platformBootstrap.setPlatformError(null);
const deleteTask =
work.sourceType === 'published_profile' && work.profileId
? deleteRpgEntryWorldProfile(work.profileId).then(
async (entries) => {
platformBootstrap.setSavedCustomWorldEntries(entries);
await platformBootstrap
.refreshCustomWorldWorks()
.catch(() => []);
},
)
: work.sourceType === 'agent_session' && work.sessionId
? deleteRpgCreationAgentSession(work.sessionId).then(
(items) => {
platformBootstrap.setCustomWorldWorkEntries(items);
},
)
: Promise.reject(new Error('当前 RPG 作品缺少可删除 ID。'));
void deleteTask
.then(async () => {
markDraftNoticeSeen(deleteModel.noticeKeys);
await platformBootstrap.refreshPublishedGallery().catch(() => []);
})
.catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '删除自定义世界失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
platformBootstrap,
requestDeleteCreationWork,
],
);
const handleDeleteBigFishWork = useCallback(
(work: BigFishWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'big-fish',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
setBigFishError(null);
void deleteBigFishWork(work.sourceSessionId)
.then(async (response) => {
markDraftNoticeSeen(deleteModel.noticeKeys);
setBigFishWorks(response.items);
await refreshBigFishGallery().catch(() => []);
})
.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '删除大鱼吃小鱼作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshBigFishGallery,
requestDeleteCreationWork,
resolveBigFishErrorMessage,
setBigFishError,
],
);
const handleDeletePuzzleWork = useCallback(
(work: PuzzleWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'puzzle',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
setPuzzleFormDraftPayload(null);
setPuzzleError(null);
void deletePuzzleWork(work.profileId)
.then((response) => {
markDraftNoticeSeen(deleteModel.noticeKeys);
setPuzzleWorks(response.items);
void refreshPuzzleGallery();
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '删除拼图作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshPuzzleGallery,
requestDeleteCreationWork,
resolvePuzzleErrorMessage,
setPuzzleError,
],
);
const handleDeleteMatch3DWork = useCallback(
(work: Match3DWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'match3d',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
setMatch3DFormDraftPayload(null);
setMatch3DError(null);
void deleteMatch3DWork(work.profileId)
.then((response) => {
markDraftNoticeSeen(deleteModel.noticeKeys);
setMatch3DWorks(mapMatch3DWorksForRuntimeUi(response.items));
void refreshMatch3DGallery();
})
.catch((error) => {
setMatch3DError(
resolveMatch3DErrorMessage(error, '删除抓大鹅作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshMatch3DGallery,
requestDeleteCreationWork,
resolveMatch3DErrorMessage,
setMatch3DFormDraftPayload,
setMatch3DError,
],
);
const handleDeleteSquareHoleWork = useCallback(
(work: SquareHoleWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'square-hole',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
setSquareHoleError(null);
void deleteSquareHoleWork(work.profileId)
.then((response) => {
markDraftNoticeSeen(deleteModel.noticeKeys);
setSquareHoleWorks(response.items);
void refreshSquareHoleGallery();
})
.catch((error) => {
setSquareHoleError(
resolveSquareHoleErrorMessage(error, '删除方洞挑战作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshSquareHoleGallery,
requestDeleteCreationWork,
resolveSquareHoleErrorMessage,
setSquareHoleError,
],
);
const handleDeleteJumpHopWork = useCallback(
(work: JumpHopWorkSummaryResponse) => {
if (deletingCreationWorkId) {
return;
}
const noticeKeys = collectDraftNoticeKeys('jump-hop', [
work.workId,
work.profileId,
work.sourceSessionId,
]);
requestDeleteCreationWork({
id: work.workId,
title: work.workTitle || '未命名跳一跳',
detail:
work.publicationStatus === 'published'
? '删除后会从你的作品列表和公开广场中移除。'
: '删除后会从你的作品列表中移除。',
run: () => {
setDeletingCreationWorkId(work.workId);
setJumpHopError(null);
void jumpHopClient
.deleteWork(work.profileId)
.then((response) => {
markDraftNoticeSeen(noticeKeys);
setJumpHopWorks(response.items);
void refreshJumpHopGallery();
})
.catch((error) => {
setJumpHopError(
resolvePuzzleErrorMessage(error, '删除跳一跳作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshJumpHopGallery,
requestDeleteCreationWork,
resolvePuzzleErrorMessage,
setJumpHopError,
],
);
const handleDeleteWoodenFishWork = useCallback(
(work: WoodenFishWorkSummaryResponse) => {
if (deletingCreationWorkId) {
return;
}
const noticeKeys = collectDraftNoticeKeys('wooden-fish', [
work.workId,
work.profileId,
work.sourceSessionId,
]);
requestDeleteCreationWork({
id: work.workId,
title: work.workTitle || '未命名敲木鱼',
detail:
work.publicationStatus === 'published'
? '删除后会从你的作品列表和公开广场中移除。'
: '删除后会从你的作品列表中移除。',
run: () => {
setDeletingCreationWorkId(work.workId);
setWoodenFishError(null);
void woodenFishClient
.deleteWork(work.profileId)
.then((response) => {
markDraftNoticeSeen(noticeKeys);
setWoodenFishWorks(response.items);
void refreshWoodenFishGallery();
})
.catch((error) => {
setWoodenFishError(
resolvePuzzleErrorMessage(error, '删除敲木鱼作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshWoodenFishGallery,
requestDeleteCreationWork,
resolvePuzzleErrorMessage,
setWoodenFishError,
],
);
const handleDeleteBarkBattleWork = useCallback(
(work: BarkBattleWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
const noticeKeys = collectDraftNoticeKeys('bark-battle', [
work.workId,
work.draftId,
]);
requestDeleteCreationWork({
id: work.workId,
title: work.title || '未命名汪汪声浪',
detail:
work.status === 'published'
? '删除后会从你的作品列表和公开广场中移除。'
: '删除后会从你的作品列表中移除。',
run: () => {
setDeletingCreationWorkId(work.workId);
setBarkBattleError(null);
void deleteBarkBattleWork(work.workId)
.then((response) => {
markDraftNoticeSeen(noticeKeys);
setBarkBattleWorks(mergeBarkBattleWorksByWorkId(response.items));
void refreshBarkBattleGallery();
})
.catch((error) => {
setBarkBattleError(
resolveBarkBattleErrorMessage(error, '删除汪汪声浪作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshBarkBattleGallery,
requestDeleteCreationWork,
resolveBarkBattleErrorMessage,
setBarkBattleError,
],
);
const handleDeleteVisualNovelWork = useCallback(
(work: VisualNovelWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'visual-novel',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
setVisualNovelError(null);
void deleteVisualNovelWork(work.profileId)
.then(async (response) => {
markDraftNoticeSeen(deleteModel.noticeKeys);
setVisualNovelWorks(response.works);
await refreshVisualNovelGallery();
})
.catch((error) => {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '删除视觉小说作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
markDraftNoticeSeen,
refreshVisualNovelGallery,
requestDeleteCreationWork,
resolvePuzzleErrorMessage,
setVisualNovelError,
],
);
const handleDeleteBabyObjectMatchWork = useCallback(
(work: BabyObjectMatchDraft) => {
if (deletingCreationWorkId) {
return;
}
const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'baby-object-match',
work,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
setDeletingCreationWorkId(deleteModel.id);
setBabyObjectMatchError(null);
void deleteLocalBabyObjectMatchDraft(work.profileId)
.then((nextDrafts) => {
markDraftNoticeSeen(deleteModel.noticeKeys);
setBabyObjectMatchDrafts(nextDrafts);
setBabyObjectMatchDraft((current) =>
current?.profileId === work.profileId ? null : current,
);
if (
babyObjectMatchDraft?.profileId === work.profileId &&
(selectionStage === 'baby-object-match-result' ||
selectionStage === 'baby-object-match-runtime')
) {
enterCreateTab();
setSelectionStage('platform');
}
})
.catch((error) => {
setBabyObjectMatchError(
resolvePuzzleErrorMessage(error, '删除宝贝识物作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
babyObjectMatchDraft?.profileId,
deletingCreationWorkId,
enterCreateTab,
markDraftNoticeSeen,
requestDeleteCreationWork,
resolvePuzzleErrorMessage,
selectionStage,
setSelectionStage,
],
);
const clearSelectedPublicWorkAuthor = useCallback(() => {
publicWorkAuthorRequestKeyRef.current += 1;
setSelectedPublicWorkAuthor(null);
}, []);
const loadSelectedPublicWorkAuthor = useCallback(
(entry: PlatformPublicGalleryCard) => {
const requestKey = publicWorkAuthorRequestKeyRef.current + 1;
publicWorkAuthorRequestKeyRef.current = requestKey;
setSelectedPublicWorkAuthor(null);
void resolvePublicWorkAuthorSummary(entry)
.then((author) => {
if (publicWorkAuthorRequestKeyRef.current === requestKey) {
setSelectedPublicWorkAuthor(author);
}
})
.catch(() => {
if (publicWorkAuthorRequestKeyRef.current === requestKey) {
setSelectedPublicWorkAuthor(null);
}
});
},
[],
);
const openPublicWorkDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
const decision = resolvePlatformPublicWorkDetailOpenDecision(entry);
if (decision.type === 'blocked') {
setSelectedPublicWorkDetail(decision.selectedDetail);
setPublicWorkDetailError(decision.errorMessage);
setSelectionStage(decision.selectionStage);
return;
}
setSelectedPublicWorkDetail(decision.selectedDetail);
setPublicWorkDetailError(decision.errorMessage);
setSelectionStage(decision.selectionStage);
if (decision.historyPath) {
pushAppHistoryPath(decision.historyPath);
}
},
[setSelectionStage],
);
const handleClaimPuzzlePointIncentive = useCallback(
(work: PuzzleWorkSummary) => {
if (claimingPuzzlePointIncentiveProfileId) {
return;
}
runProtectedAction(() => {
setClaimingPuzzlePointIncentiveProfileId(work.profileId);
setPuzzleError(null);
void claimPuzzleWorkPointIncentive(work.profileId)
.then((response) => {
const updatedWork = response.item;
setPuzzleWorks((current) =>
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
);
setPuzzleGalleryEntries((current) =>
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
);
setSelectedPuzzleDetail((current) =>
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
);
syncUpdatedPublicWorkDetail(
mapPuzzleWorkToPublicWorkDetail(updatedWork),
);
void platformBootstrap.refreshProfileDashboard();
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '领取拼图积分激励失败。'),
);
})
.finally(() => {
setClaimingPuzzlePointIncentiveProfileId(null);
});
});
},
[
claimingPuzzlePointIncentiveProfileId,
platformBootstrap,
resolvePuzzleErrorMessage,
runProtectedAction,
setPuzzleError,
syncUpdatedPublicWorkDetail,
],
);
const likePublicWork = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (isPublicWorkDetailBusy) {
return;
}
runProtectedAction(() => {
setIsPublicWorkDetailBusy(true);
setPublicWorkDetailError(null);
const intent = resolvePlatformPublicWorkLikeIntent(
entry,
publicWorkInteractions,
);
if (intent.type === 'like-big-fish') {
void likeBigFishGalleryWork(intent.profileId)
.then((response) => {
const updatedWork = response.items.find(
(item) => item.sourceSessionId === intent.profileId,
);
if (!updatedWork) {
return;
}
setBigFishGalleryEntries((current) =>
current.map((item) =>
mergeBigFishWorkSummary(item, updatedWork),
),
);
setBigFishWorks((current) =>
current.map((item) =>
mergeBigFishWorkSummary(item, updatedWork),
),
);
syncUpdatedPublicWorkDetail(
mapBigFishWorkToPublicWorkDetail(updatedWork),
);
setBigFishRuntimeWork((current) =>
current
? mergeBigFishWorkSummary(current, updatedWork)
: current,
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveBigFishErrorMessage(error, '点赞大鱼吃小鱼作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (intent.type === 'like-puzzle') {
void likePuzzleGalleryWork(intent.profileId)
.then((response) => {
const updatedWork = response.item;
setPuzzleGalleryEntries((current) =>
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
);
setPuzzleWorks((current) =>
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
);
setSelectedPuzzleDetail((current) =>
current
? mergePuzzleWorkSummary(current, updatedWork)
: current,
);
syncUpdatedPublicWorkDetail(
mapPuzzleWorkToPublicWorkDetail(updatedWork),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, '点赞拼图作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (intent.type === 'unsupported') {
setPublicWorkDetailError(intent.errorMessage);
setIsPublicWorkDetailBusy(false);
return;
}
void likeRpgEntryWorldGallery(intent.ownerUserId, intent.profileId)
.then((updatedEntry) => {
setSelectedDetailEntry((current) =>
current?.profileId === updatedEntry.profileId
? updatedEntry
: current,
);
platformBootstrap.setPublishedGalleryEntries((current) =>
current.map((item) =>
item.profileId === updatedEntry.profileId ? updatedEntry : item,
),
);
syncUpdatedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
});
},
[
isPublicWorkDetailBusy,
platformBootstrap,
publicWorkInteractions,
resolveBigFishErrorMessage,
resolvePuzzleErrorMessage,
runProtectedAction,
syncUpdatedPublicWorkDetail,
],
);
useEffect(() => {
const detailEntry = resolveActivePlatformPublicWorkAuthorEntry({
selectionStage,
selectedPublicWorkDetail,
selectedRpgDetailEntry: selectedDetailEntry,
});
if (!detailEntry) {
clearSelectedPublicWorkAuthor();
return;
}
loadSelectedPublicWorkAuthor(detailEntry);
}, [
clearSelectedPublicWorkAuthor,
loadSelectedPublicWorkAuthor,
selectedDetailEntry,
selectedPublicWorkDetail,
selectionStage,
]);
const openRpgPublicWorkDetail = useCallback(
async (entry: CustomWorldGalleryCard) => {
if (!canExposePublicWork(entry)) {
setSelectedPublicWorkDetail(null);
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
setSelectionStage('platform');
return;
}
setIsPublicWorkDetailBusy(true);
setPublicWorkDetailError(null);
clearSelectedPublicWorkAuthor();
setSelectedPublicWorkDetail(entry);
setSelectionStage('work-detail');
try {
const detailEntry =
await detailNavigation.loadGalleryDetailEntry(entry);
setSelectedDetailEntry(detailEntry);
const detailCard = mapRpgGalleryCardToPublicWorkDetail(detailEntry);
if (!canExposePublicWork(detailCard)) {
setSelectedDetailEntry(null);
setSelectedPublicWorkDetail(null);
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
setSelectionStage('platform');
return;
}
setSelectedPublicWorkDetail(detailCard);
if (detailEntry.publicWorkCode?.trim()) {
pushAppHistoryPath(
buildPublicWorkStagePath('work-detail', detailEntry.publicWorkCode),
);
}
} catch (error) {
setSelectedPublicWorkDetail(entry);
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '读取作品详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
clearSelectedPublicWorkAuthor,
detailNavigation,
setSelectedDetailEntry,
setSelectionStage,
],
);
const openPuzzlePublicWorkDetail = useCallback(
async (
profileId: string,
returnTarget: PuzzleDetailReturnTarget = {
tab: platformBootstrap.platformTab,
},
) => {
setIsPuzzleBusy(true);
setIsPublicWorkDetailBusy(true);
setPuzzleError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const { item } = await getPuzzleGalleryDetail(profileId);
const detailEntry = mapPuzzleWorkToPublicWorkDetail(item);
if (!canExposePublicWork(detailEntry)) {
setSelectedPuzzleDetail(null);
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
setSelectionStage('platform');
return;
}
setSelectedPuzzleDetail(item);
setPuzzleDetailReturnTarget(returnTarget);
openPublicWorkDetail(detailEntry);
} catch (error) {
if (isMissingPuzzleWorkError(error)) {
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleGalleryEntries((current) =>
current.filter((entry) => entry.profileId !== profileId),
);
setPuzzleError(null);
setPublicWorkDetailError(null);
returnPlatformHomeAfterMissingWork();
return;
}
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, '读取拼图详情失败。'),
);
} finally {
setIsPuzzleBusy(false);
setIsPublicWorkDetailBusy(false);
}
},
[
openPublicWorkDetail,
platformBootstrap.platformTab,
returnPlatformHomeAfterMissingWork,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
],
);
const openMatch3DPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setMatch3DError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const entries =
match3dGalleryEntries.length > 0
? match3dGalleryEntries
: await refreshMatch3DGallery();
const matchedEntry = entries.find(
(entry) => entry.profileId === profileId,
);
if (!matchedEntry) {
throw new Error('未找到抓大鹅作品。');
}
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(matchedEntry));
} catch (error) {
setPublicWorkDetailError(
resolveMatch3DErrorMessage(error, '读取抓大鹅详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
match3dGalleryEntries,
openPublicWorkDetail,
refreshMatch3DGallery,
resolveMatch3DErrorMessage,
setMatch3DError,
setSelectionStage,
],
);
const openSquareHolePublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setSquareHoleError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const entries =
squareHoleGalleryEntries.length > 0
? squareHoleGalleryEntries
: await refreshSquareHoleGallery();
const matchedEntry = entries.find(
(entry) => entry.profileId === profileId,
);
if (!matchedEntry) {
throw new Error('未找到方洞挑战作品。');
}
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(matchedEntry));
} catch (error) {
setPublicWorkDetailError(
resolveSquareHoleErrorMessage(error, '读取方洞挑战详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
openPublicWorkDetail,
refreshSquareHoleGallery,
resolveSquareHoleErrorMessage,
setSelectionStage,
setSquareHoleError,
squareHoleGalleryEntries,
],
);
const openVisualNovelPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setVisualNovelError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const entries =
visualNovelGalleryEntries.length > 0
? visualNovelGalleryEntries
: await refreshVisualNovelGallery();
const matchedEntry = entries.find(
(entry) => entry.profileId === profileId,
);
if (!matchedEntry) {
throw new Error('未找到视觉小说作品。');
}
openPublicWorkDetail(
mapVisualNovelWorkToPublicWorkDetail(matchedEntry),
);
} catch (error) {
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, '读取视觉小说详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
openPublicWorkDetail,
refreshVisualNovelGallery,
resolvePuzzleErrorMessage,
setSelectionStage,
setVisualNovelError,
visualNovelGalleryEntries,
],
);
const openJumpHopPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setJumpHopError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const detail = await jumpHopClient.getWorkDetail(profileId);
setJumpHopWork(detail.item);
openPublicWorkDetail(mapJumpHopWorkToPublicWorkDetail(detail.item));
} catch (error) {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '读取跳一跳详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[openPublicWorkDetail, setJumpHopError, setSelectionStage],
);
const openJumpHopDraft = useCallback(
async (item: JumpHopWorkSummaryResponse) => {
const openIntent = resolveJumpHopDraftOpenIntent({
item,
notices: draftGenerationNotices,
generation: {
activeSessionId: jumpHopSession?.sessionId,
hasActiveGenerationFailure:
jumpHopGenerationState?.phase === 'failed',
},
});
markDraftNoticeSeen(openIntent.noticeKeys);
if (openIntent.type === 'open-published-detail') {
void openJumpHopPublicWorkDetail(item.profileId);
return;
}
setJumpHopError(null);
setPublicWorkDetailError(null);
setIsJumpHopBusy(true);
if (openIntent.type === 'active-failed-generation') {
enterCreateTab();
setSelectionStage('jump-hop-generating');
setIsJumpHopBusy(false);
return;
}
if (openIntent.type === 'restore-generating') {
const pendingSession = buildJumpHopPendingSession(item);
setJumpHopSession(pendingSession);
setJumpHopRun(null);
setJumpHopWork(null);
setJumpHopGenerationState(
createMiniGameDraftGenerationState(
'jump-hop',
resolveMiniGameDraftGenerationStartedAtMs(item.updatedAt),
),
);
writeCreationUrlState(
buildJumpHopCreationUrlState({ session: pendingSession }),
);
enterCreateTab();
setSelectionStage('jump-hop-generating');
setIsJumpHopBusy(false);
return;
}
try {
const detail = await jumpHopClient.getWorkDetail(item.profileId, {
audience: 'creation',
});
setJumpHopSession(null);
setJumpHopRun(null);
setJumpHopWork(detail.item);
setJumpHopRuntimeReturnStage('jump-hop-result');
enterCreateTab();
pushAppHistoryPath(resolvePathForSelectionStage('jump-hop-result'));
writeCreationUrlState(
buildJumpHopCreationUrlState({ work: detail.item }),
);
setSelectionStage('jump-hop-result');
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '读取跳一跳草稿失败。'),
);
} finally {
setIsJumpHopBusy(false);
}
},
[
draftGenerationNotices,
enterCreateTab,
jumpHopGenerationState?.phase,
jumpHopSession?.sessionId,
markDraftNoticeSeen,
openJumpHopPublicWorkDetail,
setSelectionStage,
writeCreationUrlState,
],
);
const openPuzzleClearPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setPuzzleClearError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const detail = await puzzleClearClient.getRuntimeWorkDetail(profileId);
setPuzzleClearWork(detail.item);
openPublicWorkDetail(
mapPuzzleClearWorkToPlatformGalleryCard(detail.item),
);
} catch (error) {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '读取拼消消详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[openPublicWorkDetail, setSelectionStage],
);
const openPuzzleClearDraft = useCallback(
async (item: PuzzleClearWorkSummaryResponse) => {
markDraftNoticeSeen(
collectDraftNoticeKeys('puzzle-clear', [
item.workId,
item.profileId,
item.sourceSessionId,
]),
);
if (item.publicationStatus === 'published') {
void openPuzzleClearPublicWorkDetail(item.profileId);
return;
}
setPuzzleClearError(null);
setPublicWorkDetailError(null);
setIsPuzzleClearBusy(true);
if (item.generationStatus === 'generating') {
const pendingSession = buildPuzzleClearPendingSession(item);
setPuzzleClearSession(pendingSession);
setPuzzleClearRun(null);
setPuzzleClearWork(null);
writeCreationUrlState(
buildPuzzleClearCreationUrlState({ session: pendingSession }),
);
enterCreateTab();
setSelectionStage('puzzle-clear-generating');
setIsPuzzleClearBusy(false);
return;
}
try {
const detail = await puzzleClearClient.getWorkDetail(item.profileId);
const recoveredSession = buildPuzzleClearSessionFromWorkDetail(
detail.item,
item,
);
setPuzzleClearSession(recoveredSession);
setPuzzleClearRun(null);
setPuzzleClearWork(detail.item);
setPuzzleClearRuntimeReturnStage('puzzle-clear-result');
writeCreationUrlState(
buildPuzzleClearCreationUrlState({
session: recoveredSession,
work: detail.item,
}),
);
enterCreateTab();
setSelectionStage('puzzle-clear-result');
} catch (error) {
setPuzzleClearError(
resolveRpgCreationErrorMessage(error, '读取拼消消草稿失败。'),
);
enterCreateTab();
setSelectionStage('puzzle-clear-generating');
} finally {
setIsPuzzleClearBusy(false);
}
},
[
enterCreateTab,
markDraftNoticeSeen,
openPuzzleClearPublicWorkDetail,
setSelectionStage,
],
);
const openWoodenFishPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setWoodenFishError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const detail = await woodenFishClient.getWorkDetail(profileId);
setWoodenFishWork(detail.item);
openPublicWorkDetail(mapWoodenFishWorkToPublicWorkDetail(detail.item));
} catch (error) {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '读取敲木鱼详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[openPublicWorkDetail, setSelectionStage],
);
const openWoodenFishDraft = useCallback(
async (item: WoodenFishWorkSummaryResponse) => {
const openIntent = resolveWoodenFishDraftOpenIntent({
item,
notices: draftGenerationNotices,
generation: {
activeSessionId: woodenFishSession?.sessionId,
hasActiveGenerationFailure:
woodenFishGenerationState?.phase === 'failed',
},
});
markDraftNoticeSeen(openIntent.noticeKeys);
if (openIntent.type === 'open-published-detail') {
void openWoodenFishPublicWorkDetail(item.profileId);
return;
}
setWoodenFishError(null);
setPublicWorkDetailError(null);
setIsWoodenFishBusy(true);
if (openIntent.type === 'active-failed-generation') {
enterCreateTab();
setSelectionStage('wooden-fish-generating');
setIsWoodenFishBusy(false);
return;
}
if (openIntent.type === 'restore-generating') {
const pendingSession = buildWoodenFishPendingSession(item);
setWoodenFishSession(pendingSession);
setWoodenFishRun(null);
setWoodenFishWork(null);
setWoodenFishGenerationState(
createMiniGameDraftGenerationState(
'wooden-fish',
resolveMiniGameDraftGenerationStartedAtMs(item.updatedAt),
),
);
writeCreationUrlState(
buildWoodenFishCreationUrlState({ session: pendingSession }),
);
enterCreateTab();
setSelectionStage('wooden-fish-generating');
setIsWoodenFishBusy(false);
return;
}
try {
const detail = await woodenFishClient.getWorkDetail(item.profileId);
const recoveredSession = buildWoodenFishSessionFromWorkDetail(
detail.item,
item,
);
setWoodenFishSession(recoveredSession);
setWoodenFishRun(null);
setWoodenFishWork(detail.item);
setWoodenFishRuntimeReturnStage('wooden-fish-result');
writeCreationUrlState(
buildWoodenFishCreationUrlState({
session: recoveredSession,
work: detail.item,
}),
);
enterCreateTab();
setSelectionStage('wooden-fish-result');
} catch (error) {
setWoodenFishError(
resolveRpgCreationErrorMessage(error, '读取敲木鱼草稿失败。'),
);
enterCreateTab();
setSelectionStage(openIntent.failureFallbackStage);
} finally {
setIsWoodenFishBusy(false);
}
},
[
draftGenerationNotices,
enterCreateTab,
markDraftNoticeSeen,
openWoodenFishPublicWorkDetail,
woodenFishGenerationState?.phase,
woodenFishSession?.sessionId,
writeCreationUrlState,
setSelectionStage,
],
);
const openPublicGalleryDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
const strategy = resolvePlatformPublicWorkDetailOpenStrategy(entry);
switch (strategy.type) {
case 'use-entry':
openPublicWorkDetail(entry);
return;
case 'load-puzzle-detail':
void openPuzzlePublicWorkDetail(strategy.profileId, {
tab: platformBootstrap.platformTab,
});
return;
case 'load-jump-hop-detail':
void openJumpHopPublicWorkDetail(strategy.profileId);
return;
case 'load-wooden-fish-detail':
void openWoodenFishPublicWorkDetail(strategy.profileId);
return;
case 'load-visual-novel-detail':
void openVisualNovelPublicWorkDetail(strategy.profileId);
return;
case 'load-rpg-detail':
void openRpgPublicWorkDetail(strategy.entry);
return;
default: {
const exhaustive: never = strategy;
return exhaustive;
}
}
},
[
openPuzzlePublicWorkDetail,
openPublicWorkDetail,
openPuzzleClearPublicWorkDetail,
openJumpHopPublicWorkDetail,
openWoodenFishPublicWorkDetail,
openRpgPublicWorkDetail,
openVisualNovelPublicWorkDetail,
platformBootstrap.platformTab,
],
);
const openRecommendGalleryDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
runProtectedAction(() => {
openPublicGalleryDetail(entry);
});
},
[openPublicGalleryDetail, runProtectedAction],
);
const openPuzzleDetail = useCallback(
async (
profileId: string,
returnTarget: PuzzleDetailReturnTarget = {
tab: platformBootstrap.platformTab,
},
) => {
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { item } = await getPuzzleGalleryDetail(profileId);
setSelectedPuzzleDetail(item);
setPuzzleDetailReturnTarget(returnTarget);
setSelectionStage('puzzle-gallery-detail');
pushAppHistoryPath(
buildPublicWorkStagePath(
'puzzle-gallery-detail',
buildPuzzlePublicWorkCode(item.profileId),
),
);
} catch (error) {
if (isMissingPuzzleWorkError(error)) {
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleError(null);
setPublicWorkDetailError(null);
returnPlatformHomeAfterMissingWork();
return;
}
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图详情失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
platformBootstrap.platformTab,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
returnPlatformHomeAfterMissingWork,
],
);
const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => {
const sourceSessionId = item.sourceSessionId?.trim() ?? '';
const backgroundTask = sourceSessionId
? getPuzzleBackgroundCompileTask(sourceSessionId)
: null;
const activeGenerationState =
backgroundTask?.generationState ?? puzzleGenerationViewState;
const openIntent = resolvePuzzleDraftOpenIntent({
item,
notices: draftGenerationNotices,
generation: {
activeSessionId: puzzleSession?.sessionId,
hasActiveGenerationFailure: activeGenerationState?.phase === 'failed',
hasActiveGenerationRunning: isMiniGameDraftGenerating(
activeGenerationState ?? null,
),
hasBackgroundGenerationFailure:
backgroundTask?.generationState.phase === 'failed',
hasBackgroundGenerationRunning: isMiniGameDraftGenerating(
backgroundTask?.generationState ?? null,
),
},
});
const { noticeKeys } = openIntent;
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setSelectedPuzzleDetail(null);
if (openIntent.type === 'open-published-detail') {
await openPuzzleDetail(item.profileId, { tab: 'create' });
return;
}
if (openIntent.type === 'missing-session') {
setPuzzleError(openIntent.errorMessage);
return;
}
if (openIntent.type === 'failed-generation') {
const failedGenerationState =
openIntent.source === 'background'
? backgroundTask?.generationState
: openIntent.source === 'active'
? activeGenerationState
: createFailedMiniGameDraftGenerationStateForRestoredDraft(
'puzzle',
item.updatedAt,
openIntent.errorMessage,
{ puzzleAiRedraw: true },
);
if (!failedGenerationState) {
return;
}
let failedSession = backgroundTask?.session ?? null;
let failedPayload = backgroundTask?.payload ?? null;
const failedError = backgroundTask?.error ?? openIntent.errorMessage;
if (!failedSession) {
try {
const { session: latestSession } =
await getPuzzleAgentSession(sourceSessionId);
failedSession = latestSession;
failedPayload = buildPuzzleFormPayloadFromSession(latestSession);
} catch {
failedPayload = buildPuzzleFormPayloadFromWork(item);
}
}
if (!failedPayload) {
failedPayload = failedSession
? buildPuzzleFormPayloadFromSession(failedSession)
: buildPuzzleFormPayloadFromWork(item);
}
if (backgroundTask) {
puzzleFlow.setSession(backgroundTask.session);
} else if (failedSession) {
puzzleFlow.setSession(failedSession);
}
setPuzzleFormDraftPayload(failedPayload);
setPuzzleError(failedError);
if (failedSession) {
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[failedSession!.sessionId]: {
session: failedSession!,
payload: failedPayload!,
generationState: failedGenerationState,
error: failedError,
},
}));
}
enterCreateTab();
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setPuzzleGenerationState(failedGenerationState);
setSelectionStage('puzzle-generating');
return;
}
if (openIntent.type === 'active-generation') {
if (!activeGenerationState) {
return;
}
const rebasedGenerationState =
rebaseMiniGameDraftGenerationStateForDisplay(activeGenerationState);
enterCreateTab();
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setPuzzleGenerationState(rebasedGenerationState);
if (backgroundTask) {
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[backgroundTask.session.sessionId]: {
...backgroundTask,
generationState: rebasedGenerationState,
},
}));
}
setSelectionStage('puzzle-generating');
return;
}
if (openIntent.type === 'background-generation' && backgroundTask) {
const rebasedTask =
rebaseMiniGameDraftBackgroundCompileTaskForDisplay(backgroundTask);
puzzleFlow.setSession(rebasedTask.session);
setPuzzleFormDraftPayload(rebasedTask.payload);
setPuzzleGenerationState(rebasedTask.generationState);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[rebasedTask.session.sessionId]: rebasedTask,
}));
if (backgroundTask.error) {
setPuzzleError(backgroundTask.error);
}
enterCreateTab();
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('puzzle-generating');
return;
}
if (openIntent.type === 'restore-generating') {
try {
const { session: latestSession } =
await getPuzzleAgentSession(sourceSessionId);
const payload = buildPuzzleFormPayloadFromSession(latestSession);
const startedAtMs = resolveMiniGameDraftGenerationStartedAtMs(
latestSession.updatedAt,
);
const baseGenerationState =
createMiniGameDraftGenerationStateForRestoredDraft(
'puzzle',
{
puzzleAiRedraw: payload.aiRedraw ?? true,
puzzleProgressPercent:
latestSession.draft && !latestSession.draft.formDraft
? latestSession.progressPercent
: undefined,
},
startedAtMs,
);
const generationState = mergePuzzleSessionProgressIntoGenerationState(
baseGenerationState,
latestSession,
);
puzzleFlow.setSession(latestSession);
setPuzzleFormDraftPayload(payload);
setPuzzleGenerationState(generationState);
setPuzzleBackgroundCompileTasks((current) => ({
...current,
[latestSession.sessionId]: {
session: latestSession,
payload,
generationState,
error: null,
},
}));
enterCreateTab();
selectionStageRef.current = 'puzzle-generating';
activePuzzleGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('puzzle-generating');
return;
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '读取拼图创作草稿失败。'),
);
await refreshPuzzleShelf().catch(() => undefined);
return;
}
}
markDraftNoticeSeen(noticeKeys);
const restoredSession = await puzzleFlow.restoreDraft(sourceSessionId);
if (!restoredSession) {
await refreshPuzzleShelf().catch(() => undefined);
return;
}
if (isPuzzleFormOnlyDraft(restoredSession)) {
setPuzzleFormDraftPayload(
buildPuzzleFormPayloadFromSession(restoredSession),
);
setSelectionStage('puzzle-agent-workspace');
} else {
setPuzzleFormDraftPayload(null);
}
},
[
enterCreateTab,
draftGenerationNotices,
getPuzzleBackgroundCompileTask,
markDraftNoticeSeen,
openPuzzleDetail,
puzzleFlow,
puzzleGenerationViewState,
puzzleSession?.sessionId,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setPuzzleError,
setSelectionStage,
],
);
const openMatch3DDraft = useCallback(
async (
item: Match3DWorkSummary,
options: { forceDraft?: boolean } = {},
) => {
const sourceSessionId = item.sourceSessionId?.trim() ?? '';
const backgroundTask = sourceSessionId
? getMatch3DBackgroundCompileTask(sourceSessionId)
: null;
const activeGenerationState =
backgroundTask?.generationState ?? match3dGenerationViewState;
const openIntent = resolveMatch3DDraftOpenIntent({
item,
notices: draftGenerationNotices,
forceDraft: options.forceDraft,
generation: {
activeSessionId: match3dSession?.sessionId,
hasActiveGenerationFailure: activeGenerationState?.phase === 'failed',
hasActiveGenerationRunning: isMiniGameDraftGenerating(
activeGenerationState ?? null,
),
hasBackgroundGenerationFailure:
backgroundTask?.generationState.phase === 'failed',
hasBackgroundGenerationRunning: isMiniGameDraftGenerating(
backgroundTask?.generationState ?? null,
),
},
});
const { noticeKeys } = openIntent;
setMatch3DRun(null);
setMatch3DError(null);
setMatch3DProfile(null);
setMatch3DRuntimeProfile(null);
if (openIntent.type === 'open-published-detail') {
markDraftNoticeSeen(noticeKeys);
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(item));
return;
}
if (openIntent.type === 'missing-session') {
markDraftNoticeSeen(noticeKeys);
setMatch3DError(openIntent.errorMessage);
return;
}
if (openIntent.type === 'ready-unread') {
try {
const { session: latestSession } =
await match3dCreationClient.getSession(sourceSessionId);
setMatch3DSession(latestSession);
setMatch3DFormDraftPayload(null);
const profileId = latestSession.draft?.profileId ?? item.profileId;
const { item: profile } = await getMatch3DWorkDetail(profileId);
const normalizedProfile = normalizeMatch3DWorkForRuntimeUi(profile);
match3dFlow.setIsBusy(false);
const started = await startMatch3DRunFromProfile(
normalizedProfile,
'match3d-result',
false,
{ authMode: 'isolated' },
);
if (!started) {
setMatch3DProfile(normalizedProfile);
enterCreateTab();
setSelectionStage('match3d-result');
}
markDraftNoticeSeen(noticeKeys);
return;
} catch (error) {
setMatch3DError(
resolveMatch3DErrorMessage(error, '启动抓大鹅试玩失败。'),
);
markDraftNoticeSeen(noticeKeys);
await refreshMatch3DShelf().catch(() => undefined);
return;
}
}
if (openIntent.type === 'failed-generation') {
const failedGenerationState =
openIntent.source === 'background'
? backgroundTask?.generationState
: openIntent.source === 'active'
? activeGenerationState
: createFailedMiniGameDraftGenerationStateForRestoredDraft(
'match3d',
item.updatedAt,
openIntent.errorMessage,
);
if (!failedGenerationState) {
return;
}
let failedSession = backgroundTask?.session ?? null;
let failedPayload = backgroundTask?.payload ?? null;
const failedError = backgroundTask?.error ?? openIntent.errorMessage;
if (!failedSession) {
try {
const { session: latestSession } =
await match3dCreationClient.getSession(sourceSessionId);
failedSession = latestSession;
failedPayload = buildMatch3DFormPayloadFromSession(latestSession);
} catch {
failedPayload = buildMatch3DFormPayloadFromWork(item);
}
}
if (!failedPayload) {
failedPayload = failedSession
? buildMatch3DFormPayloadFromSession(failedSession)
: buildMatch3DFormPayloadFromWork(item);
}
if (backgroundTask) {
setMatch3DSession(backgroundTask.session);
} else if (failedSession) {
setMatch3DSession(failedSession);
}
setMatch3DFormDraftPayload(failedPayload);
setMatch3DError(failedError);
if (failedSession) {
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[failedSession!.sessionId]: {
session: failedSession!,
payload: failedPayload!,
generationState: failedGenerationState,
error: failedError,
},
}));
}
enterCreateTab();
selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setMatch3DGenerationState(failedGenerationState);
setSelectionStage('match3d-generating');
return;
}
if (openIntent.type === 'active-generation') {
if (!activeGenerationState) {
return;
}
const rebasedGenerationState =
rebaseMiniGameDraftGenerationStateForDisplay(activeGenerationState);
enterCreateTab();
selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setMatch3DGenerationState(rebasedGenerationState);
if (backgroundTask) {
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[backgroundTask.session.sessionId]: {
...backgroundTask,
generationState: rebasedGenerationState,
},
}));
}
setSelectionStage('match3d-generating');
return;
}
if (openIntent.type === 'background-generation' && backgroundTask) {
const rebasedTask =
rebaseMiniGameDraftBackgroundCompileTaskForDisplay(backgroundTask);
setMatch3DSession(rebasedTask.session);
setMatch3DFormDraftPayload(rebasedTask.payload);
setMatch3DGenerationState(rebasedTask.generationState);
setMatch3DBackgroundCompileTasks((current) => ({
...current,
[rebasedTask.session.sessionId]: rebasedTask,
}));
if (backgroundTask.error) {
setMatch3DError(backgroundTask.error);
}
enterCreateTab();
selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('match3d-generating');
return;
}
if (openIntent.type === 'restore-generating') {
try {
const { session: latestSession } =
await match3dCreationClient.getSession(sourceSessionId);
setMatch3DSession(latestSession);
setMatch3DFormDraftPayload(null);
setMatch3DProfile(null);
const generationState =
createMiniGameDraftGenerationStateForRestoredDraft(
'match3d',
undefined,
resolveMiniGameDraftGenerationStartedAtMs(
latestSession.updatedAt,
),
);
setMatch3DGenerationState(generationState);
enterCreateTab();
selectionStageRef.current = 'match3d-generating';
activeMatch3DGenerationSessionIdRef.current = sourceSessionId;
setSelectionStage('match3d-generating');
return;
} catch (error) {
setMatch3DError(
resolveMatch3DErrorMessage(error, '读取抓大鹅创作草稿失败。'),
);
await refreshMatch3DShelf().catch(() => undefined);
return;
}
}
markDraftNoticeSeen(noticeKeys);
const restoredSession = await match3dFlow.restoreDraft(sourceSessionId);
if (!restoredSession) {
await refreshMatch3DShelf().catch(() => undefined);
return;
}
setMatch3DFormDraftPayload(null);
try {
const profileId = restoredSession.draft?.profileId ?? item.profileId;
const { item: profile } = await getMatch3DWorkDetail(profileId);
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(profile));
} catch (error) {
setMatch3DProfile(buildMatch3DProfileFromSession(restoredSession));
setMatch3DError(
resolveMatch3DErrorMessage(error, '读取抓大鹅作品详情失败。'),
);
}
},
[
enterCreateTab,
draftGenerationNotices,
getMatch3DBackgroundCompileTask,
markDraftNoticeSeen,
match3dFlow,
match3dGenerationViewState,
match3dSession?.sessionId,
openPublicWorkDetail,
refreshMatch3DShelf,
resolveMatch3DErrorMessage,
setMatch3DFormDraftPayload,
setMatch3DError,
setMatch3DSession,
setSelectionStage,
startMatch3DRunFromProfile,
],
);
const openSquareHoleDraft = useCallback(
async (
item: SquareHoleWorkSummary,
options: { forceDraft?: boolean } = {},
) => {
const openIntent = resolveSquareHoleDraftOpenIntent({
item,
forceDraft: options.forceDraft,
activeSessionId: squareHoleSession?.sessionId,
hasActiveGenerationRunning: isMiniGameDraftGenerating(
squareHoleGenerationState,
),
isGenerationReady: isMiniGameDraftReady(squareHoleGenerationState),
});
setSquareHoleRun(null);
setSquareHoleError(null);
setSquareHoleProfile(null);
markDraftNoticeSeen(openIntent.noticeKeys);
if (openIntent.type === 'open-published-detail') {
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(item));
return;
}
if (openIntent.type === 'missing-session') {
setSquareHoleError(openIntent.errorMessage);
return;
}
if (openIntent.type === 'active-generation') {
enterCreateTab();
selectionStageRef.current = 'square-hole-generating';
setSelectionStage('square-hole-generating');
return;
}
if (openIntent.shouldClearGenerationState) {
setSquareHoleGenerationState(null);
}
const restoredSession = await squareHoleFlow.restoreDraft(
openIntent.sourceSessionId,
);
if (!restoredSession) {
await refreshSquareHoleShelf().catch(() => undefined);
return;
}
try {
const profileId = restoredSession.draft?.profileId ?? item.profileId;
const { item: profile } = await getSquareHoleWorkDetail(profileId);
setSquareHoleProfile(profile);
} catch (error) {
setSquareHoleProfile(
buildSquareHoleProfileFromSession(restoredSession),
);
setSquareHoleError(
resolveSquareHoleErrorMessage(error, '读取方洞挑战作品详情失败。'),
);
}
},
[
enterCreateTab,
markDraftNoticeSeen,
openPublicWorkDetail,
refreshSquareHoleShelf,
resolveSquareHoleErrorMessage,
setSquareHoleError,
setSelectionStage,
squareHoleGenerationState,
squareHoleFlow,
squareHoleSession?.sessionId,
],
);
const openBigFishDraft = useCallback(
async (item: BigFishWorkSummary) => {
const openIntent = resolveBigFishDraftOpenIntent({
item,
activeSessionId: bigFishSession?.sessionId,
hasActiveGenerationRunning: isMiniGameDraftGenerating(
bigFishGenerationState,
),
});
setBigFishRun(null);
markDraftNoticeSeen(openIntent.noticeKeys);
if (openIntent.type === 'active-generation') {
enterCreateTab();
selectionStageRef.current = 'big-fish-generating';
setSelectionStage('big-fish-generating');
return;
}
const restoredSession = await bigFishFlow.restoreDraft(
openIntent.sourceSessionId,
);
if (!restoredSession) {
await refreshBigFishShelf().catch(() => undefined);
}
},
[
bigFishFlow,
bigFishGenerationState,
bigFishSession?.sessionId,
enterCreateTab,
markDraftNoticeSeen,
refreshBigFishShelf,
setSelectionStage,
],
);
const openBarkBattleDraft = useCallback(
(item: BarkBattleWorkSummary, options: { forceDraft?: boolean } = {}) => {
markDraftNoticeSeen(
collectDraftNoticeKeys('bark-battle', [item.workId, item.draftId]),
);
setBarkBattleError(null);
setBarkBattleGenerationPartialFailed(false);
setBarkBattlePublishedConfig(null);
if (item.status === 'published' && !options.forceDraft) {
openPublicWorkDetail(mapBarkBattleWorkToPublicWorkDetail(item));
return;
}
const nextDraft = buildBarkBattleDraftConfigFromWorkSummary(item);
setBarkBattleDraftConfig(nextDraft);
enterCreateTab();
selectionStageRef.current = isPersistedBarkBattleDraftGenerating(item)
? 'bark-battle-generating'
: 'bark-battle-result';
setSelectionStage(selectionStageRef.current);
writeCreationUrlState(buildBarkBattleCreationUrlState(nextDraft));
},
[
enterCreateTab,
markDraftNoticeSeen,
openPublicWorkDetail,
setSelectionStage,
],
);
const openVisualNovelDraft = useCallback(
async (
item: VisualNovelWorkSummary,
options: { forceDraft?: boolean } = {},
) => {
const openIntent = resolveVisualNovelDraftOpenIntent({
item,
forceDraft: options.forceDraft,
activeSessionId: visualNovelSession?.sessionId,
hasActiveGenerationRunning: visualNovelGenerationPhase === 'generating',
hasActiveSessionDraft: Boolean(visualNovelSession?.draft),
});
if (openIntent.type === 'open-published-detail') {
openPublicWorkDetail(mapVisualNovelWorkToPublicWorkDetail(item));
return;
}
markDraftNoticeSeen(openIntent.noticeKeys);
if (openIntent.type === 'active-generation') {
enterCreateTab();
selectionStageRef.current = 'visual-novel-generating';
setSelectionStage('visual-novel-generating');
return;
}
if (openIntent.type === 'current-result') {
enterCreateTab();
setSelectionStage('visual-novel-result');
return;
}
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const { work } = await getVisualNovelWorkDetail(openIntent.profileId);
setVisualNovelWork(work);
setVisualNovelSession(buildVisualNovelSessionFromWorkDetail(work));
enterCreateTab();
setSelectionStage('visual-novel-result');
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '读取视觉小说作品详情失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
enterCreateTab,
openPublicWorkDetail,
markDraftNoticeSeen,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
setVisualNovelSession,
visualNovelGenerationPhase,
visualNovelSession,
],
);
const openBabyObjectMatchDraft = useCallback(
(draft: BabyObjectMatchDraft) => {
markDraftNoticeSeen(
collectDraftNoticeKeys('baby-object-match', [
draft.profileId,
draft.draftId,
]),
);
setBabyObjectMatchDraft(draft);
setBabyObjectMatchFormPayload({
itemAName: draft.itemNames[0],
itemBName: draft.itemNames[1],
});
setBabyObjectMatchGenerationState(null);
setBabyObjectMatchGenerationPhase('ready');
setBabyObjectMatchError(null);
enterCreateTab();
setSelectionStage('baby-object-match-result');
writeCreationUrlState(buildBabyObjectMatchCreationUrlState(draft));
},
[enterCreateTab, markDraftNoticeSeen, setSelectionStage],
);
useEffect(() => {
const restoreDecision = resolveInitialCreationUrlRestoreDecision({
handled: handledInitialCreationUrlStateRef.current,
pathname: window.location.pathname,
state: initialCreationUrlState,
isLoadingPlatform: platformBootstrap.isLoadingPlatform,
canReadProtectedData: platformBootstrap.canReadProtectedData,
});
if (restoreDecision.type === 'skip' || restoreDecision.type === 'wait') {
return;
}
if (restoreDecision.type === 'mark-handled') {
handledInitialCreationUrlStateRef.current = true;
return;
}
handledInitialCreationUrlStateRef.current = true;
const restoreCreationUrlState = async () => {
const target = resolveCreationUrlRestoreTarget(
window.location.pathname,
initialCreationUrlState,
);
if (!target) {
return;
}
const { sessionId, profileId } = target;
if (target.kind === 'big-fish') {
const targetSessionId = target.bigFishSessionId;
if (targetSessionId) {
const matchedWork =
(bigFishWorks.length > 0
? bigFishWorks
: (await listBigFishWorks().catch(() => ({ items: [] }))).items
).find((item) =>
matchesBigFishCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedWork) {
await openBigFishDraft(matchedWork);
return;
}
await bigFishFlow.restoreDraft(targetSessionId);
}
return;
}
if (target.kind === 'match3d') {
const matchedWork =
(match3dWorks.length > 0
? match3dWorks
: mapMatch3DWorksForRuntimeUi(
(await listMatch3DWorks().catch(() => ({ items: [] }))).items,
)
).find((item) =>
matchesSessionProfileWorkCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedWork) {
await openMatch3DDraft(matchedWork, { forceDraft: true });
return;
}
if (sessionId) {
await match3dFlow.restoreDraft(sessionId);
}
return;
}
if (target.kind === 'square-hole') {
const matchedWork =
(squareHoleWorks.length > 0
? squareHoleWorks
: (await listSquareHoleWorks().catch(() => ({ items: [] }))).items
).find((item) =>
matchesSessionProfileWorkCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedWork) {
await openSquareHoleDraft(matchedWork, { forceDraft: true });
return;
}
if (sessionId) {
await squareHoleFlow.restoreDraft(sessionId);
}
return;
}
if (target.kind === 'puzzle') {
const matchedWork =
(puzzleWorks.length > 0
? puzzleWorks
: (await listPuzzleWorks().catch(() => ({ items: [] }))).items
).find((item) =>
matchesSessionProfileWorkCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedWork) {
await openPuzzleDraft(matchedWork);
return;
}
if (sessionId) {
await puzzleFlow.restoreDraft(sessionId);
}
return;
}
if (target.kind === 'visual-novel') {
const matchedWork =
(visualNovelWorks.length > 0
? visualNovelWorks
: (await listVisualNovelWorks().catch(() => ({ works: [] }))).works
).find((item) =>
matchesVisualNovelCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedWork) {
await openVisualNovelDraft(matchedWork, { forceDraft: true });
return;
}
if (sessionId) {
await visualNovelFlow.restoreDraft(sessionId);
}
return;
}
if (target.kind === 'bark-battle') {
const matchedWork =
(barkBattleWorks.length > 0
? barkBattleWorks
: (await listBarkBattleWorks().catch(() => ({ items: [] }))).items
).find((item) =>
matchesBarkBattleCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedWork) {
openBarkBattleDraft(matchedWork, { forceDraft: true });
}
return;
}
if (target.kind === 'baby-object-match') {
const matchedDraft =
(babyObjectMatchDrafts.length > 0
? babyObjectMatchDrafts
: await listLocalBabyObjectMatchDrafts().catch(() => [])
).find((item) =>
matchesBabyObjectMatchCreationUrlRestoreTarget(item, target),
) ?? null;
if (matchedDraft) {
openBabyObjectMatchDraft(matchedDraft);
}
return;
}
if (target.kind === 'jump-hop') {
let session: JumpHopSessionSnapshotResponse | null = null;
let work: JumpHopWorkProfileResponse | null = null;
try {
if (profileId) {
work = (
await jumpHopClient.getWorkDetail(profileId, {
audience: 'creation',
})
).item;
}
} catch {
work = null;
}
try {
if (sessionId) {
session = (await jumpHopClient.getSession(sessionId)).session;
}
} catch {
session = null;
}
if (!session && !work) {
return;
}
try {
setJumpHopSession(session);
setJumpHopWork(work);
writeCreationUrlState(
buildJumpHopCreationUrlState({ session, work }),
);
enterCreateTab();
setSelectionStage(
resolveJumpHopCreationUrlRestoreStage({
isGeneratingPath: target.isGeneratingPath,
hasRestoredDraft: Boolean(session?.draft),
hasRestoredWork: Boolean(work),
}),
);
} catch (error) {
setJumpHopError(
resolveRpgCreationErrorMessage(error, '读取跳一跳创作草稿失败。'),
);
}
return;
}
if (target.kind === 'wooden-fish') {
if (!sessionId) {
return;
}
try {
const { session } = await woodenFishClient.getSession(sessionId);
let work: WoodenFishWorkProfileResponse | null = null;
if (profileId) {
work = (await woodenFishClient.getWorkDetail(profileId)).item;
}
setWoodenFishSession(session);
setWoodenFishWork(work);
writeCreationUrlState(
buildWoodenFishCreationUrlState({ session, work }),
);
enterCreateTab();
setSelectionStage(
resolveWoodenFishCreationUrlRestoreStage({
isGeneratingPath: target.isGeneratingPath,
hasRestoredDraft: Boolean(session.draft),
}),
);
} catch (error) {
setWoodenFishError(
resolveRpgCreationErrorMessage(error, '读取敲木鱼创作草稿失败。'),
);
}
}
};
void restoreCreationUrlState();
}, [
babyObjectMatchDrafts,
barkBattleWorks,
bigFishFlow,
bigFishWorks,
enterCreateTab,
initialCreationUrlState,
jumpHopClient,
match3dFlow,
match3dWorks,
openBabyObjectMatchDraft,
openBarkBattleDraft,
openBigFishDraft,
openMatch3DDraft,
openPuzzleDraft,
openPuzzleClearDraft,
openSquareHoleDraft,
openVisualNovelDraft,
platformBootstrap.canReadProtectedData,
platformBootstrap.isLoadingPlatform,
puzzleFlow,
puzzleClearWorks,
puzzleWorks,
setSelectionStage,
squareHoleFlow,
squareHoleWorks,
visualNovelFlow,
visualNovelWorks,
woodenFishClient,
]);
const startBigFishRunFromWork = useCallback(
async (
item: BigFishWorkSummary,
returnStage: BigFishRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
return false;
}
const publicWorkCode = buildBigFishPublicWorkCode(item.sourceSessionId);
setBigFishError(null);
bigFishFlow.setSession(null);
setBigFishRuntimeWork(item);
setBigFishRuntimeShare({
title: item.title,
publicWorkCode,
});
setBigFishRuntimeStartedAt(null);
setBigFishRuntimeSessionSource('work');
setBigFishRuntimeReturnStage(returnStage);
setBigFishRun(null);
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'big-fish',
embedded: options.embedded,
});
const { run } = options.embedded
? await startBigFishRuntimeRun(sessionId, runtimeRequestOptions)
: await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRun(run);
if (!options.embedded) {
setSelectionStage('big-fish-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
);
}
const recordPlayPromise = options.embedded
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeRequestOptions)
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
void recordPlayPromise.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
);
});
return true;
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
return false;
}
},
[
buildRecommendRuntimeRequestOptions,
bigFishFlow,
resolveBigFishErrorMessage,
setBigFishError,
setSelectionStage,
],
);
const startBarkBattleRunFromWork = useCallback(
async (
item: BarkBattleWorkSummary,
returnStage: BarkBattleRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
if (item.status !== 'published') {
setBarkBattleError('汪汪声浪作品发布后才能进入正式玩法。');
return false;
}
setBarkBattleError(null);
setBarkBattleGenerationPartialFailed(false);
setBarkBattleRuntimeMode('published');
setBarkBattlePublishedConfig(
buildBarkBattlePublishedConfigFromWork(item),
);
setBarkBattleRuntimeReturnStage(returnStage);
try {
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({
kind: 'bark-battle',
embedded: options.embedded,
});
setBarkBattleRuntimeRequestOptions(
Object.keys(runtimeRequestOptions).length > 0
? runtimeRequestOptions
: null,
);
const runResponse = options.embedded
? await startBarkBattleRun(item.workId, {}, runtimeRequestOptions)
: await startBarkBattleRun(item.workId);
void runResponse;
selectionStageRef.current = 'bark-battle-runtime';
if (!options.embedded) {
setSelectionStage('bark-battle-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'bark-battle-runtime',
buildBarkBattlePublicWorkCode(item.workId),
),
);
}
return true;
} catch (error) {
setBarkBattleError(
resolveBarkBattleErrorMessage(error, '启动汪汪声浪玩法失败。'),
);
return false;
}
},
[
buildRecommendRuntimeRequestOptions,
resolveBarkBattleErrorMessage,
setSelectionStage,
],
);
const startSelectedPublicWork = useCallback(() => {
if (!selectedPublicWorkDetail || isPublicWorkDetailBusy) {
return;
}
if (isJumpHopGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startJumpHopRunFromProfile(selectedPublicWorkDetail.profileId, {
returnStage: 'work-detail',
});
return;
}
runProtectedAction(() => {
const intent = resolvePlatformPublicWorkStartIntent(
selectedPublicWorkDetail,
{
selectedPuzzleDetail,
selectedRpgDetailEntry: selectedDetailEntry,
barkBattleGalleryEntries,
barkBattleWorks,
mapMatch3DWork: mapPublicWorkDetailToMatch3DWork,
},
);
switch (intent.type) {
case 'blocked':
setPublicWorkDetailError(intent.errorMessage);
return;
case 'start-big-fish':
startBigFishRunFromWork(intent.work, intent.returnStage);
return;
case 'start-puzzle':
setPublicWorkDetailError(null);
void startPuzzleRunFromProfile(
intent.work.profileId,
intent.returnStage,
intent.work,
true,
null,
{ authMode: intent.authMode },
);
return;
case 'start-puzzle-clear':
setPublicWorkDetailError(null);
void startPuzzleClearRunFromProfile(intent.profileId, {
returnStage: intent.returnStage,
});
return;
case 'start-jump-hop':
setPublicWorkDetailError(null);
void startJumpHopRunFromProfile(intent.profileId, {
returnStage: intent.returnStage,
});
return;
case 'start-wooden-fish':
setPublicWorkDetailError(null);
void startWoodenFishRunFromProfile(intent.profileId, {
returnStage: intent.returnStage,
});
return;
case 'start-match3d':
setPublicWorkDetailError(null);
void startMatch3DRunFromProfile(
intent.work,
intent.returnStage,
true,
);
return;
case 'start-square-hole':
setPublicWorkDetailError(null);
void startSquareHoleRunFromProfile(
intent.work,
intent.returnStage,
true,
);
return;
case 'start-visual-novel':
setPublicWorkDetailError(null);
void startVisualNovelRunFromProfile(
intent.profileId,
intent.returnStage,
);
return;
case 'start-bark-battle':
setPublicWorkDetailError(null);
startBarkBattleRunFromWork(intent.work, intent.returnStage);
return;
case 'start-edutainment':
setPublicWorkDetailError(null);
void startBabyObjectMatchRuntimeFromEntry(
intent.entry,
intent.returnStage,
);
return;
case 'record-rpg-gallery-play':
break;
default: {
const exhaustive: never = intent;
return exhaustive;
}
}
setIsPublicWorkDetailBusy(true);
void recordRpgEntryWorldGalleryPlay(
intent.entry.ownerUserId,
intent.entry.profileId,
)
.then((updatedEntry) => {
setSelectedDetailEntry(updatedEntry);
setSelectedPublicWorkDetail(
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
);
handleCustomWorldSelect(updatedEntry.profile);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, '记录作品游玩失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
});
}, [
barkBattleGalleryEntries,
barkBattleWorks,
handleCustomWorldSelect,
isPublicWorkDetailBusy,
runProtectedAction,
selectedDetailEntry,
selectedPuzzleDetail,
selectedPublicWorkDetail,
startBarkBattleRunFromWork,
startBigFishRunFromWork,
startJumpHopRunFromProfile,
startPuzzleClearRunFromProfile,
startWoodenFishRunFromProfile,
startPuzzleRunFromProfile,
startMatch3DRunFromProfile,
startSquareHoleRunFromProfile,
startBabyObjectMatchRuntimeFromEntry,
startVisualNovelRunFromProfile,
]);
const selectRecommendRuntimeEntry = useCallback(
async (entry: PlatformPublicGalleryCard) => {
const entryKey = getPlatformPublicGalleryEntryKey(entry);
const runtimeKind = getPlatformRecommendRuntimeKind(entry);
const startRequestId = recommendRuntimeStartRequestRef.current + 1;
recommendRuntimeStartRequestRef.current = startRequestId;
const isCurrentStartRequest = () =>
recommendRuntimeStartRequestRef.current === startRequestId;
setActiveRecommendEntryKey(entryKey);
setActiveRecommendRuntimeKind(runtimeKind);
setActiveRecommendRuntimeError(null);
setIsStartingRecommendEntry(true);
if (entryKey !== activeRecommendEntryKey) {
await saveAndExitRecommendPuzzleRuntime();
if (!isCurrentStartRequest()) {
return;
}
}
setActiveRecommendEntryKey(entryKey);
setActiveRecommendRuntimeKind(runtimeKind);
setActiveRecommendRuntimeError(null);
if (
runtimeKind === 'puzzle' &&
(isPuzzleBusy || puzzleStartInFlightKeyRef.current !== null)
) {
setIsStartingRecommendEntry(false);
return;
}
setIsStartingRecommendEntry(true);
try {
let started = false;
const intent = resolvePlatformRecommendRuntimeStartIntent(entry, {
selectedPuzzleDetail,
barkBattleGalleryEntries,
mapMatch3DWork: mapPublicWorkDetailToMatch3DWork,
});
switch (intent.type) {
case 'blocked':
if (intent.errorTarget === 'big-fish') {
setBigFishError(intent.errorMessage);
} else if (intent.errorTarget === 'puzzle') {
setPuzzleError(intent.errorMessage);
} else if (intent.errorTarget === 'puzzle-clear') {
setPuzzleClearError(intent.errorMessage);
} else if (intent.errorTarget === 'match3d') {
setMatch3DError(intent.errorMessage);
} else if (intent.errorTarget === 'square-hole') {
setSquareHoleError(intent.errorMessage);
} else {
setBarkBattleError(intent.errorMessage);
}
break;
case 'start-big-fish':
started = await startBigFishRunFromWork(intent.work, 'platform', {
embedded: intent.embedded,
});
break;
case 'start-puzzle':
started = await startPuzzleRunFromProfile(
intent.work.profileId,
intent.returnStage,
intent.work,
false,
null,
{ embedded: intent.embedded },
);
break;
case 'start-puzzle-clear':
started = await startPuzzleClearRunFromProfile(intent.profileId, {
embedded: intent.embedded,
returnStage: intent.returnStage,
});
break;
case 'start-jump-hop':
started = await startJumpHopRunFromProfile(intent.profileId, {
embedded: intent.embedded,
returnStage: intent.returnStage,
});
break;
case 'start-wooden-fish':
started = await startWoodenFishRunFromProfile(intent.profileId, {
embedded: intent.embedded,
returnStage: intent.returnStage,
});
break;
case 'start-match3d':
started = await startMatch3DRunFromProfile(
intent.work,
intent.returnStage,
false,
{ embedded: intent.embedded },
);
break;
case 'start-square-hole':
started = await startSquareHoleRunFromProfile(
intent.work,
intent.returnStage,
false,
{ embedded: intent.embedded },
);
break;
case 'start-visual-novel':
started = await startVisualNovelRunFromProfile(
intent.profileId,
intent.returnStage,
{ embedded: intent.embedded },
);
break;
case 'start-bark-battle':
started = await startBarkBattleRunFromWork(
intent.work,
intent.returnStage,
{ embedded: intent.embedded },
);
break;
case 'start-edutainment':
started = await startBabyObjectMatchRuntimeFromEntry(
intent.entry,
intent.returnStage,
{
embedded: intent.embedded,
},
);
break;
case 'mark-ready':
started = true;
break;
default: {
const exhaustive: never = intent;
return exhaustive;
}
}
if (!isCurrentStartRequest()) {
return;
}
if (started) {
setActiveRecommendRuntimeKind(runtimeKind);
setActiveRecommendRuntimeError(null);
} else {
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError('作品暂时无法进入,请稍后再试。');
}
} catch (error) {
if (!isCurrentStartRequest()) {
return;
}
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError('作品暂时无法进入,请稍后再试。');
} finally {
if (isCurrentStartRequest()) {
setIsStartingRecommendEntry(false);
}
}
},
[
activeRecommendEntryKey,
barkBattleGalleryEntries,
isPuzzleBusy,
saveAndExitRecommendPuzzleRuntime,
selectedPuzzleDetail,
setBarkBattleError,
setBigFishError,
setMatch3DError,
setPuzzleError,
setPuzzleClearError,
setSquareHoleError,
startBarkBattleRunFromWork,
startBigFishRunFromWork,
startJumpHopRunFromProfile,
startPuzzleClearRunFromProfile,
startWoodenFishRunFromProfile,
startMatch3DRunFromProfile,
startPuzzleRunFromProfile,
startSquareHoleRunFromProfile,
startBabyObjectMatchRuntimeFromEntry,
startVisualNovelRunFromProfile,
],
);
const selectAdjacentRecommendRuntimeEntry = useCallback(
(direction: 1 | -1, baseEntryKey?: string | null) => {
const normalizedBaseEntryKey =
baseEntryKey?.trim() || activeRecommendEntryKey;
const nextEntry = selectAdjacentPlatformRecommendEntry(
recommendRuntimeEntries,
direction,
normalizedBaseEntryKey,
);
if (!nextEntry) {
return;
}
void selectRecommendRuntimeEntry(nextEntry);
},
[
activeRecommendEntryKey,
recommendRuntimeEntries,
selectRecommendRuntimeEntry,
],
);
const resolveRecommendRuntimeEntryKeyByProfileId = useCallback(
(profileId: string | null | undefined) => {
const normalizedProfileId = profileId?.trim();
if (!normalizedProfileId) {
return null;
}
const matchedEntry =
recommendRuntimeEntries.find(
(entry) => entry.profileId === normalizedProfileId,
) ?? null;
return matchedEntry
? getPlatformPublicGalleryEntryKey(matchedEntry)
: null;
},
[recommendRuntimeEntries],
);
const recommendRuntimeContent = useMemo(() => {
if (
isDesktopLayout ||
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
!activeRecommendRuntimeKind
) {
return null;
}
const activeEntry =
recommendRuntimeEntries.find(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
) ?? null;
if (!activeEntry) {
return null;
}
if (activeRecommendRuntimeKind === 'big-fish') {
return (
<BigFishRuntimeShell
run={bigFishRun}
assetSlots={bigFishSession?.assetSlots ?? []}
shareTitle={bigFishRuntimeShare?.title ?? null}
sharePublicWorkCode={bigFishRuntimeShare?.publicWorkCode ?? null}
isBusy={isBigFishBusy}
error={bigFishError}
embedded
onBack={() => {
reportBigFishObservedPlayTime();
setActiveRecommendRuntimeKind(null);
}}
onRestart={() => {
reportBigFishObservedPlayTime();
void restartBigFishRun();
}}
onSubmitInput={submitBigFishInput}
/>
);
}
if (activeRecommendRuntimeKind === 'match3d') {
const activeMatch3DRuntimeProfile = resolveActiveMatch3DRuntimeProfile(
match3dRun,
match3dRuntimeProfile,
match3dProfile,
);
return (
<Match3DRuntimeShell
run={match3dRun}
isBusy={isMatch3DBusy}
error={match3dError}
embedded
hideBackButton
levelName={
activeMatch3DRuntimeProfile?.gameName?.trim() ||
activeEntry.worldName
}
generatedItemAssets={resolveMatch3DRuntimeGeneratedItemAssets(
match3dRun,
activeMatch3DRuntimeProfile,
activeEntry,
)}
generatedBackgroundAsset={resolveMatch3DRuntimeGeneratedBackgroundAsset(
match3dRun,
activeMatch3DRuntimeProfile,
activeEntry,
)}
backgroundImageSrc={resolveMatch3DRuntimeBackgroundImageSrc(
match3dRun,
activeMatch3DRuntimeProfile,
activeEntry,
)}
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onRestart={() => {
if (!match3dRun?.runId || isMatch3DBusy) {
return;
}
match3dFlow.setIsBusy(true);
setMatch3DError(null);
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
.then((runtimeRequestOptions) =>
resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).restartRun(match3dRun.runId, runtimeRequestOptions),
)
.then(({ run }) => {
setMatch3DRun(run);
})
.catch((error) => {
setMatch3DError(
resolveMatch3DErrorMessage(error, '重新开始抓大鹅玩法失败。'),
);
})
.finally(() => {
match3dFlow.setIsBusy(false);
});
}}
onOptimisticRunChange={setMatch3DRun}
onClickItem={async (payload) => {
const runId = payload.runId ?? match3dRun?.runId;
if (!runId) {
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
}
const runtimeRequestOptions =
await buildRecommendRuntimeRequestOptions({ kind: 'match3d' });
return resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).clickItem(runId, payload, runtimeRequestOptions);
}}
onTimeExpired={() => {
if (!match3dRun?.runId) {
return;
}
void buildRecommendRuntimeRequestOptions({ kind: 'match3d' })
.then((runtimeRequestOptions) =>
resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).finishTimeUp(match3dRun.runId, runtimeRequestOptions),
)
.then(({ run }) => {
setMatch3DRun(run);
})
.catch((error) => {
setMatch3DError(
resolveMatch3DErrorMessage(error, '同步抓大鹅倒计时失败。'),
);
});
}}
/>
);
}
if (activeRecommendRuntimeKind === 'puzzle') {
return (
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={
isPuzzleBusy ||
isPuzzleNextLevelGenerating ||
isPuzzleLeaderboardBusy
}
error={puzzleError}
embedded
hideBackButton
hideExitControls
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onRemodelWork={
selectedPuzzleDetail?.publicationStatus === 'published'
? remodelCurrentPuzzleRuntimeWork
: undefined
}
onSwapPieces={(payload) => {
void swapPuzzlePiecesInRun(payload);
}}
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={(target) => {
const targetEntryKey = resolveRecommendRuntimeEntryKeyByProfileId(
target?.profileId,
);
void selectAdjacentRecommendRuntimeEntry(
1,
targetEntryKey ?? activeRecommendEntryKey,
);
}}
onRestartLevel={() => {
void restartPuzzleCurrentLevel();
}}
onPauseChange={setPuzzleRuntimePaused}
onUseProp={usePuzzleProp}
onTimeExpired={syncPuzzleRuntimeTimeout}
/>
);
}
if (activeRecommendRuntimeKind === 'jump-hop') {
return (
<JumpHopRuntimeShell
profile={jumpHopWork}
run={jumpHopRun}
isBusy={isJumpHopBusy}
error={jumpHopError}
runtimeRequestOptions={jumpHopRuntimeRequestOptions ?? undefined}
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onRestart={() => {
void restartJumpHopRuntimeRun();
}}
onJump={async (payload) => {
await submitJumpHopJumpAction(payload);
}}
/>
);
}
if (activeRecommendRuntimeKind === 'puzzle-clear') {
return (
<PuzzleClearRuntimeShell
profile={puzzleClearWork}
run={puzzleClearRun}
isBusy={isPuzzleClearBusy}
error={puzzleClearError}
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onRetryLevel={() => {
void retryPuzzleClearLevelRun();
}}
onNextLevel={() => {
void advancePuzzleClearLevelRun();
}}
onTimeUp={() => {
void markPuzzleClearLevelTimeUp();
}}
onSwapCards={async (payload) => {
await swapPuzzleClearCardsInRun(payload);
}}
/>
);
}
if (activeRecommendRuntimeKind === 'wooden-fish') {
return (
<WoodenFishRuntimeShell
profile={woodenFishWork}
run={woodenFishRun}
isBusy={isWoodenFishBusy}
error={woodenFishError}
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onCheckpoint={checkpointWoodenFishRuntimeRun}
/>
);
}
if (activeRecommendRuntimeKind === 'square-hole') {
return (
<SquareHoleRuntimeShell
run={squareHoleRun}
isBusy={isSquareHoleBusy}
error={squareHoleError}
embedded
onBack={() => {
if (
squareHoleRun?.runId &&
squareHoleRun.status.toLowerCase() === 'running'
) {
void buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
})
.then((runtimeRequestOptions) =>
stopSquareHoleRun(
squareHoleRun.runId,
undefined,
runtimeRequestOptions,
),
)
.catch(() => undefined);
}
setActiveRecommendRuntimeKind(null);
}}
onRestart={() => {
if (!squareHoleRun?.runId || isSquareHoleBusy) {
return;
}
squareHoleFlow.setIsBusy(true);
setSquareHoleError(null);
void buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
})
.then((runtimeRequestOptions) =>
restartSquareHoleRun(squareHoleRun.runId, runtimeRequestOptions),
)
.then(({ run }) => {
setSquareHoleRun(run);
})
.catch((error) => {
setSquareHoleError(
resolveSquareHoleErrorMessage(
error,
'重新开始方洞挑战失败。',
),
);
})
.finally(() => {
squareHoleFlow.setIsBusy(false);
});
}}
onOptimisticRunChange={setSquareHoleRun}
onDropShape={(payload) => {
const runId = payload.runId ?? squareHoleRun?.runId;
if (!runId) {
return Promise.reject(new Error('方洞挑战运行态缺少 runId。'));
}
return buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
}).then((runtimeRequestOptions) =>
dropSquareHoleShape(runId, payload, runtimeRequestOptions),
);
}}
onTimeExpired={() => {
if (!squareHoleRun?.runId) {
return;
}
void buildRecommendRuntimeRequestOptions({
kind: 'square-hole',
})
.then((runtimeRequestOptions) =>
finishSquareHoleTimeUp(
squareHoleRun.runId,
runtimeRequestOptions,
),
)
.then(({ run }) => {
setSquareHoleRun(run);
})
.catch((error) => {
setSquareHoleError(
resolveSquareHoleErrorMessage(
error,
'同步方洞挑战倒计时失败。',
),
);
});
}}
/>
);
}
if (activeRecommendRuntimeKind === 'visual-novel') {
return (
<VisualNovelRuntimeShell
draft={visualNovelWork?.draft ?? visualNovelSession?.draft}
run={visualNovelRun}
isBusy={isVisualNovelBusy}
error={visualNovelError}
embedded
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onSubmitAction={(payload) => {
void submitVisualNovelRuntimeAction(payload);
}}
/>
);
}
if (
activeRecommendRuntimeKind === 'bark-battle' &&
barkBattlePublishedConfig
) {
return (
<BarkBattleRuntimeShell
title={barkBattlePublishedConfig.title}
workId={barkBattlePublishedConfig.workId}
publishedConfig={barkBattlePublishedConfig}
runtimeMode="published"
runtimeRequestOptions={barkBattleRuntimeRequestOptions ?? undefined}
onExit={() => {
setActiveRecommendRuntimeKind(null);
}}
/>
);
}
if (activeRecommendRuntimeKind === 'edutainment' && babyObjectMatchDraft) {
return (
<BabyObjectMatchRuntimeShell
draft={babyObjectMatchDraft}
embedded
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
/>
);
}
return (
<div
className={`platform-theme ${platformThemeClass} flex h-full min-h-0 items-center justify-center bg-[var(--platform-recommend-runtime-state-fill)] px-5 text-center text-sm font-semibold leading-6 text-[var(--platform-recommend-runtime-state-text)]`}
>
正在读取世界
</div>
);
}, [
activeRecommendEntryKey,
activeRecommendRuntimeKind,
barkBattlePublishedConfig,
barkBattleRuntimeRequestOptions,
babyObjectMatchDraft,
buildRecommendRuntimeRequestOptions,
bigFishError,
bigFishRun,
bigFishRuntimeShare,
bigFishSession?.assetSlots,
isBigFishBusy,
isJumpHopBusy,
isMatch3DBusy,
isPuzzleBusy,
isPuzzleLeaderboardBusy,
isPuzzleNextLevelGenerating,
isSquareHoleBusy,
isVisualNovelBusy,
match3dError,
match3dFlow,
match3dProfile,
match3dRun,
match3dRuntimeProfile,
match3dRuntimeAdapter,
platformBootstrap.platformTab,
jumpHopError,
jumpHopRun,
jumpHopWork,
puzzleClearError,
puzzleClearRun,
puzzleClearWork,
isPuzzleClearBusy,
isWoodenFishBusy,
woodenFishError,
woodenFishRun,
woodenFishWork,
platformThemeClass,
puzzleError,
puzzleRun,
recommendRuntimeEntries,
remodelCurrentPuzzleRuntimeWork,
resolveMatch3DErrorMessage,
resolveMatch3DRuntimeAdapter,
resolveSquareHoleErrorMessage,
reportBigFishObservedPlayTime,
restartBigFishRun,
selectedPuzzleDetail,
selectionStage,
setMatch3DError,
setMatch3DRun,
setPuzzleRuntimePaused,
setSquareHoleRun,
squareHoleError,
squareHoleFlow,
squareHoleRun,
submitBigFishInput,
submitVisualNovelRuntimeAction,
dragPuzzlePiece,
resolveRecommendRuntimeEntryKeyByProfileId,
restartPuzzleCurrentLevel,
selectAdjacentRecommendRuntimeEntry,
setSquareHoleError,
swapPuzzlePiecesInRun,
syncPuzzleRuntimeTimeout,
usePuzzleProp,
visualNovelError,
visualNovelRun,
visualNovelSession,
visualNovelWork,
checkpointWoodenFishRuntimeRun,
advancePuzzleClearLevelRun,
isDesktopLayout,
markPuzzleClearLevelTimeUp,
retryPuzzleClearLevelRun,
swapPuzzleClearCardsInRun,
]);
const activeRecommendEntry =
activeRecommendEntryKey && !isDesktopLayout
? (recommendRuntimeEntries.find(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
) ?? null)
: null;
const isActiveRecommendRuntimeReady =
activeRecommendEntry !== null &&
isPlatformRecommendRuntimeReadyForEntry(activeRecommendEntry, {
activeKind: activeRecommendRuntimeKind,
hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft),
hasBigFishRun: Boolean(bigFishRun),
hasJumpHopRun: Boolean(jumpHopRun),
hasMatch3DRun: Boolean(match3dRun),
hasPuzzleClearRun: Boolean(puzzleClearRun),
hasSquareHoleRun: Boolean(squareHoleRun),
hasVisualNovelRun: Boolean(visualNovelRun),
hasWoodenFishRun: Boolean(woodenFishRun),
puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null,
puzzleRunCurrentLevelProfileId:
puzzleRun?.currentLevel?.profileId ?? null,
});
useEffect(() => {
if (
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
!activeRecommendEntry
) {
return;
}
postRecommendEntryMiniProgramShareTarget(activeRecommendEntry);
}, [
activeRecommendEntry,
platformBootstrap.platformTab,
selectionStage,
]);
useEffect(() => {
const decision = resolvePlatformRecommendRuntimeAutoStartDecision({
isDesktopLayout,
selectionStage,
platformTab: platformBootstrap.platformTab,
isLoadingPlatform: platformBootstrap.isLoadingPlatform,
entries: recommendRuntimeEntries,
activeEntryKey: activeRecommendEntryKey,
isStarting: isStartingRecommendEntry || isPuzzleBusy,
hasStartError: Boolean(activeRecommendRuntimeError),
readyState: {
activeKind: activeRecommendRuntimeKind,
hasBabyObjectMatchDraft: Boolean(babyObjectMatchDraft),
hasBigFishRun: Boolean(bigFishRun),
hasJumpHopRun: Boolean(jumpHopRun),
hasMatch3DRun: Boolean(match3dRun),
hasPuzzleClearRun: Boolean(puzzleClearRun),
hasSquareHoleRun: Boolean(squareHoleRun),
hasVisualNovelRun: Boolean(visualNovelRun),
hasWoodenFishRun: Boolean(woodenFishRun),
puzzleRunEntryProfileId: puzzleRun?.entryProfileId ?? null,
puzzleRunCurrentLevelProfileId:
puzzleRun?.currentLevel?.profileId ?? null,
},
});
if (decision.type === 'noop') {
return;
}
if (decision.type === 'clear') {
setActiveRecommendEntryKey(null);
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError(null);
return;
}
void selectRecommendRuntimeEntry(decision.entry);
}, [
activeRecommendEntryKey,
activeRecommendRuntimeKind,
activeRecommendRuntimeError,
babyObjectMatchDraft,
bigFishRun,
jumpHopRun,
isStartingRecommendEntry,
isPuzzleBusy,
match3dRun,
platformBootstrap.isLoadingPlatform,
platformBootstrap.platformTab,
isDesktopLayout,
puzzleRun,
puzzleClearRun,
recommendRuntimeEntries,
selectRecommendRuntimeEntry,
selectionStage,
squareHoleRun,
visualNovelRun,
woodenFishRun,
]);
const remixPublicWork = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (isPublicWorkDetailBusy) {
return;
}
runProtectedAction(() => {
setIsPublicWorkDetailBusy(true);
setPublicWorkDetailError(null);
const intent = resolvePlatformPublicWorkRemixIntent(
entry,
publicWorkInteractions,
);
if (intent.type === 'remix-big-fish') {
void remixBigFishGalleryWork(intent.profileId)
.then((response) => {
bigFishFlow.setSession(response.session);
enterCreateTab();
setSelectionStage(intent.selectionStage);
})
.catch((error) => {
setPublicWorkDetailError(
resolveBigFishErrorMessage(error, 'Remix 大鱼吃小鱼作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (intent.type === 'remix-puzzle') {
void remixPuzzleGalleryWork(intent.profileId)
.then((response) => {
resetRecommendRuntimeSelection();
puzzleFlow.setSession(response.session);
setPuzzleOperation(null);
enterCreateTab();
setSelectionStage(intent.selectionStage);
})
.catch((error) => {
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, 'Remix 拼图作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
return;
}
if (intent.type === 'unsupported') {
setPublicWorkDetailError(intent.errorMessage);
setIsPublicWorkDetailBusy(false);
return;
}
void remixRpgEntryWorldGallery(intent.ownerUserId, intent.profileId)
.then((response) => {
const nextEntry = response.entry;
setSelectedDetailEntry(nextEntry);
platformBootstrap.setSavedCustomWorldEntries([
nextEntry,
...platformBootstrap.savedCustomWorldEntries.filter(
(entry) => entry.profileId !== nextEntry.profileId,
),
]);
void detailNavigation.openSavedCustomWorldEditor(nextEntry);
})
.catch((error) => {
setPublicWorkDetailError(
resolveRpgCreationErrorMessage(error, 'Remix RPG 作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
});
});
},
[
bigFishFlow,
detailNavigation,
enterCreateTab,
isPublicWorkDetailBusy,
platformBootstrap,
puzzleFlow,
publicWorkInteractions,
resetRecommendRuntimeSelection,
resolveBigFishErrorMessage,
resolvePuzzleErrorMessage,
runProtectedAction,
setSelectionStage,
],
);
const editOwnedPublicWork = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (isPublicWorkDetailBusy) {
return;
}
runProtectedAction(async () => {
setPublicWorkDetailError(null);
const intent = resolvePlatformPublicWorkEditIntent(entry, {
selectedPuzzleDetail,
selectedRpgDetailEntry: selectedDetailEntry,
visualNovelWorks,
barkBattleGalleryEntries,
barkBattleWorks,
mapMatch3DWork: mapPublicWorkDetailToMatch3DWork,
});
switch (intent.type) {
case 'blocked':
setPublicWorkDetailError(intent.errorMessage);
return;
case 'edit-big-fish':
void openBigFishDraft(intent.work);
return;
case 'edit-puzzle':
void openPuzzleDraft(intent.work);
return;
case 'edit-match3d':
void openMatch3DDraft(intent.work, {
forceDraft: intent.forceDraft,
});
return;
case 'edit-square-hole':
void openSquareHoleDraft(intent.work, {
forceDraft: intent.forceDraft,
});
return;
case 'edit-visual-novel':
void openVisualNovelDraft(intent.work, {
forceDraft: intent.forceDraft,
});
return;
case 'resolve-edutainment-draft': {
const matchedDraft = await resolveBabyObjectMatchRuntimeDraft(
intent.entry,
);
if (!matchedDraft) {
setPublicWorkDetailError('这份宝贝识物缺少可编辑草稿。');
return;
}
openBabyObjectMatchDraft(matchedDraft);
return;
}
case 'edit-bark-battle':
openBarkBattleDraft(intent.work, {
forceDraft: intent.forceDraft,
});
return;
case 'edit-rpg-gallery':
void detailNavigation.openSavedCustomWorldEditor(intent.entry);
return;
default: {
const exhaustive: never = intent;
return exhaustive;
}
}
});
},
[
detailNavigation,
isPublicWorkDetailBusy,
openBigFishDraft,
openMatch3DDraft,
openPuzzleDraft,
openPuzzleClearDraft,
openSquareHoleDraft,
openVisualNovelDraft,
barkBattleGalleryEntries,
barkBattleWorks,
openBabyObjectMatchDraft,
openBarkBattleDraft,
puzzleClearWork,
puzzleClearWorks,
resolveBabyObjectMatchRuntimeDraft,
runProtectedAction,
selectedDetailEntry,
selectedPuzzleDetail,
visualNovelWorks,
],
);
const remixSelectedPublicWork = useCallback(() => {
if (!selectedPublicWorkDetail) {
return;
}
if (isSelectedPublicWorkOwned) {
editOwnedPublicWork(selectedPublicWorkDetail);
return;
}
remixPublicWork(selectedPublicWorkDetail);
}, [
editOwnedPublicWork,
isSelectedPublicWorkOwned,
remixPublicWork,
selectedPublicWorkDetail,
]);
const handlePublicCodeSearch = useCallback(
async (keyword: string) => {
const searchPlan = resolvePlatformPublicCodeSearchPlan(keyword);
if (!searchPlan) {
return;
}
const { normalizedKeyword } = searchPlan;
setIsSearchingPublicCode(true);
setPublicSearchError(null);
setSearchedPublicUser(null);
const tryOpenGalleryEntry = async () => {
const entry =
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
const card = mapRpgPublicCodeSearchDetailToGalleryCard(entry);
if (!canExposePublicWork(card)) {
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
}
setSelectedDetailEntry(entry);
openPublicWorkDetail(card);
};
const tryOpenPuzzleGalleryEntry = async () => {
const entries =
puzzleGalleryEntries.length > 0
? puzzleGalleryEntries
: await refreshPuzzleGallery();
const matchedEntry = resolvePuzzlePublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到拼图作品。');
}
await openPuzzlePublicWorkDetail(matchedEntry.detail.profileId, {
tab: platformBootstrap.platformTab,
});
};
const tryOpenBigFishGalleryEntry = async () => {
const entries =
bigFishGalleryEntries.length > 0
? bigFishGalleryEntries
: await refreshBigFishGallery();
const matchedEntry = resolveBigFishPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到大鱼吃小鱼作品。');
}
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenJumpHopGalleryEntry = async () => {
const entries =
jumpHopGalleryEntries.length > 0
? jumpHopGalleryEntries
: await refreshJumpHopGallery();
const matchedEntry = resolveJumpHopPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到跳一跳作品。');
}
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenWoodenFishGalleryEntry = async () => {
const entries =
woodenFishGalleryEntries.length > 0
? woodenFishGalleryEntries
: await refreshWoodenFishGallery();
const matchedEntry = resolveWoodenFishPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到敲木鱼作品。');
}
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenPuzzleClearGalleryEntry = async () => {
const entries =
puzzleClearGalleryEntries.length > 0
? puzzleClearGalleryEntries
: await refreshPuzzleClearGallery();
const matchedEntry = entries.find((entry) => {
const detailEntry = mapPuzzleClearWorkToPlatformGalleryCard(entry);
return (
canExposePublicWork(detailEntry) &&
isSamePuzzleClearPublicWorkCode(normalizedKeyword, entry.profileId)
);
});
if (!matchedEntry) {
throw new Error('未找到拼消消作品。');
}
await openPuzzleClearPublicWorkDetail(matchedEntry.profileId);
};
const tryOpenMatch3DGalleryEntry = async () => {
const entries =
match3dGalleryEntries.length > 0
? match3dGalleryEntries
: await refreshMatch3DGallery();
const matchedEntry = resolveMatch3DPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到抓大鹅作品。');
}
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenSquareHoleGalleryEntry = async () => {
const entries =
squareHoleGalleryEntries.length > 0
? squareHoleGalleryEntries
: await refreshSquareHoleGallery();
const matchedEntry = resolveSquareHolePublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到方洞挑战作品。');
}
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenVisualNovelGalleryEntry = async () => {
const entries =
visualNovelGalleryEntries.length > 0
? visualNovelGalleryEntries
: await refreshVisualNovelGallery();
const matchedEntry = resolveVisualNovelPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到视觉小说作品。');
}
openPublicWorkDetail(matchedEntry.detail);
};
const tryOpenBabyObjectMatchGalleryEntry = async () => {
const entries = (await listLocalBabyObjectMatchDrafts()).filter(
(draft) => draft.publicationStatus === 'published',
);
const matchedDraft = resolveBabyObjectMatchPublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedDraft) {
throw new Error('未找到宝贝识物作品。');
}
setBabyObjectMatchDraft(matchedDraft.item);
openPublicWorkDetail(matchedDraft.detail);
};
const tryOpenBarkBattleGalleryEntry = async () => {
const entries =
barkBattleGalleryEntries.length > 0
? barkBattleGalleryEntries
: await refreshBarkBattleGallery();
const matchedEntry = resolveBarkBattlePublicCodeSearchMatch(
entries,
normalizedKeyword,
);
if (!matchedEntry) {
throw new Error('未找到汪汪声浪作品。');
}
if (selectionStage === 'bark-battle-runtime') {
startBarkBattleRunFromWork(matchedEntry.item, 'platform');
return;
}
openPublicWorkDetail(matchedEntry.detail);
};
const runSearchStep = async (step: PlatformPublicCodeSearchStep) => {
switch (step) {
case 'user-id': {
const user = await getPublicAuthUserById(normalizedKeyword);
setSearchedPublicUser(user);
return;
}
case 'public-user-code': {
const user = await getPublicAuthUserByCode(normalizedKeyword);
setSearchedPublicUser(user);
return;
}
case 'rpg-work':
await tryOpenGalleryEntry();
return;
case 'puzzle-work':
await tryOpenPuzzleGalleryEntry();
return;
case 'big-fish-work':
await tryOpenBigFishGalleryEntry();
return;
case 'jump-hop-work':
await tryOpenJumpHopGalleryEntry();
return;
case 'wooden-fish-work':
await tryOpenWoodenFishGalleryEntry();
return;
case 'baby-object-match-work':
await tryOpenBabyObjectMatchGalleryEntry();
return;
case 'match3d-work':
await tryOpenMatch3DGalleryEntry();
return;
case 'square-hole-work':
await tryOpenSquareHoleGalleryEntry();
return;
case 'visual-novel-work':
await tryOpenVisualNovelGalleryEntry();
return;
case 'bark-battle-work':
await tryOpenBarkBattleGalleryEntry();
return;
default: {
const exhaustive: never = step;
return exhaustive;
}
}
};
try {
for (const [index, step] of searchPlan.steps.entries()) {
try {
await runSearchStep(step);
return;
} catch (error) {
if (index === searchPlan.steps.length - 1) {
throw error;
}
}
}
} catch (error) {
if (selectionStage === 'work-detail') {
setSelectedPublicWorkDetail(null);
setSelectedDetailEntry(null);
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleError(null);
setPublicWorkDetailError(null);
returnPlatformHomeAfterMissingWork();
return;
}
setPublicSearchError(
resolveRpgCreationErrorMessage(error, '未找到对应的陶泥号或作品号。'),
);
} finally {
setIsSearchingPublicCode(false);
}
},
[
barkBattleGalleryEntries,
bigFishGalleryEntries,
jumpHopGalleryEntries,
match3dGalleryEntries,
puzzleClearGalleryEntries,
refreshMatch3DGallery,
openPuzzlePublicWorkDetail,
openPuzzleClearPublicWorkDetail,
openPublicWorkDetail,
platformBootstrap.platformTab,
puzzleGalleryEntries,
refreshBarkBattleGallery,
refreshBigFishGallery,
refreshJumpHopGallery,
refreshPuzzleClearGallery,
refreshWoodenFishGallery,
refreshPuzzleGallery,
refreshSquareHoleGallery,
refreshVisualNovelGallery,
squareHoleGalleryEntries,
returnPlatformHomeAfterMissingWork,
selectionStage,
setPlatformTab,
setPuzzleError,
setSelectionStage,
startBarkBattleRunFromWork,
visualNovelGalleryEntries,
woodenFishGalleryEntries,
],
);
const closePublicSearchResult = useCallback(() => {
setSearchedPublicUser(null);
setPublicSearchError(null);
}, []);
const openProfilePlayedWorks = useCallback(() => {
setIsProfilePlayStatsOpen(true);
setIsProfilePlayStatsLoading(true);
setProfilePlayStatsError(null);
void getRpgProfilePlayStats()
.then(setProfilePlayStats)
.catch((error) => {
setProfilePlayStats(null);
setProfilePlayStatsError(
resolveRpgCreationErrorMessage(error, '读取玩过作品失败。'),
);
})
.finally(() => {
setIsProfilePlayStatsLoading(false);
});
}, []);
const openPlayedWork = useCallback(
(work: ProfilePlayedWorkSummary) => {
const intent = resolvePlatformPlayedWorkOpenIntent(work);
setIsProfilePlayStatsOpen(false);
switch (intent.type) {
case 'noop':
return;
case 'open-puzzle':
void openPuzzlePublicWorkDetail(intent.profileId, {
tab: intent.tab,
});
return;
case 'open-match3d':
void openMatch3DPublicWorkDetail(intent.profileId);
return;
case 'open-square-hole':
void openSquareHolePublicWorkDetail(intent.profileId);
return;
case 'open-jump-hop':
void openJumpHopPublicWorkDetail(intent.profileId);
return;
case 'open-wooden-fish':
void openWoodenFishPublicWorkDetail(intent.profileId);
return;
case 'open-big-fish':
void refreshBigFishGallery()
.then((entries) => {
const matchedEntry = entries.find(
(entry) => entry.sourceSessionId === intent.sessionId,
);
if (matchedEntry) {
openPublicWorkDetail(
mapBigFishWorkToPublicWorkDetail(matchedEntry),
);
return;
}
openPublicWorkDetail(
mapBigFishWorkToPublicWorkDetail(intent.fallbackWork),
);
})
.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '进入大鱼吃小鱼作品失败。'),
);
});
return;
case 'open-rpg':
void openRpgPublicWorkDetail(intent.detail);
return;
default: {
const exhaustive: never = intent;
return exhaustive;
}
}
},
[
openMatch3DPublicWorkDetail,
openPuzzlePublicWorkDetail,
openPublicWorkDetail,
openJumpHopPublicWorkDetail,
openPuzzleClearPublicWorkDetail,
openWoodenFishPublicWorkDetail,
openRpgPublicWorkDetail,
openSquareHolePublicWorkDetail,
refreshBigFishGallery,
resolveBigFishErrorMessage,
setBigFishError,
],
);
useEffect(() => {
const publicWorkCode = initialPublicWorkCode?.trim();
if (!publicWorkCode) {
return;
}
if (selectionStage === 'jump-hop-runtime') {
handledInitialPublicWorkCodeRef.current = publicWorkCode;
return;
}
if (handledInitialPublicWorkCodeRef.current === publicWorkCode) {
return;
}
handledInitialPublicWorkCodeRef.current = publicWorkCode;
void handlePublicCodeSearch(publicWorkCode);
}, [handlePublicCodeSearch, initialPublicWorkCode, selectionStage]);
useEffect(() => {
if (selectionStage === 'platform') {
if (isBigFishCreationVisible) {
void refreshBigFishGallery();
}
void refreshJumpHopGallery();
void refreshPuzzleClearGallery();
void refreshWoodenFishGallery();
void refreshMatch3DGallery();
void refreshPuzzleGallery();
void refreshBarkBattleGallery();
if (isSquareHoleCreationVisible) {
void refreshSquareHoleGallery();
}
if (isVisualNovelCreationOpen) {
void refreshVisualNovelGallery();
}
}
}, [
isBigFishCreationVisible,
isSquareHoleCreationVisible,
isVisualNovelCreationOpen,
refreshBigFishGallery,
refreshJumpHopGallery,
refreshPuzzleClearGallery,
refreshWoodenFishGallery,
refreshMatch3DGallery,
refreshPuzzleGallery,
refreshBarkBattleGallery,
refreshSquareHoleGallery,
refreshVisualNovelGallery,
selectionStage,
]);
useEffect(() => {
if (
platformBootstrap.platformTab === 'create' &&
platformBootstrap.canReadProtectedData
) {
// 中文注释:底部加号创作入口的“最近创作”依赖 RPG works 摘要;
// 失败草稿也必须随进入创作页刷新,不能只等草稿页刷新后才可见。
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
}
if (
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshPuzzleShelf();
void refreshPuzzleClearShelf();
void refreshMatch3DShelf();
if (isSquareHoleCreationVisible) {
void refreshSquareHoleShelf();
}
if (isVisualNovelCreationOpen) {
void refreshVisualNovelShelf();
}
void refreshWoodenFishShelf();
void refreshBabyObjectMatchShelf();
void refreshBarkBattleShelf();
}
}, [
isSquareHoleCreationVisible,
isVisualNovelCreationOpen,
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
platformBootstrap.refreshCustomWorldWorks,
platformBootstrap.setPlatformError,
refreshBabyObjectMatchShelf,
refreshBarkBattleShelf,
refreshMatch3DShelf,
refreshPuzzleShelf,
refreshPuzzleClearShelf,
refreshWoodenFishShelf,
refreshSquareHoleShelf,
refreshVisualNovelShelf,
selectionStage,
]);
useEffect(() => {
if (
isBigFishCreationVisible &&
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshBigFishShelf();
}
}, [
isBigFishCreationVisible,
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshBigFishShelf,
selectionStage,
]);
const creationHubShelfItems = useMemo(
() =>
buildCreationWorkShelfItems({
rpgItems: creationHubItems,
rpgLibraryEntries: platformBootstrap.savedCustomWorldEntries,
bigFishItems: isBigFishCreationVisible ? bigFishShelfItems : [],
jumpHopItems: isJumpHopCreationVisible ? jumpHopShelfItems : [],
woodenFishItems: woodenFishShelfItems,
match3dItems: match3dShelfItems,
squareHoleItems: isSquareHoleCreationVisible
? squareHoleShelfItems
: [],
puzzleItems: puzzleShelfItems,
babyObjectMatchItems: isBabyObjectMatchVisible
? babyObjectMatchDrafts
: [],
barkBattleItems: barkBattleShelfItems,
visualNovelItems: visualNovelShelfItems,
canDeleteRpg: true,
canDeleteBigFish: isBigFishCreationVisible,
canDeleteMatch3D: true,
canDeleteSquareHole: isSquareHoleCreationVisible,
canDeleteJumpHop: isJumpHopCreationVisible,
canDeleteWoodenFish: true,
canDeletePuzzle: true,
canDeleteBabyObjectMatch: isBabyObjectMatchVisible,
canDeleteBarkBattle: true,
canDeleteVisualNovel: true,
onOpenRpgDraft: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void detailNavigation.handleOpenCreationWork(item);
});
},
onEnterRpgPublished: (profileId) => {
runProtectedAction(() => {
const matchedWork = creationHubItems.find(
(entry) => entry.profileId === profileId,
);
if (!matchedWork) {
return;
}
markCreationFlowReturnToDraftShelf();
void detailNavigation.handleOpenCreationWork(matchedWork);
});
},
onDeleteRpg: handleDeletePublishedWork,
onOpenBigFishDetail: isBigFishCreationVisible
? (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openBigFishDraft(item);
});
}
: undefined,
onDeleteBigFish: isBigFishCreationVisible
? handleDeleteBigFishWork
: undefined,
onOpenJumpHopDetail: isJumpHopCreationVisible
? (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openJumpHopDraft(item);
});
}
: undefined,
onDeleteJumpHop: isJumpHopCreationVisible
? handleDeleteJumpHopWork
: undefined,
onOpenWoodenFishDetail: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openWoodenFishDraft(item);
});
},
onDeleteWoodenFish: handleDeleteWoodenFishWork,
onOpenMatch3DDetail: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openMatch3DDraft(item);
});
},
onDeleteMatch3D: handleDeleteMatch3DWork,
onOpenSquareHoleDetail: isSquareHoleCreationVisible
? (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openSquareHoleDraft(item);
});
}
: undefined,
onDeleteSquareHole: isSquareHoleCreationVisible
? handleDeleteSquareHoleWork
: undefined,
onOpenPuzzleDetail: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openPuzzleDraft(item);
});
},
onDeletePuzzle: handleDeletePuzzleWork,
onClaimPuzzlePointIncentive: handleClaimPuzzlePointIncentive,
onOpenBabyObjectMatchDetail: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
openBabyObjectMatchDraft(item);
});
},
onDeleteBabyObjectMatch: handleDeleteBabyObjectMatchWork,
onOpenBarkBattleDetail: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
openBarkBattleDraft(item);
});
},
onDeleteBarkBattle: handleDeleteBarkBattleWork,
onOpenVisualNovelDetail: (item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openVisualNovelDraft(item);
});
},
onDeleteVisualNovel: handleDeleteVisualNovelWork,
getItemState: getCreationWorkShelfState,
}),
[
barkBattleShelfItems,
babyObjectMatchDrafts,
bigFishShelfItems,
creationHubItems,
detailNavigation,
getCreationWorkShelfState,
handleClaimPuzzlePointIncentive,
handleDeleteBabyObjectMatchWork,
handleDeleteBigFishWork,
handleDeleteBarkBattleWork,
handleDeleteJumpHopWork,
handleDeleteMatch3DWork,
handleDeletePublishedWork,
handleDeletePuzzleWork,
handleDeleteSquareHoleWork,
handleDeleteVisualNovelWork,
handleDeleteWoodenFishWork,
isBabyObjectMatchVisible,
isBigFishCreationVisible,
isJumpHopCreationVisible,
isSquareHoleCreationVisible,
jumpHopShelfItems,
markCreationFlowReturnToDraftShelf,
match3dShelfItems,
openBabyObjectMatchDraft,
openBarkBattleDraft,
openBigFishDraft,
openJumpHopDraft,
openMatch3DDraft,
openPuzzleDraft,
openSquareHoleDraft,
openVisualNovelDraft,
openWoodenFishDraft,
platformBootstrap.savedCustomWorldEntries,
puzzleShelfItems,
runProtectedAction,
squareHoleShelfItems,
visualNovelShelfItems,
woodenFishShelfItems,
],
);
// 中文注释:最近创作必须由真实作品架/后端草稿摘要决定,不能混入本地生成中占位。
const backendRecentCreationShelfItems = useMemo(
() =>
buildCreationWorkShelfItems({
rpgItems: creationHubItems,
rpgLibraryEntries: platformBootstrap.savedCustomWorldEntries,
bigFishItems: isBigFishCreationVisible ? bigFishWorks : [],
match3dItems: match3dWorks,
squareHoleItems: isSquareHoleCreationVisible ? squareHoleWorks : [],
jumpHopItems: isJumpHopCreationVisible ? jumpHopWorks : [],
woodenFishItems: woodenFishWorks,
puzzleItems: puzzleWorks,
babyObjectMatchItems: isBabyObjectMatchVisible
? babyObjectMatchDrafts
: [],
barkBattleItems: barkBattleWorks,
visualNovelItems: isVisualNovelCreationOpen ? visualNovelWorks : [],
getItemState: getCreationWorkShelfState,
}),
[
barkBattleWorks,
babyObjectMatchDrafts,
bigFishWorks,
creationHubItems,
getCreationWorkShelfState,
isBabyObjectMatchVisible,
isBigFishCreationVisible,
isJumpHopCreationVisible,
isSquareHoleCreationVisible,
isVisualNovelCreationOpen,
jumpHopWorks,
match3dWorks,
platformBootstrap.savedCustomWorldEntries,
puzzleWorks,
squareHoleWorks,
visualNovelWorks,
woodenFishWorks,
],
);
const renderCreationHubContent = (
mode: 'start-only' | 'works-only',
fallbackLabel: string,
) => (
<Suspense fallback={<LazyPanelFallback label={fallbackLabel} />}>
{creationEntryConfig ? (
<CustomWorldCreationHub
mode={mode}
shelfItems={creationHubShelfItems}
loading={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isMatch3DLoadingLibrary ||
(isSquareHoleCreationVisible && isSquareHoleLoadingLibrary) ||
isPuzzleLoadingLibrary ||
(isVisualNovelCreationOpen && isVisualNovelLoadingLibrary) ||
isBabyObjectMatchBusy
}
error={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isMatch3DLoadingLibrary ||
(isSquareHoleCreationVisible && isSquareHoleLoadingLibrary) ||
isPuzzleLoadingLibrary ||
(isVisualNovelCreationOpen && isVisualNovelLoadingLibrary) ||
isBabyObjectMatchBusy
? null
: (platformBootstrapErrorForDisplay ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
match3dError ??
(isSquareHoleCreationVisible ? squareHoleError : null) ??
puzzleShelfError ??
puzzleError ??
(isVisualNovelCreationOpen ? visualNovelError : null) ??
babyObjectMatchError ??
puzzleClearError ??
barkBattleError)
}
onRetry={() => {
platformBootstrap.setPlatformError(null);
setCreationEntryConfigError(null);
void fetchCreationEntryConfig()
.then(setCreationEntryConfig)
.catch((error: unknown) => {
setCreationEntryConfigError(
error instanceof Error
? error.message
: '读取创作入口配置失败。',
);
});
setBigFishError(null);
setMatch3DError(null);
setSquareHoleError(null);
setPuzzleShelfError(null);
setPuzzleCreationError(null);
setPuzzleError(null);
setVisualNovelError(null);
setBabyObjectMatchError(null);
setWoodenFishError(null);
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
if (isBigFishCreationVisible) {
void refreshBigFishShelf();
}
void refreshMatch3DShelf();
void refreshPuzzleClearShelf();
if (isSquareHoleCreationVisible) {
void refreshSquareHoleShelf();
}
void refreshPuzzleShelf();
void refreshWoodenFishShelf();
if (isVisualNovelCreationOpen) {
void refreshVisualNovelShelf();
}
void refreshBabyObjectMatchShelf();
void refreshBarkBattleShelf();
}}
createBusy={
!creationEntryConfig ||
sessionController.isCreatingAgentSession ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
(isSquareHoleCreationVisible && isSquareHoleBusy) ||
isPuzzleClearBusy ||
isWoodenFishBusy ||
isPuzzleBusy ||
(isVisualNovelCreationOpen && isVisualNovelBusy) ||
(isVisualNovelCreationOpen && isVisualNovelStreamingReply) ||
isBabyObjectMatchBusy ||
isBarkBattleBusy
}
entryConfig={creationEntryConfig}
creationTypes={creationEntryTypes}
recentWorkItems={backendRecentCreationShelfItems}
onCreateType={handleCreationHubCreateType}
onOpenShelfItem={(item) => {
markDraftNoticeSeen(getGenerationNoticeShelfKeys(item));
}}
onShareWork={(payload) => {
openPublishShareModal(payload);
}}
deletingWorkId={deletingCreationWorkId}
claimingPuzzleProfileId={claimingPuzzlePointIncentiveProfileId}
/>
) : null}
</Suspense>
);
const creationStartContent = renderCreationHubContent(
'start-only',
'正在加载创作大厅...',
);
const draftHubContent = renderCreationHubContent(
'works-only',
'正在加载草稿列表...',
);
const creationLandingContent = (
<Suspense fallback={<LazyPanelFallback label="正在加载创作主页..." />}>
<CreationLandingView
onOpenProject={openEditorProject}
onOpenProjects={openProjectGallery}
onOpenCommunity={openCreationCommunity}
publicGalleryEntries={[
...featuredGalleryEntries,
...latestGalleryEntries,
]}
/>
</Suspense>
);
const handlePlatformHomeTabChange = useCallback(
(tab: PlatformHomeTab) => {
platformBootstrap.setPlatformTab(tab);
if (selectionStage === 'creation-home' && tab !== 'create') {
setSelectionStage('platform');
pushAppHistoryPath('/');
}
},
[platformBootstrap, selectionStage, setSelectionStage],
);
const platformHomeView = (
<PlatformEntryHomeView
activeTab={
selectionStage === 'creation-home'
? 'create'
: platformBootstrap.platformTab
}
onTabChange={handlePlatformHomeTabChange}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
saveEntries={platformBootstrap.saveEntries}
saveError={platformBootstrap.saveError}
featuredEntries={featuredGalleryEntries}
latestEntries={latestGalleryEntries}
myEntries={platformBootstrap.savedCustomWorldEntries}
historyEntries={platformBootstrap.historyEntries}
profileDashboard={platformBootstrap.profileDashboard}
isLoadingPlatform={platformBootstrap.isLoadingPlatform}
isLoadingDashboard={platformBootstrap.isLoadingDashboard}
hasUnreadDraftUpdate={hasUnreadDraftUpdates}
profileTaskRefreshKey={profileTaskRefreshKey}
profileGenerationQueueStatus={profileExternalGenerationQueueStatus}
isDesktopLayout={isDesktopLayout}
isResumingSaveWorldKey={platformBootstrap.isResumingSaveWorldKey}
platformError={
platformBootstrap.isLoadingPlatform
? null
: (platformBootstrapErrorForDisplay ??
sessionController.agentWorkspaceRestoreError)
}
dashboardError={
platformBootstrap.isLoadingDashboard
? null
: platformBootstrap.dashboardError
}
createTabContent={
selectionStage === 'creation-home'
? creationLandingContent
: creationStartContent
}
draftTabContent={draftHubContent}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
if (
(entry.worldType ?? '').toLowerCase() === 'puzzle' ||
entry.worldKey.startsWith('puzzle:')
) {
void resumePuzzleSaveArchive(entry);
return;
}
void platformBootstrap.handleResumeSaveEntry(entry);
}}
onOpenCreateWorld={openCreationTypeModal}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={openPublicGalleryDetail}
onOpenChildMotionDemo={startChildMotionDemo}
onOpenBabyLoveDrawing={startBabyLoveDrawingRuntime}
onOpenRecommendGalleryDetail={openRecommendGalleryDetail}
recommendRuntimeContent={recommendRuntimeContent}
activeRecommendEntryKey={activeRecommendEntryKey}
isRecommendRuntimeReady={isActiveRecommendRuntimeReady}
isStartingRecommendEntry={
isStartingRecommendEntry ||
isBigFishBusy ||
(isPuzzleBusy &&
!(activeRecommendRuntimeKind === 'puzzle' && puzzleRun)) ||
(isPuzzleClearBusy &&
!(
activeRecommendRuntimeKind === 'puzzle-clear' &&
puzzleClearRun
)) ||
isMatch3DBusy ||
isSquareHoleBusy ||
isVisualNovelBusy ||
isWoodenFishBusy
}
recommendRuntimeError={activeRecommendRuntimeError}
onSelectNextRecommendEntry={(activeEntryKey) =>
selectAdjacentRecommendRuntimeEntry(1, activeEntryKey)
}
onSelectPreviousRecommendEntry={(activeEntryKey) =>
selectAdjacentRecommendRuntimeEntry(-1, activeEntryKey)
}
onLikeRecommendEntry={(entry) => {
likePublicWork(entry);
}}
onShareRecommendEntry={(entry) => {
openRecommendShareModal(entry);
}}
onRemixRecommendEntry={(entry) => {
remixPublicWork(entry);
}}
onOpenLibraryDetail={(entry) => {
runProtectedAction(() => {
void detailNavigation.openLibraryDetail(entry);
});
}}
onDeleteLibraryEntry={(entry) => {
handleDeleteLibraryEntry(entry);
}}
deletingLibraryEntryId={deletingCreationWorkId}
onSearchPublicCode={(keyword) => {
void handlePublicCodeSearch(keyword);
}}
isSearchingPublicCode={isSearchingPublicCode}
profilePlayStats={profilePlayStats}
isProfilePlayStatsOpen={isProfilePlayStatsOpen}
isProfilePlayStatsLoading={isProfilePlayStatsLoading}
profilePlayStatsError={profilePlayStatsError}
onCloseProfilePlayStats={() => {
setIsProfilePlayStatsOpen(false);
}}
onOpenPlayedWork={openPlayedWork}
onOpenFeedback={openProfileFeedback}
onOpenProjects={openProjectGallery}
onOpenProfileDashboardCard={(cardKey) => {
if (cardKey === 'playedWorks') {
openProfilePlayedWorks();
return;
}
if (platformBootstrap.dashboardError) {
void platformBootstrap.refreshProfileDashboard();
}
}}
onRechargeSuccess={platformBootstrap.refreshProfileDashboard}
/>
);
return (
<>
<AnimatePresence mode="wait">
{selectionStage === 'image-editor' && (
<motion.div
key="image-editor"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="image-editor-stage-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载编辑器..." />}
>
<ImageCanvasEditorView />
</Suspense>
</motion.div>
)}
{selectionStage === 'project' && (
<motion.div
key="project-gallery"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="project-gallery-stage-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
<Suspense fallback={<LazyPanelFallback label="正在加载项目..." />}>
<ProjectGalleryView
onOpenProject={openEditorProject}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'creation-home' && (
<motion.div
key="creation-home"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
{platformHomeView}
</motion.div>
)}
{selectionStage === 'platform' && (
<motion.div
key="platform-home"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
{platformHomeView}
</motion.div>
)}
{selectionStage === 'profile-feedback' && (
<motion.div
key="platform-profile-feedback"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PlatformFeedbackView
onBack={() => {
setPlatformTab('profile');
setSelectionStage('platform');
}}
onSubmit={submitRpgProfileFeedback}
/>
</motion.div>
)}
{selectionStage === 'work-detail' && !selectedPublicWorkDetail && (
<motion.div
key="platform-work-detail-empty"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 items-center justify-center"
>
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-5 py-4 text-sm text-[var(--platform-text-base)]"
>
{publicWorkDetailError || '正在读取作品详情...'}
</PlatformSubpanel>
</motion.div>
)}
{selectionStage === 'work-detail' && selectedPublicWorkDetail && (
<motion.div
key="platform-work-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PlatformWorkDetailView
entry={selectedPublicWorkDetail}
authorSummary={selectedPublicWorkAuthor}
isBusy={
isPublicWorkDetailBusy ||
isPuzzleBusy ||
isPuzzleClearBusy ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isVisualNovelBusy
}
error={publicWorkDetailError}
actionMode={selectedPublicWorkActionMode}
visibleCoverCount={resolveVisiblePuzzleDetailCoverCount(
selectedPublicWorkDetail,
puzzleRun,
)}
onBack={() => {
setPublicWorkDetailError(null);
clearSelectedPublicWorkAuthor();
setSelectionStage('platform');
}}
onLike={() => {
likePublicWork(selectedPublicWorkDetail);
}}
onStart={startSelectedPublicWork}
onRemix={remixSelectedPublicWork}
/>
</motion.div>
)}
{selectionStage === 'detail' && (
<motion.div
key="platform-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
{detailNavigation.isDetailLoading || !selectedDetailEntry ? (
<div className="flex h-full items-center justify-center">
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-5 py-4 text-sm text-[var(--platform-text-base)]"
>
{detailNavigation.detailError || '正在读取作品详情...'}
</PlatformSubpanel>
</div>
) : selectedDetailEntry.visibility !== 'draft' ? (
<PlatformWorkDetailView
entry={mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)}
authorSummary={selectedPublicWorkAuthor}
isBusy={detailNavigation.isMutatingDetail}
error={detailNavigation.detailError}
actionMode={
detailNavigation.isSelectedWorldOwned ? 'edit' : 'remix'
}
onBack={() => {
detailNavigation.setDetailError(null);
clearSelectedPublicWorkAuthor();
entryNavigation.backToPlatformHome();
}}
onLike={() => {
likePublicWork(
mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry),
);
}}
onStart={handleStartSelectedWorld}
onRemix={() => {
const publicWorkEntry =
mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry);
if (detailNavigation.isSelectedWorldOwned) {
editOwnedPublicWork(publicWorkEntry);
return;
}
remixPublicWork(publicWorkEntry);
}}
/>
) : (
<PlatformEntryWorldDetailView
entry={selectedDetailEntry}
isMutating={detailNavigation.isMutatingDetail}
error={detailNavigation.detailError}
onBack={() => {
detailNavigation.setDetailError(null);
entryNavigation.backToPlatformHome();
}}
onStartGame={handleStartSelectedWorld}
onContinueEdit={
detailNavigation.isSelectedWorldOwned
? () => {
runProtectedAction(() => {
void detailNavigation.openSavedCustomWorldEditor(
selectedDetailEntry,
);
});
}
: null
}
onPublish={
selectedDetailEntry.visibility === 'draft' &&
detailNavigation.isSelectedWorldOwned
? () => {
runProtectedAction(() => {
void detailNavigation.handlePublishSelectedWorld();
});
}
: null
}
onUnpublish={
selectedDetailEntry.visibility !== 'draft' &&
detailNavigation.isSelectedWorldOwned
? () => {
runProtectedAction(() => {
void detailNavigation.handleUnpublishSelectedWorld();
});
}
: null
}
onDelete={
detailNavigation.isSelectedWorldOwned
? () => {
const deleteModel =
resolvePlatformCreationWorkDeleteConfirmationModel({
kind: 'rpg-library',
entry: selectedDetailEntry,
});
requestDeleteCreationWork({
id: deleteModel.id,
title: deleteModel.title,
detail: deleteModel.detail,
run: () => {
runProtectedAction(() => {
void detailNavigation.handleDeleteSelectedWorld();
});
},
});
}
: null
}
/>
)}
</motion.div>
)}
{selectionStage === 'agent-workspace' && (
<motion.div
key="agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载 Agent 共创工作区..." />
}
>
<UnifiedCreationPage
spec={getUnifiedSpec('rpg')}
onBack={leaveAgentWorkspace}
isBackDisabled={
sessionController.isStreamingAgentReply ||
Boolean(sessionController.agentOperation)
}
>
{sessionController.agentSession ? (
<CustomWorldAgentWorkspace
session={sessionController.agentSession}
activeOperation={sessionController.agentOperation}
streamingReplyText={
sessionController.streamingAgentReplyText
}
isStreamingReply={sessionController.isStreamingAgentReply}
onBack={leaveAgentWorkspace}
showBackButton={false}
onSubmitMessage={(payload) => {
void sessionController.submitAgentMessage(payload);
}}
onExecuteAction={(payload) => {
void sessionController.executeAgentAction(payload);
}}
/>
) : (
<div className="flex h-full items-center justify-center">
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-5 py-4 text-sm text-[var(--platform-text-base)]"
>
{sessionController.isLoadingAgentSession
? '正在准备 Agent 共创工作区...'
: sessionController.agentWorkspaceRestoreError ||
'正在恢复创作工作区...'}
</PlatformSubpanel>
</div>
)}
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'big-fish-agent-workspace' && (
<motion.div
key="big-fish-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载大鱼吃小鱼共创工作区..." />
}
>
<UnifiedCreationPage
spec={getUnifiedSpec('big-fish')}
onBack={leaveBigFishFlow}
isBackDisabled={isBigFishBusy || isStreamingBigFishReply}
>
<BigFishAgentWorkspace
session={bigFishSession}
streamingReplyText={streamingBigFishReplyText}
isStreamingReply={isStreamingBigFishReply}
isBusy={isBigFishBusy || isStreamingBigFishReply}
error={bigFishError}
onBack={leaveBigFishFlow}
showBackButton={false}
onSubmitMessage={(payload) => {
void submitBigFishMessage(payload);
}}
onExecuteAction={(payload) => {
void executeBigFishAction(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'big-fish-generating' && (
<motion.div
key="big-fish-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载大鱼吃小鱼生成面板..." />
}
>
<CustomWorldGenerationView
settingText={
bigFishSession?.lastAssistantReply ?? '正在整理当前玩法草稿。'
}
anchorEntries={buildBigFishGenerationAnchorEntries(
bigFishSession,
)}
progress={buildMiniGameDraftGenerationProgress(
bigFishGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isBigFishBusy}
error={bigFishError}
onBack={returnToCreationCenterFromGeneration}
onEditSetting={() => {
setSelectionStage('big-fish-agent-workspace');
}}
onRetry={() => {
void executeBigFishAction({
action: 'big_fish_compile_draft',
});
}}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成草稿"
settingDescription={null}
progressTitle="大鱼吃小鱼草稿生成进度"
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'big-fish-result' && bigFishSession?.draft && (
<motion.div
key="big-fish-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载玩法结果页..." />}
>
<BigFishResultView
session={bigFishSession}
isBusy={isBigFishBusy}
error={bigFishError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveBigFishFlow();
return;
}
setSelectionStage('big-fish-agent-workspace');
}}
onDismissError={() => {
setBigFishError(null);
}}
onExecuteAction={(payload) => {
void executeBigFishAction(payload);
}}
onStartTestRun={() => {
void startBigFishRun();
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'big-fish-runtime' && (
<motion.div
key="big-fish-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载大鱼吃小鱼玩法..." />}
>
<BigFishRuntimeShell
run={bigFishRun}
assetSlots={bigFishSession?.assetSlots ?? []}
shareTitle={bigFishRuntimeShare?.title ?? null}
sharePublicWorkCode={
bigFishRuntimeShare?.publicWorkCode ?? null
}
isBusy={isBigFishBusy}
error={bigFishError}
onBack={() => {
reportBigFishObservedPlayTime();
setSelectionStage(bigFishRuntimeReturnStage);
}}
onRestart={() => {
reportBigFishObservedPlayTime();
void restartBigFishRun();
}}
onSubmitInput={submitBigFishInput}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'match3d-agent-workspace' && (
<motion.div
key="match3d-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载抓大鹅共创工作区..." />
}
>
<UnifiedCreationWorkspace
playId="match3d"
spec={{
...getUnifiedSpec('match3d'),
mudPointCost: match3DDraftGenerationPointCost,
}}
session={match3dSession}
isBusy={isStreamingMatch3DReply}
error={match3dError}
onBack={leaveMatch3DFlow}
onExecuteAction={(payload) => {
void executeMatch3DAction(payload);
}}
initialFormPayload={match3dFormDraftPayload}
onCreateFromForm={(payload) => {
runProtectedAction(() => {
void createMatch3DDraftFromForm(payload);
});
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'match3d-generating' && (
<motion.div
key="match3d-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载抓大鹅生成面板..." />}
>
<UnifiedGenerationPage
playId="match3d"
settingText={
match3dGenerationViewSession?.lastAssistantReply ??
'正在生成本局抓大鹅物品素材。'
}
anchorEntries={buildMatch3DGenerationAnchorEntries(
match3dGenerationViewSession,
match3dGenerationViewPayload,
)}
progress={buildMiniGameDraftGenerationProgress(
match3dGenerationViewState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isMatch3DGenerationViewBusy}
error={match3dGenerationViewError}
onBack={returnToCreationCenterFromGeneration}
onEditSetting={() => {
setSelectionStage('match3d-agent-workspace');
}}
onRetry={retryMatch3DDraftGeneration}
hideBatchModule
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'match3d-result' && match3dSession?.draft && (
<motion.div
key="match3d-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载抓大鹅结果..." />}
>
<Match3DResultView
profile={
match3dProfile ??
buildMatch3DProfileFromSession(match3dSession)!
}
draft={match3dSession.draft}
isBusy={isMatch3DBusy}
error={match3dError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveMatch3DFlow();
return;
}
returnToCreationCenterFromGeneration();
}}
onSaved={(profile) => {
setMatch3DProfile(normalizeMatch3DWorkForRuntimeUi(profile));
}}
onPublished={(profile) => {
const normalizedProfile =
normalizeMatch3DWorkForRuntimeUi(profile);
setMatch3DProfile(normalizedProfile);
void Promise.allSettled([
refreshMatch3DShelf(),
refreshMatch3DGallery(),
]);
openPublicWorkDetail(
mapMatch3DWorkToPublicWorkDetail(normalizedProfile),
);
openPublishShareModal({
title: normalizedProfile.gameName,
publicWorkCode: buildMatch3DPublicWorkCode(
normalizedProfile.profileId,
),
stage: 'work-detail',
});
}}
onStartTestRun={(profile, options) => {
const normalizedProfile =
normalizeMatch3DWorkForRuntimeUi(profile);
setMatch3DProfile(normalizedProfile);
void startMatch3DRunFromProfile(
normalizedProfile,
'match3d-result',
false,
{
...options,
authMode:
normalizedProfile.publicationStatus === 'draft'
? 'isolated'
: undefined,
},
);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'match3d-runtime' &&
(() => {
const activeMatch3DRuntimeProfile =
resolveActiveMatch3DRuntimeProfile(
match3dRun,
match3dRuntimeProfile,
match3dProfile,
);
return (
<motion.div
key="match3d-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载抓大鹅玩法..." />}
>
<Match3DRuntimeShell
run={match3dRun}
isBusy={isMatch3DBusy}
error={match3dError}
generatedItemAssets={resolveMatch3DRuntimeGeneratedItemAssets(
match3dRun,
activeMatch3DRuntimeProfile,
selectedPublicWorkDetail,
)}
generatedBackgroundAsset={resolveMatch3DRuntimeGeneratedBackgroundAsset(
match3dRun,
activeMatch3DRuntimeProfile,
selectedPublicWorkDetail,
)}
backgroundImageSrc={resolveMatch3DRuntimeBackgroundImageSrc(
match3dRun,
activeMatch3DRuntimeProfile,
selectedPublicWorkDetail,
)}
onBack={() => {
if (
match3dRun?.runId &&
match3dRun.status === 'running'
) {
void resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
)
.stopRun(match3dRun.runId)
.catch(() => undefined);
}
setSelectionStage(match3dRuntimeReturnStage);
}}
onRestart={() => {
if (!match3dRun?.runId || isMatch3DBusy) {
return;
}
match3dFlow.setIsBusy(true);
setMatch3DError(null);
void resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
)
.restartRun(match3dRun.runId)
.then(({ run }) => {
setMatch3DRun(run);
})
.catch((error) => {
setMatch3DError(
resolveMatch3DErrorMessage(
error,
'重新开始抓大鹅玩法失败。',
),
);
})
.finally(() => {
match3dFlow.setIsBusy(false);
});
}}
onOptimisticRunChange={setMatch3DRun}
onClickItem={(payload) => {
const runId = payload.runId ?? match3dRun?.runId;
if (!runId) {
return Promise.reject(
new Error('抓大鹅运行态缺少 runId。'),
);
}
return resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
).clickItem(runId, payload);
}}
onTimeExpired={() => {
if (!match3dRun?.runId) {
return;
}
void resolveMatch3DRuntimeAdapter(
activeMatch3DRuntimeProfile?.profileId,
)
.finishTimeUp(match3dRun.runId)
.then(({ run }) => {
setMatch3DRun(run);
})
.catch((error) => {
setMatch3DError(
resolveMatch3DErrorMessage(
error,
'同步抓大鹅倒计时失败。',
),
);
});
}}
/>
</Suspense>
</motion.div>
);
})()}
{selectionStage === 'baby-object-match-workspace' && (
<motion.div
key="baby-object-match-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载宝贝识物创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('baby-object-match')}
onBack={leaveBabyObjectMatchFlow}
isBackDisabled={isBabyObjectMatchBusy}
>
<BabyObjectMatchWorkspace
isBusy={isBabyObjectMatchBusy}
error={babyObjectMatchError}
onBack={leaveBabyObjectMatchFlow}
showBackButton={false}
title={null}
initialPayload={babyObjectMatchFormPayload}
onCreateDraft={(payload) => {
void createBabyObjectMatchDraftFromForm(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'baby-object-match-generating' && (
<motion.div
key="baby-object-match-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载宝贝识物生成面板..." />
}
>
<CustomWorldGenerationView
settingText={
babyObjectMatchFormPayload
? `${babyObjectMatchFormPayload.itemAName} / ${babyObjectMatchFormPayload.itemBName}`
: '正在整理当前宝贝识物草稿。'
}
anchorEntries={buildBabyObjectMatchGenerationAnchorEntries(
babyObjectMatchFormPayload,
babyObjectMatchDraft,
)}
progress={buildMiniGameDraftGenerationProgress(
babyObjectMatchGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={
isBabyObjectMatchBusy &&
babyObjectMatchGenerationPhase === 'generating'
}
error={babyObjectMatchError}
onBack={leaveBabyObjectMatchFlow}
onEditSetting={() => {
setSelectionStage('baby-object-match-workspace');
}}
onRetry={retryBabyObjectMatchDraftGeneration}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成草稿"
settingDescription={null}
progressTitle="宝贝识物草稿生成进度"
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'baby-object-match-result' &&
babyObjectMatchDraft && (
<motion.div
key="baby-object-match-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载宝贝识物结果..." />}
>
<BabyObjectMatchResultView
draft={babyObjectMatchDraft}
isBusy={isBabyObjectMatchBusy}
error={babyObjectMatchError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveBabyObjectMatchFlow();
return;
}
setSelectionStage('baby-object-match-workspace');
}}
onSaveDraft={(draft) => {
void saveBabyObjectMatchResultDraft(draft);
}}
onPublish={(draft) => {
void publishBabyObjectMatchResultDraft(draft);
}}
onRegenerateAssets={(draft) => {
void regenerateBabyObjectMatchResultAssets(draft);
}}
onStartTestRun={(draft) => {
void startBabyObjectMatchRuntimeFromDraft(
draft,
'baby-object-match-result',
);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'baby-object-match-runtime' &&
babyObjectMatchDraft && (
<motion.div
key="baby-object-match-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载宝贝识物玩法..." />}
>
<BabyObjectMatchRuntimeShell
draft={babyObjectMatchDraft}
onBack={() => {
setSelectionStage(babyObjectMatchRuntimeReturnStage);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'baby-love-drawing-runtime' && (
<motion.div
key="baby-love-drawing-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载宝贝爱画..." />}
>
<BabyLoveDrawingRuntimeShell
onBack={() => {
setSelectionStage('platform');
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'bark-battle-workspace' && (
<motion.div
key="bark-battle-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载汪汪声浪创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('bark-battle')}
onBack={leaveBarkBattleFlow}
isBackDisabled={isBarkBattleBusy}
>
<BarkBattleConfigEditor
isBusy={isBarkBattleBusy}
error={barkBattleError}
onBack={leaveBarkBattleFlow}
showBackButton={false}
title={null}
onPreview={(payload) => {
void createBarkBattleGeneratingDraft(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'square-hole-agent-workspace' && (
<motion.div
key="square-hole-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载方洞挑战共创工作区..." />
}
>
<UnifiedCreationPage
spec={getUnifiedSpec('square-hole')}
onBack={leaveSquareHoleFlow}
isBackDisabled={isSquareHoleBusy || isStreamingSquareHoleReply}
>
<SquareHoleAgentWorkspace
session={squareHoleSession}
streamingReplyText={streamingSquareHoleReplyText}
isStreamingReply={isStreamingSquareHoleReply}
isBusy={isSquareHoleBusy || isStreamingSquareHoleReply}
error={squareHoleError}
onBack={leaveSquareHoleFlow}
showBackButton={false}
onSubmitMessage={(payload) => {
void submitSquareHoleMessage(payload);
}}
onExecuteAction={(payload) => {
void executeSquareHoleAction(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'square-hole-generating' && (
<motion.div
key="square-hole-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载方洞挑战生成面板..." />
}
>
<CustomWorldGenerationView
settingText={
squareHoleSession?.lastAssistantReply ??
'正在整理当前方洞挑战草稿。'
}
anchorEntries={buildSquareHoleGenerationAnchorEntries(
squareHoleSession,
)}
progress={buildMiniGameDraftGenerationProgress(
squareHoleGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isSquareHoleBusy}
error={squareHoleError}
onBack={returnToCreationCenterFromGeneration}
onEditSetting={() => {
setSelectionStage('square-hole-agent-workspace');
}}
onRetry={retrySquareHoleAssetGeneration}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成图片"
settingDescription={null}
progressTitle="方洞挑战图片生成进度"
activeBadgeLabel="图片生成中"
pausedBadgeLabel="图片生成已暂停"
idleBadgeLabel="等待返回结果页"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'square-hole-result' &&
squareHoleSession?.draft && (
<motion.div
key="square-hole-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载方洞挑战结果..." />}
>
<SquareHoleResultView
profile={
squareHoleProfile ??
buildSquareHoleProfileFromSession(squareHoleSession)!
}
draft={squareHoleSession.draft}
isBusy={isSquareHoleBusy}
error={squareHoleError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveSquareHoleFlow();
return;
}
setSelectionStage('square-hole-agent-workspace');
}}
onSaved={(profile) => {
setSquareHoleProfile(profile);
}}
onPublished={(profile) => {
setSquareHoleProfile(profile);
void Promise.allSettled([
refreshSquareHoleShelf(),
refreshSquareHoleGallery(),
]);
openPublicWorkDetail(
mapSquareHoleWorkToPublicWorkDetail(profile),
);
openPublishShareModal({
title: profile.gameName,
publicWorkCode: buildSquareHolePublicWorkCode(
profile.profileId,
),
stage: 'work-detail',
});
}}
onStartTestRun={(profile) => {
setSquareHoleProfile(profile);
void startSquareHoleRunFromProfile(
profile,
'square-hole-result',
);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'square-hole-runtime' && (
<motion.div
key="square-hole-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载方洞挑战玩法..." />}
>
<SquareHoleRuntimeShell
run={squareHoleRun}
isBusy={isSquareHoleBusy}
error={squareHoleError}
onBack={() => {
if (
squareHoleRun?.runId &&
squareHoleRun.status.toLowerCase() === 'running'
) {
void stopSquareHoleRun(squareHoleRun.runId).catch(
() => undefined,
);
}
setSelectionStage(squareHoleRuntimeReturnStage);
}}
onRestart={() => {
if (!squareHoleRun?.runId || isSquareHoleBusy) {
return;
}
squareHoleFlow.setIsBusy(true);
setSquareHoleError(null);
void restartSquareHoleRun(squareHoleRun.runId)
.then(({ run }) => {
setSquareHoleRun(run);
})
.catch((error) => {
setSquareHoleError(
resolveSquareHoleErrorMessage(
error,
'重新开始方洞挑战失败。',
),
);
})
.finally(() => {
squareHoleFlow.setIsBusy(false);
});
}}
onOptimisticRunChange={setSquareHoleRun}
onDropShape={(payload) => {
const runId = payload.runId ?? squareHoleRun?.runId;
if (!runId) {
return Promise.reject(
new Error('方洞挑战运行态缺少 runId。'),
);
}
return dropSquareHoleShape(runId, payload);
}}
onTimeExpired={() => {
if (!squareHoleRun?.runId) {
return;
}
void finishSquareHoleTimeUp(squareHoleRun.runId)
.then(({ run }) => {
setSquareHoleRun(run);
})
.catch((error) => {
setSquareHoleError(
resolveSquareHoleErrorMessage(
error,
'同步方洞挑战倒计时失败。',
),
);
});
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'jump-hop-workspace' && (
<motion.div
key="jump-hop-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载跳一跳创作..." />}
>
<UnifiedCreationWorkspace
playId="jump-hop"
spec={getUnifiedSpec('jump-hop')}
isBusy={isJumpHopBusy}
error={jumpHopError}
onBack={leaveJumpHopFlow}
onSubmitted={(result, payload) => {
void compileJumpHopSession(result, payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'jump-hop-generating' && (
<motion.div
key="jump-hop-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载跳一跳生成面板..." />}
>
<UnifiedGenerationPage
playId="jump-hop"
settingText={
jumpHopSession?.draft?.workTitle?.trim() ||
jumpHopSession?.draft?.workDescription?.trim() ||
'正在整理跳一跳草稿。'
}
anchorEntries={buildJumpHopGenerationAnchorEntries(
jumpHopSession,
null,
)}
progress={buildMiniGameDraftGenerationProgress(
jumpHopGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isJumpHopGenerationViewBusy}
error={jumpHopError}
onBack={leaveJumpHopFlow}
onEditSetting={() => {
setSelectionStage('jump-hop-workspace');
}}
onRetry={retryJumpHopDraftGeneration}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'jump-hop-result' &&
(() => {
const activeJumpHopResultProfile =
jumpHopWork ?? jumpHopSession?.draft ?? null;
return (
<motion.div
key="jump-hop-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
{activeJumpHopResultProfile ? (
<Suspense
fallback={
<LazyPanelFallback label="正在加载跳一跳结果..." />
}
>
<JumpHopResultView
profile={activeJumpHopResultProfile}
error={jumpHopError}
onBack={leaveJumpHopFlow}
onEdit={() => {
setSelectionStage('jump-hop-workspace');
}}
onStartTestRun={startJumpHopTestRunFromProfile}
onPublish={publishJumpHopDraft}
onRegenerateTiles={() => {
void regenerateJumpHopAsset('regenerate-tiles');
}}
/>
</Suspense>
) : (
<CreationResultRecoveryPanel
title="跳一跳草稿未恢复"
message="当前链接缺少可恢复的跳一跳草稿信息。"
actionLabel="返回创作"
onAction={() => {
setSelectionStage('jump-hop-workspace');
}}
/>
)}
</motion.div>
);
})()}
{selectionStage === 'jump-hop-runtime' && (
<motion.div
key="jump-hop-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载跳一跳玩法..." />}
>
<JumpHopRuntimeShell
snapshot={jumpHopRun}
profile={jumpHopWork}
isBusy={isJumpHopBusy}
error={jumpHopError}
runtimeRequestOptions={
jumpHopRuntimeRequestOptions ?? undefined
}
onBack={() => {
setSelectionStage(jumpHopRuntimeReturnStage);
}}
onRestart={() => {
void restartJumpHopRuntimeRun();
}}
onJump={submitJumpHopJumpAction}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-clear-workspace' && (
<motion.div
key="puzzle-clear-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼消消创作..." />}
>
<PuzzleClearWorkspace
isBusy={isPuzzleClearBusy}
error={puzzleClearError}
onBack={leavePuzzleClearFlow}
onSubmitted={(result, payload) => {
void compilePuzzleClearSession(result, payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-clear-generating' && (
<motion.div
key="puzzle-clear-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼消消生成面板..." />}
>
<CustomWorldGenerationView
settingText={
puzzleClearSession?.draft?.workTitle?.trim() ||
puzzleClearSession?.draft?.themePrompt?.trim() ||
'正在整理拼消消草稿。'
}
anchorEntries={buildPuzzleClearGenerationAnchorEntries(
puzzleClearSession,
null,
)}
progress={buildMiniGameDraftGenerationProgress(
puzzleClearGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isPuzzleClearGenerationViewBusy}
error={puzzleClearError}
onBack={leavePuzzleClearFlow}
onEditSetting={() => {
setSelectionStage('puzzle-clear-workspace');
}}
onRetry={retryPuzzleClearDraftGeneration}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成草稿"
settingTitle="当前拼消消信息"
settingDescription={null}
progressTitle="拼消消草稿生成进度"
activeBadgeLabel="素材生成中"
pausedBadgeLabel="素材生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-clear-result' &&
puzzleClearSession?.draft && (
<motion.div
key="puzzle-clear-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼消消结果..." />}
>
<PuzzleClearResultView
profile={puzzleClearWork ?? puzzleClearSession.draft}
isBusy={isPuzzleClearBusy}
error={puzzleClearError}
onBack={leavePuzzleClearFlow}
onEdit={() => {
setSelectionStage('puzzle-clear-workspace');
}}
onStartTestRun={startPuzzleClearTestRunFromProfile}
onPublish={publishPuzzleClearDraft}
onRegenerateAtlas={() => {
void regeneratePuzzleClearAtlas();
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-clear-runtime' && (
<motion.div
key="puzzle-clear-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼消消玩法..." />}
>
<PuzzleClearRuntimeShell
snapshot={puzzleClearRun}
profile={puzzleClearWork}
isBusy={isPuzzleClearBusy}
error={puzzleClearError}
onBack={() => {
setSelectionStage(puzzleClearRuntimeReturnStage);
}}
onRetryLevel={() => {
void retryPuzzleClearLevelRun();
}}
onNextLevel={() => {
void advancePuzzleClearLevelRun();
}}
onTimeUp={() => {
void markPuzzleClearLevelTimeUp();
}}
onSwapCards={async (payload) => {
await swapPuzzleClearCardsInRun(payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'wooden-fish-workspace' && (
<motion.div
key="wooden-fish-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载敲木鱼创作..." />}
>
<UnifiedCreationWorkspace
playId="wooden-fish"
spec={getUnifiedSpec('wooden-fish')}
isBusy={isWoodenFishBusy}
error={woodenFishError}
onBack={leaveWoodenFishFlow}
onSubmitted={(result, payload) => {
void compileWoodenFishSession(result, payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'wooden-fish-generating' && (
<motion.div
key="wooden-fish-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载敲木鱼生成面板..." />}
>
<UnifiedGenerationPage
playId="wooden-fish"
settingText={
woodenFishSession?.draft?.workTitle?.trim() ||
woodenFishSession?.draft?.workDescription?.trim() ||
'正在整理敲木鱼草稿。'
}
anchorEntries={buildWoodenFishGenerationAnchorEntries(
woodenFishSession,
null,
)}
progress={buildMiniGameDraftGenerationProgress(
woodenFishGenerationState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isWoodenFishGenerationViewBusy}
error={woodenFishError}
onBack={leaveWoodenFishFlow}
onEditSetting={() => {
setSelectionStage('wooden-fish-workspace');
}}
onRetry={retryWoodenFishDraftGeneration}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'wooden-fish-result' &&
woodenFishSession?.draft && (
<motion.div
key="wooden-fish-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载敲木鱼结果..." />}
>
<WoodenFishResultView
profile={woodenFishWork ?? woodenFishSession.draft}
isBusy={isWoodenFishBusy}
error={woodenFishError}
onBack={leaveWoodenFishFlow}
onEdit={() => {
setSelectionStage('wooden-fish-workspace');
}}
onStartTestRun={startWoodenFishTestRunFromProfile}
onPublish={publishWoodenFishDraft}
onRegenerateHitObject={() => {
void regenerateWoodenFishAsset('regenerate-hit-object');
}}
onUpdateWorkMeta={updateWoodenFishWorkMeta}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'wooden-fish-runtime' && (
<motion.div
key="wooden-fish-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载敲木鱼玩法..." />}
>
<WoodenFishRuntimeShell
snapshot={woodenFishRun}
profile={woodenFishWork}
isBusy={isWoodenFishBusy}
error={woodenFishError}
onBack={() => {
setSelectionStage(woodenFishRuntimeReturnStage);
}}
onCheckpoint={checkpointWoodenFishRuntimeRun}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'creative-agent-workspace' && (
<motion.div
key="creative-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载智能创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('creative-agent')}
onBack={leaveCreativeAgentWorkspace}
isBackDisabled={isCreativeAgentBusy || isPuzzleBusy}
>
<CreativeAgentWorkspace
session={creativeAgentSession}
isBusy={isCreativeAgentBusy || isPuzzleBusy}
isStreaming={isCreativeAgentStreaming}
error={creativeAgentError}
eventLog={creativeAgentEvents}
onBack={leaveCreativeAgentWorkspace}
showBackButton={false}
onSubmitMessage={(payload) => {
void submitCreativeAgentMessage(payload);
}}
onConfirmTemplate={(selection) => {
void confirmCreativeTemplateSelection(selection);
}}
onOpenTarget={() => {
void openCreativeAgentTarget();
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-agent-workspace' && (
<motion.div
key="puzzle-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图创作..." />}
>
<UnifiedCreationWorkspace
playId="puzzle"
spec={{
...getUnifiedSpec('puzzle'),
mudPointCost: puzzleDraftGenerationPointCost,
}}
session={puzzleSession}
isBusy={isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
onSubmitMessage={(payload) => {
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
executePuzzleWorkspaceAction(payload);
}}
initialFormPayload={puzzleFormDraftPayload}
onCreateFromForm={(payload) => {
void createPuzzleDraftFromForm(payload);
}}
onAutoSaveForm={(payload) => {
void savePuzzleFormDraft(payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-onboarding' && (
<motion.div
key="puzzle-onboarding"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<PuzzleOnboardingView
prompt={puzzleOnboardingPrompt}
phase={puzzleOnboardingPhase}
error={puzzleOnboardingError}
copy={PUZZLE_ONBOARDING_COPY}
onPromptChange={setPuzzleOnboardingPrompt}
onSubmit={() => {
void submitPuzzleOnboardingPrompt();
}}
onSkip={skipPuzzleOnboarding}
/>
</motion.div>
)}
{selectionStage === 'puzzle-generating' && (
<motion.div
key="puzzle-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图生成面板..." />}
>
<UnifiedGenerationPage
playId="puzzle"
settingText={
puzzleGenerationViewSession?.lastAssistantReply ??
'正在整理当前拼图草稿。'
}
anchorEntries={buildPuzzleGenerationAnchorEntries(
puzzleGenerationViewSession,
puzzleGenerationViewPayload,
)}
progress={buildMiniGameDraftGenerationProgress(
puzzleGenerationViewState,
miniGameGenerationProgressNowMs,
)}
isGenerating={isPuzzleGenerationViewBusy}
error={puzzleGenerationViewError}
onBack={returnToCreationCenterFromGeneration}
onEditSetting={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onRetry={retryPuzzleDraftGeneration}
hideBatchModule
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-result' &&
puzzleSession?.draft &&
!isPuzzleFormOnlyDraft(puzzleSession) && (
<motion.div
key="puzzle-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图结果..." />}
>
<PuzzleResultView
session={puzzleSession}
profileId={
puzzleSession.publishedProfileId ??
buildPuzzleResultProfileId(puzzleSession.sessionId)
}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={leavePuzzleFlow}
onExecuteAction={(payload) => {
executePuzzleWorkspaceAction(payload);
}}
onStartTestRun={startPuzzleTestRunFromDraft}
creativeDraftEdit={
activeCreativeAgentSessionId
? {
isBusy: isCreativeDraftEditBusy,
error: creativeDraftEditError,
onSubmit: submitCreativeDraftEdit,
}
: null
}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-agent-workspace' && (
<motion.div
key="visual-novel-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('visual-novel')}
onBack={leaveVisualNovelFlow}
isBackDisabled={
isVisualNovelBusy || isVisualNovelStreamingReply
}
>
<VisualNovelAgentWorkspace
session={visualNovelSession}
isBusy={isVisualNovelBusy || isVisualNovelStreamingReply}
error={visualNovelError}
onBack={leaveVisualNovelFlow}
showBackButton={false}
title={null}
initialFormPayload={visualNovelFormDraftPayload}
onCreateFromForm={(payload) => {
void createVisualNovelDraftFromForm(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-generating' && (
<motion.div
key="visual-novel-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载视觉小说生成面板..." />
}
>
<CustomWorldGenerationView
settingText={
visualNovelFormDraftPayload?.seedText ??
visualNovelSession?.messages.find(
(message) => message.role === 'user',
)?.text ??
'正在整理当前视觉小说草稿。'
}
anchorEntries={buildVisualNovelEntryGenerationAnchorEntries(
visualNovelFormDraftPayload,
)}
progress={buildVisualNovelEntryGenerationProgress(
visualNovelGenerationStartedAtMs,
visualNovelGenerationPhase,
miniGameGenerationProgressNowMs,
)}
isGenerating={isVisualNovelBusy || isVisualNovelStreamingReply}
error={visualNovelError}
onBack={returnToCreationCenterFromGeneration}
onEditSetting={() => {
setSelectionStage('visual-novel-agent-workspace');
}}
onRetry={retryVisualNovelDraftGeneration}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成草稿"
settingDescription={null}
progressTitle="视觉小说草稿生成进度"
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-result' &&
(visualNovelSession?.draft || visualNovelWork?.draft) && (
<motion.div
key="visual-novel-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说结果..." />}
>
<VisualNovelResultView
draft={visualNovelWork?.draft ?? visualNovelSession?.draft}
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveVisualNovelFlow();
return;
}
setSelectionStage('visual-novel-agent-workspace');
}}
onSaveDraft={(draft) => {
void saveVisualNovelDraft(draft);
}}
onCompileWorkProfile={(draft) => {
void compileVisualNovelDraft(draft);
}}
onStartTestRun={(draft) => {
void startVisualNovelTestRunFromDraft(draft);
}}
onPublish={(draft) => {
void publishVisualNovelDraft(draft);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-runtime' && (
<motion.div
key="visual-novel-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说试玩..." />}
>
<VisualNovelRuntimeShell
draft={visualNovelWork?.draft ?? visualNovelSession?.draft}
run={visualNovelRun}
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
setSelectionStage(visualNovelRuntimeReturnStage);
}}
onSubmitAction={(payload) => {
void submitVisualNovelRuntimeAction(payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-gallery-detail' &&
visualNovelWork && (
<motion.div
key="visual-novel-gallery-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说详情..." />}
>
<VisualNovelResultView
draft={visualNovelWork.draft}
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
setSelectionStage('platform');
}}
onStartTestRun={(draft) => {
setVisualNovelRuntimeReturnStage(
'visual-novel-gallery-detail',
);
void startVisualNovelTestRunFromDraft(draft);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-gallery-detail' && selectedPuzzleDetail && (
<motion.div
key="puzzle-gallery-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图详情..." />}
>
<PuzzleGalleryDetailView
item={selectedPuzzleDetail}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
platformBootstrap.setPlatformTab(
puzzleDetailReturnTarget?.tab ?? 'home',
);
setPuzzleDetailReturnTarget(null);
setSelectionStage('platform');
}}
onEdit={
selectedPuzzleDetail.ownerUserId === authUi?.user?.id &&
Boolean(selectedPuzzleDetail.sourceSessionId?.trim())
? () => {
runProtectedAction(() => {
void openPuzzleDraft(selectedPuzzleDetail);
});
}
: null
}
onStartGame={() => {
void startPuzzleRunFromProfile(
selectedPuzzleDetail.profileId,
'puzzle-gallery-detail',
selectedPuzzleDetail,
false,
null,
{ authMode: 'isolated' },
);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-runtime' && (
<motion.div
key="puzzle-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}
>
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={
isPuzzleBusy ||
isPuzzleNextLevelGenerating ||
isPuzzleLeaderboardBusy
}
error={puzzleError}
hideBackButton={Boolean(puzzleOnboardingDraft)}
onBack={returnFromPuzzleRuntime}
onRemodelWork={
selectedPuzzleDetail?.publicationStatus === 'published'
? remodelCurrentPuzzleRuntimeWork
: undefined
}
onSwapPieces={(payload) => {
void swapPuzzlePiecesInRun(payload);
}}
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={(target) => {
void advancePuzzleLevel(target);
}}
onRestartLevel={() => {
void restartPuzzleCurrentLevel();
}}
onPauseChange={setPuzzleRuntimePaused}
onUseProp={usePuzzleProp}
onTimeExpired={syncPuzzleRuntimeTimeout}
/>
</Suspense>
{isPuzzleNextLevelGenerating ? (
<PuzzleRuntimeBlockingOverlay
title="正在准备下一关"
description="广场暂无可接续作品,正在生成新的候选图。"
/>
) : null}
{puzzleOnboardingDraft &&
puzzleRun?.currentLevel?.status === 'cleared' ? (
<PuzzleOnboardingLoginOverlay
isSaving={isPuzzleOnboardingSaving}
error={puzzleOnboardingError}
copy={PUZZLE_ONBOARDING_CLEAR_COPY}
onLogin={requestPuzzleOnboardingLogin}
/>
) : null}
</motion.div>
)}
{selectionStage === 'custom-world-generating' && (
<motion.div
key="custom-world-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载世界生成面板..." />}
>
<CustomWorldGenerationView
settingText={sessionController.agentDraftSettingPreview}
anchorEntries={sessionController.agentDraftAnchorPreviewEntries}
progress={sessionController.agentDraftGenerationProgress}
isGenerating={sessionController.isActiveGenerationRunning}
error={sessionController.activeGenerationError}
onBack={returnToCreationCenterFromGeneration}
onEditSetting={leaveAgentDraftGeneration}
onRetry={() => {
void sessionController.executeAgentAction({
action: 'draft_foundation',
});
}}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="继续生成草稿"
settingDescription={null}
progressTitle="世界草稿生成进度"
activeBadgeLabel="草稿编译中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'bark-battle-generating' &&
barkBattleDraftConfig && (
<motion.div
key="bark-battle-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在生成汪汪声浪素材..." />}
>
<BarkBattleGeneratingView
draft={barkBattleDraftConfig}
isBusy={isBarkBattleBusy}
error={barkBattleError}
onBack={() => {
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}}
onComplete={handleBarkBattleGenerationComplete}
onError={setBarkBattleError}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'bark-battle-result' && barkBattleDraftConfig && (
<motion.div
key="bark-battle-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载汪汪声浪草稿..." />}
>
<BarkBattleResultView
draft={barkBattleDraftConfig}
isBusy={isBarkBattleBusy}
error={
barkBattleGenerationPartialFailed
? (barkBattleError ??
'部分素材未生成成功,可以在对应槽位重新生成或上传。')
: barkBattleError
}
onBack={() => {
if (shouldReturnToDraftShelf()) {
leaveBarkBattleFlow();
return;
}
enterCreateTab();
selectionStageRef.current = 'platform';
setSelectionStage('platform');
}}
onDraftChange={(draft) => {
void handleBarkBattleDraftChange(draft);
}}
onStartTestRun={testBarkBattleDraft}
onPublish={(draft) => {
void publishBarkBattleDraft(draft);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'bark-battle-runtime' &&
barkBattlePublishedConfig && (
<motion.div
key="bark-battle-runtime"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载汪汪声浪试玩..." />}
>
<BarkBattleRuntimeShell
title={barkBattlePublishedConfig.title}
workId={barkBattlePublishedConfig.workId}
publishedConfig={barkBattlePublishedConfig}
runtimeMode={barkBattleRuntimeMode}
runtimeRequestOptions={
barkBattleRuntimeRequestOptions ?? undefined
}
onExit={() => {
if (
barkBattleRuntimeReturnStage === 'bark-battle-result' &&
barkBattleDraftConfig
) {
setSelectionStage('bark-battle-result');
} else {
enterCreateTab();
setSelectionStage('platform');
}
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'custom-world-result' &&
sessionController.generatedCustomWorldProfile && (
<motion.div
key="custom-world-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载世界编辑器..." />}
>
<RpgCreationResultView
profile={sessionController.generatedCustomWorldProfile}
previewCharacters={previewCustomWorldCharacters}
isGenerating={false}
progress={0}
progressLabel=""
error={resultViewError}
onProfileChange={(profile) => {
sessionController.setGeneratedCustomWorldProfile(profile);
}}
onBack={
sessionController.isAgentDraftResultView
? () => {
leaveAgentDraftResult();
}
: leaveCustomWorldResult
}
onEditSetting={undefined}
onRegenerate={undefined}
onContinueExpand={undefined}
onEnterWorld={() => {
runProtectedAction(() => {
void enterWorldCoordinator
.enterWorldFromCurrentResult()
.catch((error) => {
sessionController.setCustomWorldError(
resolveRpgCreationErrorMessage(
error,
'发布并进入世界失败。',
),
);
});
});
}}
onTestWorld={
sessionController.isAgentDraftResultView &&
sessionController.agentSession?.stage !== 'published'
? () => {
runProtectedAction(() => {
void enterWorldCoordinator
.enterWorldForTestFromCurrentResult()
.catch((error) => {
sessionController.setCustomWorldError(
resolveRpgCreationErrorMessage(
error,
'进入作品测试失败。',
),
);
});
});
}
: undefined
}
onPublishWorld={
sessionController.isAgentDraftResultView &&
sessionController.agentSession?.stage !== 'published'
? async () => {
try {
const publishedProfile =
await enterWorldCoordinator.publishCurrentResult();
void openRpgPublishShareModal(publishedProfile);
} catch (error) {
sessionController.setCustomWorldError(
resolveRpgCreationErrorMessage(
error,
'发布到广场失败。',
),
);
throw error;
}
}
: undefined
}
onGenerateEntity={
sessionController.isAgentDraftResultView
? async (kind) => {
const action =
kind === 'landmark'
? 'generate_landmarks'
: 'generate_characters';
await autosaveCoordinator.executeAgentActionAndWait({
action,
count: 1,
...(kind === 'playable'
? { roleType: 'playable' as const }
: kind === 'story'
? { roleType: 'story' as const }
: {}),
});
const latestView =
sessionController.activeAgentSessionId
? await sessionController.syncAgentCreationResultView(
sessionController.activeAgentSessionId,
)
: null;
const latestProfile =
rpgCreationPreviewAdapter.buildPreviewFromResultView(
latestView,
);
if (latestProfile) {
sessionController.setGeneratedCustomWorldProfile(
latestProfile,
);
}
return { profile: latestProfile };
}
: undefined
}
onDeleteEntities={
sessionController.isAgentDraftResultView
? async (kind, ids) => {
if (ids.length === 0) return;
await autosaveCoordinator.executeAgentActionAndWait(
kind === 'story'
? { action: 'delete_characters', roleIds: ids }
: { action: 'delete_landmarks', sceneIds: ids },
);
const latestView =
sessionController.activeAgentSessionId
? await sessionController.syncAgentCreationResultView(
sessionController.activeAgentSessionId,
)
: null;
const latestProfile =
rpgCreationPreviewAdapter.buildPreviewFromResultView(
latestView,
);
if (latestProfile) {
sessionController.setGeneratedCustomWorldProfile(
latestProfile,
);
}
}
: undefined
}
readOnly={false}
compactAgentResultMode={
sessionController.isAgentDraftResultView
}
backLabel={
sessionController.isAgentDraftResultView
? '返回创作'
: undefined
}
editActionLabel="继续调整设定"
enterWorldActionLabel={
sessionController.isAgentDraftResultView &&
sessionController.agentSession?.stage !== 'published'
? '发布并进入世界'
: '进入世界'
}
publishReady={
sessionController.isAgentDraftResultView
? agentResultPublishGateView.publishReady
: true
}
publishBlockers={
sessionController.isAgentDraftResultView
? agentResultPublishGateView.blockers
: []
}
qualityFindings={
sessionController.isAgentDraftResultView
? agentResultPreviewQualityFindings
: []
}
previewSourceLabel={
sessionController.isAgentDraftResultView
? agentResultPreviewSourceLabel
: null
}
autoSaveState={autosaveCoordinator.customWorldAutoSaveState}
/>
</Suspense>
</motion.div>
)}
</AnimatePresence>
{creationEntryConfig ? (
<PlatformEntryCreationTypeModal
isOpen={showCreationTypeModal}
isBusy={
sessionController.isCreatingAgentSession ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isPuzzleBusy ||
isJumpHopBusy ||
isWoodenFishBusy ||
(isVisualNovelCreationOpen && isVisualNovelBusy) ||
(isVisualNovelCreationOpen && isVisualNovelStreamingReply) ||
isBabyObjectMatchBusy
}
entryConfig={creationEntryConfig}
creationTypes={creationEntryTypes}
onClose={() => {
if (
sessionController.isCreatingAgentSession ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isPuzzleBusy ||
isJumpHopBusy ||
isWoodenFishBusy ||
(isVisualNovelCreationOpen && isVisualNovelBusy) ||
(isVisualNovelCreationOpen && isVisualNovelStreamingReply) ||
isBabyObjectMatchBusy
) {
return;
}
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
}}
onSelectBigFish={() => {
runProtectedAction(() => {
void openBigFishAgentWorkspace();
});
}}
onSelectMatch3D={() => {
handleCreationHubCreateType('match3d');
}}
onSelectSquareHole={() => {
runProtectedAction(() => {
void openSquareHoleAgentWorkspace();
});
}}
onSelectPuzzle={() => {
handleCreationHubCreateType('puzzle');
}}
onSelectJumpHop={() => {
handleCreationHubCreateType('jump-hop');
}}
onSelectWoodenFish={() => {
handleCreationHubCreateType('wooden-fish');
}}
onSelectCreativeAgent={() => {
runProtectedAction(() => {
void openCreativeAgentWorkspace();
});
}}
onSelectBarkBattle={() => {
handleCreationHubCreateType('bark-battle');
}}
onSelectVisualNovel={() => {
handleCreationHubCreateType('visual-novel');
}}
onSelectBabyObjectMatch={() => {
handleCreationHubCreateType('baby-object-match');
}}
/>
) : null}
<PlatformDraftGenerationPointNoticeDialog
notice={draftGenerationPointNotice}
onClose={() => setDraftGenerationPointNotice(null)}
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.75rem]"
/>
<PublishShareModal
open={Boolean(publishSharePayload)}
payload={publishSharePayload}
onClose={() => setPublishSharePayload(null)}
/>
<PlatformErrorDialog
error={activePlatformErrorDialog}
onClose={closePlatformErrorDialog}
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.5rem]"
/>
<PlatformTaskCompletionDialog
completion={activePlatformTaskCompletionDialog}
onClose={closePlatformTaskCompletionDialog}
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.5rem]"
/>
{isCreationCommunityOpen ? (
<PlatformProfileReferralModal
panel="community"
center={null}
isLoading={false}
isSubmittingRedeem={false}
redeemCode=""
copyInviteState="idle"
error={null}
success={null}
onClose={() => setIsCreationCommunityOpen(false)}
onCopyInvite={() => undefined}
onRedeemCodeChange={() => undefined}
onSubmitRedeemCode={() => undefined}
/>
) : null}
<PlatformAcknowledgeStatusDialog
open={Boolean(workNotFoundRecoveryDialog)}
status="error"
title="作品不可用"
onClose={confirmWorkNotFoundRecovery}
showHeader
showCloseButton
closeOnBackdrop
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.75rem]"
>
{workNotFoundRecoveryDialog?.message}
</PlatformAcknowledgeStatusDialog>
<PlatformDangerConfirmDialog
open={Boolean(pendingDeleteCreationWork)}
title="删除作品"
description={
pendingDeleteCreationWork
? `确认删除《${pendingDeleteCreationWork.title}》吗?`
: undefined
}
onClose={closeDeleteCreationWorkConfirmation}
busy={Boolean(deletingCreationWorkId)}
closeOnBackdrop={!deletingCreationWorkId}
size="sm"
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
panelClassName="platform-remap-surface rounded-[1.75rem]"
confirmLabel="确认删除"
busyConfirmLabel="删除中"
onConfirm={confirmDeleteCreationWork}
>
{pendingDeleteCreationWork?.detail}
</PlatformDangerConfirmDialog>
<AnimatePresence>
{publicSearchError ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<PlatformAcknowledgeStatusDialog
status="error"
title="未找到结果"
description="公开编号搜索"
onClose={closePublicSearchResult}
showHeader
showCloseButton
closeLabel="关闭搜索结果"
overlayClassName={`platform-theme ${platformThemeClass} !items-center bg-black/45 !p-4`}
panelClassName="platform-remap-surface rounded-[1.6rem]"
>
{publicSearchError}
</PlatformAcknowledgeStatusDialog>
</motion.div>
) : searchedPublicUser ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<UnifiedModal
open
title="命中用户"
description="公开编号搜索"
onClose={closePublicSearchResult}
closeOnBackdrop={false}
closeLabel="关闭搜索结果"
portal={false}
size="sm"
overlayClassName={`platform-theme ${platformThemeClass} !items-center bg-black/45 !p-4`}
panelClassName="platform-remap-surface rounded-[1.6rem]"
>
<PlatformSubpanel
as="section"
surface="flat"
radius="sm"
padding="md"
title={searchedPublicUser.displayName}
titleVariant="strong"
bodyClassName="mt-3"
>
<PlatformFieldLabel>陶泥号</PlatformFieldLabel>
<div className="mt-1 text-sm font-semibold text-[var(--platform-text-base)]">
{searchedPublicUser.publicUserCode}
</div>
</PlatformSubpanel>
</UnifiedModal>
</motion.div>
) : null}
</AnimatePresence>
</>
);
}
export default PlatformEntryFlowShellImpl;