6841 lines
222 KiB
TypeScript
6841 lines
222 KiB
TypeScript
import { Loader2, Sparkles } from 'lucide-react';
|
||
import { AnimatePresence, motion } from 'motion/react';
|
||
import {
|
||
lazy,
|
||
Suspense,
|
||
useCallback,
|
||
useEffect,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
} from 'react';
|
||
|
||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||
import type {
|
||
BigFishRuntimeSnapshotResponse,
|
||
BigFishSessionSnapshotResponse,
|
||
ExecuteBigFishActionRequest,
|
||
SendBigFishMessageRequest,
|
||
SubmitBigFishInputRequest,
|
||
} from '../../../packages/shared/src/contracts/bigFish';
|
||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||
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 { PuzzleResultDraft } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
|
||
import type {
|
||
CreatePuzzleAgentSessionRequest,
|
||
PuzzleAgentSessionSnapshot,
|
||
SendPuzzleAgentMessageRequest,
|
||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||
import type {
|
||
PuzzleRunSnapshot,
|
||
PuzzleRuntimePropKind,
|
||
SubmitPuzzleLeaderboardRequest,
|
||
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||
import type {
|
||
CustomWorldGalleryCard,
|
||
CustomWorldLibraryEntry,
|
||
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 { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||
import {
|
||
buildPublicWorkStagePath,
|
||
pushAppHistoryPath,
|
||
} from '../../routing/appPageRoutes';
|
||
import {
|
||
getPublicAuthUserByCode,
|
||
getPublicAuthUserById,
|
||
} from '../../services/authService';
|
||
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 {
|
||
readCustomWorldAgentUiState,
|
||
shouldRestoreCustomWorldAgentUiState,
|
||
} from '../../services/customWorldAgentUiState';
|
||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||
import {
|
||
clickMatch3DItem,
|
||
finishMatch3DTimeUp,
|
||
restartMatch3DRun,
|
||
startMatch3DRun,
|
||
stopMatch3DRun,
|
||
} from '../../services/match3d-runtime';
|
||
import {
|
||
deleteMatch3DWork,
|
||
getMatch3DWorkDetail,
|
||
listMatch3DGallery,
|
||
listMatch3DWorks,
|
||
} from '../../services/match3d-works';
|
||
import {
|
||
buildBigFishGenerationAnchorEntries,
|
||
buildMiniGameDraftGenerationProgress,
|
||
buildPuzzleGenerationAnchorEntries,
|
||
buildSquareHoleGenerationAnchorEntries,
|
||
createMiniGameDraftGenerationState,
|
||
type MiniGameDraftGenerationState,
|
||
} from '../../services/miniGameDraftGenerationProgress';
|
||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||
import {
|
||
buildBigFishPublicWorkCode,
|
||
buildMatch3DPublicWorkCode,
|
||
buildPuzzlePublicWorkCode,
|
||
buildSquareHolePublicWorkCode,
|
||
isSameBigFishPublicWorkCode,
|
||
isSameMatch3DPublicWorkCode,
|
||
isSamePuzzlePublicWorkCode,
|
||
isSameSquareHolePublicWorkCode,
|
||
} from '../../services/publicWorkCode';
|
||
import {
|
||
createPuzzleAgentSession,
|
||
executePuzzleAgentAction,
|
||
getPuzzleAgentSession,
|
||
streamPuzzleAgentMessage,
|
||
} from '../../services/puzzle-agent';
|
||
import {
|
||
getPuzzleGalleryDetail,
|
||
likePuzzleGalleryWork,
|
||
listPuzzleGallery,
|
||
remixPuzzleGalleryWork,
|
||
} from '../../services/puzzle-gallery';
|
||
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 {
|
||
generatePuzzleOnboardingWork,
|
||
savePuzzleOnboardingWork,
|
||
} from '../../services/puzzle-onboarding';
|
||
import {
|
||
claimPuzzleWorkPointIncentive,
|
||
deletePuzzleWork,
|
||
listPuzzleWorks,
|
||
updatePuzzleWork,
|
||
} from '../../services/puzzle-works';
|
||
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 } from '../../services/rpg-entry/rpgProfileClient';
|
||
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
|
||
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 type { CustomWorldProfile } from '../../types';
|
||
import { useAuthUi } from '../auth/AuthUiContext';
|
||
import { PublishShareModal } from '../common/PublishShareModal';
|
||
import type { PublishShareModalPayload } from '../common/publishShareModalModel';
|
||
import { UnifiedModal } from '../common/UnifiedModal';
|
||
import {
|
||
isBigFishGalleryEntry,
|
||
isMatch3DGalleryEntry,
|
||
isPuzzleGalleryEntry,
|
||
isSquareHoleGalleryEntry,
|
||
mapBigFishWorkToPlatformGalleryCard,
|
||
mapMatch3DWorkToPlatformGalleryCard,
|
||
mapPuzzleWorkToPlatformGalleryCard,
|
||
mapSquareHoleWorkToPlatformGalleryCard,
|
||
type PlatformPublicGalleryCard,
|
||
} 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 { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||
import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||
import { isPlatformCreationTypeVisible } from './platformEntryCreationTypes';
|
||
import {
|
||
PlatformEntryHomeView,
|
||
type PlatformHomeTab,
|
||
} from './PlatformEntryHomeView';
|
||
import {
|
||
buildCreationHubFallbackItems,
|
||
resolveRpgCreationErrorMessage,
|
||
} from './platformEntryShared';
|
||
import type { PlatformEntryFlowShellProps } from './platformEntryTypes';
|
||
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
|
||
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
||
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
|
||
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
|
||
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
|
||
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
|
||
|
||
type AgentResultPublishGateView = {
|
||
blockers: string[];
|
||
publishReady: boolean;
|
||
};
|
||
|
||
type PuzzleDetailReturnTarget = {
|
||
tab: PlatformHomeTab;
|
||
};
|
||
|
||
type PuzzleRuntimeReturnStage =
|
||
| 'puzzle-result'
|
||
| 'puzzle-gallery-detail'
|
||
| 'work-detail'
|
||
| 'platform';
|
||
|
||
type PuzzleOnboardingPhase = 'input' | 'generating' | 'generated';
|
||
|
||
type PuzzleOnboardingDraft = {
|
||
promptText: string;
|
||
item: PuzzleWorkSummary;
|
||
};
|
||
|
||
type BigFishRuntimeReturnStage = 'big-fish-result' | 'work-detail' | 'platform';
|
||
type BigFishRuntimeSessionSource = 'draft' | 'work' | null;
|
||
type SquareHoleRuntimeReturnStage =
|
||
| 'square-hole-result'
|
||
| 'work-detail'
|
||
| 'platform';
|
||
|
||
type PuzzleSaveArchiveState = {
|
||
runtimeKind?: unknown;
|
||
entryProfileId?: unknown;
|
||
currentProfileId?: unknown;
|
||
currentLevelId?: unknown;
|
||
};
|
||
|
||
type DeleteCreationWorkConfirmation = {
|
||
id: string;
|
||
title: string;
|
||
detail: string;
|
||
run: () => void;
|
||
};
|
||
|
||
async function resumePuzzleProfileSaveArchiveRaw(worldKey: string) {
|
||
return requestRpgRuntimeJson<
|
||
ProfileSaveArchiveResumeResponse<PuzzleSaveArchiveState>
|
||
>(
|
||
`/profile/save-archives/${encodeURIComponent(worldKey)}`,
|
||
{ method: 'POST' },
|
||
'恢复拼图存档失败',
|
||
);
|
||
}
|
||
|
||
type AgentResultBlockerView = {
|
||
code?: string;
|
||
message: string;
|
||
};
|
||
|
||
const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
|
||
'publish_missing_world_hook',
|
||
'publish_missing_player_premise',
|
||
'publish_missing_core_conflict',
|
||
'publish_missing_main_chapter',
|
||
'publish_missing_first_act',
|
||
]);
|
||
|
||
function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
|
||
const rawTime = entry.publishedAt ?? entry.updatedAt;
|
||
const timestamp = new Date(rawTime).getTime();
|
||
return Number.isNaN(timestamp) ? 0 : timestamp;
|
||
}
|
||
|
||
function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
|
||
const kind = isBigFishGalleryEntry(entry)
|
||
? 'big-fish'
|
||
: isPuzzleGalleryEntry(entry)
|
||
? 'puzzle'
|
||
: isMatch3DGalleryEntry(entry)
|
||
? 'match3d'
|
||
: isSquareHoleGalleryEntry(entry)
|
||
? 'square-hole'
|
||
: 'rpg';
|
||
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
|
||
}
|
||
|
||
function isSamePlatformPublicGalleryEntry(
|
||
left: PlatformPublicGalleryCard,
|
||
right: PlatformPublicGalleryCard,
|
||
) {
|
||
return (
|
||
getPlatformPublicGalleryEntryKey(left) ===
|
||
getPlatformPublicGalleryEntryKey(right)
|
||
);
|
||
}
|
||
|
||
function mergePlatformPublicGalleryEntries(
|
||
rpgEntries: CustomWorldGalleryCard[],
|
||
puzzleEntries: PlatformPublicGalleryCard[],
|
||
) {
|
||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||
|
||
[...rpgEntries, ...puzzleEntries].forEach((entry) => {
|
||
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
|
||
});
|
||
|
||
return Array.from(entryMap.values()).sort(
|
||
(left, right) =>
|
||
getPlatformPublicGalleryEntryTime(right) -
|
||
getPlatformPublicGalleryEntryTime(left),
|
||
);
|
||
}
|
||
|
||
function mapRpgGalleryCardToPublicWorkDetail(
|
||
entry: CustomWorldGalleryCard,
|
||
): PlatformPublicGalleryCard {
|
||
return entry;
|
||
}
|
||
|
||
function mapPuzzleWorkToPublicWorkDetail(
|
||
item: PuzzleWorkSummary,
|
||
): PlatformPublicGalleryCard {
|
||
return mapPuzzleWorkToPlatformGalleryCard(item);
|
||
}
|
||
|
||
function resolveVisiblePuzzleDetailCoverCount(
|
||
entry: PlatformPublicGalleryCard | null,
|
||
run: PuzzleRunSnapshot | null,
|
||
) {
|
||
if (!entry || !isPuzzleGalleryEntry(entry)) {
|
||
return 1;
|
||
}
|
||
|
||
if (run?.entryProfileId !== entry.profileId) {
|
||
return 1;
|
||
}
|
||
|
||
// 中文注释:封面首图永远公开,后续封面跟随当前玩家本次 run 的通关进度即时解锁。
|
||
return Math.max(1, run.clearedLevelCount + 1);
|
||
}
|
||
|
||
function mapMatch3DWorkToPublicWorkDetail(
|
||
item: Match3DWorkSummary,
|
||
): PlatformPublicGalleryCard {
|
||
return mapMatch3DWorkToPlatformGalleryCard(item);
|
||
}
|
||
|
||
function mapSquareHoleWorkToPublicWorkDetail(
|
||
item: SquareHoleWorkSummary,
|
||
): PlatformPublicGalleryCard {
|
||
return mapSquareHoleWorkToPlatformGalleryCard(item);
|
||
}
|
||
|
||
function mapBigFishWorkToPublicWorkDetail(
|
||
item: BigFishWorkSummary,
|
||
): PlatformPublicGalleryCard {
|
||
return mapBigFishWorkToPlatformGalleryCard(item);
|
||
}
|
||
|
||
function mapPublicWorkDetailToMatch3DWork(
|
||
entry: PlatformPublicGalleryCard,
|
||
): Match3DWorkSummary | null {
|
||
if (!isMatch3DGalleryEntry(entry)) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
workId: entry.workId,
|
||
profileId: entry.profileId,
|
||
ownerUserId: entry.ownerUserId,
|
||
sourceSessionId:
|
||
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
||
? entry.sourceSessionId
|
||
: null,
|
||
gameName: entry.worldName,
|
||
themeText: entry.themeTags[0] ?? '经典消除',
|
||
summary: entry.summaryText,
|
||
tags: entry.themeTags,
|
||
coverImageSrc: entry.coverImageSrc,
|
||
referenceImageSrc: null,
|
||
clearCount: 12,
|
||
difficulty: 4,
|
||
publicationStatus: 'published',
|
||
playCount: entry.playCount ?? 0,
|
||
updatedAt: entry.updatedAt,
|
||
publishedAt: entry.publishedAt,
|
||
publishReady: true,
|
||
};
|
||
}
|
||
|
||
function buildMatch3DProfileFromSession(
|
||
session: Match3DAgentSessionSnapshot | null,
|
||
): Match3DWorkProfile | null {
|
||
const draft = session?.draft;
|
||
if (!session || !draft?.profileId) {
|
||
return null;
|
||
}
|
||
|
||
const now = session.updatedAt || new Date().toISOString();
|
||
return {
|
||
workId: draft.profileId,
|
||
profileId: draft.profileId,
|
||
ownerUserId: 'current-user',
|
||
sourceSessionId: session.sessionId,
|
||
gameName: draft.gameName,
|
||
themeText: draft.themeText,
|
||
summary: draft.summary ?? draft.summaryText ?? '',
|
||
tags: draft.tags,
|
||
coverImageSrc: draft.coverImageSrc ?? draft.referenceImageSrc ?? null,
|
||
referenceImageSrc: draft.referenceImageSrc ?? null,
|
||
clearCount: draft.clearCount,
|
||
difficulty: draft.difficulty,
|
||
publicationStatus: 'draft',
|
||
playCount: 0,
|
||
updatedAt: now,
|
||
publishedAt: null,
|
||
publishReady: Boolean(draft.publishReady),
|
||
};
|
||
}
|
||
|
||
function mapPublicWorkDetailToPuzzleWork(
|
||
entry: PlatformPublicGalleryCard,
|
||
): PuzzleWorkSummary | null {
|
||
if (!isPuzzleGalleryEntry(entry)) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
workId: entry.workId,
|
||
profileId: entry.profileId,
|
||
ownerUserId: entry.ownerUserId,
|
||
sourceSessionId:
|
||
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
||
? entry.sourceSessionId
|
||
: null,
|
||
authorDisplayName: entry.authorDisplayName,
|
||
levelName: entry.worldName,
|
||
summary: entry.summaryText,
|
||
themeTags: entry.themeTags,
|
||
coverImageSrc: entry.coverImageSrc,
|
||
publicationStatus: 'published',
|
||
updatedAt: entry.updatedAt,
|
||
publishedAt: entry.publishedAt,
|
||
playCount: entry.playCount ?? 0,
|
||
remixCount: entry.remixCount ?? 0,
|
||
likeCount: entry.likeCount ?? 0,
|
||
pointIncentiveTotalHalfPoints: 0,
|
||
pointIncentiveClaimedPoints: 0,
|
||
pointIncentiveTotalPoints: 0,
|
||
pointIncentiveClaimablePoints: 0,
|
||
publishReady: true,
|
||
levels:
|
||
entry.coverSlides?.map((slide, index) => ({
|
||
levelId: slide.id || `puzzle-level-${index + 1}`,
|
||
levelName: slide.label,
|
||
pictureDescription: entry.summaryText,
|
||
candidates: [],
|
||
selectedCandidateId: null,
|
||
coverImageSrc: slide.imageSrc,
|
||
coverAssetId: null,
|
||
generationStatus: 'ready' as const,
|
||
})) ?? [],
|
||
};
|
||
}
|
||
|
||
function mapPublicWorkDetailToBigFishWork(
|
||
entry: PlatformPublicGalleryCard,
|
||
): BigFishWorkSummary | null {
|
||
if (!isBigFishGalleryEntry(entry)) {
|
||
return null;
|
||
}
|
||
|
||
const levelCount = Number.parseInt(
|
||
entry.themeTags.find((tag) => /^\d+级$/u.test(tag))?.replace('级', '') ??
|
||
'0',
|
||
10,
|
||
);
|
||
|
||
return {
|
||
workId: entry.workId,
|
||
sourceSessionId: entry.profileId,
|
||
ownerUserId: entry.ownerUserId,
|
||
authorDisplayName: entry.authorDisplayName,
|
||
title: entry.worldName,
|
||
subtitle: entry.subtitle,
|
||
summary: entry.summaryText,
|
||
coverImageSrc: entry.coverImageSrc,
|
||
status: 'published',
|
||
updatedAt: entry.updatedAt,
|
||
publishedAt: entry.publishedAt,
|
||
publishReady: true,
|
||
levelCount: Number.isNaN(levelCount) ? 0 : levelCount,
|
||
levelMainImageReadyCount: 0,
|
||
levelMotionReadyCount: 0,
|
||
backgroundReady: Boolean(entry.coverImageSrc),
|
||
playCount: entry.playCount ?? 0,
|
||
remixCount: entry.remixCount ?? 0,
|
||
likeCount: entry.likeCount ?? 0,
|
||
};
|
||
}
|
||
|
||
function mapPublicWorkDetailToSquareHoleWork(
|
||
entry: PlatformPublicGalleryCard,
|
||
): SquareHoleWorkSummary | null {
|
||
if (!isSquareHoleGalleryEntry(entry)) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
workId: entry.workId,
|
||
profileId: entry.profileId,
|
||
ownerUserId: entry.ownerUserId,
|
||
sourceSessionId:
|
||
'sourceSessionId' in entry && typeof entry.sourceSessionId === 'string'
|
||
? entry.sourceSessionId
|
||
: null,
|
||
gameName: entry.worldName,
|
||
themeText: entry.themeTags[0] ?? '方洞挑战',
|
||
twistRule: entry.subtitle,
|
||
summary: entry.summaryText,
|
||
tags: entry.themeTags,
|
||
coverImageSrc: entry.coverImageSrc,
|
||
backgroundPrompt: entry.backgroundPrompt ?? '方洞挑战运行背景',
|
||
backgroundImageSrc: entry.backgroundImageSrc ?? null,
|
||
shapeOptions: entry.shapeOptions ?? [],
|
||
holeOptions: entry.holeOptions ?? [],
|
||
shapeCount: entry.shapeCount ?? 8,
|
||
difficulty: entry.difficulty ?? 4,
|
||
publicationStatus: 'published',
|
||
playCount: entry.playCount ?? 0,
|
||
updatedAt: entry.updatedAt,
|
||
publishedAt: entry.publishedAt,
|
||
publishReady: true,
|
||
};
|
||
}
|
||
|
||
function buildSquareHoleProfileFromSession(
|
||
session: SquareHoleSessionSnapshot | null,
|
||
): SquareHoleWorkProfile | null {
|
||
const draft = session?.draft;
|
||
if (!session || !draft?.profileId) {
|
||
return null;
|
||
}
|
||
|
||
const now = session.updatedAt || new Date().toISOString();
|
||
return {
|
||
workId: draft.profileId,
|
||
profileId: draft.profileId,
|
||
ownerUserId: 'current-user',
|
||
sourceSessionId: session.sessionId,
|
||
gameName: draft.gameName,
|
||
themeText: draft.themeText,
|
||
twistRule: draft.twistRule,
|
||
summary: draft.summary,
|
||
tags: draft.tags,
|
||
coverImageSrc: draft.coverImageSrc ?? null,
|
||
backgroundPrompt: draft.backgroundPrompt,
|
||
backgroundImageSrc: draft.backgroundImageSrc ?? null,
|
||
shapeOptions: draft.shapeOptions,
|
||
holeOptions: draft.holeOptions,
|
||
shapeCount: draft.shapeCount,
|
||
difficulty: draft.difficulty,
|
||
publicationStatus: 'draft',
|
||
playCount: 0,
|
||
updatedAt: now,
|
||
publishedAt: null,
|
||
publishReady: Boolean(draft.publishReady),
|
||
};
|
||
}
|
||
|
||
function mergePuzzleWorkSummary(
|
||
current: PuzzleWorkSummary,
|
||
updated: PuzzleWorkSummary,
|
||
): PuzzleWorkSummary {
|
||
return current.profileId === updated.profileId ? updated : current;
|
||
}
|
||
|
||
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 hasSeenPuzzleOnboarding() {
|
||
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 不可写时只降级为本次会话展示,不影响主流程。
|
||
}
|
||
}
|
||
|
||
function PuzzleOnboardingView({
|
||
prompt,
|
||
phase,
|
||
error,
|
||
onPromptChange,
|
||
onSubmit,
|
||
}: {
|
||
prompt: string;
|
||
phase: PuzzleOnboardingPhase;
|
||
error: string | null;
|
||
onPromptChange: (value: string) => void;
|
||
onSubmit: () => void;
|
||
}) {
|
||
const isGenerating = phase === 'generating';
|
||
const isGenerated = phase === 'generated';
|
||
const canSubmit = Boolean(prompt.trim()) && !isGenerating && !isGenerated;
|
||
|
||
return (
|
||
<div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-[radial-gradient(circle_at_30%_15%,rgba(251,191,36,0.22),transparent_30%),linear-gradient(135deg,#0f172a,#111827_46%,#1e1b4b)] px-4 py-8 text-white">
|
||
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.045)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.04)_1px,transparent_1px)] bg-[length:38px_38px] opacity-30" />
|
||
<section className="relative flex w-full max-w-[34rem] flex-col items-center gap-5 text-center">
|
||
<div className="grid h-14 w-14 place-items-center rounded-[1.2rem] border border-amber-200/32 bg-amber-200/14 text-amber-100 shadow-[0_18px_48px_rgba(251,191,36,0.18)]">
|
||
{isGenerating ? (
|
||
<Loader2 className="h-6 w-6 animate-spin" />
|
||
) : (
|
||
<Sparkles className="h-6 w-6" />
|
||
)}
|
||
</div>
|
||
<h1 className="text-[2rem] font-black leading-tight sm:text-[2.85rem]">
|
||
{PUZZLE_ONBOARDING_COPY}
|
||
</h1>
|
||
<form
|
||
className="flex w-full flex-col gap-3"
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
onSubmit();
|
||
}}
|
||
>
|
||
<textarea
|
||
value={prompt}
|
||
disabled={isGenerating || isGenerated}
|
||
onChange={(event) => onPromptChange(event.target.value)}
|
||
placeholder="把你的梦讲给我听吧"
|
||
rows={4}
|
||
className="min-h-32 w-full resize-none rounded-[1.25rem] border border-white/14 bg-black/28 px-4 py-4 text-base font-semibold leading-7 text-white shadow-[0_18px_50px_rgba(0,0,0,0.24)] outline-none backdrop-blur placeholder:text-white/42 focus:border-amber-200/70 focus:ring-2 focus:ring-amber-200/20 disabled:opacity-70"
|
||
/>
|
||
<button
|
||
type="submit"
|
||
disabled={!canSubmit}
|
||
className="inline-flex min-h-12 items-center justify-center gap-2 rounded-[1rem] bg-amber-200 px-5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||
>
|
||
{isGenerating ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
生成
|
||
</>
|
||
) : (
|
||
'生成'
|
||
)}
|
||
</button>
|
||
</form>
|
||
{error ? (
|
||
<div className="w-full rounded-[1rem] border border-red-300/30 bg-red-500/14 px-4 py-3 text-sm font-semibold text-red-50">
|
||
{error}
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PuzzleOnboardingLoginOverlay({
|
||
isSaving,
|
||
error,
|
||
onLogin,
|
||
}: {
|
||
isSaving: boolean;
|
||
error: string | null;
|
||
onLogin: () => void;
|
||
}) {
|
||
return (
|
||
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-slate-950/72 px-4 py-6 text-white backdrop-blur-md">
|
||
<section className="flex w-full max-w-[24rem] flex-col items-center gap-5 rounded-[1.35rem] border border-white/14 bg-slate-950/94 px-5 py-6 text-center shadow-[0_28px_90px_rgba(0,0,0,0.5)]">
|
||
<div className="grid h-12 w-12 place-items-center rounded-[1rem] bg-amber-200 text-slate-950">
|
||
{isSaving ? (
|
||
<Loader2 className="h-5 w-5 animate-spin" />
|
||
) : (
|
||
<Sparkles className="h-5 w-5" />
|
||
)}
|
||
</div>
|
||
<h2 className="text-2xl font-black leading-tight">
|
||
{PUZZLE_ONBOARDING_CLEAR_COPY}
|
||
</h2>
|
||
<button
|
||
type="button"
|
||
disabled={isSaving}
|
||
onClick={onLogin}
|
||
className="inline-flex min-h-12 w-full items-center justify-center gap-2 rounded-[1rem] bg-amber-200 px-5 text-sm font-black text-slate-950 transition hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-45"
|
||
>
|
||
{isSaving ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
注册账号 / 登录
|
||
</>
|
||
) : (
|
||
'注册账号 / 登录'
|
||
)}
|
||
</button>
|
||
{error ? (
|
||
<div className="w-full rounded-[1rem] border border-red-300/30 bg-red-500/14 px-4 py-3 text-sm font-semibold text-red-50">
|
||
{error}
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function mergeBigFishWorkSummary(
|
||
current: BigFishWorkSummary,
|
||
updated: BigFishWorkSummary,
|
||
): BigFishWorkSummary {
|
||
return current.sourceSessionId === updated.sourceSessionId
|
||
? updated
|
||
: current;
|
||
}
|
||
|
||
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 readProfileTextField(
|
||
profile: CustomWorldProfile | null,
|
||
paths: string[],
|
||
) {
|
||
for (const path of paths) {
|
||
let current: unknown = profile;
|
||
for (const segment of path.split('.')) {
|
||
if (!current || typeof current !== 'object') {
|
||
current = null;
|
||
break;
|
||
}
|
||
current = (current as Record<string, unknown>)[segment];
|
||
}
|
||
if (typeof current === 'string' && current.trim()) {
|
||
return current.trim();
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function hasProfileTextArray(profile: CustomWorldProfile | null, key: string) {
|
||
const value = profile
|
||
? (profile as unknown as Record<string, unknown>)[key]
|
||
: null;
|
||
return Array.isArray(value)
|
||
? value.some((entry) => typeof entry === 'string' && entry.trim())
|
||
: false;
|
||
}
|
||
|
||
function hasProfileArray(profile: CustomWorldProfile | null, key: string) {
|
||
const value = profile
|
||
? (profile as unknown as Record<string, unknown>)[key]
|
||
: null;
|
||
return Array.isArray(value) && value.length > 0;
|
||
}
|
||
|
||
function hasSceneAct(profile: CustomWorldProfile | null) {
|
||
const rawProfile = profile as unknown as Record<string, unknown> | null;
|
||
const chapters =
|
||
rawProfile &&
|
||
(Array.isArray(rawProfile.sceneChapterBlueprints)
|
||
? rawProfile.sceneChapterBlueprints
|
||
: Array.isArray(rawProfile.sceneChapters)
|
||
? rawProfile.sceneChapters
|
||
: []);
|
||
return Array.isArray(chapters)
|
||
? chapters.some((chapter) => {
|
||
const acts =
|
||
chapter && typeof chapter === 'object'
|
||
? (chapter as Record<string, unknown>).acts
|
||
: null;
|
||
return Array.isArray(acts) && acts.length > 0;
|
||
})
|
||
: false;
|
||
}
|
||
|
||
function isAgentResultStructuralBlockerResolved(
|
||
profile: CustomWorldProfile,
|
||
code: string | undefined,
|
||
) {
|
||
if (!code || !AGENT_RESULT_STRUCTURAL_BLOCKER_CODES.has(code)) {
|
||
return false;
|
||
}
|
||
|
||
if (code === 'publish_missing_world_hook') {
|
||
return Boolean(
|
||
readProfileTextField(profile, [
|
||
'worldHook',
|
||
'creatorIntent.worldHook',
|
||
'anchorContent.worldPromise',
|
||
'anchorContent.worldPromise.hook',
|
||
'settingText',
|
||
]),
|
||
);
|
||
}
|
||
if (code === 'publish_missing_player_premise') {
|
||
return Boolean(
|
||
readProfileTextField(profile, [
|
||
'playerPremise',
|
||
'creatorIntent.playerPremise',
|
||
'anchorContent.playerEntryPoint',
|
||
'anchorContent.playerEntryPoint.openingIdentity',
|
||
'anchorContent.playerEntryPoint.openingProblem',
|
||
'anchorContent.playerEntryPoint.entryMotivation',
|
||
]),
|
||
);
|
||
}
|
||
if (code === 'publish_missing_core_conflict') {
|
||
return hasProfileTextArray(profile, 'coreConflicts');
|
||
}
|
||
if (code === 'publish_missing_main_chapter') {
|
||
return (
|
||
hasProfileArray(profile, 'chapters') ||
|
||
hasProfileArray(profile, 'sceneChapterBlueprints') ||
|
||
hasProfileArray(profile, 'sceneChapters')
|
||
);
|
||
}
|
||
return hasSceneAct(profile);
|
||
}
|
||
|
||
function buildAgentResultPublishGateView(
|
||
profile: CustomWorldProfile | null,
|
||
fallbackBlockers: AgentResultBlockerView[],
|
||
fallbackPublishReady: boolean,
|
||
): AgentResultPublishGateView {
|
||
if (!profile) {
|
||
return {
|
||
blockers: fallbackBlockers.map((entry) => entry.message),
|
||
publishReady: fallbackPublishReady,
|
||
};
|
||
}
|
||
|
||
const blockers = fallbackBlockers
|
||
.filter(
|
||
(entry) => !isAgentResultStructuralBlockerResolved(profile, entry.code),
|
||
)
|
||
.map((entry) => entry.message);
|
||
|
||
return {
|
||
blockers,
|
||
publishReady: blockers.length === 0,
|
||
};
|
||
}
|
||
|
||
function buildPuzzleResultProfileId(sessionId: string | null | undefined) {
|
||
const normalizedSessionId = sessionId?.trim();
|
||
if (!normalizedSessionId) {
|
||
return null;
|
||
}
|
||
const stableSuffix = normalizedSessionId.startsWith('puzzle-session-')
|
||
? normalizedSessionId.slice('puzzle-session-'.length)
|
||
: normalizedSessionId;
|
||
return `puzzle-profile-${stableSuffix}`;
|
||
}
|
||
|
||
function buildPuzzleCompileActionFromFormPayload(
|
||
payload: CreatePuzzleAgentSessionRequest | null,
|
||
): PuzzleAgentActionRequest {
|
||
const pictureDescription =
|
||
payload?.pictureDescription?.trim() || payload?.seedText?.trim();
|
||
|
||
return {
|
||
action: 'compile_puzzle_draft',
|
||
promptText: pictureDescription,
|
||
...(pictureDescription ? { pictureDescription } : {}),
|
||
referenceImageSrc: payload?.referenceImageSrc || null,
|
||
imageModel: payload?.imageModel ?? null,
|
||
candidateCount: 1,
|
||
};
|
||
}
|
||
|
||
function buildPuzzleFormPayloadFromSession(
|
||
session: PuzzleAgentSessionSnapshot,
|
||
): CreatePuzzleAgentSessionRequest {
|
||
const formDraft = session.draft?.formDraft;
|
||
const pictureDescription =
|
||
formDraft?.pictureDescription?.trim() ||
|
||
session.draft?.levels?.[0]?.pictureDescription?.trim() ||
|
||
session.anchorPack.visualSubject.value.trim() ||
|
||
session.seedText?.trim() ||
|
||
'';
|
||
|
||
return {
|
||
seedText: pictureDescription,
|
||
pictureDescription,
|
||
referenceImageSrc: null,
|
||
imageModel: null,
|
||
};
|
||
}
|
||
|
||
function buildPuzzleFormPayloadFromAction(
|
||
payload: PuzzleAgentActionRequest,
|
||
): CreatePuzzleAgentSessionRequest | null {
|
||
if (
|
||
payload.action !== 'compile_puzzle_draft' &&
|
||
payload.action !== 'save_puzzle_form_draft'
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
const workTitle = payload.workTitle?.trim() ?? '';
|
||
const workDescription = payload.workDescription?.trim() ?? '';
|
||
const pictureDescription =
|
||
payload.pictureDescription?.trim() || payload.promptText?.trim() || '';
|
||
|
||
return {
|
||
seedText: pictureDescription,
|
||
...(workTitle ? { workTitle } : {}),
|
||
...(workDescription ? { workDescription } : {}),
|
||
pictureDescription,
|
||
referenceImageSrc:
|
||
payload.action === 'compile_puzzle_draft'
|
||
? (payload.referenceImageSrc ?? null)
|
||
: null,
|
||
imageModel:
|
||
payload.action === 'compile_puzzle_draft'
|
||
? (payload.imageModel ?? null)
|
||
: null,
|
||
};
|
||
}
|
||
|
||
function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) {
|
||
return Boolean(
|
||
session?.stage === 'collecting_anchors' && session.draft?.formDraft,
|
||
);
|
||
}
|
||
|
||
function isEmptyPuzzleFormOnlyDraft(
|
||
session: PuzzleAgentSessionSnapshot | null,
|
||
) {
|
||
if (!isPuzzleFormOnlyDraft(session)) {
|
||
return false;
|
||
}
|
||
|
||
const formDraft = session?.draft?.formDraft;
|
||
return !(
|
||
session?.seedText?.trim() ||
|
||
formDraft?.workTitle?.trim() ||
|
||
formDraft?.workDescription?.trim() ||
|
||
formDraft?.pictureDescription?.trim()
|
||
);
|
||
}
|
||
|
||
const CustomWorldGenerationView = lazy(async () => {
|
||
const module = await import('../CustomWorldGenerationView');
|
||
return {
|
||
default: module.CustomWorldGenerationView,
|
||
};
|
||
});
|
||
|
||
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 Match3DAgentWorkspace = lazy(async () => {
|
||
const module = await import('../match3d-creation/Match3DAgentWorkspace');
|
||
return {
|
||
default: module.Match3DAgentWorkspace,
|
||
};
|
||
});
|
||
|
||
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 CustomWorldCreationHub = lazy(async () => {
|
||
const module = await import('../custom-world-home/CustomWorldCreationHub');
|
||
return {
|
||
default: module.CustomWorldCreationHub,
|
||
};
|
||
});
|
||
|
||
const PuzzleAgentWorkspace = lazy(async () => {
|
||
const module = await import('../puzzle-agent/PuzzleAgentWorkspace');
|
||
return {
|
||
default: module.PuzzleAgentWorkspace,
|
||
};
|
||
});
|
||
|
||
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">
|
||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||
{label}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function mergePuzzleServiceRuntimeState(
|
||
currentRun: PuzzleRunSnapshot,
|
||
serviceRun: PuzzleRunSnapshot,
|
||
): PuzzleRunSnapshot {
|
||
if (!currentRun.currentLevel || !serviceRun.currentLevel) {
|
||
return currentRun;
|
||
}
|
||
|
||
const serviceLevel = serviceRun.currentLevel;
|
||
const leaderboardEntries =
|
||
serviceLevel.leaderboardEntries.length > 0
|
||
? serviceLevel.leaderboardEntries
|
||
: serviceRun.leaderboardEntries;
|
||
|
||
// 中文注释:拼块布局和通关状态由前端即时裁决;后端快照只合并榜单与下一关 handoff。
|
||
return {
|
||
...currentRun,
|
||
runId: serviceRun.runId,
|
||
entryProfileId: serviceRun.entryProfileId,
|
||
clearedLevelCount: Math.max(
|
||
currentRun.clearedLevelCount,
|
||
serviceRun.clearedLevelCount,
|
||
),
|
||
recommendedNextProfileId: serviceRun.recommendedNextProfileId,
|
||
nextLevelMode: serviceRun.nextLevelMode,
|
||
nextLevelProfileId: serviceRun.nextLevelProfileId,
|
||
nextLevelId: serviceRun.nextLevelId,
|
||
recommendedNextWorks: serviceRun.recommendedNextWorks,
|
||
leaderboardEntries,
|
||
currentLevel: {
|
||
...currentRun.currentLevel,
|
||
leaderboardEntries:
|
||
leaderboardEntries.length > 0
|
||
? leaderboardEntries
|
||
: currentRun.currentLevel.leaderboardEntries,
|
||
},
|
||
};
|
||
}
|
||
|
||
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 [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
|
||
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 [match3dRun, setMatch3DRun] = useState<Match3DRunSnapshot | null>(null);
|
||
const [match3dRuntimeReturnStage, setMatch3DRuntimeReturnStage] = useState<
|
||
'match3d-result' | 'work-detail'
|
||
>('match3d-result');
|
||
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 [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 [, setPuzzleOperation] = useState<PuzzleAgentOperationRecord | 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 [isPuzzleLeaderboardBusy, setIsPuzzleLeaderboardBusy] = useState(false);
|
||
const submittedPuzzleLeaderboardKeysRef = useRef(new Set<string>());
|
||
const [puzzleRun, setPuzzleRun] = useState<PuzzleRunSnapshot | null>(null);
|
||
const puzzleRunRef = useRef<PuzzleRunSnapshot | null>(null);
|
||
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 [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 [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 [
|
||
claimingPuzzlePointIncentiveProfileId,
|
||
setClaimingPuzzlePointIncentiveProfileId,
|
||
] = useState<string | null>(null);
|
||
const [publishSharePayload, setPublishSharePayload] =
|
||
useState<PublishShareModalPayload | null>(null);
|
||
const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish');
|
||
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 platformBootstrap = usePlatformEntryBootstrap({
|
||
user: authUi?.user,
|
||
canAccessProtectedData: authUi?.canAccessProtectedData,
|
||
getProfileDashboard: getPlatformProfileDashboard,
|
||
handleContinueGame,
|
||
hasInitialAgentSession,
|
||
});
|
||
const entryNavigation = usePlatformEntryNavigation({
|
||
setSelectionStage,
|
||
setSelectedDetailEntry,
|
||
});
|
||
const { setPlatformTab } = platformBootstrap;
|
||
|
||
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 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 refreshBigFishShelf = useCallback(async () => {
|
||
setIsBigFishLoadingLibrary(true);
|
||
|
||
try {
|
||
const worksResponse = await listBigFishWorks();
|
||
setBigFishWorks(worksResponse.items);
|
||
setBigFishError(null);
|
||
} catch (error) {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
|
||
);
|
||
} finally {
|
||
setIsBigFishLoadingLibrary(false);
|
||
}
|
||
}, [resolveBigFishErrorMessage]);
|
||
|
||
const refreshBigFishGallery = useCallback(async () => {
|
||
try {
|
||
const galleryResponse = await listBigFishGallery();
|
||
setBigFishGalleryEntries(galleryResponse.items);
|
||
return galleryResponse.items;
|
||
} catch (error) {
|
||
setBigFishGalleryEntries([]);
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼广场失败。'),
|
||
);
|
||
return [];
|
||
}
|
||
}, [resolveBigFishErrorMessage]);
|
||
|
||
const refreshMatch3DShelf = useCallback(async () => {
|
||
setIsMatch3DLoadingLibrary(true);
|
||
|
||
try {
|
||
const worksResponse = await listMatch3DWorks();
|
||
setMatch3DWorks(worksResponse.items);
|
||
setMatch3DError(null);
|
||
} catch (error) {
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(error, '读取抓大鹅作品列表失败。'),
|
||
);
|
||
} finally {
|
||
setIsMatch3DLoadingLibrary(false);
|
||
}
|
||
}, [resolveMatch3DErrorMessage]);
|
||
|
||
const refreshMatch3DGallery = useCallback(async () => {
|
||
try {
|
||
const galleryResponse = await listMatch3DGallery();
|
||
setMatch3DGalleryEntries(galleryResponse.items);
|
||
return galleryResponse.items;
|
||
} catch {
|
||
// 中文注释:公开广场是首页展示数据,失败时只降级为空列表;
|
||
// 不写入创作错误态,避免挡住抓大鹅共创入口。
|
||
setMatch3DGalleryEntries([]);
|
||
return [];
|
||
}
|
||
}, []);
|
||
|
||
const refreshSquareHoleShelf = useCallback(async () => {
|
||
setIsSquareHoleLoadingLibrary(true);
|
||
|
||
try {
|
||
const worksResponse = await listSquareHoleWorks();
|
||
setSquareHoleWorks(worksResponse.items);
|
||
setSquareHoleError(null);
|
||
} catch (error) {
|
||
setSquareHoleError(
|
||
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 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([]);
|
||
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图广场失败。'));
|
||
return [];
|
||
}
|
||
}, [resolvePuzzleErrorMessage]);
|
||
|
||
const sessionController = useRpgCreationSessionController({
|
||
userId: authUi?.user?.id,
|
||
openLoginModal: authUi?.openLoginModal,
|
||
selectionStage,
|
||
setSelectionStage,
|
||
enterCreateTab,
|
||
onSessionOpened: () => {
|
||
setShowCreationTypeModal(false);
|
||
},
|
||
});
|
||
|
||
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,
|
||
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,
|
||
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(),
|
||
]);
|
||
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(
|
||
() =>
|
||
buildAgentResultPublishGateView(
|
||
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 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(() => {
|
||
if (!agentResultPreview?.source) {
|
||
return null;
|
||
}
|
||
if (agentResultPreview.source === 'published_profile') {
|
||
return '已发布世界';
|
||
}
|
||
if (agentResultPreview.source === 'session_preview') {
|
||
return '会话预览';
|
||
}
|
||
return '服务端预览';
|
||
}, [agentResultPreview]);
|
||
|
||
const featuredGalleryEntries = useMemo(() => {
|
||
const bigFishPublicEntries = isBigFishCreationVisible
|
||
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
|
||
: [];
|
||
const match3dPublicEntries = match3dGalleryEntries.map(
|
||
mapMatch3DWorkToPlatformGalleryCard,
|
||
);
|
||
const puzzlePublicEntries = puzzleGalleryEntries.map(
|
||
mapPuzzleWorkToPlatformGalleryCard,
|
||
);
|
||
const squareHolePublicEntries = squareHoleGalleryEntries.map(
|
||
mapSquareHoleWorkToPlatformGalleryCard,
|
||
);
|
||
return mergePlatformPublicGalleryEntries(
|
||
platformBootstrap.publishedGalleryEntries,
|
||
[
|
||
...bigFishPublicEntries,
|
||
...match3dPublicEntries,
|
||
...puzzlePublicEntries,
|
||
...squareHolePublicEntries,
|
||
],
|
||
).slice(0, 6);
|
||
}, [
|
||
isBigFishCreationVisible,
|
||
bigFishGalleryEntries,
|
||
match3dGalleryEntries,
|
||
platformBootstrap.publishedGalleryEntries,
|
||
puzzleGalleryEntries,
|
||
squareHoleGalleryEntries,
|
||
]);
|
||
const latestGalleryEntries = useMemo(
|
||
() =>
|
||
mergePlatformPublicGalleryEntries(
|
||
platformBootstrap.publishedGalleryEntries,
|
||
[
|
||
...(isBigFishCreationVisible
|
||
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
|
||
: []),
|
||
...match3dGalleryEntries.map(mapMatch3DWorkToPlatformGalleryCard),
|
||
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
|
||
...squareHoleGalleryEntries.map(
|
||
mapSquareHoleWorkToPlatformGalleryCard,
|
||
),
|
||
],
|
||
),
|
||
[
|
||
isBigFishCreationVisible,
|
||
bigFishGalleryEntries,
|
||
match3dGalleryEntries,
|
||
platformBootstrap.publishedGalleryEntries,
|
||
puzzleGalleryEntries,
|
||
squareHoleGalleryEntries,
|
||
],
|
||
);
|
||
|
||
const creationHubItems =
|
||
platformBootstrap.customWorldWorkEntries.length > 0
|
||
? platformBootstrap.customWorldWorkEntries
|
||
: buildCreationHubFallbackItems(
|
||
platformBootstrap.savedCustomWorldEntries,
|
||
);
|
||
const resultViewError =
|
||
autosaveCoordinator.customWorldAutoSaveError ??
|
||
sessionController.customWorldError;
|
||
const isSelectedPublicWorkOwned = Boolean(
|
||
authUi?.user?.id &&
|
||
selectedPublicWorkDetail?.ownerUserId === authUi.user.id,
|
||
);
|
||
const selectedPublicWorkActionMode = isSelectedPublicWorkOwned
|
||
? 'edit'
|
||
: 'remix';
|
||
|
||
useEffect(() => {
|
||
if (
|
||
selectionStage === 'custom-world-result' &&
|
||
!sessionController.generatedCustomWorldProfile
|
||
) {
|
||
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
|
||
}
|
||
}, [
|
||
selectedDetailEntry,
|
||
selectionStage,
|
||
sessionController.generatedCustomWorldProfile,
|
||
setSelectionStage,
|
||
]);
|
||
|
||
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);
|
||
setPuzzleOnboardingDraft(null);
|
||
setPuzzleOnboardingPrompt('');
|
||
setPuzzleOnboardingPhase('input');
|
||
platformBootstrap.setPlatformTab('home');
|
||
setSelectionStage('platform');
|
||
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]);
|
||
|
||
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 {
|
||
const response = await generatePuzzleOnboardingWork({ promptText });
|
||
const item: PuzzleWorkSummary = {
|
||
...response.item,
|
||
levels:
|
||
response.item.levels && response.item.levels.length > 0
|
||
? response.item.levels
|
||
: [response.level],
|
||
};
|
||
setPuzzleOnboardingDraft({ promptText, item });
|
||
setSelectedPuzzleDetail(item);
|
||
setPuzzleOnboardingPhase('generated');
|
||
markPuzzleOnboardingSeen();
|
||
window.setTimeout(() => {
|
||
setPuzzleRun(startLocalPuzzleRun(item));
|
||
setPuzzleRuntimeReturnStage('platform');
|
||
setSelectionStage('puzzle-runtime');
|
||
}, 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();
|
||
}
|
||
|
||
sessionController.setCreationTypeError(null);
|
||
return true;
|
||
}, [handleStartNewGame, hasSavedGame, sessionController]);
|
||
|
||
const openCreationTypePicker = 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);
|
||
},
|
||
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
|
||
? {
|
||
...current,
|
||
phase: 'ready',
|
||
completedAssetCount: response.session.assetSlots.filter(
|
||
(slot) => slot.status === 'ready',
|
||
).length,
|
||
totalAssetCount: response.session.assetSlots.length,
|
||
}
|
||
: current,
|
||
);
|
||
},
|
||
beforeExecuteAction: ({ payload }) => {
|
||
if (payload.action !== 'big_fish_compile_draft') {
|
||
return;
|
||
}
|
||
setSelectionStage('big-fish-generating');
|
||
setBigFishGenerationState(createMiniGameDraftGenerationState('big-fish'));
|
||
},
|
||
onActionError: ({ payload, errorMessage }) => {
|
||
if (payload.action !== 'big_fish_compile_draft') {
|
||
return;
|
||
}
|
||
setBigFishGenerationState((current) =>
|
||
current
|
||
? {
|
||
...current,
|
||
phase: 'failed',
|
||
error: errorMessage,
|
||
}
|
||
: current,
|
||
);
|
||
},
|
||
});
|
||
|
||
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);
|
||
},
|
||
onActionComplete: async ({ payload, response, setSession }) => {
|
||
setSession(response.session);
|
||
if (payload.action !== 'match3d_compile_draft') {
|
||
return;
|
||
}
|
||
|
||
const profileId = response.session.draft?.profileId;
|
||
if (!profileId) {
|
||
setMatch3DProfile(null);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { item } = await getMatch3DWorkDetail(profileId);
|
||
setMatch3DProfile(item);
|
||
await refreshMatch3DShelf().catch(() => undefined);
|
||
} catch {
|
||
setMatch3DProfile(buildMatch3DProfileFromSession(response.session));
|
||
}
|
||
},
|
||
});
|
||
|
||
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);
|
||
},
|
||
beforeExecuteAction: ({ payload }) => {
|
||
if (payload.action === 'square_hole_compile_draft') {
|
||
setSquareHoleGenerationState(
|
||
createMiniGameDraftGenerationState('square-hole'),
|
||
);
|
||
setSelectionStage('square-hole-generating');
|
||
}
|
||
if (payload.action === 'square_hole_generate_visual_assets') {
|
||
setSquareHoleGenerationState((current) => ({
|
||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||
phase: 'square-hole-cover',
|
||
completedAssetCount: 0,
|
||
totalAssetCount: 0,
|
||
error: null,
|
||
}));
|
||
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 profileId = response.session.draft?.profileId;
|
||
if (!profileId) {
|
||
setSquareHoleProfile(null);
|
||
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) {
|
||
setSquareHoleProfile(
|
||
buildSquareHoleProfileFromSession(assetResponse.session),
|
||
);
|
||
setSelectionStage('square-hole-result');
|
||
return;
|
||
}
|
||
const { item } = await getSquareHoleWorkDetail(assetProfileId);
|
||
setSquareHoleProfile(item);
|
||
setSquareHoleGenerationState((current) => ({
|
||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||
phase: 'ready',
|
||
completedAssetCount:
|
||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||
totalAssetCount:
|
||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||
error: null,
|
||
}));
|
||
await refreshSquareHoleShelf().catch(() => undefined);
|
||
setSelectionStage('square-hole-result');
|
||
} catch (error) {
|
||
const errorMessage = resolveSquareHoleErrorMessage(
|
||
error,
|
||
'生成方洞挑战图片失败。',
|
||
);
|
||
setSquareHoleError(errorMessage);
|
||
setSquareHoleGenerationState((current) => ({
|
||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||
phase: 'failed',
|
||
error: errorMessage,
|
||
}));
|
||
setSquareHoleProfile(
|
||
buildSquareHoleProfileFromSession(response.session),
|
||
);
|
||
setSelectionStage('square-hole-result');
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { item } = await getSquareHoleWorkDetail(profileId);
|
||
setSquareHoleProfile(item);
|
||
setSquareHoleGenerationState((current) => ({
|
||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||
phase: 'ready',
|
||
completedAssetCount:
|
||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||
totalAssetCount:
|
||
item.shapeOptions.length + item.holeOptions.length + 2,
|
||
error: null,
|
||
}));
|
||
await refreshSquareHoleShelf().catch(() => undefined);
|
||
setSelectionStage('square-hole-result');
|
||
} catch {
|
||
setSquareHoleProfile(
|
||
buildSquareHoleProfileFromSession(response.session),
|
||
);
|
||
setSelectionStage('square-hole-result');
|
||
}
|
||
},
|
||
onActionError: ({ payload, errorMessage }) => {
|
||
if (
|
||
payload.action === 'square_hole_compile_draft' ||
|
||
payload.action === 'square_hole_generate_visual_assets'
|
||
) {
|
||
setSquareHoleGenerationState((current) => ({
|
||
...(current ?? createMiniGameDraftGenerationState('square-hole')),
|
||
phase: 'failed',
|
||
error: errorMessage,
|
||
}));
|
||
setSelectionStage('square-hole-generating');
|
||
}
|
||
},
|
||
});
|
||
|
||
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);
|
||
},
|
||
onOpenError: ({ errorMessage }) => {
|
||
sessionController.setCreationTypeError(errorMessage);
|
||
setPuzzleCreationError(errorMessage);
|
||
},
|
||
onActionComplete: async ({ payload, response, 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'
|
||
) {
|
||
await Promise.allSettled([
|
||
refreshPuzzleShelf(),
|
||
refreshPuzzleGallery(),
|
||
]);
|
||
}
|
||
|
||
if (payload.action === 'compile_puzzle_draft') {
|
||
setPuzzleGenerationState((current) =>
|
||
current
|
||
? {
|
||
...current,
|
||
phase: 'ready',
|
||
completedAssetCount: 1,
|
||
totalAssetCount: 1,
|
||
}
|
||
: current,
|
||
);
|
||
}
|
||
|
||
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: ({ payload }) => {
|
||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||
if (formPayload) {
|
||
setPuzzleFormDraftPayload(formPayload);
|
||
}
|
||
|
||
if (payload.action !== 'compile_puzzle_draft') {
|
||
return;
|
||
}
|
||
setSelectionStage('puzzle-generating');
|
||
setPuzzleGenerationState(createMiniGameDraftGenerationState('puzzle'));
|
||
},
|
||
onActionError: ({ payload, errorMessage }) => {
|
||
if (payload.action !== 'compile_puzzle_draft') {
|
||
return;
|
||
}
|
||
setPuzzleGenerationState((current) =>
|
||
current
|
||
? {
|
||
...current,
|
||
phase: 'failed',
|
||
error: errorMessage,
|
||
}
|
||
: current,
|
||
);
|
||
},
|
||
});
|
||
|
||
const bigFishSession = bigFishFlow.session;
|
||
const bigFishError = bigFishFlow.error;
|
||
const setBigFishError = bigFishFlow.setError;
|
||
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;
|
||
const isMatch3DBusy = match3dFlow.isBusy;
|
||
const streamingMatch3DReplyText = match3dFlow.streamingReplyText;
|
||
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;
|
||
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 setPuzzleError = puzzleFlow.setError;
|
||
const isPuzzleBusy = puzzleFlow.isBusy;
|
||
const setIsPuzzleBusy = puzzleFlow.setIsBusy;
|
||
const isStreamingPuzzleReply = puzzleFlow.isStreamingReply;
|
||
const resetRpgSessionViewState = sessionController.resetSessionViewState;
|
||
const setRpgGeneratedCustomWorldProfile =
|
||
sessionController.setGeneratedCustomWorldProfile;
|
||
const setRpgCustomWorldError = sessionController.setCustomWorldError;
|
||
const persistRpgAgentUiState = sessionController.persistAgentUiState;
|
||
const resetAutoSaveTrackingToIdle =
|
||
autosaveCoordinator.resetAutoSaveTrackingToIdle;
|
||
|
||
useEffect(() => {
|
||
puzzleRunRef.current = puzzleRun;
|
||
}, [puzzleRun]);
|
||
|
||
const openBigFishAgentWorkspace = useCallback(async () => {
|
||
setBigFishRun(null);
|
||
await bigFishFlow.openWorkspace();
|
||
}, [bigFishFlow]);
|
||
|
||
const openMatch3DAgentWorkspace = useCallback(async () => {
|
||
setMatch3DSession(null);
|
||
setMatch3DProfile(null);
|
||
setMatch3DRun(null);
|
||
setMatch3DError(null);
|
||
setStreamingMatch3DReplyText('');
|
||
setIsStreamingMatch3DReply(false);
|
||
await match3dFlow.openWorkspace();
|
||
}, [
|
||
match3dFlow,
|
||
setIsStreamingMatch3DReply,
|
||
setMatch3DError,
|
||
setMatch3DProfile,
|
||
setMatch3DRun,
|
||
setMatch3DSession,
|
||
setStreamingMatch3DReplyText,
|
||
]);
|
||
|
||
const openSquareHoleAgentWorkspace = useCallback(async () => {
|
||
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,
|
||
]);
|
||
|
||
const openPuzzleAgentWorkspace = useCallback(async () => {
|
||
setPuzzleRun(null);
|
||
setPuzzleOperation(null);
|
||
setPuzzleGenerationState(null);
|
||
setPuzzleFormDraftPayload(null);
|
||
sessionController.setCreationTypeError(null);
|
||
setPuzzleCreationError(null);
|
||
const nextSession = await puzzleFlow.openWorkspace({});
|
||
if (nextSession) {
|
||
void refreshPuzzleShelf();
|
||
}
|
||
}, [puzzleFlow, refreshPuzzleShelf]);
|
||
|
||
const createPuzzleDraftFromForm = useCallback(
|
||
async (payload: CreatePuzzleAgentSessionRequest) => {
|
||
setPuzzleFormDraftPayload(payload);
|
||
const nextSession =
|
||
puzzleFlow.session && !isEmptyPuzzleFormOnlyDraft(puzzleFlow.session)
|
||
? puzzleFlow.session
|
||
: await puzzleFlow.openWorkspace(payload);
|
||
if (!nextSession) {
|
||
return;
|
||
}
|
||
|
||
await puzzleFlow.executeAction(
|
||
buildPuzzleCompileActionFromFormPayload(payload),
|
||
nextSession,
|
||
);
|
||
},
|
||
[puzzleFlow],
|
||
);
|
||
|
||
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,
|
||
pictureDescription: payload.pictureDescription ?? '',
|
||
imageModel: payload.imageModel ?? null,
|
||
});
|
||
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);
|
||
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);
|
||
setPuzzleOperation(null);
|
||
setPuzzleWorks([]);
|
||
setSelectedPuzzleDetail(null);
|
||
setPuzzleRuntimeReturnStage('puzzle-gallery-detail');
|
||
setPuzzleRun(null);
|
||
setPuzzleGenerationState(null);
|
||
setIsPuzzleNextLevelGenerating(false);
|
||
setPuzzleShelfError(null);
|
||
setPuzzleCreationError(null);
|
||
setPuzzleError(null);
|
||
setDeletingCreationWorkId(null);
|
||
setClaimingPuzzlePointIncentiveProfileId(null);
|
||
setPublishSharePayload(null);
|
||
setProfilePlayStats(null);
|
||
setProfilePlayStatsError(null);
|
||
setIsProfilePlayStatsOpen(false);
|
||
resetRpgSessionViewState();
|
||
setRpgGeneratedCustomWorldProfile(null);
|
||
setRpgCustomWorldError(null);
|
||
persistRpgAgentUiState(null, null);
|
||
resetAutoSaveTrackingToIdle();
|
||
|
||
if (
|
||
selectionStage !== 'platform' &&
|
||
selectionStage !== 'work-detail' &&
|
||
selectionStage !== 'detail' &&
|
||
selectionStage !== 'puzzle-gallery-detail'
|
||
) {
|
||
setSelectionStage('platform');
|
||
}
|
||
}, [
|
||
authUi?.user,
|
||
platformBootstrap.canReadProtectedData,
|
||
persistRpgAgentUiState,
|
||
resetAutoSaveTrackingToIdle,
|
||
resetRpgSessionViewState,
|
||
selectionStage,
|
||
setBigFishError,
|
||
setIsStreamingMatch3DReply,
|
||
setIsStreamingSquareHoleReply,
|
||
setMatch3DError,
|
||
setMatch3DSession,
|
||
setPuzzleError,
|
||
setRpgCustomWorldError,
|
||
setRpgGeneratedCustomWorldProfile,
|
||
setSelectionStage,
|
||
setSquareHoleError,
|
||
setSquareHoleSession,
|
||
setStreamingMatch3DReplyText,
|
||
setStreamingSquareHoleReplyText,
|
||
]);
|
||
|
||
const handleCreationHubCreateType = useCallback(
|
||
(type: PlatformCreationTypeId) => {
|
||
if (type === 'airp' || type === 'visual-novel') {
|
||
return;
|
||
}
|
||
|
||
if (!prepareCreationLaunch()) {
|
||
return;
|
||
}
|
||
|
||
if (type === 'rpg') {
|
||
runProtectedAction(() => {
|
||
void sessionController.openRpgAgentWorkspace();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'big-fish') {
|
||
runProtectedAction(() => {
|
||
void openBigFishAgentWorkspace();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'match3d') {
|
||
runProtectedAction(() => {
|
||
void openMatch3DAgentWorkspace();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'square-hole') {
|
||
runProtectedAction(() => {
|
||
void openSquareHoleAgentWorkspace();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'puzzle') {
|
||
runProtectedAction(() => {
|
||
void openPuzzleAgentWorkspace();
|
||
});
|
||
}
|
||
},
|
||
[
|
||
openBigFishAgentWorkspace,
|
||
openMatch3DAgentWorkspace,
|
||
openPuzzleAgentWorkspace,
|
||
openSquareHoleAgentWorkspace,
|
||
prepareCreationLaunch,
|
||
runProtectedAction,
|
||
sessionController,
|
||
],
|
||
);
|
||
|
||
const leaveBigFishFlow = useCallback(() => {
|
||
setBigFishRun(null);
|
||
setBigFishRuntimeWork(null);
|
||
setBigFishRuntimeStartedAt(null);
|
||
setBigFishRuntimeReturnStage('platform');
|
||
setBigFishGenerationState(null);
|
||
bigFishFlow.leaveFlow();
|
||
}, [bigFishFlow]);
|
||
|
||
const leaveMatch3DFlow = useCallback(() => {
|
||
setMatch3DRun(null);
|
||
setMatch3DRuntimeReturnStage('match3d-result');
|
||
match3dFlow.leaveFlow();
|
||
}, [match3dFlow]);
|
||
|
||
const leaveSquareHoleFlow = useCallback(() => {
|
||
setSquareHoleRun(null);
|
||
setSquareHoleRuntimeReturnStage('square-hole-result');
|
||
setSquareHoleGenerationState(null);
|
||
squareHoleFlow.leaveFlow();
|
||
}, [squareHoleFlow]);
|
||
|
||
const leavePuzzleFlow = useCallback(() => {
|
||
setPuzzleOperation(null);
|
||
setPuzzleRun(null);
|
||
setPuzzleGenerationState(null);
|
||
setIsPuzzleNextLevelGenerating(false);
|
||
puzzleFlow.leaveFlow();
|
||
}, [puzzleFlow]);
|
||
|
||
const submitBigFishMessage = bigFishFlow.submitMessage;
|
||
|
||
const submitMatch3DMessage = match3dFlow.submitMessage;
|
||
|
||
const submitSquareHoleMessage = squareHoleFlow.submitMessage;
|
||
|
||
const submitPuzzleMessage = puzzleFlow.submitMessage;
|
||
|
||
const executeBigFishAction = bigFishFlow.executeAction;
|
||
|
||
const executeMatch3DAction = match3dFlow.executeAction;
|
||
|
||
const executeSquareHoleAction = squareHoleFlow.executeAction;
|
||
|
||
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 executePuzzleAction = puzzleFlow.executeAction;
|
||
|
||
const retryPuzzleDraftGeneration = useCallback(() => {
|
||
if (puzzleFormDraftPayload) {
|
||
void createPuzzleDraftFromForm(puzzleFormDraftPayload);
|
||
return;
|
||
}
|
||
|
||
void executePuzzleAction(
|
||
buildPuzzleCompileActionFromFormPayload(puzzleFormDraftPayload),
|
||
);
|
||
}, [createPuzzleDraftFromForm, executePuzzleAction, puzzleFormDraftPayload]);
|
||
|
||
const executePuzzleWorkspaceAction = useCallback(
|
||
(payload: PuzzleAgentActionRequest) => {
|
||
if (
|
||
payload.action === 'compile_puzzle_draft' &&
|
||
isEmptyPuzzleFormOnlyDraft(puzzleFlow.session)
|
||
) {
|
||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||
if (formPayload) {
|
||
void createPuzzleDraftFromForm(formPayload);
|
||
return;
|
||
}
|
||
}
|
||
|
||
void executePuzzleAction(payload);
|
||
},
|
||
[createPuzzleDraftFromForm, executePuzzleAction, puzzleFlow.session],
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (selectionStage === 'big-fish-result' && !bigFishSession?.draft) {
|
||
setSelectionStage(
|
||
bigFishSession ? 'big-fish-agent-workspace' : 'platform',
|
||
);
|
||
}
|
||
if (selectionStage === 'big-fish-runtime' && !bigFishRun) {
|
||
setSelectionStage(bigFishSession?.draft ? 'big-fish-result' : 'platform');
|
||
}
|
||
}, [bigFishRun, bigFishSession, selectionStage, setSelectionStage]);
|
||
|
||
useEffect(() => {
|
||
if (selectionStage === 'match3d-result' && !match3dSession?.draft) {
|
||
setSelectionStage(
|
||
match3dSession ? 'match3d-agent-workspace' : 'platform',
|
||
);
|
||
}
|
||
if (selectionStage === 'match3d-runtime' && !match3dRun) {
|
||
setSelectionStage(match3dSession?.draft ? 'match3d-result' : 'platform');
|
||
}
|
||
}, [match3dRun, match3dSession, selectionStage, setSelectionStage]);
|
||
|
||
useEffect(() => {
|
||
if (selectionStage === 'square-hole-result' && !squareHoleSession?.draft) {
|
||
setSelectionStage(
|
||
squareHoleSession ? 'square-hole-agent-workspace' : 'platform',
|
||
);
|
||
}
|
||
if (selectionStage === 'square-hole-runtime' && !squareHoleRun) {
|
||
setSelectionStage(
|
||
squareHoleSession?.draft ? 'square-hole-result' : 'platform',
|
||
);
|
||
}
|
||
}, [selectionStage, setSelectionStage, squareHoleRun, squareHoleSession]);
|
||
|
||
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,
|
||
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,
|
||
setSelectionStage,
|
||
]);
|
||
|
||
const startPuzzleRunFromProfile = useCallback(
|
||
async (
|
||
profileId: string,
|
||
returnStage: PuzzleRuntimeReturnStage = 'work-detail',
|
||
detailItem?: PuzzleWorkSummary,
|
||
mirrorErrorToPublicDetail = false,
|
||
levelId?: string | null,
|
||
) => {
|
||
if (isPuzzleBusy) {
|
||
return;
|
||
}
|
||
|
||
setIsPuzzleBusy(true);
|
||
setPuzzleError(null);
|
||
|
||
try {
|
||
const item =
|
||
detailItem ?? (await getPuzzleGalleryDetail(profileId)).item;
|
||
const { run } = await startPuzzleRun({
|
||
profileId: item.profileId,
|
||
levelId: levelId ?? null,
|
||
});
|
||
setSelectedPuzzleDetail(item);
|
||
setPuzzleRun(run);
|
||
setPuzzleRuntimeReturnStage(returnStage);
|
||
setSelectionStage('puzzle-runtime');
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath(
|
||
'puzzle-runtime',
|
||
buildPuzzlePublicWorkCode(item.profileId),
|
||
),
|
||
);
|
||
} catch (error) {
|
||
const message = resolvePuzzleErrorMessage(error, '启动拼图玩法失败。');
|
||
setPuzzleError(message);
|
||
if (mirrorErrorToPublicDetail) {
|
||
setPublicWorkDetailError(message);
|
||
}
|
||
} finally {
|
||
setIsPuzzleBusy(false);
|
||
}
|
||
},
|
||
[
|
||
isPuzzleBusy,
|
||
resolvePuzzleErrorMessage,
|
||
setIsPuzzleBusy,
|
||
setPuzzleError,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const startMatch3DRunFromProfile = useCallback(
|
||
async (
|
||
profile: Match3DWorkProfile | Match3DWorkSummary,
|
||
returnStage: 'match3d-result' | 'work-detail' = 'match3d-result',
|
||
mirrorErrorToPublicDetail = false,
|
||
) => {
|
||
if (isMatch3DBusy) {
|
||
return;
|
||
}
|
||
|
||
match3dFlow.setIsBusy(true);
|
||
setMatch3DError(null);
|
||
|
||
try {
|
||
const { run } = await startMatch3DRun(profile.profileId);
|
||
setMatch3DRun(run);
|
||
setMatch3DRuntimeReturnStage(returnStage);
|
||
setSelectionStage('match3d-runtime');
|
||
if (profile.publicationStatus === 'published') {
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath(
|
||
'work-detail',
|
||
buildMatch3DPublicWorkCode(profile.profileId),
|
||
),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
const message = resolveMatch3DErrorMessage(
|
||
error,
|
||
'启动抓大鹅玩法失败。',
|
||
);
|
||
setMatch3DError(message);
|
||
if (mirrorErrorToPublicDetail) {
|
||
setPublicWorkDetailError(message);
|
||
}
|
||
} finally {
|
||
match3dFlow.setIsBusy(false);
|
||
}
|
||
},
|
||
[
|
||
isMatch3DBusy,
|
||
match3dFlow,
|
||
resolveMatch3DErrorMessage,
|
||
setMatch3DError,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const startSquareHoleRunFromProfile = useCallback(
|
||
async (
|
||
profile: SquareHoleWorkProfile | SquareHoleWorkSummary,
|
||
returnStage: SquareHoleRuntimeReturnStage = 'square-hole-result',
|
||
mirrorErrorToPublicDetail = false,
|
||
) => {
|
||
if (isSquareHoleBusy) {
|
||
return;
|
||
}
|
||
|
||
squareHoleFlow.setIsBusy(true);
|
||
setSquareHoleError(null);
|
||
|
||
try {
|
||
const { run } = await startSquareHoleRun(profile.profileId);
|
||
setSquareHoleRun(run);
|
||
setSquareHoleRuntimeReturnStage(returnStage);
|
||
setSelectionStage('square-hole-runtime');
|
||
if (profile.publicationStatus === 'published') {
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath(
|
||
'work-detail',
|
||
buildSquareHolePublicWorkCode(profile.profileId),
|
||
),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
const message = resolveSquareHoleErrorMessage(
|
||
error,
|
||
'启动方洞挑战玩法失败。',
|
||
);
|
||
setSquareHoleError(message);
|
||
if (mirrorErrorToPublicDetail) {
|
||
setPublicWorkDetailError(message);
|
||
}
|
||
} finally {
|
||
squareHoleFlow.setIsBusy(false);
|
||
}
|
||
},
|
||
[
|
||
isSquareHoleBusy,
|
||
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) => {
|
||
if (isPuzzleBusy) {
|
||
return;
|
||
}
|
||
if (!draft.coverImageSrc) {
|
||
setPuzzleError('请先选择一张正式拼图图片。');
|
||
return;
|
||
}
|
||
const profileId =
|
||
puzzleSession?.publishedProfileId ??
|
||
buildPuzzleResultProfileId(puzzleSession?.sessionId);
|
||
if (!profileId) {
|
||
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
|
||
return;
|
||
}
|
||
|
||
setIsPuzzleBusy(true);
|
||
setPuzzleError(null);
|
||
try {
|
||
const { item } = await updatePuzzleWork(profileId, {
|
||
workTitle: draft.workTitle,
|
||
workDescription: draft.workDescription,
|
||
levelName: draft.levelName,
|
||
summary: draft.summary,
|
||
themeTags: draft.themeTags,
|
||
coverImageSrc: draft.coverImageSrc,
|
||
coverAssetId: draft.coverAssetId,
|
||
levels: draft.levels ?? [],
|
||
});
|
||
const run = startLocalPuzzleRun(item);
|
||
setSelectedPuzzleDetail(item);
|
||
setPuzzleRun(run);
|
||
setPuzzleRuntimeReturnStage('puzzle-result');
|
||
setSelectionStage('puzzle-runtime');
|
||
} catch (error) {
|
||
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'));
|
||
} finally {
|
||
setIsPuzzleBusy(false);
|
||
}
|
||
},
|
||
[
|
||
isPuzzleBusy,
|
||
puzzleSession?.publishedProfileId,
|
||
puzzleSession?.sessionId,
|
||
resolvePuzzleErrorMessage,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const submitBigFishInput = useCallback(
|
||
async (payload: SubmitBigFishInputRequest) => {
|
||
if (
|
||
!bigFishRun ||
|
||
bigFishRun.status !== 'running' ||
|
||
bigFishInputInFlightRef.current
|
||
) {
|
||
return;
|
||
}
|
||
|
||
bigFishInputInFlightRef.current = true;
|
||
try {
|
||
const { run } = await submitBigFishRuntimeInput(
|
||
bigFishRun.runId,
|
||
payload,
|
||
);
|
||
setBigFishRun(run);
|
||
} catch (error) {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '同步大鱼吃小鱼输入失败。'),
|
||
);
|
||
} finally {
|
||
bigFishInputInFlightRef.current = false;
|
||
}
|
||
},
|
||
[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);
|
||
void recordBigFishPlay(sessionId, { elapsedMs }).catch((error) => {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
|
||
);
|
||
});
|
||
}, [
|
||
bigFishRun?.sessionId,
|
||
bigFishRuntimeStartedAt,
|
||
resolveBigFishErrorMessage,
|
||
]);
|
||
|
||
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],
|
||
);
|
||
|
||
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],
|
||
);
|
||
|
||
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]);
|
||
|
||
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 { run } = await startPuzzleRun({
|
||
profileId: currentLevel.profileId,
|
||
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
|
||
});
|
||
setSelectedPuzzleDetail(detailItem);
|
||
puzzleRunRef.current = run;
|
||
setPuzzleRun(run);
|
||
} catch (error) {
|
||
setPuzzleError(
|
||
resolvePuzzleErrorMessage(error, '重新开始拼图关卡失败。'),
|
||
);
|
||
} finally {
|
||
setIsPuzzleBusy(false);
|
||
}
|
||
}, [
|
||
isPuzzleBusy,
|
||
puzzleRun,
|
||
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,
|
||
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.gridSize}:${currentLevel.elapsedMs}`;
|
||
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;
|
||
}
|
||
|
||
void submitPuzzleLeaderboard(puzzleRun.runId, payload)
|
||
.then(({ run }) => {
|
||
setPuzzleRun((currentRun) => {
|
||
if (!currentRun) {
|
||
return currentRun;
|
||
}
|
||
return mergePuzzleServiceRuntimeState(currentRun, run);
|
||
});
|
||
void platformBootstrap.refreshSaveArchives();
|
||
})
|
||
.catch((error) => {
|
||
submittedPuzzleLeaderboardKeysRef.current.delete(submitKey);
|
||
setPuzzleError(
|
||
resolvePuzzleErrorMessage(error, '提交拼图排行榜失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setIsPuzzleLeaderboardBusy(false);
|
||
});
|
||
}, [
|
||
authUi?.user?.displayName,
|
||
platformBootstrap,
|
||
puzzleRun,
|
||
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 targetProfileId = _target?.profileId?.trim() ?? '';
|
||
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
||
const itemPromise =
|
||
selectedPuzzleDetail?.profileId === targetProfileId
|
||
? Promise.resolve(selectedPuzzleDetail)
|
||
: getPuzzleGalleryDetail(targetProfileId).then(
|
||
(response) => response.item,
|
||
);
|
||
const [{ run }, item] = await Promise.all([
|
||
advancePuzzleNextLevel(puzzleRun.runId, {
|
||
targetProfileId,
|
||
}),
|
||
itemPromise,
|
||
]);
|
||
setSelectedPuzzleDetail(item);
|
||
setPuzzleRun(run);
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath(
|
||
'puzzle-runtime',
|
||
buildPuzzlePublicWorkCode(item.profileId),
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
const { run } = await advancePuzzleNextLevel(puzzleRun.runId);
|
||
setPuzzleRun(run);
|
||
} catch (error) {
|
||
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
|
||
} finally {
|
||
setIsPuzzleNextLevelGenerating(false);
|
||
setIsPuzzleBusy(false);
|
||
}
|
||
},
|
||
[
|
||
isPuzzleBusy,
|
||
isPuzzleLeaderboardBusy,
|
||
puzzleRun,
|
||
resolvePuzzleErrorMessage,
|
||
selectedPuzzleDetail,
|
||
],
|
||
);
|
||
|
||
const remodelCurrentPuzzleRuntimeWork = useCallback(
|
||
(profileId: string) => {
|
||
const targetProfileId = profileId.trim();
|
||
if (!targetProfileId || isPublicWorkDetailBusy || isPuzzleBusy) {
|
||
return;
|
||
}
|
||
|
||
runProtectedAction(() => {
|
||
setIsPublicWorkDetailBusy(true);
|
||
setIsPuzzleBusy(true);
|
||
setPuzzleError(null);
|
||
setPublicWorkDetailError(null);
|
||
|
||
void remixPuzzleGalleryWork(targetProfileId)
|
||
.then((response) => {
|
||
puzzleFlow.setSession(response.session);
|
||
setPuzzleOperation(null);
|
||
setPuzzleRun(null);
|
||
enterCreateTab();
|
||
setSelectionStage('puzzle-result');
|
||
})
|
||
.catch((error) => {
|
||
setPuzzleError(
|
||
resolvePuzzleErrorMessage(error, '改造拼图作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setIsPublicWorkDetailBusy(false);
|
||
setIsPuzzleBusy(false);
|
||
});
|
||
});
|
||
},
|
||
[
|
||
enterCreateTab,
|
||
isPublicWorkDetailBusy,
|
||
isPuzzleBusy,
|
||
puzzleFlow,
|
||
resolvePuzzleErrorMessage,
|
||
runProtectedAction,
|
||
setIsPuzzleBusy,
|
||
setPuzzleError,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const leaveAgentWorkspace = useCallback(() => {
|
||
enterCreateTab();
|
||
sessionController.resetSessionViewState();
|
||
sessionController.setGeneratedCustomWorldProfile(null);
|
||
autosaveCoordinator.resetAutoSaveTrackingToIdle();
|
||
sessionController.persistAgentUiState(
|
||
sessionController.activeAgentSessionId,
|
||
null,
|
||
);
|
||
setSelectionStage('platform');
|
||
}, [
|
||
autosaveCoordinator,
|
||
enterCreateTab,
|
||
sessionController,
|
||
setSelectionStage,
|
||
]);
|
||
|
||
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);
|
||
enterCreateTab();
|
||
setSelectionStage('platform');
|
||
}, [
|
||
autosaveCoordinator,
|
||
enterCreateTab,
|
||
sessionController,
|
||
setSelectionStage,
|
||
]);
|
||
|
||
const leaveCustomWorldResult = useCallback(() => {
|
||
sessionController.setGeneratedCustomWorldProfile(null);
|
||
sessionController.setCustomWorldError(null);
|
||
autosaveCoordinator.resetAutoSaveTrackingToIdle();
|
||
sessionController.setCustomWorldGenerationViewSource(null);
|
||
sessionController.setCustomWorldResultViewSource(null);
|
||
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
|
||
}, [
|
||
autosaveCoordinator,
|
||
selectedDetailEntry,
|
||
sessionController,
|
||
setSelectionStage,
|
||
]);
|
||
|
||
const handleStartSelectedWorld = useCallback(() => {
|
||
if (!selectedDetailEntry) {
|
||
return;
|
||
}
|
||
|
||
runProtectedAction(() => {
|
||
handleCustomWorldSelect(selectedDetailEntry.profile);
|
||
});
|
||
}, [handleCustomWorldSelect, runProtectedAction, selectedDetailEntry]);
|
||
|
||
const handleDeleteLibraryEntry = useCallback(
|
||
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
|
||
if (!entry.profileId || deletingCreationWorkId) {
|
||
return;
|
||
}
|
||
|
||
requestDeleteCreationWork({
|
||
id: entry.profileId,
|
||
title: entry.worldName,
|
||
detail: '删除后会从你的作品列表和公开广场中移除。',
|
||
run: () => {
|
||
setDeletingCreationWorkId(entry.profileId);
|
||
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;
|
||
}
|
||
|
||
requestDeleteCreationWork({
|
||
id: work.workId,
|
||
title: work.title,
|
||
detail:
|
||
work.status === 'published'
|
||
? '删除后会从你的作品列表和公开广场中移除。'
|
||
: '删除后会从你的作品列表中移除。',
|
||
run: () => {
|
||
setDeletingCreationWorkId(work.workId);
|
||
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 () => {
|
||
await platformBootstrap.refreshPublishedGallery().catch(() => []);
|
||
})
|
||
.catch((error) => {
|
||
platformBootstrap.setPlatformError(
|
||
resolveRpgCreationErrorMessage(error, '删除自定义世界失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setDeletingCreationWorkId(null);
|
||
});
|
||
},
|
||
});
|
||
},
|
||
[deletingCreationWorkId, platformBootstrap, requestDeleteCreationWork],
|
||
);
|
||
|
||
const handleDeleteBigFishWork = useCallback(
|
||
(work: BigFishWorkSummary) => {
|
||
if (deletingCreationWorkId) {
|
||
return;
|
||
}
|
||
|
||
requestDeleteCreationWork({
|
||
id: work.workId,
|
||
title: work.title,
|
||
detail:
|
||
work.status === 'published'
|
||
? '删除后会从你的作品列表和公开广场中移除。'
|
||
: '删除后会从你的作品列表中移除。',
|
||
run: () => {
|
||
setDeletingCreationWorkId(work.workId);
|
||
setBigFishError(null);
|
||
|
||
void deleteBigFishWork(work.sourceSessionId)
|
||
.then(async (response) => {
|
||
setBigFishWorks(response.items);
|
||
await refreshBigFishGallery().catch(() => []);
|
||
})
|
||
.catch((error) => {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '删除大鱼吃小鱼作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setDeletingCreationWorkId(null);
|
||
});
|
||
},
|
||
});
|
||
},
|
||
[
|
||
deletingCreationWorkId,
|
||
refreshBigFishGallery,
|
||
requestDeleteCreationWork,
|
||
resolveBigFishErrorMessage,
|
||
setBigFishError,
|
||
],
|
||
);
|
||
|
||
const handleDeletePuzzleWork = useCallback(
|
||
(work: PuzzleWorkSummary) => {
|
||
if (deletingCreationWorkId) {
|
||
return;
|
||
}
|
||
|
||
const displayName =
|
||
work.workTitle?.trim() || work.levelName.trim() || '未命名拼图';
|
||
requestDeleteCreationWork({
|
||
id: work.workId,
|
||
title: displayName,
|
||
detail:
|
||
work.publicationStatus === 'published'
|
||
? '删除后会从你的作品列表和公开广场中移除。'
|
||
: '删除后会从你的作品列表中移除。',
|
||
run: () => {
|
||
setDeletingCreationWorkId(work.workId);
|
||
setPuzzleFormDraftPayload(null);
|
||
setPuzzleError(null);
|
||
|
||
void deletePuzzleWork(work.profileId)
|
||
.then((response) => {
|
||
setPuzzleWorks(response.items);
|
||
void refreshPuzzleGallery();
|
||
})
|
||
.catch((error) => {
|
||
setPuzzleError(
|
||
resolvePuzzleErrorMessage(error, '删除拼图作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setDeletingCreationWorkId(null);
|
||
});
|
||
},
|
||
});
|
||
},
|
||
[
|
||
deletingCreationWorkId,
|
||
refreshPuzzleGallery,
|
||
requestDeleteCreationWork,
|
||
resolvePuzzleErrorMessage,
|
||
setPuzzleError,
|
||
],
|
||
);
|
||
|
||
const handleDeleteMatch3DWork = useCallback(
|
||
(work: Match3DWorkSummary) => {
|
||
if (deletingCreationWorkId) {
|
||
return;
|
||
}
|
||
|
||
requestDeleteCreationWork({
|
||
id: work.workId,
|
||
title: work.gameName,
|
||
detail:
|
||
work.publicationStatus === 'published'
|
||
? '删除后会从你的作品列表和公开广场中移除。'
|
||
: '删除后会从你的作品列表中移除。',
|
||
run: () => {
|
||
setDeletingCreationWorkId(work.workId);
|
||
setMatch3DError(null);
|
||
|
||
void deleteMatch3DWork(work.profileId)
|
||
.then((response) => {
|
||
setMatch3DWorks(response.items);
|
||
void refreshMatch3DGallery();
|
||
})
|
||
.catch((error) => {
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(error, '删除抓大鹅作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setDeletingCreationWorkId(null);
|
||
});
|
||
},
|
||
});
|
||
},
|
||
[
|
||
deletingCreationWorkId,
|
||
refreshMatch3DGallery,
|
||
requestDeleteCreationWork,
|
||
resolveMatch3DErrorMessage,
|
||
setMatch3DError,
|
||
],
|
||
);
|
||
|
||
const handleDeleteSquareHoleWork = useCallback(
|
||
(work: SquareHoleWorkSummary) => {
|
||
if (deletingCreationWorkId) {
|
||
return;
|
||
}
|
||
|
||
requestDeleteCreationWork({
|
||
id: work.workId,
|
||
title: work.gameName,
|
||
detail:
|
||
work.publicationStatus === 'published'
|
||
? '删除后会从你的作品列表和公开广场中移除。'
|
||
: '删除后会从你的作品列表中移除。',
|
||
run: () => {
|
||
setDeletingCreationWorkId(work.workId);
|
||
setSquareHoleError(null);
|
||
|
||
void deleteSquareHoleWork(work.profileId)
|
||
.then((response) => {
|
||
setSquareHoleWorks(response.items);
|
||
void refreshSquareHoleGallery();
|
||
})
|
||
.catch((error) => {
|
||
setSquareHoleError(
|
||
resolveSquareHoleErrorMessage(error, '删除方洞挑战作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setDeletingCreationWorkId(null);
|
||
});
|
||
},
|
||
});
|
||
},
|
||
[
|
||
deletingCreationWorkId,
|
||
refreshSquareHoleGallery,
|
||
requestDeleteCreationWork,
|
||
resolveSquareHoleErrorMessage,
|
||
setSquareHoleError,
|
||
],
|
||
);
|
||
|
||
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) => {
|
||
setSelectedPublicWorkDetail(entry);
|
||
setPublicWorkDetailError(null);
|
||
setSelectionStage('work-detail');
|
||
if (entry.publicWorkCode?.trim()) {
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath('work-detail', entry.publicWorkCode),
|
||
);
|
||
}
|
||
},
|
||
[setSelectionStage],
|
||
);
|
||
|
||
const syncUpdatedPublicWorkDetail = useCallback(
|
||
(updatedEntry: PlatformPublicGalleryCard) => {
|
||
setSelectedPublicWorkDetail((current) =>
|
||
current && isSamePlatformPublicGalleryEntry(current, updatedEntry)
|
||
? updatedEntry
|
||
: current,
|
||
);
|
||
},
|
||
[],
|
||
);
|
||
|
||
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);
|
||
|
||
if (isBigFishGalleryEntry(entry)) {
|
||
void likeBigFishGalleryWork(entry.profileId)
|
||
.then((response) => {
|
||
const updatedWork = response.items.find(
|
||
(item) => item.sourceSessionId === entry.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 (isPuzzleGalleryEntry(entry)) {
|
||
void likePuzzleGalleryWork(entry.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 (isSquareHoleGalleryEntry(entry)) {
|
||
setPublicWorkDetailError('方洞挑战点赞将在后续版本开放。');
|
||
setIsPublicWorkDetailBusy(false);
|
||
return;
|
||
}
|
||
|
||
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.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,
|
||
resolveBigFishErrorMessage,
|
||
resolvePuzzleErrorMessage,
|
||
runProtectedAction,
|
||
syncUpdatedPublicWorkDetail,
|
||
],
|
||
);
|
||
|
||
useEffect(() => {
|
||
const detailEntry =
|
||
selectionStage === 'work-detail'
|
||
? selectedPublicWorkDetail
|
||
: selectionStage === 'detail' &&
|
||
selectedDetailEntry &&
|
||
selectedDetailEntry.visibility !== 'draft'
|
||
? mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)
|
||
: null;
|
||
|
||
if (!detailEntry) {
|
||
clearSelectedPublicWorkAuthor();
|
||
return;
|
||
}
|
||
|
||
loadSelectedPublicWorkAuthor(detailEntry);
|
||
}, [
|
||
clearSelectedPublicWorkAuthor,
|
||
loadSelectedPublicWorkAuthor,
|
||
selectedDetailEntry,
|
||
selectedPublicWorkDetail,
|
||
selectionStage,
|
||
]);
|
||
|
||
const openRpgPublicWorkDetail = useCallback(
|
||
async (entry: CustomWorldGalleryCard) => {
|
||
setIsPublicWorkDetailBusy(true);
|
||
setPublicWorkDetailError(null);
|
||
clearSelectedPublicWorkAuthor();
|
||
setSelectedPublicWorkDetail(entry);
|
||
setSelectionStage('work-detail');
|
||
|
||
try {
|
||
const detailEntry =
|
||
await detailNavigation.loadGalleryDetailEntry(entry);
|
||
setSelectedDetailEntry(detailEntry);
|
||
const detailCard = mapRpgGalleryCardToPublicWorkDetail(detailEntry);
|
||
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);
|
||
setSelectedPuzzleDetail(item);
|
||
setPuzzleDetailReturnTarget(returnTarget);
|
||
openPublicWorkDetail(mapPuzzleWorkToPublicWorkDetail(item));
|
||
} catch (error) {
|
||
setPublicWorkDetailError(
|
||
resolvePuzzleErrorMessage(error, '读取拼图详情失败。'),
|
||
);
|
||
} finally {
|
||
setIsPuzzleBusy(false);
|
||
setIsPublicWorkDetailBusy(false);
|
||
}
|
||
},
|
||
[
|
||
openPublicWorkDetail,
|
||
platformBootstrap.platformTab,
|
||
resolvePuzzleErrorMessage,
|
||
setPuzzleError,
|
||
],
|
||
);
|
||
|
||
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 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) {
|
||
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图详情失败。'));
|
||
} finally {
|
||
setIsPuzzleBusy(false);
|
||
}
|
||
},
|
||
[
|
||
platformBootstrap.platformTab,
|
||
resolvePuzzleErrorMessage,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const openPuzzleDraft = useCallback(
|
||
async (item: PuzzleWorkSummary) => {
|
||
setPuzzleOperation(null);
|
||
setPuzzleRun(null);
|
||
setSelectedPuzzleDetail(null);
|
||
if (!item.sourceSessionId?.trim()) {
|
||
if (item.publicationStatus === 'published') {
|
||
await openPuzzleDetail(item.profileId, { tab: 'create' });
|
||
return;
|
||
}
|
||
|
||
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
|
||
return;
|
||
}
|
||
|
||
const restoredSession = await puzzleFlow.restoreDraft(
|
||
item.sourceSessionId,
|
||
);
|
||
if (!restoredSession) {
|
||
await refreshPuzzleShelf().catch(() => undefined);
|
||
return;
|
||
}
|
||
|
||
if (isPuzzleFormOnlyDraft(restoredSession)) {
|
||
setPuzzleFormDraftPayload(
|
||
buildPuzzleFormPayloadFromSession(restoredSession),
|
||
);
|
||
setSelectionStage('puzzle-agent-workspace');
|
||
} else {
|
||
setPuzzleFormDraftPayload(null);
|
||
}
|
||
},
|
||
[
|
||
openPuzzleDetail,
|
||
puzzleFlow,
|
||
refreshPuzzleShelf,
|
||
setPuzzleError,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const openMatch3DDraft = useCallback(
|
||
async (
|
||
item: Match3DWorkSummary,
|
||
options: { forceDraft?: boolean } = {},
|
||
) => {
|
||
setMatch3DRun(null);
|
||
setMatch3DError(null);
|
||
setMatch3DProfile(null);
|
||
|
||
if (item.publicationStatus === 'published' && !options.forceDraft) {
|
||
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(item));
|
||
return;
|
||
}
|
||
|
||
if (!item.sourceSessionId?.trim()) {
|
||
setMatch3DError('这份抓大鹅草稿缺少会话信息,请重新开始创作。');
|
||
return;
|
||
}
|
||
|
||
const restoredSession = await match3dFlow.restoreDraft(
|
||
item.sourceSessionId,
|
||
);
|
||
if (!restoredSession) {
|
||
await refreshMatch3DShelf().catch(() => undefined);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { item: profile } = await getMatch3DWorkDetail(item.profileId);
|
||
setMatch3DProfile(profile);
|
||
} catch (error) {
|
||
setMatch3DProfile(buildMatch3DProfileFromSession(restoredSession));
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(error, '读取抓大鹅作品详情失败。'),
|
||
);
|
||
}
|
||
},
|
||
[
|
||
match3dFlow,
|
||
openPublicWorkDetail,
|
||
refreshMatch3DShelf,
|
||
resolveMatch3DErrorMessage,
|
||
setMatch3DError,
|
||
],
|
||
);
|
||
|
||
const openSquareHoleDraft = useCallback(
|
||
async (
|
||
item: SquareHoleWorkSummary,
|
||
options: { forceDraft?: boolean } = {},
|
||
) => {
|
||
setSquareHoleRun(null);
|
||
setSquareHoleError(null);
|
||
setSquareHoleProfile(null);
|
||
|
||
if (item.publicationStatus === 'published' && !options.forceDraft) {
|
||
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(item));
|
||
return;
|
||
}
|
||
|
||
if (!item.sourceSessionId?.trim()) {
|
||
setSquareHoleError('这份方洞挑战草稿缺少会话信息,请重新开始创作。');
|
||
return;
|
||
}
|
||
|
||
setSquareHoleGenerationState(null);
|
||
const restoredSession = await squareHoleFlow.restoreDraft(
|
||
item.sourceSessionId,
|
||
);
|
||
if (!restoredSession) {
|
||
await refreshSquareHoleShelf().catch(() => undefined);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { item: profile } = await getSquareHoleWorkDetail(item.profileId);
|
||
setSquareHoleProfile(profile);
|
||
} catch (error) {
|
||
setSquareHoleProfile(
|
||
buildSquareHoleProfileFromSession(restoredSession),
|
||
);
|
||
setSquareHoleError(
|
||
resolveSquareHoleErrorMessage(error, '读取方洞挑战作品详情失败。'),
|
||
);
|
||
}
|
||
},
|
||
[
|
||
openPublicWorkDetail,
|
||
refreshSquareHoleShelf,
|
||
resolveSquareHoleErrorMessage,
|
||
setSquareHoleError,
|
||
squareHoleFlow,
|
||
],
|
||
);
|
||
|
||
const openBigFishDraft = useCallback(
|
||
async (item: BigFishWorkSummary) => {
|
||
setBigFishRun(null);
|
||
const restoredSession = await bigFishFlow.restoreDraft(
|
||
item.sourceSessionId,
|
||
);
|
||
if (!restoredSession) {
|
||
await refreshBigFishShelf().catch(() => undefined);
|
||
}
|
||
},
|
||
[bigFishFlow, refreshBigFishShelf],
|
||
);
|
||
|
||
const startBigFishRunFromWork = useCallback(
|
||
async (
|
||
item: BigFishWorkSummary,
|
||
returnStage: BigFishRuntimeReturnStage = 'work-detail',
|
||
) => {
|
||
const sessionId = item.sourceSessionId?.trim();
|
||
if (!sessionId) {
|
||
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
|
||
return;
|
||
}
|
||
|
||
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 { run } = await startBigFishRuntimeRun(sessionId);
|
||
setBigFishRuntimeStartedAt(Date.now());
|
||
setBigFishRun(run);
|
||
setSelectionStage('big-fish-runtime');
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
|
||
);
|
||
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
|
||
);
|
||
});
|
||
} catch (error) {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
|
||
);
|
||
}
|
||
},
|
||
[
|
||
bigFishFlow,
|
||
resolveBigFishErrorMessage,
|
||
setBigFishRuntimeReturnStage,
|
||
setSelectionStage,
|
||
startBigFishRuntimeRun,
|
||
],
|
||
);
|
||
|
||
const startSelectedPublicWork = useCallback(() => {
|
||
if (!selectedPublicWorkDetail || isPublicWorkDetailBusy) {
|
||
return;
|
||
}
|
||
|
||
runProtectedAction(() => {
|
||
if (isBigFishGalleryEntry(selectedPublicWorkDetail)) {
|
||
const work = mapPublicWorkDetailToBigFishWork(selectedPublicWorkDetail);
|
||
if (!work) {
|
||
setPublicWorkDetailError('当前作品缺少会话信息,暂时无法进入玩法。');
|
||
return;
|
||
}
|
||
startBigFishRunFromWork(work);
|
||
return;
|
||
}
|
||
|
||
if (isPuzzleGalleryEntry(selectedPublicWorkDetail)) {
|
||
const work =
|
||
selectedPuzzleDetail?.profileId === selectedPublicWorkDetail.profileId
|
||
? selectedPuzzleDetail
|
||
: mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail);
|
||
if (!work) {
|
||
setPublicWorkDetailError(
|
||
'当前拼图作品信息不完整,暂时无法进入玩法。',
|
||
);
|
||
return;
|
||
}
|
||
setPublicWorkDetailError(null);
|
||
void startPuzzleRunFromProfile(
|
||
work.profileId,
|
||
'work-detail',
|
||
work,
|
||
true,
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (isMatch3DGalleryEntry(selectedPublicWorkDetail)) {
|
||
const work = mapPublicWorkDetailToMatch3DWork(selectedPublicWorkDetail);
|
||
if (!work) {
|
||
setPublicWorkDetailError(
|
||
'当前抓大鹅作品信息不完整,暂时无法进入玩法。',
|
||
);
|
||
return;
|
||
}
|
||
setPublicWorkDetailError(null);
|
||
void startMatch3DRunFromProfile(work, 'work-detail', true);
|
||
return;
|
||
}
|
||
|
||
if (isSquareHoleGalleryEntry(selectedPublicWorkDetail)) {
|
||
const work = mapPublicWorkDetailToSquareHoleWork(
|
||
selectedPublicWorkDetail,
|
||
);
|
||
if (!work) {
|
||
setPublicWorkDetailError(
|
||
'当前方洞挑战作品信息不完整,暂时无法进入玩法。',
|
||
);
|
||
return;
|
||
}
|
||
setPublicWorkDetailError(null);
|
||
void startSquareHoleRunFromProfile(work, 'work-detail', true);
|
||
return;
|
||
}
|
||
|
||
const launchEntry =
|
||
selectedDetailEntry?.profileId === selectedPublicWorkDetail.profileId
|
||
? selectedDetailEntry
|
||
: null;
|
||
if (!launchEntry) {
|
||
setPublicWorkDetailError('作品详情尚未读取完成。');
|
||
return;
|
||
}
|
||
|
||
setIsPublicWorkDetailBusy(true);
|
||
void recordRpgEntryWorldGalleryPlay(
|
||
launchEntry.ownerUserId,
|
||
launchEntry.profileId,
|
||
)
|
||
.then((updatedEntry) => {
|
||
setSelectedDetailEntry(updatedEntry);
|
||
setSelectedPublicWorkDetail(
|
||
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
|
||
);
|
||
handleCustomWorldSelect(updatedEntry.profile);
|
||
})
|
||
.catch((error) => {
|
||
setPublicWorkDetailError(
|
||
resolveRpgCreationErrorMessage(error, '记录作品游玩失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setIsPublicWorkDetailBusy(false);
|
||
});
|
||
});
|
||
}, [
|
||
handleCustomWorldSelect,
|
||
isPublicWorkDetailBusy,
|
||
runProtectedAction,
|
||
selectedDetailEntry,
|
||
selectedPuzzleDetail,
|
||
selectedPublicWorkDetail,
|
||
startBigFishRunFromWork,
|
||
startPuzzleRunFromProfile,
|
||
startMatch3DRunFromProfile,
|
||
startSquareHoleRunFromProfile,
|
||
]);
|
||
|
||
const remixPublicWork = useCallback(
|
||
(entry: PlatformPublicGalleryCard) => {
|
||
if (isPublicWorkDetailBusy) {
|
||
return;
|
||
}
|
||
|
||
runProtectedAction(() => {
|
||
setIsPublicWorkDetailBusy(true);
|
||
setPublicWorkDetailError(null);
|
||
|
||
if (isBigFishGalleryEntry(entry)) {
|
||
void remixBigFishGalleryWork(entry.profileId)
|
||
.then((response) => {
|
||
bigFishFlow.setSession(response.session);
|
||
enterCreateTab();
|
||
setSelectionStage('big-fish-result');
|
||
})
|
||
.catch((error) => {
|
||
setPublicWorkDetailError(
|
||
resolveBigFishErrorMessage(error, 'Remix 大鱼吃小鱼作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setIsPublicWorkDetailBusy(false);
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isPuzzleGalleryEntry(entry)) {
|
||
void remixPuzzleGalleryWork(entry.profileId)
|
||
.then((response) => {
|
||
puzzleFlow.setSession(response.session);
|
||
setPuzzleOperation(null);
|
||
enterCreateTab();
|
||
setSelectionStage('puzzle-result');
|
||
})
|
||
.catch((error) => {
|
||
setPublicWorkDetailError(
|
||
resolvePuzzleErrorMessage(error, 'Remix 拼图作品失败。'),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
setIsPublicWorkDetailBusy(false);
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isMatch3DGalleryEntry(entry)) {
|
||
setPublicWorkDetailError('抓大鹅作品改造将在后续版本开放。');
|
||
setIsPublicWorkDetailBusy(false);
|
||
return;
|
||
}
|
||
|
||
if (isSquareHoleGalleryEntry(entry)) {
|
||
setPublicWorkDetailError('方洞挑战作品改造将在后续版本开放。');
|
||
setIsPublicWorkDetailBusy(false);
|
||
return;
|
||
}
|
||
|
||
void remixRpgEntryWorldGallery(entry.ownerUserId, entry.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,
|
||
resolveBigFishErrorMessage,
|
||
resolvePuzzleErrorMessage,
|
||
runProtectedAction,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const editOwnedPublicWork = useCallback(
|
||
(entry: PlatformPublicGalleryCard) => {
|
||
if (isPublicWorkDetailBusy) {
|
||
return;
|
||
}
|
||
|
||
runProtectedAction(() => {
|
||
setPublicWorkDetailError(null);
|
||
|
||
// 中文注释:自有公开作品必须恢复原草稿,不能复用 remix 复制链路。
|
||
if (isBigFishGalleryEntry(entry)) {
|
||
const work = mapPublicWorkDetailToBigFishWork(entry);
|
||
if (!work?.sourceSessionId?.trim()) {
|
||
setPublicWorkDetailError(
|
||
'这份大鱼吃小鱼作品缺少原草稿会话,暂时无法编辑。',
|
||
);
|
||
return;
|
||
}
|
||
void openBigFishDraft(work);
|
||
return;
|
||
}
|
||
|
||
if (isPuzzleGalleryEntry(entry)) {
|
||
const work =
|
||
selectedPuzzleDetail?.profileId === entry.profileId
|
||
? selectedPuzzleDetail
|
||
: mapPublicWorkDetailToPuzzleWork(entry);
|
||
if (!work?.sourceSessionId?.trim()) {
|
||
setPublicWorkDetailError(
|
||
'这份拼图作品缺少原草稿会话,暂时无法编辑。',
|
||
);
|
||
return;
|
||
}
|
||
void openPuzzleDraft(work);
|
||
return;
|
||
}
|
||
|
||
if (isMatch3DGalleryEntry(entry)) {
|
||
const work = mapPublicWorkDetailToMatch3DWork(entry);
|
||
if (!work?.sourceSessionId?.trim()) {
|
||
setPublicWorkDetailError(
|
||
'这份抓大鹅作品缺少原草稿会话,暂时无法编辑。',
|
||
);
|
||
return;
|
||
}
|
||
void openMatch3DDraft(work, { forceDraft: true });
|
||
return;
|
||
}
|
||
|
||
if (isSquareHoleGalleryEntry(entry)) {
|
||
const work = mapPublicWorkDetailToSquareHoleWork(entry);
|
||
if (!work?.sourceSessionId?.trim()) {
|
||
setPublicWorkDetailError(
|
||
'这份方洞挑战作品缺少原草稿会话,暂时无法编辑。',
|
||
);
|
||
return;
|
||
}
|
||
void openSquareHoleDraft(work, { forceDraft: true });
|
||
return;
|
||
}
|
||
|
||
const editEntry =
|
||
selectedDetailEntry?.profileId === entry.profileId
|
||
? selectedDetailEntry
|
||
: null;
|
||
if (!editEntry) {
|
||
setPublicWorkDetailError('作品详情尚未读取完成。');
|
||
return;
|
||
}
|
||
|
||
void detailNavigation.openSavedCustomWorldEditor(editEntry);
|
||
});
|
||
},
|
||
[
|
||
detailNavigation,
|
||
isPublicWorkDetailBusy,
|
||
openBigFishDraft,
|
||
openMatch3DDraft,
|
||
openPuzzleDraft,
|
||
openSquareHoleDraft,
|
||
runProtectedAction,
|
||
selectedDetailEntry,
|
||
selectedPuzzleDetail,
|
||
],
|
||
);
|
||
|
||
const remixSelectedPublicWork = useCallback(() => {
|
||
if (!selectedPublicWorkDetail) {
|
||
return;
|
||
}
|
||
if (isSelectedPublicWorkOwned) {
|
||
editOwnedPublicWork(selectedPublicWorkDetail);
|
||
return;
|
||
}
|
||
remixPublicWork(selectedPublicWorkDetail);
|
||
}, [
|
||
editOwnedPublicWork,
|
||
isSelectedPublicWorkOwned,
|
||
remixPublicWork,
|
||
selectedPublicWorkDetail,
|
||
]);
|
||
|
||
const handlePublicCodeSearch = useCallback(
|
||
async (keyword: string) => {
|
||
const normalizedKeyword = keyword.trim();
|
||
if (!normalizedKeyword) {
|
||
return;
|
||
}
|
||
|
||
setIsSearchingPublicCode(true);
|
||
setPublicSearchError(null);
|
||
setSearchedPublicUser(null);
|
||
|
||
const upperKeyword = normalizedKeyword.toUpperCase();
|
||
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(
|
||
normalizedKeyword,
|
||
);
|
||
const shouldSearchBigFishFirst = upperKeyword.startsWith('BF');
|
||
const shouldSearchMatch3DFirst = upperKeyword.startsWith('M3');
|
||
const shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ');
|
||
const shouldSearchSquareHoleFirst = upperKeyword.startsWith('SH');
|
||
const shouldSearchWorkFirst =
|
||
!shouldSearchUserIdFirst &&
|
||
!shouldSearchBigFishFirst &&
|
||
!shouldSearchMatch3DFirst &&
|
||
!shouldSearchPuzzleFirst &&
|
||
!shouldSearchSquareHoleFirst &&
|
||
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
||
const shouldSearchUserFirst =
|
||
shouldSearchUserIdFirst ||
|
||
upperKeyword.startsWith('SY') ||
|
||
(!shouldSearchWorkFirst &&
|
||
!shouldSearchBigFishFirst &&
|
||
!shouldSearchMatch3DFirst &&
|
||
!shouldSearchPuzzleFirst &&
|
||
!shouldSearchSquareHoleFirst);
|
||
|
||
const tryOpenGalleryEntry = async () => {
|
||
const entry =
|
||
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
||
const card = {
|
||
ownerUserId: entry.ownerUserId,
|
||
profileId: entry.profileId,
|
||
publicWorkCode: entry.publicWorkCode,
|
||
authorPublicUserCode: entry.authorPublicUserCode,
|
||
visibility: 'published',
|
||
publishedAt: entry.publishedAt,
|
||
updatedAt: entry.updatedAt,
|
||
authorDisplayName: entry.authorDisplayName,
|
||
worldName: entry.worldName,
|
||
subtitle: entry.subtitle,
|
||
summaryText: entry.summaryText,
|
||
coverImageSrc: entry.coverImageSrc,
|
||
themeMode: entry.themeMode,
|
||
playableNpcCount: entry.playableNpcCount,
|
||
landmarkCount: entry.landmarkCount,
|
||
playCount: entry.playCount ?? 0,
|
||
remixCount: entry.remixCount ?? 0,
|
||
likeCount: entry.likeCount ?? 0,
|
||
} satisfies CustomWorldGalleryCard;
|
||
setSelectedDetailEntry(entry);
|
||
openPublicWorkDetail(card);
|
||
};
|
||
const tryOpenPuzzleGalleryEntry = async () => {
|
||
const entries =
|
||
puzzleGalleryEntries.length > 0
|
||
? puzzleGalleryEntries
|
||
: await refreshPuzzleGallery();
|
||
const matchedEntry = entries.find((entry) =>
|
||
isSamePuzzlePublicWorkCode(normalizedKeyword, entry.profileId),
|
||
);
|
||
|
||
if (!matchedEntry) {
|
||
throw new Error('未找到拼图作品。');
|
||
}
|
||
|
||
await openPuzzlePublicWorkDetail(matchedEntry.profileId, {
|
||
tab: platformBootstrap.platformTab,
|
||
});
|
||
};
|
||
const tryOpenBigFishGalleryEntry = async () => {
|
||
const entries =
|
||
bigFishGalleryEntries.length > 0
|
||
? bigFishGalleryEntries
|
||
: await refreshBigFishGallery();
|
||
const matchedEntry = entries.find((entry) =>
|
||
isSameBigFishPublicWorkCode(normalizedKeyword, entry.sourceSessionId),
|
||
);
|
||
|
||
if (!matchedEntry) {
|
||
throw new Error('未找到大鱼吃小鱼作品。');
|
||
}
|
||
|
||
openPublicWorkDetail(mapBigFishWorkToPublicWorkDetail(matchedEntry));
|
||
};
|
||
const tryOpenMatch3DGalleryEntry = async () => {
|
||
const entries =
|
||
match3dGalleryEntries.length > 0
|
||
? match3dGalleryEntries
|
||
: await refreshMatch3DGallery();
|
||
const matchedEntry = entries.find((entry) =>
|
||
isSameMatch3DPublicWorkCode(normalizedKeyword, entry.profileId),
|
||
);
|
||
|
||
if (!matchedEntry) {
|
||
throw new Error('未找到抓大鹅作品。');
|
||
}
|
||
|
||
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(matchedEntry));
|
||
};
|
||
const tryOpenSquareHoleGalleryEntry = async () => {
|
||
const entries =
|
||
squareHoleGalleryEntries.length > 0
|
||
? squareHoleGalleryEntries
|
||
: await refreshSquareHoleGallery();
|
||
const matchedEntry = entries.find((entry) =>
|
||
isSameSquareHolePublicWorkCode(normalizedKeyword, entry.profileId),
|
||
);
|
||
|
||
if (!matchedEntry) {
|
||
throw new Error('未找到方洞挑战作品。');
|
||
}
|
||
|
||
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(matchedEntry));
|
||
};
|
||
|
||
try {
|
||
if (shouldSearchUserIdFirst) {
|
||
const user = await getPublicAuthUserById(normalizedKeyword);
|
||
setSearchedPublicUser(user);
|
||
return;
|
||
}
|
||
|
||
if (shouldSearchPuzzleFirst) {
|
||
await tryOpenPuzzleGalleryEntry();
|
||
return;
|
||
}
|
||
|
||
if (shouldSearchBigFishFirst) {
|
||
await tryOpenBigFishGalleryEntry();
|
||
return;
|
||
}
|
||
|
||
if (shouldSearchMatch3DFirst) {
|
||
await tryOpenMatch3DGalleryEntry();
|
||
return;
|
||
}
|
||
|
||
if (shouldSearchSquareHoleFirst) {
|
||
await tryOpenSquareHoleGalleryEntry();
|
||
return;
|
||
}
|
||
|
||
if (shouldSearchWorkFirst) {
|
||
try {
|
||
await tryOpenGalleryEntry();
|
||
return;
|
||
} catch {
|
||
// 作品号优先时允许继续回退到用户号搜索。
|
||
}
|
||
}
|
||
|
||
if (shouldSearchUserFirst) {
|
||
try {
|
||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||
setSearchedPublicUser(user);
|
||
return;
|
||
} catch {
|
||
// 用户号优先时允许继续回退到作品号搜索。
|
||
}
|
||
}
|
||
|
||
if (!shouldSearchWorkFirst) {
|
||
await tryOpenGalleryEntry();
|
||
return;
|
||
}
|
||
|
||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||
setSearchedPublicUser(user);
|
||
} catch (error) {
|
||
setPublicSearchError(
|
||
resolveRpgCreationErrorMessage(error, '未找到对应的百梦号或作品号。'),
|
||
);
|
||
} finally {
|
||
setIsSearchingPublicCode(false);
|
||
}
|
||
},
|
||
[
|
||
bigFishGalleryEntries,
|
||
match3dGalleryEntries,
|
||
refreshMatch3DGallery,
|
||
openPuzzlePublicWorkDetail,
|
||
openPublicWorkDetail,
|
||
platformBootstrap.platformTab,
|
||
puzzleGalleryEntries,
|
||
refreshBigFishGallery,
|
||
refreshPuzzleGallery,
|
||
refreshSquareHoleGallery,
|
||
squareHoleGalleryEntries,
|
||
],
|
||
);
|
||
|
||
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 worldType = (work.worldType ?? '').toLowerCase();
|
||
setIsProfilePlayStatsOpen(false);
|
||
|
||
if (worldType === 'puzzle' || work.worldKey.startsWith('puzzle:')) {
|
||
const profileId =
|
||
work.profileId ?? work.worldKey.replace(/^puzzle:/u, '');
|
||
if (profileId) {
|
||
void openPuzzlePublicWorkDetail(profileId, { tab: 'profile' });
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (
|
||
worldType === 'match3d' ||
|
||
worldType === 'match_3d' ||
|
||
work.worldKey.startsWith('match3d:')
|
||
) {
|
||
const profileId =
|
||
work.profileId ?? work.worldKey.replace(/^match3d:/u, '');
|
||
if (profileId) {
|
||
void openMatch3DPublicWorkDetail(profileId);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (
|
||
worldType === 'square-hole' ||
|
||
worldType === 'square_hole' ||
|
||
work.worldKey.startsWith('square-hole:')
|
||
) {
|
||
const profileId =
|
||
work.profileId ?? work.worldKey.replace(/^square-hole:/u, '');
|
||
if (profileId) {
|
||
void openSquareHolePublicWorkDetail(profileId);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (
|
||
worldType === 'big_fish' ||
|
||
worldType === 'big-fish' ||
|
||
work.worldKey.startsWith('big-fish:')
|
||
) {
|
||
const sessionId =
|
||
work.profileId ?? work.worldKey.replace(/^big-fish:/u, '');
|
||
if (!sessionId) {
|
||
return;
|
||
}
|
||
void refreshBigFishGallery()
|
||
.then((entries) => {
|
||
const matchedEntry = entries.find(
|
||
(entry) => entry.sourceSessionId === sessionId,
|
||
);
|
||
if (matchedEntry) {
|
||
openPublicWorkDetail(
|
||
mapBigFishWorkToPublicWorkDetail(matchedEntry),
|
||
);
|
||
return;
|
||
}
|
||
openPublicWorkDetail(
|
||
mapBigFishWorkToPublicWorkDetail({
|
||
workId: `big-fish:${sessionId}`,
|
||
sourceSessionId: sessionId,
|
||
ownerUserId: work.ownerUserId ?? '',
|
||
authorDisplayName: work.worldSubtitle || '玩家',
|
||
title: work.worldTitle,
|
||
subtitle: work.worldSubtitle,
|
||
summary: work.worldSubtitle,
|
||
coverImageSrc: null,
|
||
status: 'published',
|
||
updatedAt: work.lastPlayedAt,
|
||
publishReady: true,
|
||
levelCount: 0,
|
||
levelMainImageReadyCount: 0,
|
||
levelMotionReadyCount: 0,
|
||
backgroundReady: false,
|
||
}),
|
||
);
|
||
})
|
||
.catch((error) => {
|
||
setBigFishError(
|
||
resolveBigFishErrorMessage(error, '进入大鱼吃小鱼作品失败。'),
|
||
);
|
||
});
|
||
return;
|
||
}
|
||
|
||
const profileId = work.profileId ?? work.worldKey;
|
||
const ownerUserId = work.ownerUserId;
|
||
if (!ownerUserId || !profileId) {
|
||
return;
|
||
}
|
||
|
||
void openRpgPublicWorkDetail({
|
||
ownerUserId,
|
||
profileId,
|
||
publicWorkCode: null,
|
||
authorPublicUserCode: null,
|
||
visibility: 'published',
|
||
publishedAt: work.firstPlayedAt,
|
||
updatedAt: work.lastPlayedAt,
|
||
authorDisplayName: work.worldSubtitle,
|
||
worldName: work.worldTitle,
|
||
subtitle: work.worldSubtitle,
|
||
summaryText: '',
|
||
coverImageSrc: null,
|
||
themeMode: 'martial',
|
||
playableNpcCount: 0,
|
||
landmarkCount: 0,
|
||
playCount: 0,
|
||
remixCount: 0,
|
||
likeCount: 0,
|
||
});
|
||
},
|
||
[
|
||
openMatch3DPublicWorkDetail,
|
||
openPuzzlePublicWorkDetail,
|
||
openPublicWorkDetail,
|
||
openRpgPublicWorkDetail,
|
||
openSquareHolePublicWorkDetail,
|
||
refreshBigFishGallery,
|
||
resolveBigFishErrorMessage,
|
||
],
|
||
);
|
||
|
||
useEffect(() => {
|
||
const publicWorkCode = initialPublicWorkCode?.trim();
|
||
if (
|
||
!publicWorkCode ||
|
||
handledInitialPublicWorkCodeRef.current === publicWorkCode
|
||
) {
|
||
return;
|
||
}
|
||
|
||
handledInitialPublicWorkCodeRef.current = publicWorkCode;
|
||
void handlePublicCodeSearch(publicWorkCode);
|
||
}, [handlePublicCodeSearch, initialPublicWorkCode]);
|
||
|
||
useEffect(() => {
|
||
if (selectionStage === 'platform') {
|
||
if (isBigFishCreationVisible) {
|
||
void refreshBigFishGallery();
|
||
}
|
||
void refreshMatch3DGallery();
|
||
void refreshPuzzleGallery();
|
||
void refreshSquareHoleGallery();
|
||
}
|
||
}, [
|
||
isBigFishCreationVisible,
|
||
refreshBigFishGallery,
|
||
refreshMatch3DGallery,
|
||
refreshPuzzleGallery,
|
||
refreshSquareHoleGallery,
|
||
selectionStage,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
(platformBootstrap.platformTab === 'create' ||
|
||
selectionStage === 'platform') &&
|
||
platformBootstrap.canReadProtectedData
|
||
) {
|
||
void refreshPuzzleShelf();
|
||
void refreshMatch3DShelf();
|
||
void refreshSquareHoleShelf();
|
||
}
|
||
}, [
|
||
platformBootstrap.canReadProtectedData,
|
||
platformBootstrap.platformTab,
|
||
refreshMatch3DShelf,
|
||
refreshPuzzleShelf,
|
||
refreshSquareHoleShelf,
|
||
selectionStage,
|
||
]);
|
||
|
||
useEffect(() => {
|
||
if (
|
||
isBigFishCreationVisible &&
|
||
(platformBootstrap.platformTab === 'create' ||
|
||
selectionStage === 'platform') &&
|
||
platformBootstrap.canReadProtectedData
|
||
) {
|
||
void refreshBigFishShelf();
|
||
}
|
||
}, [
|
||
isBigFishCreationVisible,
|
||
platformBootstrap.canReadProtectedData,
|
||
platformBootstrap.platformTab,
|
||
refreshBigFishShelf,
|
||
selectionStage,
|
||
]);
|
||
|
||
const creationHubContent = (
|
||
<Suspense fallback={<LazyPanelFallback label="正在加载创作中心..." />}>
|
||
<CustomWorldCreationHub
|
||
items={creationHubItems}
|
||
loading={
|
||
platformBootstrap.isLoadingPlatform ||
|
||
isBigFishLoadingLibrary ||
|
||
isMatch3DLoadingLibrary ||
|
||
isSquareHoleLoadingLibrary ||
|
||
isPuzzleLoadingLibrary
|
||
}
|
||
error={
|
||
platformBootstrap.isLoadingPlatform ||
|
||
isBigFishLoadingLibrary ||
|
||
isMatch3DLoadingLibrary ||
|
||
isSquareHoleLoadingLibrary ||
|
||
isPuzzleLoadingLibrary
|
||
? null
|
||
: (platformBootstrap.platformError ??
|
||
sessionController.agentWorkspaceRestoreError ??
|
||
bigFishError ??
|
||
match3dError ??
|
||
squareHoleError ??
|
||
puzzleShelfError ??
|
||
puzzleError)
|
||
}
|
||
onRetry={() => {
|
||
platformBootstrap.setPlatformError(null);
|
||
setBigFishError(null);
|
||
setMatch3DError(null);
|
||
setSquareHoleError(null);
|
||
setPuzzleShelfError(null);
|
||
setPuzzleCreationError(null);
|
||
setPuzzleError(null);
|
||
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
|
||
platformBootstrap.setPlatformError(
|
||
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
|
||
);
|
||
});
|
||
if (isBigFishCreationVisible) {
|
||
void refreshBigFishShelf();
|
||
}
|
||
void refreshMatch3DShelf();
|
||
void refreshSquareHoleShelf();
|
||
void refreshPuzzleShelf();
|
||
}}
|
||
createError={
|
||
sessionController.creationTypeError ??
|
||
bigFishError ??
|
||
match3dError ??
|
||
squareHoleError ??
|
||
puzzleCreationError ??
|
||
puzzleError
|
||
}
|
||
createBusy={
|
||
sessionController.isCreatingAgentSession ||
|
||
isBigFishBusy ||
|
||
isMatch3DBusy ||
|
||
isSquareHoleBusy ||
|
||
isPuzzleBusy
|
||
}
|
||
onCreateType={handleCreationHubCreateType}
|
||
onOpenDraft={(item) => {
|
||
runProtectedAction(() => {
|
||
void detailNavigation.handleOpenCreationWork(item);
|
||
});
|
||
}}
|
||
onEnterPublished={(profileId) => {
|
||
runProtectedAction(() => {
|
||
const matchedWork = creationHubItems.find(
|
||
(entry) => entry.profileId === profileId,
|
||
);
|
||
if (!matchedWork) {
|
||
return;
|
||
}
|
||
void detailNavigation.handleOpenCreationWork(matchedWork);
|
||
});
|
||
}}
|
||
onDeletePublished={(item) => {
|
||
handleDeletePublishedWork(item);
|
||
}}
|
||
deletingWorkId={deletingCreationWorkId}
|
||
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
|
||
bigFishItems={isBigFishCreationVisible ? bigFishWorks : []}
|
||
onOpenBigFishDetail={
|
||
isBigFishCreationVisible
|
||
? (item) => {
|
||
runProtectedAction(() => {
|
||
void openBigFishDraft(item);
|
||
});
|
||
}
|
||
: undefined
|
||
}
|
||
onDeleteBigFish={
|
||
isBigFishCreationVisible
|
||
? (item) => {
|
||
handleDeleteBigFishWork(item);
|
||
}
|
||
: null
|
||
}
|
||
match3dItems={match3dWorks}
|
||
onOpenMatch3DDetail={(item) => {
|
||
runProtectedAction(() => {
|
||
void openMatch3DDraft(item);
|
||
});
|
||
}}
|
||
onDeleteMatch3D={(item) => {
|
||
handleDeleteMatch3DWork(item);
|
||
}}
|
||
squareHoleItems={squareHoleWorks}
|
||
onOpenSquareHoleDetail={(item) => {
|
||
runProtectedAction(() => {
|
||
void openSquareHoleDraft(item);
|
||
});
|
||
}}
|
||
onDeleteSquareHole={(item) => {
|
||
handleDeleteSquareHoleWork(item);
|
||
}}
|
||
puzzleItems={puzzleWorks}
|
||
onOpenPuzzleDetail={(item) => {
|
||
runProtectedAction(() => {
|
||
void openPuzzleDraft(item);
|
||
});
|
||
}}
|
||
onDeletePuzzle={(item) => {
|
||
handleDeletePuzzleWork(item);
|
||
}}
|
||
onClaimPuzzlePointIncentive={(item) => {
|
||
handleClaimPuzzlePointIncentive(item);
|
||
}}
|
||
claimingPuzzleProfileId={claimingPuzzlePointIncentiveProfileId}
|
||
/>
|
||
</Suspense>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<AnimatePresence mode="wait">
|
||
{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"
|
||
>
|
||
<PlatformEntryHomeView
|
||
activeTab={platformBootstrap.platformTab}
|
||
onTabChange={platformBootstrap.setPlatformTab}
|
||
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}
|
||
isResumingSaveWorldKey={platformBootstrap.isResumingSaveWorldKey}
|
||
platformError={
|
||
platformBootstrap.isLoadingPlatform
|
||
? null
|
||
: (platformBootstrap.platformError ??
|
||
sessionController.agentWorkspaceRestoreError)
|
||
}
|
||
dashboardError={
|
||
platformBootstrap.isLoadingDashboard
|
||
? null
|
||
: platformBootstrap.dashboardError
|
||
}
|
||
createTabContent={creationHubContent}
|
||
onContinueGame={handleContinueGame}
|
||
onResumeSave={(entry) => {
|
||
if (
|
||
(entry.worldType ?? '').toLowerCase() === 'puzzle' ||
|
||
entry.worldKey.startsWith('puzzle:')
|
||
) {
|
||
void resumePuzzleSaveArchive(entry);
|
||
return;
|
||
}
|
||
void platformBootstrap.handleResumeSaveEntry(entry);
|
||
}}
|
||
onOpenCreateWorld={openCreationTypePicker}
|
||
onOpenCreateTypePicker={openCreationTypePicker}
|
||
onOpenGalleryDetail={(entry) => {
|
||
if (isBigFishGalleryEntry(entry)) {
|
||
openPublicWorkDetail(entry);
|
||
return;
|
||
}
|
||
|
||
if (isPuzzleGalleryEntry(entry)) {
|
||
void openPuzzlePublicWorkDetail(entry.profileId, {
|
||
tab: platformBootstrap.platformTab,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (isMatch3DGalleryEntry(entry)) {
|
||
openPublicWorkDetail(entry);
|
||
return;
|
||
}
|
||
|
||
if (isSquareHoleGalleryEntry(entry)) {
|
||
openPublicWorkDetail(entry);
|
||
return;
|
||
}
|
||
|
||
void openRpgPublicWorkDetail(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}
|
||
onOpenProfileDashboardCard={(cardKey) => {
|
||
if (cardKey === 'playedWorks') {
|
||
openProfilePlayedWorks();
|
||
return;
|
||
}
|
||
if (platformBootstrap.dashboardError) {
|
||
void platformBootstrap.refreshProfileDashboard();
|
||
}
|
||
}}
|
||
onRechargeSuccess={platformBootstrap.refreshProfileDashboard}
|
||
/>
|
||
</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');
|
||
}}
|
||
/>
|
||
</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}
|
||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
|
||
isBusy={
|
||
isPublicWorkDetailBusy ||
|
||
isPuzzleBusy ||
|
||
isBigFishBusy ||
|
||
isMatch3DBusy ||
|
||
isSquareHoleBusy
|
||
}
|
||
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">
|
||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||
{detailNavigation.detailError || '正在读取作品详情...'}
|
||
</div>
|
||
</div>
|
||
) : selectedDetailEntry.visibility !== 'draft' ? (
|
||
<PlatformWorkDetailView
|
||
entry={mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)}
|
||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||
authorDisplayName={
|
||
selectedPublicWorkAuthor?.displayName ?? null
|
||
}
|
||
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
|
||
? () => {
|
||
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 共创工作区..." />
|
||
}
|
||
>
|
||
{sessionController.agentSession ? (
|
||
<CustomWorldAgentWorkspace
|
||
session={sessionController.agentSession}
|
||
activeOperation={sessionController.agentOperation}
|
||
streamingReplyText={sessionController.streamingAgentReplyText}
|
||
isStreamingReply={sessionController.isStreamingAgentReply}
|
||
onBack={leaveAgentWorkspace}
|
||
onSubmitMessage={(payload) => {
|
||
void sessionController.submitAgentMessage(payload);
|
||
}}
|
||
onExecuteAction={(payload) => {
|
||
void sessionController.executeAgentAction(payload);
|
||
}}
|
||
/>
|
||
) : (
|
||
<div className="flex h-full items-center justify-center">
|
||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||
{sessionController.isLoadingAgentSession
|
||
? '正在准备 Agent 共创工作区...'
|
||
: sessionController.agentWorkspaceRestoreError ||
|
||
'正在恢复创作工作区...'}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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="正在加载大鱼吃小鱼共创工作区..." />
|
||
}
|
||
>
|
||
<BigFishAgentWorkspace
|
||
session={bigFishSession}
|
||
streamingReplyText={streamingBigFishReplyText}
|
||
isStreamingReply={isStreamingBigFishReply}
|
||
isBusy={isBigFishBusy || isStreamingBigFishReply}
|
||
error={bigFishError}
|
||
onBack={leaveBigFishFlow}
|
||
onSubmitMessage={(payload) => {
|
||
void submitBigFishMessage(payload);
|
||
}}
|
||
onExecuteAction={(payload) => {
|
||
void executeBigFishAction(payload);
|
||
}}
|
||
/>
|
||
</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,
|
||
)}
|
||
isGenerating={isBigFishBusy}
|
||
error={bigFishError}
|
||
onBack={leaveBigFishFlow}
|
||
onEditSetting={() => {
|
||
setSelectionStage('big-fish-agent-workspace');
|
||
}}
|
||
onRetry={() => {
|
||
void executeBigFishAction({
|
||
action: 'big_fish_compile_draft',
|
||
});
|
||
}}
|
||
onInterrupt={undefined}
|
||
backLabel="返回创作中心"
|
||
settingActionLabel={null}
|
||
retryLabel="重新生成草稿"
|
||
settingTitle="当前玩法信息"
|
||
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={() => {
|
||
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="正在加载抓大鹅共创工作区..." />
|
||
}
|
||
>
|
||
<Match3DAgentWorkspace
|
||
session={match3dSession}
|
||
streamingReplyText={streamingMatch3DReplyText}
|
||
isStreamingReply={isStreamingMatch3DReply}
|
||
isBusy={isMatch3DBusy || isStreamingMatch3DReply}
|
||
error={match3dError}
|
||
onBack={leaveMatch3DFlow}
|
||
onSubmitMessage={(payload) => {
|
||
void submitMatch3DMessage(payload);
|
||
}}
|
||
onExecuteAction={(payload) => {
|
||
void executeMatch3DAction(payload);
|
||
}}
|
||
/>
|
||
</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={() => {
|
||
setSelectionStage('match3d-agent-workspace');
|
||
}}
|
||
onSaved={(profile) => {
|
||
setMatch3DProfile(profile);
|
||
}}
|
||
onPublished={(profile) => {
|
||
setMatch3DProfile(profile);
|
||
void Promise.allSettled([
|
||
refreshMatch3DShelf(),
|
||
refreshMatch3DGallery(),
|
||
]);
|
||
openPublicWorkDetail(
|
||
mapMatch3DWorkToPublicWorkDetail(profile),
|
||
);
|
||
openPublishShareModal({
|
||
title: profile.gameName,
|
||
publicWorkCode: buildMatch3DPublicWorkCode(
|
||
profile.profileId,
|
||
),
|
||
stage: 'work-detail',
|
||
});
|
||
}}
|
||
onStartTestRun={(profile) => {
|
||
setMatch3DProfile(profile);
|
||
void startMatch3DRunFromProfile(profile, 'match3d-result');
|
||
}}
|
||
/>
|
||
</Suspense>
|
||
</motion.div>
|
||
)}
|
||
|
||
{selectionStage === 'match3d-runtime' && (
|
||
<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}
|
||
onBack={() => {
|
||
if (match3dRun?.runId && match3dRun.status === 'running') {
|
||
void stopMatch3DRun(match3dRun.runId).catch(
|
||
() => undefined,
|
||
);
|
||
}
|
||
setSelectionStage(match3dRuntimeReturnStage);
|
||
}}
|
||
onRestart={() => {
|
||
if (!match3dRun?.runId || isMatch3DBusy) {
|
||
return;
|
||
}
|
||
|
||
match3dFlow.setIsBusy(true);
|
||
setMatch3DError(null);
|
||
void restartMatch3DRun(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 clickMatch3DItem(runId, payload);
|
||
}}
|
||
onTimeExpired={() => {
|
||
if (!match3dRun?.runId) {
|
||
return;
|
||
}
|
||
|
||
void finishMatch3DTimeUp(match3dRun.runId)
|
||
.then(({ run }) => {
|
||
setMatch3DRun(run);
|
||
})
|
||
.catch((error) => {
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(
|
||
error,
|
||
'同步抓大鹅倒计时失败。',
|
||
),
|
||
);
|
||
});
|
||
}}
|
||
/>
|
||
</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="正在加载方洞挑战共创工作区..." />
|
||
}
|
||
>
|
||
<SquareHoleAgentWorkspace
|
||
session={squareHoleSession}
|
||
streamingReplyText={streamingSquareHoleReplyText}
|
||
isStreamingReply={isStreamingSquareHoleReply}
|
||
isBusy={isSquareHoleBusy || isStreamingSquareHoleReply}
|
||
error={squareHoleError}
|
||
onBack={leaveSquareHoleFlow}
|
||
onSubmitMessage={(payload) => {
|
||
void submitSquareHoleMessage(payload);
|
||
}}
|
||
onExecuteAction={(payload) => {
|
||
void executeSquareHoleAction(payload);
|
||
}}
|
||
/>
|
||
</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,
|
||
)}
|
||
isGenerating={isSquareHoleBusy}
|
||
error={squareHoleError}
|
||
onBack={leaveSquareHoleFlow}
|
||
onEditSetting={() => {
|
||
setSelectionStage('square-hole-agent-workspace');
|
||
}}
|
||
onRetry={retrySquareHoleAssetGeneration}
|
||
onInterrupt={undefined}
|
||
backLabel="返回创作中心"
|
||
settingActionLabel={null}
|
||
retryLabel="重新生成图片"
|
||
settingTitle="当前方洞挑战"
|
||
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={() => {
|
||
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 === '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="正在加载拼图创作..." />}
|
||
>
|
||
<PuzzleAgentWorkspace
|
||
session={puzzleSession}
|
||
isBusy={isPuzzleBusy || 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}
|
||
onPromptChange={setPuzzleOnboardingPrompt}
|
||
onSubmit={() => {
|
||
void submitPuzzleOnboardingPrompt();
|
||
}}
|
||
/>
|
||
</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="正在加载拼图生成面板..." />}
|
||
>
|
||
<CustomWorldGenerationView
|
||
settingText={
|
||
puzzleSession?.lastAssistantReply ?? '正在整理当前拼图草稿。'
|
||
}
|
||
anchorEntries={buildPuzzleGenerationAnchorEntries(
|
||
puzzleSession,
|
||
puzzleFormDraftPayload,
|
||
)}
|
||
progress={buildMiniGameDraftGenerationProgress(
|
||
puzzleGenerationState,
|
||
)}
|
||
isGenerating={isPuzzleBusy}
|
||
error={puzzleError}
|
||
onBack={leavePuzzleFlow}
|
||
onEditSetting={() => {
|
||
setSelectionStage('puzzle-agent-workspace');
|
||
}}
|
||
onRetry={retryPuzzleDraftGeneration}
|
||
onInterrupt={undefined}
|
||
backLabel="返回创作中心"
|
||
settingActionLabel={null}
|
||
retryLabel="重新生成草稿"
|
||
settingTitle="当前拼图信息"
|
||
settingDescription={null}
|
||
progressTitle="拼图草稿生成进度"
|
||
activeBadgeLabel="草稿生成中"
|
||
pausedBadgeLabel="草稿生成已暂停"
|
||
idleBadgeLabel="等待返回工作区"
|
||
/>
|
||
</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) => {
|
||
void executePuzzleAction(payload);
|
||
}}
|
||
onStartTestRun={startPuzzleTestRunFromDraft}
|
||
/>
|
||
</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,
|
||
);
|
||
}}
|
||
/>
|
||
</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={() => {
|
||
setSelectionStage(puzzleRuntimeReturnStage);
|
||
}}
|
||
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 ? (
|
||
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-slate-950/62 px-5 backdrop-blur-sm">
|
||
<div className="flex max-w-[18rem] flex-col items-center gap-3 rounded-[1.5rem] border border-white/12 bg-slate-950/92 px-6 py-5 text-center text-white shadow-[0_28px_80px_rgba(0,0,0,0.35)]">
|
||
<Loader2 className="h-6 w-6 animate-spin text-amber-200" />
|
||
<div className="text-sm font-bold">正在准备下一关</div>
|
||
<div className="text-xs leading-5 text-white/68">
|
||
广场暂无可接续作品,正在生成新的候选图。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
{puzzleOnboardingDraft &&
|
||
puzzleRun?.currentLevel?.status === 'cleared' ? (
|
||
<PuzzleOnboardingLoginOverlay
|
||
isSaving={isPuzzleOnboardingSaving}
|
||
error={puzzleOnboardingError}
|
||
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={leaveAgentDraftGeneration}
|
||
onEditSetting={leaveAgentDraftGeneration}
|
||
onRetry={() => {
|
||
void sessionController.executeAgentAction({
|
||
action: 'draft_foundation',
|
||
});
|
||
}}
|
||
onInterrupt={undefined}
|
||
backLabel="返回工作区"
|
||
settingActionLabel={null}
|
||
retryLabel="继续生成草稿"
|
||
settingTitle="当前世界信息"
|
||
settingDescription={null}
|
||
progressTitle="世界草稿生成进度"
|
||
activeBadgeLabel="草稿编译中"
|
||
pausedBadgeLabel="草稿生成已暂停"
|
||
idleBadgeLabel="等待返回工作区"
|
||
/>
|
||
</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>
|
||
|
||
<PlatformEntryCreationTypeModal
|
||
isOpen={showCreationTypeModal}
|
||
isBusy={
|
||
sessionController.isCreatingAgentSession ||
|
||
isBigFishBusy ||
|
||
isMatch3DBusy ||
|
||
isSquareHoleBusy ||
|
||
isPuzzleBusy
|
||
}
|
||
error={
|
||
bigFishError ??
|
||
match3dError ??
|
||
squareHoleError ??
|
||
puzzleCreationError ??
|
||
puzzleError ??
|
||
sessionController.creationTypeError
|
||
}
|
||
onClose={() => {
|
||
if (
|
||
sessionController.isCreatingAgentSession ||
|
||
isBigFishBusy ||
|
||
isMatch3DBusy ||
|
||
isSquareHoleBusy ||
|
||
isPuzzleBusy
|
||
) {
|
||
return;
|
||
}
|
||
setShowCreationTypeModal(false);
|
||
}}
|
||
onSelectRpg={() => {
|
||
runProtectedAction(() => {
|
||
void sessionController.openRpgAgentWorkspace();
|
||
});
|
||
}}
|
||
onSelectBigFish={() => {
|
||
runProtectedAction(() => {
|
||
void openBigFishAgentWorkspace();
|
||
});
|
||
}}
|
||
onSelectMatch3D={() => {
|
||
runProtectedAction(() => {
|
||
void openMatch3DAgentWorkspace();
|
||
});
|
||
}}
|
||
onSelectSquareHole={() => {
|
||
runProtectedAction(() => {
|
||
void openSquareHoleAgentWorkspace();
|
||
});
|
||
}}
|
||
onSelectPuzzle={() => {
|
||
runProtectedAction(() => {
|
||
void openPuzzleAgentWorkspace();
|
||
});
|
||
}}
|
||
/>
|
||
<PublishShareModal
|
||
open={Boolean(publishSharePayload)}
|
||
payload={publishSharePayload}
|
||
onClose={() => setPublishSharePayload(null)}
|
||
/>
|
||
<UnifiedModal
|
||
open={Boolean(pendingDeleteCreationWork)}
|
||
title="删除作品"
|
||
description={
|
||
pendingDeleteCreationWork
|
||
? `确认删除《${pendingDeleteCreationWork.title}》吗?`
|
||
: undefined
|
||
}
|
||
onClose={closeDeleteCreationWorkConfirmation}
|
||
closeDisabled={Boolean(deletingCreationWorkId)}
|
||
closeOnBackdrop={!deletingCreationWorkId}
|
||
size="sm"
|
||
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
|
||
panelClassName="platform-remap-surface rounded-[1.75rem]"
|
||
footer={
|
||
<>
|
||
<button
|
||
type="button"
|
||
onClick={closeDeleteCreationWorkConfirmation}
|
||
disabled={Boolean(deletingCreationWorkId)}
|
||
className="platform-button platform-button--ghost min-h-0 rounded-full px-4 py-2 text-sm"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={confirmDeleteCreationWork}
|
||
disabled={Boolean(deletingCreationWorkId)}
|
||
className="platform-button platform-button--danger min-h-0 rounded-full px-4 py-2 text-sm disabled:cursor-not-allowed disabled:opacity-55"
|
||
>
|
||
{deletingCreationWorkId ? '删除中' : '确认删除'}
|
||
</button>
|
||
</>
|
||
}
|
||
>
|
||
<div className="text-sm leading-6 text-[var(--platform-text-base)]">
|
||
{pendingDeleteCreationWork?.detail}
|
||
</div>
|
||
</UnifiedModal>
|
||
<AnimatePresence>
|
||
{(searchedPublicUser || publicSearchError) && (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/45 p-4"
|
||
>
|
||
<div className="platform-surface w-full max-w-md rounded-[1.6rem] p-5">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div>
|
||
<div className="text-sm font-semibold text-[var(--platform-text-soft)]">
|
||
公开编号搜索
|
||
</div>
|
||
<div className="mt-1 text-xl font-black text-[var(--platform-text-strong)]">
|
||
{publicSearchError ? '未找到结果' : '命中用户'}
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setSearchedPublicUser(null);
|
||
setPublicSearchError(null);
|
||
}}
|
||
className="platform-icon-button"
|
||
aria-label="关闭搜索结果"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
{publicSearchError ? (
|
||
<div className="mt-4 text-sm leading-6 text-[var(--platform-text-soft)]">
|
||
{publicSearchError}
|
||
</div>
|
||
) : searchedPublicUser ? (
|
||
<div className="mt-4 rounded-[1.2rem] border border-[var(--platform-line-soft)] p-4">
|
||
<div className="text-lg font-bold text-[var(--platform-text-strong)]">
|
||
{searchedPublicUser.displayName}
|
||
</div>
|
||
<div className="mt-2 text-sm text-[var(--platform-text-soft)]">
|
||
百梦号 {searchedPublicUser.publicUserCode}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export default PlatformEntryFlowShellImpl;
|