d19f8ba1da
在项目页头部增加返回主站按钮。 将返回动作接入平台首页路由。 补充项目页返回主站测试与文档说明。
17970 lines
589 KiB
TypeScript
17970 lines
589 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 {
|
|
type CreationUrlState,
|
|
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,
|
|
buildPuzzleDraftOpenNoticeKeys,
|
|
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));
|
|
}
|
|
|
|
function pushPuzzleGeneratingHistoryEntry(
|
|
session: PuzzleAgentSessionSnapshot | null,
|
|
fallbackState?: CreationUrlState,
|
|
) {
|
|
pushAppHistoryPath('/creation/puzzle/generating');
|
|
writeCreationUrlState(
|
|
session ? buildPuzzleCreationUrlState(session) : (fallbackState ?? {}),
|
|
);
|
|
}
|
|
|
|
function buildPuzzleCreationUrlStateFromWork(
|
|
item: PuzzleWorkSummary,
|
|
): CreationUrlState {
|
|
const sourceSessionId = item.sourceSessionId?.trim() || null;
|
|
|
|
return {
|
|
sessionId: sourceSessionId,
|
|
profileId:
|
|
item.profileId?.trim() || buildPuzzleResultProfileId(sourceSessionId),
|
|
workId: item.workId?.trim() || buildPuzzleResultWorkId(sourceSessionId),
|
|
};
|
|
}
|
|
|
|
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, '&')
|
|
.replace(/</gu, '<')
|
|
.replace(/>/gu, '>')
|
|
.replace(/"/gu, '"');
|
|
}
|
|
|
|
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;
|
|
pushPuzzleGeneratingHistoryEntry(response.session);
|
|
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;
|
|
pushPuzzleGeneratingHistoryEntry(session);
|
|
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;
|
|
pushPuzzleGeneratingHistoryEntry(nextSession);
|
|
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);
|
|
const restorePuzzleGeneratingSession = (
|
|
latestSession: PuzzleAgentSessionSnapshot,
|
|
) => {
|
|
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 = latestSession.sessionId;
|
|
pushPuzzleGeneratingHistoryEntry(latestSession);
|
|
setSelectionStage('puzzle-generating');
|
|
};
|
|
const openPuzzleReadyResultSession = (
|
|
latestSession: PuzzleAgentSessionSnapshot,
|
|
) => {
|
|
const payload = buildPuzzleFormPayloadFromSession(latestSession);
|
|
puzzleFlow.setSession(latestSession);
|
|
setPuzzleFormDraftPayload(payload);
|
|
setPuzzleGenerationState(null);
|
|
setPuzzleBackgroundCompileTasks((current) => {
|
|
const next = { ...current };
|
|
delete next[latestSession.sessionId];
|
|
return next;
|
|
});
|
|
enterCreateTab();
|
|
selectionStageRef.current = 'puzzle-result';
|
|
activePuzzleGenerationSessionIdRef.current = null;
|
|
markDraftNoticeSeen(buildPuzzleDraftOpenNoticeKeys(item));
|
|
pushPuzzleResultHistoryEntry(latestSession);
|
|
setSelectionStage('puzzle-result');
|
|
};
|
|
|
|
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;
|
|
pushPuzzleGeneratingHistoryEntry(
|
|
failedSession,
|
|
buildPuzzleCreationUrlStateFromWork(item),
|
|
);
|
|
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;
|
|
pushPuzzleGeneratingHistoryEntry(
|
|
puzzleSession ?? backgroundTask?.session ?? null,
|
|
buildPuzzleCreationUrlStateFromWork(item),
|
|
);
|
|
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;
|
|
pushPuzzleGeneratingHistoryEntry(rebasedTask.session);
|
|
setSelectionStage('puzzle-generating');
|
|
return;
|
|
}
|
|
|
|
if (openIntent.type === 'restore-generating') {
|
|
try {
|
|
const { session: latestSession } =
|
|
await getPuzzleAgentSession(sourceSessionId);
|
|
if (
|
|
!isPuzzleFormOnlyDraft(latestSession) &&
|
|
isPuzzleCompileActionReady(latestSession)
|
|
) {
|
|
openPuzzleReadyResultSession(latestSession);
|
|
return;
|
|
}
|
|
restorePuzzleGeneratingSession(latestSession);
|
|
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);
|
|
pushPuzzleResultHistoryEntry(restoredSession);
|
|
selectionStageRef.current = 'puzzle-result';
|
|
activePuzzleGenerationSessionIdRef.current = null;
|
|
setSelectionStage('puzzle-result');
|
|
}
|
|
},
|
|
[
|
|
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 projectGalleryContent = (
|
|
<Suspense fallback={<LazyPanelFallback label="正在加载项目..." />}>
|
|
<ProjectGalleryView
|
|
onOpenProject={openEditorProject}
|
|
onReturnToMainSite={() => {
|
|
platformBootstrap.setPlatformTab('home');
|
|
setSelectionStage('platform');
|
|
pushAppHistoryPath('/');
|
|
}}
|
|
/>
|
|
</Suspense>
|
|
);
|
|
const handlePlatformHomeTabChange = useCallback(
|
|
(tab: PlatformHomeTab) => {
|
|
platformBootstrap.setPlatformTab(tab);
|
|
if (
|
|
(selectionStage === 'creation-home' && tab !== 'create') ||
|
|
selectionStage === 'project'
|
|
) {
|
|
setSelectionStage('platform');
|
|
pushAppHistoryPath('/');
|
|
}
|
|
},
|
|
[platformBootstrap, selectionStage, setSelectionStage],
|
|
);
|
|
const platformHomeView = (
|
|
<PlatformEntryHomeView
|
|
activeTab={
|
|
selectionStage === 'creation-home' || selectionStage === 'project'
|
|
? 'create'
|
|
: platformBootstrap.platformTab
|
|
}
|
|
activeDesktopNavTab={selectionStage === 'project' ? 'projects' : undefined}
|
|
allowHiddenActiveTab={
|
|
selectionStage === 'creation-home' || selectionStage === 'project'
|
|
}
|
|
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 === 'project'
|
|
? projectGalleryContent
|
|
: 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 === 'creation-home' && (
|
|
<motion.div
|
|
key="creation-home"
|
|
initial={{ opacity: 0, y: 12 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -12 }}
|
|
className="creation-home-stage flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
|
|
>
|
|
{platformHomeView}
|
|
</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="creation-home-stage 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;
|