Files
Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
kdletters 86fc382413
Some checks failed
CI / verify (push) Has been cancelled
feat: add shared runtime input device layer
2026-05-10 17:50:00 +08:00

9228 lines
299 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ArrowRight, Loader2, Sparkles } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import {
type Dispatch,
lazy,
type SetStateAction,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
import type {
BigFishRuntimeSnapshotResponse,
BigFishSessionSnapshotResponse,
ExecuteBigFishActionRequest,
SendBigFishMessageRequest,
SubmitBigFishInputRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type {
CreativeAgentSessionSnapshot,
CreativeAgentSseEvent,
CreativeDraftEditResult,
StreamCreativeAgentMessageRequest,
} from '../../../packages/shared/src/contracts/creativeAgent';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
import type {
CreateMatch3DSessionRequest,
ExecuteMatch3DActionRequest,
Match3DActionResponse,
Match3DAgentSessionSnapshot,
Match3DSessionResponse,
SendMatch3DMessageRequest,
} from '../../../packages/shared/src/contracts/match3dAgent';
import type { Match3DRunSnapshot } from '../../../packages/shared/src/contracts/match3dRuntime';
import type {
Match3DWorkProfile,
Match3DWorkSummary,
} from '../../../packages/shared/src/contracts/match3dWorks';
import type {
PuzzleAgentActionRequest,
PuzzleAgentOperationRecord,
} from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
PuzzleDraftLevel,
PuzzleResultDraft,
} from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { PuzzleCreativeTemplateSelection } from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
import type {
PuzzleRunSnapshot,
PuzzleRuntimePropKind,
SubmitPuzzleLeaderboardRequest,
} from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
ProfilePlayedWorkSummary,
ProfilePlayStatsResponse,
ProfileSaveArchiveResumeResponse,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import type {
CreateSquareHoleSessionRequest,
ExecuteSquareHoleActionRequest,
SendSquareHoleMessageRequest,
SquareHoleActionResponse,
SquareHoleSessionResponse,
SquareHoleSessionSnapshot,
} from '../../../packages/shared/src/contracts/squareHoleAgent';
import type { SquareHoleRunSnapshot } from '../../../packages/shared/src/contracts/squareHoleRuntime';
import type {
SquareHoleWorkProfile,
SquareHoleWorkSummary,
} from '../../../packages/shared/src/contracts/squareHoleWorks';
import type {
CreateVisualNovelSessionRequest,
ExecuteVisualNovelAgentActionRequest,
SendVisualNovelMessageRequest,
VisualNovelAgentSessionSnapshot,
VisualNovelResultDraft,
VisualNovelRunSnapshot,
VisualNovelRuntimeActionRequest,
VisualNovelSessionResponse,
VisualNovelWorkDetail,
VisualNovelWorkSummary,
} from '../../../packages/shared/src/contracts/visualNovel';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
buildPublicWorkStagePath,
pushAppHistoryPath,
} from '../../routing/appPageRoutes';
import {
ApiClientError,
BACKGROUND_AUTH_REQUEST_OPTIONS,
} from '../../services/apiClient';
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 {
cancelCreativeAgentSession,
confirmCreativePuzzleTemplate,
createCreativeAgentSession,
streamCreativeAgentMessage,
streamCreativeDraftEdit,
} from '../../services/creative-agent';
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,
buildVisualNovelPublicWorkCode,
isSameBigFishPublicWorkCode,
isSameMatch3DPublicWorkCode,
isSamePuzzlePublicWorkCode,
isSameSquareHolePublicWorkCode,
isSameVisualNovelPublicWorkCode,
} from '../../services/publicWorkCode';
import {
createPuzzleAgentSession,
executePuzzleAgentAction,
getPuzzleAgentSession,
streamPuzzleAgentMessage,
} from '../../services/puzzle-agent';
import {
getPuzzleGalleryDetail,
likePuzzleGalleryWork,
listPuzzleGallery,
remixPuzzleGalleryWork,
} from '../../services/puzzle-gallery';
import {
generatePuzzleOnboardingWork,
savePuzzleOnboardingWork,
} from '../../services/puzzle-onboarding';
import {
advancePuzzleNextLevel,
startPuzzleRun,
submitPuzzleLeaderboard,
} from '../../services/puzzle-runtime';
import {
advanceLocalPuzzleLevel,
applyLocalPuzzleFreezeTime,
dragLocalPuzzlePiece,
extendLocalPuzzleTime,
isLocalPuzzleRun,
refreshLocalPuzzleTimer,
resolvePuzzleRestartLevelId,
restartLocalPuzzleLevel,
setLocalPuzzlePaused,
startLocalPuzzleRun,
submitLocalPuzzleLeaderboard,
swapLocalPuzzlePieces,
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
import {
claimPuzzleWorkPointIncentive,
deletePuzzleWork,
listPuzzleWorks,
updatePuzzleWork,
} from '../../services/puzzle-works';
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetailByCode,
likeRpgEntryWorldGallery,
recordRpgEntryWorldGalleryPlay,
remixRpgEntryWorldGallery,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import {
getRpgProfilePlayStats,
submitRpgProfileFeedback,
} from '../../services/rpg-entry/rpgProfileClient';
import { requestRpgRuntimeJson } from '../../services/rpg-runtime/rpgRuntimeRequest';
import { squareHoleCreationClient } from '../../services/square-hole-creation';
import {
dropSquareHoleShape,
finishSquareHoleTimeUp,
restartSquareHoleRun,
startSquareHoleRun,
stopSquareHoleRun,
} from '../../services/square-hole-runtime';
import {
deleteSquareHoleWork,
getSquareHoleWorkDetail,
listSquareHoleGallery,
listSquareHoleWorks,
} from '../../services/square-hole-works';
import {
compileVisualNovelWorkProfile,
createVisualNovelSession,
executeVisualNovelAction,
getVisualNovelSession,
streamVisualNovelMessage,
} from '../../services/visual-novel-creation';
import {
listVisualNovelGallery,
startVisualNovelRun,
streamVisualNovelRuntimeAction,
} from '../../services/visual-novel-runtime';
import {
deleteVisualNovelWork,
getVisualNovelWorkDetail,
listVisualNovelWorks,
publishVisualNovelWork,
updateVisualNovelWork,
} from '../../services/visual-novel-works';
import 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 { resolveCreativeAgentTargetSelectionStage } from '../creative-agent/creativeAgentViewModel';
import {
isBigFishGalleryEntry,
isMatch3DGalleryEntry,
isPuzzleGalleryEntry,
isSquareHoleGalleryEntry,
isVisualNovelGalleryEntry,
mapBigFishWorkToPlatformGalleryCard,
mapMatch3DWorkToPlatformGalleryCard,
mapPuzzleWorkToPlatformGalleryCard,
mapSquareHoleWorkToPlatformGalleryCard,
mapVisualNovelWorkToPlatformGalleryCard,
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 { createMockVisualNovelRunFromDraft } from '../visual-novel-runtime/visualNovelMockData';
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
import {
getVisiblePlatformCreationTypes,
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 PuzzleRuntimeAuthMode = 'default' | 'isolated';
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 RecommendRuntimeKind =
| 'big-fish'
| 'match3d'
| 'puzzle'
| 'square-hole'
| 'visual-novel'
| 'rpg';
type SquareHoleRuntimeReturnStage =
| 'square-hole-result'
| 'work-detail'
| 'platform';
type VisualNovelRuntimeReturnStage =
| 'visual-novel-result'
| 'visual-novel-gallery-detail'
| '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',
]);
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
BACKGROUND_AUTH_REQUEST_OPTIONS;
const PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS =
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
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'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
function getPlatformRecommendRuntimeKind(
entry: PlatformPublicGalleryCard,
): RecommendRuntimeKind {
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
if (isPuzzleGalleryEntry(entry)) {
return 'puzzle';
}
if (isMatch3DGalleryEntry(entry)) {
return 'match3d';
}
if (isSquareHoleGalleryEntry(entry)) {
return 'square-hole';
}
if (isVisualNovelGalleryEntry(entry)) {
return 'visual-novel';
}
return 'rpg';
}
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 mapVisualNovelWorkToPublicWorkDetail(
item: VisualNovelWorkSummary,
): PlatformPublicGalleryCard {
return mapVisualNovelWorkToPlatformGalleryCard(item);
}
function mapVisualNovelWorkDetailToSession(
work: VisualNovelWorkDetail,
): VisualNovelAgentSessionSnapshot {
return {
sessionId: work.sourceSessionId?.trim() || work.workId,
ownerUserId: work.summary.ownerUserId,
sourceMode: work.draft.sourceMode,
status: 'ready',
messages: [],
draft: work.draft,
pendingAction: null,
createdAt: work.createdAt,
updatedAt: work.summary.updatedAt,
};
}
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 escapePuzzleOnboardingSvgText(value: string) {
return value
.replace(/&/gu, '&amp;')
.replace(/</gu, '&lt;')
.replace(/>/gu, '&gt;')
.replace(/"/gu, '&quot;');
}
function buildPuzzleOnboardingFallbackImage(promptText: string) {
const trimmedPrompt = promptText.trim();
const displayPrompt = escapePuzzleOnboardingSvgText(
trimmedPrompt.slice(0, 12) || '百梦拼图',
);
return (
'data:image/svg+xml;utf8,' +
encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fde68a" />
<stop offset="0.48" stop-color="#38bdf8" />
<stop offset="1" stop-color="#312e81" />
</linearGradient>
<radialGradient id="glow" cx="34%" cy="30%" r="42%">
<stop offset="0" stop-color="#fff7ed" stop-opacity="0.86" />
<stop offset="1" stop-color="#fff7ed" stop-opacity="0" />
</radialGradient>
</defs>
<rect width="1024" height="1024" fill="url(#sky)" />
<circle cx="352" cy="292" r="248" fill="url(#glow)" />
<path d="M0 654 C156 570 296 680 452 610 C626 532 782 598 1024 500 V1024 H0 Z" fill="#0f172a" opacity="0.88" />
<path d="M0 802 C196 710 332 838 540 750 C704 682 846 724 1024 670 V1024 H0 Z" fill="#111827" opacity="0.72" />
<path d="M160 344 C242 268 340 282 430 350 C532 428 646 388 752 322 C844 266 918 302 958 362" fill="none" stroke="#fff7ed" stroke-width="18" stroke-linecap="round" opacity="0.72" />
<path d="M188 538 H836" stroke="#ffffff" stroke-width="16" stroke-linecap="round" opacity="0.26" />
<path d="M260 620 H764" stroke="#ffffff" stroke-width="12" stroke-linecap="round" opacity="0.2" />
<text x="512" y="868" text-anchor="middle" font-family="sans-serif" font-size="54" font-weight="800" fill="#ffffff" opacity="0.84">${displayPrompt}</text>
</svg>`)
);
}
function buildPuzzleOnboardingFallbackWork(
promptText: string,
): PuzzleWorkSummary {
const now = new Date().toISOString();
const seed = Date.now();
const coverImageSrc = buildPuzzleOnboardingFallbackImage(promptText);
const level: PuzzleDraftLevel = {
levelId: 'onboarding-local-level-1',
levelName: '梦境拼图',
pictureDescription: promptText,
pictureReference: null,
candidates: [
{
candidateId: 'onboarding-local-candidate-1',
imageSrc: coverImageSrc,
assetId: 'onboarding-local-asset-1',
prompt: promptText,
actualPrompt: promptText,
sourceType: 'generated',
selected: true,
},
],
selectedCandidateId: 'onboarding-local-candidate-1',
coverImageSrc,
coverAssetId: 'onboarding-local-asset-1',
generationStatus: 'ready',
};
return {
workId: `onboarding-local-work-${seed}`,
profileId: `onboarding-local-profile-${seed}`,
ownerUserId: 'onboarding-guest',
sourceSessionId: null,
authorDisplayName: '百梦主',
workTitle: '梦境拼图',
workDescription: promptText,
levelName: level.levelName,
summary: promptText,
themeTags: ['新手引导', '拼图'],
coverImageSrc,
coverAssetId: level.coverAssetId,
publicationStatus: 'draft',
updatedAt: now,
publishedAt: null,
playCount: 0,
remixCount: 0,
likeCount: 0,
publishReady: true,
levels: [level],
};
}
function shouldUseLocalPuzzleOnboardingFallback(error: unknown) {
return (
error instanceof ApiClientError &&
error.status === 404 &&
(error.code === 'NOT_FOUND' || error.message.includes('资源不存在'))
);
}
function isMissingPuzzleWorkError(error: unknown) {
return (
(error instanceof ApiClientError &&
error.status === 404 &&
(error.code === 'NOT_FOUND' ||
error.message.includes('资源不存在') ||
error.message.includes('未找到'))) ||
(error instanceof Error &&
(error.message.includes('资源不存在') ||
error.message.includes('未找到拼图作品')))
);
}
function 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,
onSkip,
}: {
prompt: string;
phase: PuzzleOnboardingPhase;
error: string | null;
onPromptChange: (value: string) => void;
onSubmit: () => void;
onSkip: () => 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" />
<button
type="button"
disabled={isGenerating}
onClick={onSkip}
className="absolute right-4 top-4 z-10 inline-flex min-h-10 items-center justify-center rounded-full border border-white/14 bg-black/24 px-4 text-sm font-black text-white/86 shadow-[0_12px_28px_rgba(0,0,0,0.22)] backdrop-blur transition hover:border-amber-200/45 hover:text-amber-100 disabled:cursor-not-allowed disabled:opacity-45 sm:right-6 sm:top-6"
>
</button>
<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,
aiRedraw: payload?.aiRedraw ?? true,
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,
aiRedraw: true,
};
}
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)
: (payload.referenceImageSrc ?? null),
imageModel:
payload.action === 'compile_puzzle_draft'
? (payload.imageModel ?? null)
: (payload.imageModel ?? null),
aiRedraw:
payload.action === 'compile_puzzle_draft'
? (payload.aiRedraw ?? true)
: (payload.aiRedraw ?? true),
};
}
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 CreativeAgentWorkspace = lazy(async () => {
const module = await import('../creative-agent/CreativeAgentWorkspace');
return {
default: module.CreativeAgentWorkspace,
};
});
const VisualNovelAgentWorkspace = lazy(async () => {
const module = await import(
'../visual-novel-creation/VisualNovelAgentWorkspace'
);
return {
default: module.VisualNovelAgentWorkspace,
};
});
const VisualNovelResultView = lazy(async () => {
const module = await import('../visual-novel-result/VisualNovelResultView');
return {
default: module.VisualNovelResultView,
};
});
const VisualNovelRuntimeShell = lazy(async () => {
const module = await import('../visual-novel-runtime/VisualNovelRuntimeShell');
return {
default: module.VisualNovelRuntimeShell,
};
});
const PuzzleResultView = lazy(async () => {
const module = await import('../puzzle-result/PuzzleResultView');
return {
default: module.PuzzleResultView,
};
});
const PuzzleGalleryDetailView = lazy(async () => {
const module = await import('../puzzle-gallery/PuzzleGalleryDetailView');
return {
default: module.PuzzleGalleryDetailView,
};
});
const PuzzleRuntimeShell = lazy(async () => {
const module = await import('../puzzle-runtime/PuzzleRuntimeShell');
return {
default: module.PuzzleRuntimeShell,
};
});
function LazyPanelFallback({ label }: { label: string }) {
return (
<div className="flex h-full min-h-0 items-center justify-center">
<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 [activeRecommendEntryKey, setActiveRecommendEntryKey] = useState<
string | null
>(null);
const [activeRecommendRuntimeKind, setActiveRecommendRuntimeKind] =
useState<RecommendRuntimeKind | null>(null);
const [activeRecommendRuntimeError, setActiveRecommendRuntimeError] =
useState<string | null>(null);
const [isStartingRecommendEntry, setIsStartingRecommendEntry] =
useState(false);
const recommendRuntimeStartRequestRef = useRef(0);
const [, 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 [puzzleRuntimeAuthMode, setPuzzleRuntimeAuthMode] =
useState<PuzzleRuntimeAuthMode>('default');
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 errorSetterRefNoop = useMemo(
() =>
((_: SetStateAction<string | null>) => undefined) as Dispatch<
SetStateAction<string | null>
>,
[],
);
const bigFishErrorSetterRef = useRef(errorSetterRefNoop);
const match3DErrorSetterRef = useRef(errorSetterRefNoop);
const squareHoleErrorSetterRef = useRef(errorSetterRefNoop);
const puzzleErrorSetterRef = useRef(errorSetterRefNoop);
const visualNovelErrorSetterRef = useRef(errorSetterRefNoop);
const [isPuzzleLoadingLibrary, setIsPuzzleLoadingLibrary] = useState(false);
const [puzzleShelfError, setPuzzleShelfError] = useState<string | null>(null);
const [puzzleCreationError, setPuzzleCreationError] = useState<string | null>(
null,
);
const [puzzleGenerationState, setPuzzleGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const [puzzleGenerationProgressNowMs, setPuzzleGenerationProgressNowMs] =
useState(() => Date.now());
const [puzzleFormDraftPayload, setPuzzleFormDraftPayload] =
useState<CreatePuzzleAgentSessionRequest | null>(null);
const [puzzleOnboardingPrompt, setPuzzleOnboardingPrompt] = useState('');
const [puzzleOnboardingPhase, setPuzzleOnboardingPhase] =
useState<PuzzleOnboardingPhase>('input');
const [puzzleOnboardingDraft, setPuzzleOnboardingDraft] =
useState<PuzzleOnboardingDraft | null>(null);
const [puzzleOnboardingError, setPuzzleOnboardingError] = useState<
string | null
>(null);
const [isPuzzleOnboardingSaving, setIsPuzzleOnboardingSaving] =
useState(false);
const [creativeAgentSession, setCreativeAgentSession] =
useState<CreativeAgentSessionSnapshot | null>(null);
const [creativeAgentError, setCreativeAgentError] = useState<string | null>(
null,
);
const [isCreativeAgentBusy, setIsCreativeAgentBusy] = useState(false);
const [isCreativeAgentStreaming, setIsCreativeAgentStreaming] =
useState(false);
const [creativeAgentEvents, setCreativeAgentEvents] = useState<
CreativeAgentSseEvent[]
>([]);
const [activeCreativeAgentSessionId, setActiveCreativeAgentSessionId] =
useState<string | null>(null);
const [creativeDraftEditError, setCreativeDraftEditError] = useState<
string | null
>(null);
const [isCreativeDraftEditBusy, setIsCreativeDraftEditBusy] = useState(false);
const [visualNovelWork, setVisualNovelWork] =
useState<VisualNovelWorkDetail | null>(null);
const [visualNovelWorks, setVisualNovelWorks] = useState<
VisualNovelWorkSummary[]
>([]);
const [visualNovelGalleryEntries, setVisualNovelGalleryEntries] = useState<
VisualNovelWorkSummary[]
>([]);
const [visualNovelRun, setVisualNovelRun] =
useState<VisualNovelRunSnapshot | null>(null);
const [visualNovelRuntimeReturnStage, setVisualNovelRuntimeReturnStage] =
useState<VisualNovelRuntimeReturnStage>('visual-novel-result');
const [isVisualNovelLoadingLibrary, setIsVisualNovelLoadingLibrary] =
useState(false);
const [isPuzzleNextLevelGenerating, setIsPuzzleNextLevelGenerating] =
useState(false);
const [isSearchingPublicCode, setIsSearchingPublicCode] = useState(false);
const [publicSearchError, setPublicSearchError] = useState<string | null>(
null,
);
const [searchedPublicUser, setSearchedPublicUser] =
useState<PublicUserSummary | null>(null);
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
string | null
>(null);
const [pendingDeleteCreationWork, setPendingDeleteCreationWork] =
useState<DeleteCreationWorkConfirmation | null>(null);
const [
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 enterDraftTab = useCallback(() => {
setPlatformTab('saves');
}, [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);
bigFishErrorSetterRef.current(null);
} catch (error) {
bigFishErrorSetterRef.current(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
);
} finally {
setIsBigFishLoadingLibrary(false);
}
}, [resolveBigFishErrorMessage]);
const refreshBigFishGallery = useCallback(async () => {
try {
const galleryResponse = await listBigFishGallery();
setBigFishGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch (error) {
setBigFishGalleryEntries([]);
bigFishErrorSetterRef.current(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼广场失败。'),
);
return [];
}
}, [resolveBigFishErrorMessage]);
const refreshMatch3DShelf = useCallback(async () => {
setIsMatch3DLoadingLibrary(true);
try {
const worksResponse = await listMatch3DWorks();
setMatch3DWorks(worksResponse.items);
match3DErrorSetterRef.current(null);
} catch (error) {
match3DErrorSetterRef.current(
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);
squareHoleErrorSetterRef.current(null);
} catch (error) {
squareHoleErrorSetterRef.current(
resolveSquareHoleErrorMessage(error, '读取方洞挑战作品列表失败。'),
);
} finally {
setIsSquareHoleLoadingLibrary(false);
}
}, [resolveSquareHoleErrorMessage]);
const refreshSquareHoleGallery = useCallback(async () => {
try {
const galleryResponse = await listSquareHoleGallery();
setSquareHoleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch {
setSquareHoleGalleryEntries([]);
return [];
}
}, []);
const refreshPuzzleShelf = useCallback(async () => {
setIsPuzzleLoadingLibrary(true);
try {
const worksResponse = await listPuzzleWorks();
setPuzzleWorks(worksResponse.items);
setPuzzleShelfError(null);
} catch (error) {
setPuzzleShelfError(
resolvePuzzleErrorMessage(error, '读取拼图作品列表失败。'),
);
} finally {
setIsPuzzleLoadingLibrary(false);
}
}, [resolvePuzzleErrorMessage]);
const refreshPuzzleGallery = useCallback(async () => {
try {
const galleryResponse = await listPuzzleGallery();
setPuzzleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch (error) {
setPuzzleGalleryEntries([]);
puzzleErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '读取拼图广场失败。'),
);
return [];
}
}, [resolvePuzzleErrorMessage]);
const refreshVisualNovelShelf = useCallback(async () => {
setIsVisualNovelLoadingLibrary(true);
try {
const worksResponse = await listVisualNovelWorks();
setVisualNovelWorks(worksResponse.works);
visualNovelErrorSetterRef.current(null);
return worksResponse.works;
} catch (error) {
visualNovelErrorSetterRef.current(
resolvePuzzleErrorMessage(error, '读取视觉小说作品列表失败。'),
);
return [];
} finally {
setIsVisualNovelLoadingLibrary(false);
}
}, [resolvePuzzleErrorMessage]);
const refreshVisualNovelGallery = useCallback(async () => {
try {
const galleryResponse = await listVisualNovelGallery();
setVisualNovelGalleryEntries(galleryResponse.works);
return galleryResponse.works;
} catch (error) {
setVisualNovelGalleryEntries([]);
visualNovelErrorSetterRef.current(
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,
setPlatformTabToDraft: enterDraftTab,
setPlatformError: platformBootstrap.setPlatformError,
appendBrowseHistoryEntry: platformBootstrap.appendBrowseHistoryEntry,
refreshCustomWorldWorks: platformBootstrap.refreshCustomWorldWorks,
refreshPublishedGallery: platformBootstrap.refreshPublishedGallery,
persistAgentUiState: sessionController.persistAgentUiState,
syncAgentCreationResultView: sessionController.syncAgentCreationResultView,
buildDraftResultProfile: (view) =>
rpgCreationPreviewAdapter.buildPreviewFromResultView(view),
suppressAgentDraftResultAutoOpen:
sessionController.suppressAgentDraftResultAutoOpen,
releaseAgentDraftResultAutoOpenSuppression:
sessionController.releaseAgentDraftResultAutoOpenSuppression,
resetAutoSaveTrackingToIdle:
autosaveCoordinator.resetAutoSaveTrackingToIdle,
markAutoSavedProfile: autosaveCoordinator.markAutoSavedProfile,
});
const enterWorldCoordinator = useRpgCreationEnterWorld({
isAgentDraftResultView: sessionController.isAgentDraftResultView,
activeAgentSessionId: sessionController.activeAgentSessionId,
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,
);
const visualNovelPublicEntries = visualNovelGalleryEntries.map(
mapVisualNovelWorkToPlatformGalleryCard,
);
return mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
[
...bigFishPublicEntries,
...match3dPublicEntries,
...puzzlePublicEntries,
...squareHolePublicEntries,
...visualNovelPublicEntries,
],
).slice(0, 6);
}, [
isBigFishCreationVisible,
bigFishGalleryEntries,
match3dGalleryEntries,
platformBootstrap.publishedGalleryEntries,
puzzleGalleryEntries,
squareHoleGalleryEntries,
visualNovelGalleryEntries,
]);
const latestGalleryEntries = useMemo(
() =>
mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
[
...(isBigFishCreationVisible
? bigFishGalleryEntries.map(mapBigFishWorkToPlatformGalleryCard)
: []),
...match3dGalleryEntries.map(mapMatch3DWorkToPlatformGalleryCard),
...puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
...squareHoleGalleryEntries.map(
mapSquareHoleWorkToPlatformGalleryCard,
),
...visualNovelGalleryEntries.map(
mapVisualNovelWorkToPlatformGalleryCard,
),
],
),
[
isBigFishCreationVisible,
bigFishGalleryEntries,
match3dGalleryEntries,
platformBootstrap.publishedGalleryEntries,
puzzleGalleryEntries,
squareHoleGalleryEntries,
visualNovelGalleryEntries,
],
);
const recommendRuntimeEntries = useMemo(
() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredGalleryEntries, ...latestGalleryEntries].forEach((entry) => {
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
});
return Array.from(entryMap.values());
},
[featuredGalleryEntries, latestGalleryEntries],
);
const creationHubItems = useMemo<CustomWorldWorkSummary[]>(
() =>
platformBootstrap.customWorldWorkEntries.length > 0
? platformBootstrap.customWorldWorkEntries
: buildCreationHubFallbackItems(
platformBootstrap.savedCustomWorldEntries,
),
[
platformBootstrap.customWorldWorkEntries,
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,
]);
useEffect(() => {
const shouldTickPuzzleProgress =
selectionStage === 'puzzle-generating' &&
puzzleGenerationState != null &&
puzzleGenerationState.phase !== 'ready' &&
puzzleGenerationState.phase !== 'failed';
if (!shouldTickPuzzleProgress) {
return undefined;
}
setPuzzleGenerationProgressNowMs(Date.now());
const timerId = window.setInterval(() => {
setPuzzleGenerationProgressNowMs(Date.now());
}, 500);
return () => window.clearInterval(timerId);
}, [puzzleGenerationState, selectionStage]);
const runProtectedAction = useCallback(
(action: () => void) => {
if (!authUi?.requireAuth) {
action();
return;
}
authUi.requireAuth(action);
},
[authUi],
);
const savePuzzleOnboardingDraft = useCallback(async () => {
if (!puzzleOnboardingDraft || isPuzzleOnboardingSaving) {
return;
}
setIsPuzzleOnboardingSaving(true);
setPuzzleOnboardingError(null);
try {
const response = await savePuzzleOnboardingWork({
promptText: puzzleOnboardingDraft.promptText,
item: puzzleOnboardingDraft.item,
});
setPuzzleWorks((current) => [response.item, ...current]);
setSelectedPuzzleDetail(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleOnboardingDraft(null);
setPuzzleOnboardingPrompt('');
setPuzzleOnboardingPhase('input');
platformBootstrap.setPlatformTab('home');
setSelectionStage('platform');
void refreshPuzzleShelf();
} catch (error) {
setPuzzleOnboardingError(
resolvePuzzleErrorMessage(error, '保存新手引导拼图失败。'),
);
} finally {
setIsPuzzleOnboardingSaving(false);
}
}, [
isPuzzleOnboardingSaving,
platformBootstrap,
puzzleOnboardingDraft,
refreshPuzzleShelf,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
const requestPuzzleOnboardingLogin = useCallback(() => {
if (isPuzzleOnboardingSaving) {
return;
}
authUi?.openLoginModal(() => {
void savePuzzleOnboardingDraft();
});
}, [authUi, isPuzzleOnboardingSaving, savePuzzleOnboardingDraft]);
const skipPuzzleOnboarding = useCallback(() => {
markPuzzleOnboardingSeen();
setPuzzleOnboardingDraft(null);
setPuzzleOnboardingPrompt('');
setPuzzleOnboardingPhase('input');
setPuzzleOnboardingError(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setSelectedPuzzleDetail(null);
platformBootstrap.setPlatformTab(authUi?.user ? 'home' : 'category');
setSelectionStage('platform');
}, [authUi?.user, platformBootstrap, setSelectionStage]);
useEffect(() => {
if (
!authUi ||
authUi?.user ||
selectionStage !== 'platform' ||
hasSeenPuzzleOnboarding()
) {
return;
}
setPuzzleOnboardingPhase('input');
setPuzzleOnboardingError(null);
setSelectionStage('puzzle-onboarding');
}, [authUi, authUi?.user, selectionStage, setSelectionStage]);
const submitPuzzleOnboardingPrompt = useCallback(async () => {
const promptText = puzzleOnboardingPrompt.trim();
if (!promptText || puzzleOnboardingPhase === 'generating') {
return;
}
setPuzzleOnboardingPhase('generating');
setPuzzleOnboardingError(null);
try {
let item: PuzzleWorkSummary;
try {
const response = await generatePuzzleOnboardingWork({ promptText });
item = {
...response.item,
levels:
response.item.levels && response.item.levels.length > 0
? response.item.levels
: [response.level],
};
} catch (error) {
if (!shouldUseLocalPuzzleOnboardingFallback(error)) {
throw error;
}
// 中文注释:旧后端或资源路由尚未更新时,首访体验继续用前端临时图面兜底,不写入作品库。
item = buildPuzzleOnboardingFallbackWork(promptText);
}
setPuzzleOnboardingDraft({ promptText, item });
setSelectedPuzzleDetail(item);
setPuzzleOnboardingPhase('generated');
markPuzzleOnboardingSeen();
window.setTimeout(() => {
setPuzzleRun(startLocalPuzzleRun(item));
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('platform');
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 visualNovelFlow = usePlatformCreationAgentFlowController<
VisualNovelAgentSessionSnapshot,
CreateVisualNovelSessionRequest,
VisualNovelSessionResponse,
SendVisualNovelMessageRequest,
ExecuteVisualNovelAgentActionRequest,
VisualNovelSessionResponse
>({
client: {
createSession: createVisualNovelSession,
getSession: getVisualNovelSession,
streamMessage: streamVisualNovelMessage,
executeAction: executeVisualNovelAction,
selectSession: (response) => response.session,
},
createPayload: {
sourceMode: 'idea',
seedText: null,
sourceAssetIds: [],
},
workspaceStage: 'visual-novel-agent-workspace',
resultStage: 'visual-novel-result',
platformStage: 'platform',
isCompileAction: (payload) => payload.kind === 'compile_work_profile',
resolveErrorMessage: resolvePuzzleErrorMessage,
errorMessages: {
open: '开启视觉小说共创工作台失败。',
restoreMissingSession: '这份视觉小说草稿缺少会话信息,请重新开始创作。',
restore: '读取视觉小说创作草稿失败。',
submit: '发送视觉小说共创消息失败。',
execute: '执行视觉小说操作失败。',
},
enterCreateTab,
setSelectionStage,
onSessionOpened: () => {
setShowCreationTypeModal(false);
},
onActionComplete: ({ response, setSession }) => {
setSession(response.session);
},
});
const bigFishSession = bigFishFlow.session;
const bigFishError = bigFishFlow.error;
const setBigFishError = bigFishFlow.setError;
bigFishErrorSetterRef.current = setBigFishError;
const isBigFishBusy = bigFishFlow.isBusy;
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
const match3dSession = match3dFlow.session;
const match3dError = match3dFlow.error;
const setMatch3DSession = match3dFlow.setSession;
const setMatch3DError = match3dFlow.setError;
match3DErrorSetterRef.current = setMatch3DError;
const isMatch3DBusy = match3dFlow.isBusy;
const 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;
squareHoleErrorSetterRef.current = setSquareHoleError;
const isSquareHoleBusy = squareHoleFlow.isBusy;
const streamingSquareHoleReplyText = squareHoleFlow.streamingReplyText;
const setStreamingSquareHoleReplyText = squareHoleFlow.setStreamingReplyText;
const isStreamingSquareHoleReply = squareHoleFlow.isStreamingReply;
const setIsStreamingSquareHoleReply = squareHoleFlow.setIsStreamingReply;
const puzzleSession = puzzleFlow.session;
const puzzleError = puzzleFlow.error;
const setPuzzleError = puzzleFlow.setError;
puzzleErrorSetterRef.current = setPuzzleError;
const isPuzzleBusy = puzzleFlow.isBusy;
const setIsPuzzleBusy = puzzleFlow.setIsBusy;
const isStreamingPuzzleReply = puzzleFlow.isStreamingReply;
const visualNovelSession = visualNovelFlow.session;
const visualNovelError = visualNovelFlow.error;
const setVisualNovelSession = visualNovelFlow.setSession;
const setVisualNovelError = visualNovelFlow.setError;
visualNovelErrorSetterRef.current = setVisualNovelError;
const isVisualNovelBusy = visualNovelFlow.isBusy;
const setIsVisualNovelBusy = visualNovelFlow.setIsBusy;
const visualNovelStreamingReplyText = visualNovelFlow.streamingReplyText;
const isVisualNovelStreamingReply = visualNovelFlow.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);
setPuzzleRuntimeAuthMode('default');
setPuzzleOperation(null);
setPuzzleGenerationState(null);
setPuzzleFormDraftPayload(null);
sessionController.setCreationTypeError(null);
setPuzzleCreationError(null);
const nextSession = await puzzleFlow.openWorkspace({});
if (nextSession) {
void refreshPuzzleShelf();
}
}, [puzzleFlow, refreshPuzzleShelf, sessionController]);
const openVisualNovelAgentWorkspace = useCallback(() => {
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
visualNovelFlow.resetTransientState();
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('visual-novel-agent-workspace');
}, [enterCreateTab, setSelectionStage, visualNovelFlow]);
const leaveCreativeAgentWorkspace = useCallback(() => {
const sessionId = creativeAgentSession?.sessionId?.trim();
if (sessionId && creativeAgentSession?.stage !== 'target_ready') {
void cancelCreativeAgentSession(sessionId).catch(() => undefined);
}
setCreativeAgentSession(null);
setCreativeAgentEvents([]);
setActiveCreativeAgentSessionId(null);
setCreativeAgentError(null);
setIsCreativeAgentBusy(false);
setIsCreativeAgentStreaming(false);
setCreativeDraftEditError(null);
setIsCreativeDraftEditBusy(false);
enterCreateTab();
setSelectionStage('platform');
}, [creativeAgentSession, enterCreateTab, setSelectionStage]);
const openCreativeAgentWorkspace = useCallback(async () => {
if (isCreativeAgentBusy || isCreativeAgentStreaming) {
return;
}
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleOperation(null);
setPuzzleGenerationState(null);
setPuzzleFormDraftPayload(null);
setCreativeAgentError(null);
setCreativeAgentEvents([]);
setIsCreativeAgentBusy(true);
try {
const response = await createCreativeAgentSession({
entryContext: 'creation_home',
});
setCreativeAgentSession(response.session);
setActiveCreativeAgentSessionId(response.session.sessionId);
enterCreateTab();
setShowCreationTypeModal(false);
setSelectionStage('creative-agent-workspace');
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '开启智能创作工作区失败。'),
);
} finally {
setIsCreativeAgentBusy(false);
}
}, [
enterCreateTab,
isCreativeAgentBusy,
isCreativeAgentStreaming,
resolvePuzzleErrorMessage,
setSelectionStage,
]);
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 ?? '',
referenceImageSrc: payload.referenceImageSrc ?? null,
imageModel: payload.imageModel ?? null,
aiRedraw: payload.aiRedraw ?? true,
});
setPuzzleOperation(response.operation);
puzzleFlow.setSession(response.session);
setPuzzleError(null);
void refreshPuzzleShelf();
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '保存拼图表单草稿失败。'),
);
}
},
[puzzleFlow, refreshPuzzleShelf, resolvePuzzleErrorMessage, setPuzzleError],
);
useEffect(() => {
if (platformBootstrap.canReadProtectedData) {
hadReadableProtectedDataRef.current = true;
return;
}
if (authUi?.user || !hadReadableProtectedDataRef.current) {
return;
}
hadReadableProtectedDataRef.current = false;
// 创作中心只展示当前登录用户的私有作品。
// 一旦退出登录或鉴权上下文被收回,三类作品缓存必须同步清空,不能等刷新页面。
setShowCreationTypeModal(false);
setSelectedDetailEntry(null);
setSelectedPublicWorkDetail(null);
setPublicWorkDetailError(null);
setIsPublicWorkDetailBusy(false);
setBigFishWorks([]);
setBigFishRun(null);
setBigFishRuntimeShare(null);
setBigFishRuntimeWork(null);
setBigFishRuntimeStartedAt(null);
setBigFishRuntimeReturnStage('platform');
setBigFishGenerationState(null);
setBigFishError(null);
setMatch3DSession(null);
setMatch3DProfile(null);
setMatch3DWorks([]);
setMatch3DGalleryEntries([]);
setMatch3DRun(null);
setMatch3DRuntimeReturnStage('match3d-result');
setMatch3DError(null);
setStreamingMatch3DReplyText('');
setIsStreamingMatch3DReply(false);
setSquareHoleSession(null);
setSquareHoleProfile(null);
setSquareHoleWorks([]);
setSquareHoleGalleryEntries([]);
setSquareHoleRun(null);
setSquareHoleRuntimeReturnStage('square-hole-result');
setSquareHoleGenerationState(null);
setSquareHoleError(null);
setStreamingSquareHoleReplyText('');
setIsStreamingSquareHoleReply(false);
setCreativeAgentSession(null);
setCreativeAgentEvents([]);
setActiveCreativeAgentSessionId(null);
setCreativeAgentError(null);
setIsCreativeAgentBusy(false);
setIsCreativeAgentStreaming(false);
setCreativeDraftEditError(null);
setIsCreativeDraftEditBusy(false);
setPuzzleOperation(null);
setPuzzleWorks([]);
setSelectedPuzzleDetail(null);
setPuzzleRuntimeReturnStage('puzzle-gallery-detail');
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleGenerationState(null);
setIsPuzzleNextLevelGenerating(false);
setPuzzleShelfError(null);
setPuzzleCreationError(null);
setPuzzleError(null);
setVisualNovelSession(null);
setVisualNovelWork(null);
setVisualNovelWorks([]);
setVisualNovelGalleryEntries([]);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setVisualNovelError(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 !== 'creative-agent-workspace' &&
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,
setVisualNovelError,
setVisualNovelSession,
]);
const handleCreationHubCreateType = useCallback(
(type: PlatformCreationTypeId) => {
if (type === 'airp') {
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();
});
return;
}
if (type === 'visual-novel') {
runProtectedAction(() => {
openVisualNovelAgentWorkspace();
});
return;
}
},
[
openBigFishAgentWorkspace,
openMatch3DAgentWorkspace,
openPuzzleAgentWorkspace,
openSquareHoleAgentWorkspace,
openVisualNovelAgentWorkspace,
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);
setPuzzleRuntimeAuthMode('default');
setPuzzleGenerationState(null);
setIsPuzzleNextLevelGenerating(false);
setActiveCreativeAgentSessionId(null);
setCreativeDraftEditError(null);
puzzleFlow.leaveFlow();
}, [puzzleFlow]);
const leaveVisualNovelFlow = useCallback(() => {
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
visualNovelFlow.leaveFlow();
}, [visualNovelFlow]);
const openVisualNovelResult = useCallback(
(session: VisualNovelAgentSessionSnapshot) => {
setVisualNovelSession(session);
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-result');
},
[setSelectionStage, setVisualNovelSession],
);
const saveVisualNovelDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const currentSession = visualNovelSession;
if (!currentSession || !currentSession.sessionId.trim()) {
return;
}
setVisualNovelError(null);
const now = new Date().toISOString();
// 中文注释VN-07 的保存草稿先保证结果页编辑不会丢失;
// 后端 work profile 编译仍由“编译草稿”显式触发,避免把本地保存伪装成正式持久化。
setVisualNovelSession({
...currentSession,
draft,
status: draft.publishReady ? 'ready' : currentSession.status,
updatedAt: now,
});
if (!visualNovelWork) {
return;
}
setIsVisualNovelBusy(true);
try {
const response = await updateVisualNovelWork(
visualNovelWork.summary.profileId,
{ draft },
);
setVisualNovelWork(response.work);
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '保存视觉小说草稿失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
setVisualNovelSession,
visualNovelSession,
visualNovelWork,
],
);
const compileVisualNovelDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const sessionId = visualNovelSession?.sessionId?.trim();
if (!sessionId) {
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const response = await compileVisualNovelWorkProfile(sessionId, {
draft,
});
setVisualNovelSession(response.session);
setVisualNovelWork(response.work);
setSelectionStage('visual-novel-result');
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '编译视觉小说草稿失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
setVisualNovelSession,
visualNovelSession?.sessionId,
],
);
const publishVisualNovelDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const profileId = draft.profileId?.trim();
if (!profileId) {
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const { work } = await updateVisualNovelWork(profileId, {
draft,
});
setVisualNovelWork(work);
const publishedResponse = await publishVisualNovelWork(profileId);
setVisualNovelWork(publishedResponse.work);
setSelectedPublicWorkDetail(
mapVisualNovelWorkToPublicWorkDetail(publishedResponse.work.summary),
);
await Promise.allSettled([
refreshVisualNovelShelf(),
refreshVisualNovelGallery(),
]);
openPublishShareModal({
title: publishedResponse.work.summary.title,
publicWorkCode: buildVisualNovelPublicWorkCode(
publishedResponse.work.summary.profileId,
),
stage: 'work-detail',
});
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '发布视觉小说作品失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
openPublishShareModal,
refreshVisualNovelGallery,
refreshVisualNovelShelf,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
],
);
const startVisualNovelTestRunFromDraft = useCallback(
async (draft: VisualNovelResultDraft) => {
const profileId =
visualNovelWork?.summary.profileId?.trim() ?? draft.profileId?.trim();
if (!profileId) {
const run = createMockVisualNovelRunFromDraft(draft);
setVisualNovelRun(run);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-runtime');
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const { run } = await startVisualNovelRun(profileId, {
profileId,
mode: 'test',
});
setVisualNovelRun(run);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-runtime');
} catch (error) {
// 中文注释VN-07 只要求结果页进入测试运行时;真实 runtime 接口未就绪时,
// 使用当前 draft 生成本地 test run不扩展到正式玩家运行链路。
setVisualNovelRun(createMockVisualNovelRunFromDraft(draft));
setVisualNovelRuntimeReturnStage('visual-novel-result');
setSelectionStage('visual-novel-runtime');
setVisualNovelError(
resolvePuzzleErrorMessage(error, '已进入本地试玩,真实运行接口暂不可用。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
setSelectionStage,
visualNovelWork?.summary.profileId,
],
);
const startVisualNovelRunFromProfile = useCallback(
async (
profileId: string,
returnStage: VisualNovelRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
const targetProfileId = profileId.trim();
if (!targetProfileId) {
return false;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
let workDetail =
visualNovelWork?.summary.profileId === targetProfileId
? visualNovelWork
: null;
if (!workDetail) {
const response = await getVisualNovelWorkDetail(
targetProfileId,
options.embedded ? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS : {},
);
workDetail = response.work;
}
const startRunPayload = {
profileId: targetProfileId,
mode: 'play' as const,
};
const { run } = options.embedded
? await startVisualNovelRun(
targetProfileId,
startRunPayload,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
: await startVisualNovelRun(targetProfileId, startRunPayload);
setVisualNovelWork(workDetail);
setVisualNovelRun(run);
setVisualNovelRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('visual-novel-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'visual-novel-runtime',
buildVisualNovelPublicWorkCode(targetProfileId),
),
);
}
return true;
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '启动视觉小说试玩失败。'),
);
return false;
} finally {
setIsVisualNovelBusy(false);
}
},
[
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
visualNovelWork,
],
);
const submitVisualNovelRuntimeAction = useCallback(
async (payload: VisualNovelRuntimeActionRequest) => {
if (!visualNovelRun || isVisualNovelBusy) {
return;
}
if (visualNovelRun.runId.startsWith('vn-run-local')) {
return;
}
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const nextRun = await streamVisualNovelRuntimeAction(
visualNovelRun.runId,
payload,
);
setVisualNovelRun(nextRun);
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '推进视觉小说运行失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
isVisualNovelBusy,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setVisualNovelError,
visualNovelRun,
],
);
const streamCreativeAgentPayloadForSession = useCallback(
async (
session: CreativeAgentSessionSnapshot,
payload: StreamCreativeAgentMessageRequest,
) => {
setCreativeAgentError(null);
setIsCreativeAgentStreaming(true);
setCreativeAgentEvents([]);
setCreativeAgentSession((current) =>
current
? {
...current,
messages: [
...current.messages,
{
id: payload.clientMessageId,
role: 'user',
kind: 'chat',
text: payload.content
.map((part) =>
part.type === 'input_text' ? part.text.trim() : '参考图',
)
.filter(Boolean)
.join(' / '),
createdAt: new Date().toISOString(),
},
],
}
: current,
);
try {
const nextSession = await streamCreativeAgentMessage(
session.sessionId,
payload,
{
onEvent: (event) => {
setCreativeAgentEvents((current) => [...current, event]);
if (event.event === 'stage') {
setCreativeAgentSession((current) =>
current
? {
...current,
stage: event.data.stage,
updatedAt: new Date().toISOString(),
}
: current,
);
}
if (event.event === 'session') {
setCreativeAgentSession(event.data.session);
}
},
},
);
setCreativeAgentSession(nextSession);
if (nextSession.targetBinding) {
setActiveCreativeAgentSessionId(nextSession.sessionId);
}
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '发送智能创作消息失败。'),
);
} finally {
setIsCreativeAgentStreaming(false);
}
},
[resolvePuzzleErrorMessage],
);
const submitCreativeAgentMessage = useCallback(
async (payload: StreamCreativeAgentMessageRequest) => {
if (!creativeAgentSession || isCreativeAgentStreaming) {
return;
}
await streamCreativeAgentPayloadForSession(creativeAgentSession, payload);
},
[
creativeAgentSession,
isCreativeAgentStreaming,
streamCreativeAgentPayloadForSession,
],
);
const confirmCreativeTemplateSelection = useCallback(
async (selection: PuzzleCreativeTemplateSelection) => {
const sessionId = creativeAgentSession?.sessionId?.trim();
if (!sessionId || isCreativeAgentBusy || isCreativeAgentStreaming) {
return;
}
setIsCreativeAgentBusy(true);
setCreativeAgentError(null);
try {
const response = await confirmCreativePuzzleTemplate(sessionId, {
selection,
});
setCreativeAgentSession(response.session);
if (response.session.targetBinding) {
setActiveCreativeAgentSessionId(response.session.sessionId);
}
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '确认拼图模板失败。'),
);
} finally {
setIsCreativeAgentBusy(false);
}
},
[
creativeAgentSession?.sessionId,
isCreativeAgentBusy,
isCreativeAgentStreaming,
resolvePuzzleErrorMessage,
],
);
const submitBigFishMessage = bigFishFlow.submitMessage;
const 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],
);
const openCreativeAgentTarget = useCallback(async () => {
const binding = creativeAgentSession?.targetBinding;
if (!binding || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const response = await getPuzzleAgentSession(binding.targetSessionId);
puzzleFlow.setSession(response.session);
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
enterCreateTab();
setActiveCreativeAgentSessionId(creativeAgentSession.sessionId);
setCreativeDraftEditError(null);
setSelectionStage(resolveCreativeAgentTargetSelectionStage(binding.targetStage));
} catch (error) {
setCreativeAgentError(
resolvePuzzleErrorMessage(error, '打开拼图草稿失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
}, [
creativeAgentSession,
enterCreateTab,
isPuzzleBusy,
puzzleFlow,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
]);
const submitCreativeDraftEdit = useCallback(
async ({
instruction,
currentDraft,
}: {
instruction: string;
currentDraft: PuzzleResultDraft;
}): Promise<CreativeDraftEditResult | null> => {
const creativeSessionId =
activeCreativeAgentSessionId ?? creativeAgentSession?.sessionId ?? null;
const targetPuzzleSessionId = puzzleSession?.sessionId?.trim();
if (
!creativeSessionId ||
!targetPuzzleSessionId ||
isCreativeDraftEditBusy
) {
return null;
}
setIsCreativeDraftEditBusy(true);
setCreativeDraftEditError(null);
try {
const result = await streamCreativeDraftEdit(creativeSessionId, {
clientMessageId: `creative-edit-${Date.now().toString(36)}`,
instruction,
targetPuzzleSessionId,
currentDraft,
});
if ('puzzleSession' in result) {
puzzleFlow.setSession(result.puzzleSession);
setCreativeAgentSession(result.session);
setActiveCreativeAgentSessionId(result.session.sessionId);
return result;
}
setCreativeAgentSession(result);
setActiveCreativeAgentSessionId(result.sessionId);
const response = await getPuzzleAgentSession(targetPuzzleSessionId);
puzzleFlow.setSession(response.session);
return null;
} catch (error) {
setCreativeDraftEditError(
resolvePuzzleErrorMessage(error, '智能修订拼图草稿失败。'),
);
return null;
} finally {
setIsCreativeDraftEditBusy(false);
}
},
[
activeCreativeAgentSessionId,
creativeAgentSession?.sessionId,
isCreativeDraftEditBusy,
puzzleFlow,
puzzleSession?.sessionId,
resolvePuzzleErrorMessage,
],
);
useEffect(() => {
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]);
useEffect(() => {
if (
selectionStage === 'visual-novel-result' &&
!visualNovelSession?.draft &&
!visualNovelWork?.draft
) {
setSelectionStage(
visualNovelSession ? 'visual-novel-agent-workspace' : 'platform',
);
}
if (selectionStage === 'visual-novel-runtime' && !visualNovelRun) {
setSelectionStage(
visualNovelSession?.draft || visualNovelWork?.draft
? 'visual-novel-result'
: 'platform',
);
}
if (selectionStage === 'visual-novel-gallery-detail' && !visualNovelWork) {
setSelectionStage('platform');
}
}, [
selectionStage,
setSelectionStage,
visualNovelRun,
visualNovelSession,
visualNovelWork,
]);
const startBigFishRun = useCallback(async () => {
if (!bigFishSession) {
return;
}
const sessionId = bigFishSession.sessionId;
setBigFishError(null);
setBigFishRuntimeShare(null);
setBigFishRuntimeWork(null);
setBigFishRuntimeStartedAt(null);
setBigFishRun(null);
try {
const { run } = await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRuntimeSessionSource('draft');
setBigFishRuntimeReturnStage('big-fish-result');
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
);
});
void refreshBigFishShelf();
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
}
}, [
bigFishSession,
refreshBigFishShelf,
resolveBigFishErrorMessage,
setBigFishError,
setSelectionStage,
]);
const restartBigFishRun = useCallback(async () => {
if (!bigFishSession && !bigFishRun) {
return;
}
const sessionId = bigFishSession?.sessionId ?? bigFishRun?.sessionId;
if (!sessionId) {
return;
}
setBigFishError(null);
if (bigFishSession) {
setBigFishRuntimeShare(null);
setBigFishRuntimeReturnStage('big-fish-result');
}
setBigFishRuntimeStartedAt(null);
setBigFishRun(null);
try {
const { run } = await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRuntimeSessionSource(bigFishSession ? 'draft' : 'work');
setBigFishRuntimeReturnStage(
bigFishSession ? 'big-fish-result' : bigFishRuntimeReturnStage,
);
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
void recordBigFishPlay(sessionId, { elapsedMs: 0 }).catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
);
});
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
}
}, [
bigFishRun,
bigFishRuntimeReturnStage,
bigFishSession,
resolveBigFishErrorMessage,
setBigFishError,
setSelectionStage,
]);
const startPuzzleRunFromProfile = useCallback(
async (
profileId: string,
returnStage: PuzzleRuntimeReturnStage = 'work-detail',
detailItem?: PuzzleWorkSummary,
mirrorErrorToPublicDetail = false,
levelId?: string | null,
options: { embedded?: boolean; authMode?: PuzzleRuntimeAuthMode } = {},
) => {
if (isPuzzleBusy) {
return false;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const item =
detailItem ?? (await getPuzzleGalleryDetail(profileId)).item;
const startRunPayload = {
profileId: item.profileId,
levelId: levelId ?? null,
};
const authMode = options.embedded
? 'isolated'
: (options.authMode ?? 'default');
const { run } =
authMode === 'isolated'
? await startPuzzleRun(
startRunPayload,
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
: await startPuzzleRun(startRunPayload);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
setPuzzleRuntimeAuthMode(authMode);
setPuzzleRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('puzzle-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath(
'puzzle-runtime',
buildPuzzlePublicWorkCode(item.profileId),
),
);
}
return true;
} catch (error) {
if (isMissingPuzzleWorkError(error)) {
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleError(null);
setPublicWorkDetailError(null);
setPlatformTab('home');
setSelectionStage('platform');
pushAppHistoryPath('/');
return false;
}
const message = resolvePuzzleErrorMessage(error, '启动拼图玩法失败。');
setPuzzleError(message);
if (mirrorErrorToPublicDetail) {
setPublicWorkDetailError(message);
}
return false;
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setPlatformTab,
setSelectionStage,
],
);
const startMatch3DRunFromProfile = useCallback(
async (
profile: Match3DWorkProfile | Match3DWorkSummary,
returnStage: 'match3d-result' | 'work-detail' = 'match3d-result',
mirrorErrorToPublicDetail = false,
options: { embedded?: boolean } = {},
) => {
if (isMatch3DBusy) {
return false;
}
match3dFlow.setIsBusy(true);
setMatch3DError(null);
try {
const { run } = options.embedded
? await startMatch3DRun(
profile.profileId,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
: await startMatch3DRun(profile.profileId);
setMatch3DRun(run);
setMatch3DRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('match3d-runtime');
}
if (!options.embedded && profile.publicationStatus === 'published') {
pushAppHistoryPath(
buildPublicWorkStagePath(
'work-detail',
buildMatch3DPublicWorkCode(profile.profileId),
),
);
}
return true;
} catch (error) {
const message = resolveMatch3DErrorMessage(
error,
'启动抓大鹅玩法失败。',
);
setMatch3DError(message);
if (mirrorErrorToPublicDetail) {
setPublicWorkDetailError(message);
}
return false;
} finally {
match3dFlow.setIsBusy(false);
}
},
[
isMatch3DBusy,
match3dFlow,
resolveMatch3DErrorMessage,
setMatch3DError,
setSelectionStage,
],
);
const startSquareHoleRunFromProfile = useCallback(
async (
profile: SquareHoleWorkProfile | SquareHoleWorkSummary,
returnStage: SquareHoleRuntimeReturnStage = 'square-hole-result',
mirrorErrorToPublicDetail = false,
options: { embedded?: boolean } = {},
) => {
if (isSquareHoleBusy) {
return false;
}
squareHoleFlow.setIsBusy(true);
setSquareHoleError(null);
try {
const { run } = options.embedded
? await startSquareHoleRun(
profile.profileId,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
: await startSquareHoleRun(profile.profileId);
setSquareHoleRun(run);
setSquareHoleRuntimeReturnStage(returnStage);
if (!options.embedded) {
setSelectionStage('square-hole-runtime');
}
if (!options.embedded && profile.publicationStatus === 'published') {
pushAppHistoryPath(
buildPublicWorkStagePath(
'work-detail',
buildSquareHolePublicWorkCode(profile.profileId),
),
);
}
return true;
} catch (error) {
const message = resolveSquareHoleErrorMessage(
error,
'启动方洞挑战玩法失败。',
);
setSquareHoleError(message);
if (mirrorErrorToPublicDetail) {
setPublicWorkDetailError(message);
}
return false;
} finally {
squareHoleFlow.setIsBusy(false);
}
},
[
isSquareHoleBusy,
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);
setPuzzleRuntimeAuthMode('default');
setPuzzleRuntimeReturnStage('puzzle-result');
setSelectionStage('puzzle-runtime');
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图试玩失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleSession?.publishedProfileId,
puzzleSession?.sessionId,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
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);
const reportPromise =
activeRecommendRuntimeKind === 'big-fish'
? recordBigFishPlay(
sessionId,
{ elapsedMs },
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
: recordBigFishPlay(sessionId, { elapsedMs });
void reportPromise.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩时长失败。'),
);
});
}, [
activeRecommendRuntimeKind,
bigFishRun?.sessionId,
bigFishRuntimeStartedAt,
resolveBigFishErrorMessage,
setBigFishError,
]);
const swapPuzzlePiecesInRun = useCallback(
async (payload: { firstPieceId: string; secondPieceId: string }) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
setPuzzleRun(
swapLocalPuzzlePieces(
puzzleRun,
payload,
isLocalPuzzleRun(puzzleRun) ? selectedPuzzleDetail : null,
),
);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '交换拼图块失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleRun,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
],
);
const dragPuzzlePiece = useCallback(
async (payload: {
pieceId: string;
targetRow: number;
targetCol: number;
}) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
setPuzzleRun(
dragLocalPuzzlePiece(
puzzleRun,
payload,
isLocalPuzzleRun(puzzleRun) ? selectedPuzzleDetail : null,
),
);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '拖动拼图块失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
puzzleRun,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
],
);
useEffect(() => {
if (selectionStage !== 'puzzle-runtime' || !puzzleRun?.currentLevel) {
return;
}
if (puzzleRun.currentLevel.status !== 'playing') {
return;
}
const timerId = window.setInterval(() => {
// 中文注释:拼图运行态的棋盘交互和倒计时展示都在前端即时裁决,超时后只刷新本地失败态。
setPuzzleRun((currentRun) =>
currentRun ? refreshLocalPuzzleTimer(currentRun) : currentRun,
);
}, 250);
return () => window.clearInterval(timerId);
}, [puzzleRun, selectionStage]);
const setPuzzleRuntimePaused = useCallback(
async (paused: boolean) => {
if (!puzzleRun?.currentLevel) {
return;
}
setPuzzleRun((currentRun) =>
currentRun ? setLocalPuzzlePaused(currentRun, paused) : currentRun,
);
},
[puzzleRun],
);
const syncPuzzleRuntimeTimeout = useCallback(async () => {
if (
!puzzleRun?.currentLevel ||
puzzleRun.currentLevel.status === 'cleared'
) {
return;
}
setPuzzleRun((currentRun) =>
currentRun ? refreshLocalPuzzleTimer(currentRun) : currentRun,
);
}, [puzzleRun]);
const usePuzzleProp = useCallback(
async (propKind: PuzzleRuntimePropKind) => {
if (!puzzleRun?.currentLevel) {
return null;
}
const canUseProp =
propKind === 'extendTime'
? puzzleRun.currentLevel.status !== 'cleared'
: puzzleRun.currentLevel.status === 'playing';
if (!canUseProp) {
return null;
}
const currentRun = puzzleRunRef.current ?? puzzleRun;
if (!currentRun.currentLevel) {
return null;
}
const nextRun =
propKind === 'extendTime'
? extendLocalPuzzleTime(currentRun)
: propKind === 'freezeTime'
? applyLocalPuzzleFreezeTime(currentRun)
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return nextRun;
},
[puzzleRun],
);
const restartPuzzleCurrentLevel = useCallback(async () => {
const currentRun = puzzleRunRef.current ?? puzzleRun;
const currentLevel = currentRun?.currentLevel ?? null;
if (!currentRun || !currentLevel || isPuzzleBusy) {
return;
}
setPuzzleError(null);
setIsPuzzleBusy(true);
try {
if (isLocalPuzzleRun(currentRun)) {
const nextRun = restartLocalPuzzleLevel(currentRun);
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return;
}
const detailItem =
selectedPuzzleDetail?.profileId === currentLevel.profileId
? selectedPuzzleDetail
: await getPuzzleGalleryDetail(currentLevel.profileId).then(
(response) => response.item,
);
const startRunPayload = {
profileId: currentLevel.profileId,
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
};
const { run } =
puzzleRuntimeAuthMode === 'isolated'
? await startPuzzleRun(
startRunPayload,
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
: await startPuzzleRun(startRunPayload);
setSelectedPuzzleDetail(detailItem);
puzzleRunRef.current = run;
setPuzzleRun(run);
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '重新开始拼图关卡失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
}, [
isPuzzleBusy,
puzzleRun,
puzzleRuntimeAuthMode,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
]);
const resumePuzzleSaveArchive = useCallback(
async (entry: ProfileSaveArchiveSummary) => {
if (isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
platformBootstrap.setSaveError(null);
try {
const resumedArchive = await resumePuzzleProfileSaveArchiveRaw(
entry.worldKey,
);
platformBootstrap.setSaveEntries((currentEntries) =>
currentEntries.map((currentEntry) =>
currentEntry.worldKey === resumedArchive.entry.worldKey
? resumedArchive.entry
: currentEntry,
),
);
const gameState = resumedArchive.snapshot.gameState;
const profileId =
typeof gameState.currentProfileId === 'string' &&
gameState.currentProfileId.trim()
? gameState.currentProfileId
: typeof gameState.entryProfileId === 'string' &&
gameState.entryProfileId.trim()
? gameState.entryProfileId
: (entry.profileId ?? entry.worldKey.replace(/^puzzle:/u, ''));
const levelId =
typeof gameState.currentLevelId === 'string' &&
gameState.currentLevelId.trim()
? gameState.currentLevelId
: null;
const item =
selectedPuzzleDetail?.profileId === profileId
? selectedPuzzleDetail
: await getPuzzleGalleryDetail(profileId).then(
(response) => response.item,
);
await startPuzzleRunFromProfile(
item.profileId,
'platform',
item,
false,
levelId,
);
} catch (error) {
platformBootstrap.setSaveError(
resolvePuzzleErrorMessage(error, '恢复拼图存档失败。'),
);
} finally {
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
platformBootstrap,
selectedPuzzleDetail,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
startPuzzleRunFromProfile,
],
);
useEffect(() => {
const currentLevel = puzzleRun?.currentLevel ?? null;
if (!puzzleRun || !currentLevel || currentLevel.status !== 'cleared') {
return;
}
if (currentLevel.elapsedMs === null) {
return;
}
if ((currentLevel.leaderboardEntries ?? []).length > 0) {
return;
}
const submitKey = `${puzzleRun.runId}:${currentLevel.profileId}:${currentLevel.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;
}
const submitLeaderboardPromise =
puzzleRuntimeAuthMode === 'isolated'
? submitPuzzleLeaderboard(
puzzleRun.runId,
payload,
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
void submitLeaderboardPromise
.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,
puzzleRuntimeAuthMode,
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 advancePromise =
puzzleRuntimeAuthMode === 'isolated'
? advancePuzzleNextLevel(
puzzleRun.runId,
{
targetProfileId,
},
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
: advancePuzzleNextLevel(puzzleRun.runId, {
targetProfileId,
});
const [{ run }, item] = await Promise.all([
advancePromise,
itemPromise,
]);
setSelectedPuzzleDetail(item);
setPuzzleRun(run);
pushAppHistoryPath(
buildPublicWorkStagePath(
'puzzle-runtime',
buildPuzzlePublicWorkCode(item.profileId),
),
);
return;
}
const { run } =
puzzleRuntimeAuthMode === 'isolated'
? await advancePuzzleNextLevel(
puzzleRun.runId,
{},
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
)
: await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {
setIsPuzzleNextLevelGenerating(false);
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
isPuzzleLeaderboardBusy,
puzzleRun,
puzzleRuntimeAuthMode,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
setIsPuzzleBusy,
setPuzzleError,
],
);
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);
setPuzzleRuntimeAuthMode('default');
enterCreateTab();
setSelectionStage('puzzle-result');
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '改造拼图作品失败。'),
);
})
.finally(() => {
setIsPublicWorkDetailBusy(false);
setIsPuzzleBusy(false);
});
});
},
[
enterCreateTab,
isPublicWorkDetailBusy,
isPuzzleBusy,
puzzleFlow,
resolvePuzzleErrorMessage,
runProtectedAction,
setIsPuzzleBusy,
setPuzzleError,
setSelectionStage,
],
);
const saveAndExitRecommendPuzzleRuntime = useCallback(async () => {
if (activeRecommendRuntimeKind !== 'puzzle') {
return;
}
const currentRun = puzzleRunRef.current;
if (!currentRun) {
setActiveRecommendRuntimeKind(null);
return;
}
// 中文注释:推荐页嵌入拼图的“保存并退出”沿用现有运行态语义;
// 正式 run 的每次交换/拖动/通关已写回后端,退出时只收口暂停和本地快照。
const closedRun = currentRun.currentLevel
? setLocalPuzzlePaused(currentRun, false)
: currentRun;
puzzleRunRef.current = null;
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setActiveRecommendRuntimeKind(null);
if (closedRun.currentLevel) {
setPuzzleError(null);
}
}, [activeRecommendRuntimeKind, setPuzzleError]);
const leaveAgentWorkspace = useCallback(() => {
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 handleDeleteVisualNovelWork = useCallback(
(work: VisualNovelWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
requestDeleteCreationWork({
id: work.profileId,
title: work.title || '未命名视觉小说',
detail:
work.publishStatus === 'published'
? '删除后会从你的作品列表和公开广场中移除。'
: '删除后会从你的作品列表中移除。',
run: () => {
setDeletingCreationWorkId(work.profileId);
setVisualNovelError(null);
void deleteVisualNovelWork(work.profileId)
.then(async (response) => {
setVisualNovelWorks(response.works);
await refreshVisualNovelGallery();
})
.catch((error) => {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '删除视觉小说作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
},
});
},
[
deletingCreationWorkId,
refreshVisualNovelGallery,
requestDeleteCreationWork,
resolvePuzzleErrorMessage,
setVisualNovelError,
],
);
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;
}
if (isVisualNovelGalleryEntry(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) {
if (isMissingPuzzleWorkError(error)) {
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleError(null);
setPublicWorkDetailError(null);
setPlatformTab('home');
setSelectionStage('platform');
pushAppHistoryPath('/');
return;
}
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, '读取拼图详情失败。'),
);
} finally {
setIsPuzzleBusy(false);
setIsPublicWorkDetailBusy(false);
}
},
[
openPublicWorkDetail,
platformBootstrap.platformTab,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setPlatformTab,
setSelectionStage,
],
);
const openMatch3DPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setMatch3DError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const entries =
match3dGalleryEntries.length > 0
? match3dGalleryEntries
: await refreshMatch3DGallery();
const matchedEntry = entries.find(
(entry) => entry.profileId === profileId,
);
if (!matchedEntry) {
throw new Error('未找到抓大鹅作品。');
}
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(matchedEntry));
} catch (error) {
setPublicWorkDetailError(
resolveMatch3DErrorMessage(error, '读取抓大鹅详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
match3dGalleryEntries,
openPublicWorkDetail,
refreshMatch3DGallery,
resolveMatch3DErrorMessage,
setMatch3DError,
setSelectionStage,
],
);
const openSquareHolePublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setSquareHoleError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const entries =
squareHoleGalleryEntries.length > 0
? squareHoleGalleryEntries
: await refreshSquareHoleGallery();
const matchedEntry = entries.find(
(entry) => entry.profileId === profileId,
);
if (!matchedEntry) {
throw new Error('未找到方洞挑战作品。');
}
openPublicWorkDetail(mapSquareHoleWorkToPublicWorkDetail(matchedEntry));
} catch (error) {
setPublicWorkDetailError(
resolveSquareHoleErrorMessage(error, '读取方洞挑战详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
openPublicWorkDetail,
refreshSquareHoleGallery,
resolveSquareHoleErrorMessage,
setSelectionStage,
setSquareHoleError,
squareHoleGalleryEntries,
],
);
const openVisualNovelPublicWorkDetail = useCallback(
async (profileId: string) => {
setIsPublicWorkDetailBusy(true);
setVisualNovelError(null);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
try {
const entries =
visualNovelGalleryEntries.length > 0
? visualNovelGalleryEntries
: await refreshVisualNovelGallery();
const matchedEntry = entries.find(
(entry) => entry.profileId === profileId,
);
if (!matchedEntry) {
throw new Error('未找到视觉小说作品。');
}
openPublicWorkDetail(mapVisualNovelWorkToPublicWorkDetail(matchedEntry));
} catch (error) {
setPublicWorkDetailError(
resolvePuzzleErrorMessage(error, '读取视觉小说详情失败。'),
);
} finally {
setIsPublicWorkDetailBusy(false);
}
},
[
openPublicWorkDetail,
refreshVisualNovelGallery,
resolvePuzzleErrorMessage,
setSelectionStage,
setVisualNovelError,
visualNovelGalleryEntries,
],
);
const openPublicGalleryDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
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;
}
if (isVisualNovelGalleryEntry(entry)) {
void openVisualNovelPublicWorkDetail(entry.profileId);
return;
}
void openRpgPublicWorkDetail(entry);
},
[
openPuzzlePublicWorkDetail,
openPublicWorkDetail,
openRpgPublicWorkDetail,
openVisualNovelPublicWorkDetail,
platformBootstrap.platformTab,
],
);
const openRecommendGalleryDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
runProtectedAction(() => {
openPublicGalleryDetail(entry);
});
},
[openPublicGalleryDetail, runProtectedAction],
);
const openPuzzleDetail = useCallback(
async (
profileId: string,
returnTarget: PuzzleDetailReturnTarget = {
tab: platformBootstrap.platformTab,
},
) => {
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { item } = await getPuzzleGalleryDetail(profileId);
setSelectedPuzzleDetail(item);
setPuzzleDetailReturnTarget(returnTarget);
setSelectionStage('puzzle-gallery-detail');
pushAppHistoryPath(
buildPublicWorkStagePath(
'puzzle-gallery-detail',
buildPuzzlePublicWorkCode(item.profileId),
),
);
} catch (error) {
if (isMissingPuzzleWorkError(error)) {
setSelectedPuzzleDetail(null);
setPuzzleDetailReturnTarget(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
setPuzzleError(null);
setPublicWorkDetailError(null);
setPlatformTab('home');
setSelectionStage('platform');
pushAppHistoryPath('/');
return;
}
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图详情失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
platformBootstrap.platformTab,
resolvePuzzleErrorMessage,
setIsPuzzleBusy,
setPuzzleError,
setPlatformTab,
setSelectionStage,
],
);
const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => {
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleRuntimeAuthMode('default');
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 openVisualNovelDraft = useCallback(
async (
item: VisualNovelWorkSummary,
options: { forceDraft?: boolean } = {},
) => {
if (item.publishStatus === 'published' && !options.forceDraft) {
openPublicWorkDetail(mapVisualNovelWorkToPublicWorkDetail(item));
return;
}
setVisualNovelWork(null);
setVisualNovelRun(null);
setVisualNovelRuntimeReturnStage('visual-novel-result');
setVisualNovelError(null);
setIsVisualNovelBusy(true);
try {
const { work } = await getVisualNovelWorkDetail(item.profileId);
setVisualNovelWork(work);
setVisualNovelSession(mapVisualNovelWorkDetailToSession(work));
enterCreateTab();
setSelectionStage('visual-novel-result');
} catch (error) {
setVisualNovelError(
resolvePuzzleErrorMessage(error, '读取视觉小说作品详情失败。'),
);
} finally {
setIsVisualNovelBusy(false);
}
},
[
enterCreateTab,
openPublicWorkDetail,
resolvePuzzleErrorMessage,
setIsVisualNovelBusy,
setSelectionStage,
setVisualNovelError,
setVisualNovelSession,
],
);
const startBigFishRunFromWork = useCallback(
async (
item: BigFishWorkSummary,
returnStage: BigFishRuntimeReturnStage = 'work-detail',
options: { embedded?: boolean } = {},
) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
return false;
}
const publicWorkCode = buildBigFishPublicWorkCode(item.sourceSessionId);
setBigFishError(null);
bigFishFlow.setSession(null);
setBigFishRuntimeWork(item);
setBigFishRuntimeShare({
title: item.title,
publicWorkCode,
});
setBigFishRuntimeStartedAt(null);
setBigFishRuntimeSessionSource('work');
setBigFishRuntimeReturnStage(returnStage);
setBigFishRun(null);
try {
const { run } = options.embedded
? await startBigFishRuntimeRun(
sessionId,
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
: await startBigFishRuntimeRun(sessionId);
setBigFishRuntimeStartedAt(Date.now());
setBigFishRun(run);
if (!options.embedded) {
setSelectionStage('big-fish-runtime');
pushAppHistoryPath(
buildPublicWorkStagePath('big-fish-runtime', publicWorkCode),
);
}
const recordPlayPromise = options.embedded
? recordBigFishPlay(
sessionId,
{ elapsedMs: 0 },
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
)
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
void recordPlayPromise.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '记录大鱼吃小鱼游玩失败。'),
);
});
return true;
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
return false;
}
},
[
bigFishFlow,
resolveBigFishErrorMessage,
setBigFishError,
setBigFishRuntimeReturnStage,
setSelectionStage,
],
);
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,
null,
{ authMode: 'isolated' },
);
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;
}
if (isVisualNovelGalleryEntry(selectedPublicWorkDetail)) {
setPublicWorkDetailError(null);
void startVisualNovelRunFromProfile(
selectedPublicWorkDetail.profileId,
'work-detail',
);
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,
startVisualNovelRunFromProfile,
]);
const selectRecommendRuntimeEntry = useCallback(
async (entry: PlatformPublicGalleryCard) => {
const entryKey = getPlatformPublicGalleryEntryKey(entry);
const runtimeKind = getPlatformRecommendRuntimeKind(entry);
const startRequestId = recommendRuntimeStartRequestRef.current + 1;
recommendRuntimeStartRequestRef.current = startRequestId;
const isCurrentStartRequest = () =>
recommendRuntimeStartRequestRef.current === startRequestId;
if (entryKey !== activeRecommendEntryKey) {
await saveAndExitRecommendPuzzleRuntime();
if (!isCurrentStartRequest()) {
return;
}
}
setActiveRecommendEntryKey(entryKey);
setActiveRecommendRuntimeKind(runtimeKind);
setActiveRecommendRuntimeError(null);
setIsStartingRecommendEntry(true);
try {
let started = false;
if (isBigFishGalleryEntry(entry)) {
const work = mapPublicWorkDetailToBigFishWork(entry);
if (!work) {
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
} else {
started = await startBigFishRunFromWork(work, 'platform', {
embedded: true,
});
}
} else if (isPuzzleGalleryEntry(entry)) {
const work =
selectedPuzzleDetail?.profileId === entry.profileId
? selectedPuzzleDetail
: mapPublicWorkDetailToPuzzleWork(entry);
if (!work) {
setPuzzleError('当前拼图作品信息不完整,暂时无法进入玩法。');
} else {
started = await startPuzzleRunFromProfile(
work.profileId,
'platform',
work,
false,
null,
{ embedded: true },
);
}
} else if (isMatch3DGalleryEntry(entry)) {
const work = mapPublicWorkDetailToMatch3DWork(entry);
if (!work) {
setMatch3DError('当前抓大鹅作品信息不完整,暂时无法进入玩法。');
} else {
started = await startMatch3DRunFromProfile(
work,
'work-detail',
false,
{ embedded: true },
);
}
} else if (isSquareHoleGalleryEntry(entry)) {
const work = mapPublicWorkDetailToSquareHoleWork(entry);
if (!work) {
setSquareHoleError('当前方洞挑战作品信息不完整,暂时无法进入玩法。');
} else {
started = await startSquareHoleRunFromProfile(
work,
'platform',
false,
{ embedded: true },
);
}
} else if (isVisualNovelGalleryEntry(entry)) {
started = await startVisualNovelRunFromProfile(
entry.profileId,
'platform',
{ embedded: true },
);
} else {
started = true;
}
if (!isCurrentStartRequest()) {
return;
}
if (started) {
setActiveRecommendRuntimeKind(runtimeKind);
setActiveRecommendRuntimeError(null);
} else {
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError('作品暂时无法进入,请稍后再试。');
}
} catch (error) {
if (!isCurrentStartRequest()) {
return;
}
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError('作品暂时无法进入,请稍后再试。');
} finally {
if (isCurrentStartRequest()) {
setIsStartingRecommendEntry(false);
}
}
},
[
activeRecommendEntryKey,
saveAndExitRecommendPuzzleRuntime,
selectedPuzzleDetail,
setBigFishError,
setMatch3DError,
setPuzzleError,
setSquareHoleError,
startBigFishRunFromWork,
startMatch3DRunFromProfile,
startPuzzleRunFromProfile,
startSquareHoleRunFromProfile,
startVisualNovelRunFromProfile,
],
);
const selectAdjacentRecommendRuntimeEntry = useCallback(
(direction: 1 | -1) => {
if (recommendRuntimeEntries.length === 0) {
return;
}
const activeIndex = recommendRuntimeEntries.findIndex(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
);
const baseIndex = activeIndex >= 0 ? activeIndex : 0;
const nextIndex =
(baseIndex + direction + recommendRuntimeEntries.length) %
recommendRuntimeEntries.length;
const nextEntry = recommendRuntimeEntries[nextIndex];
if (!nextEntry) {
return;
}
if (
getPlatformPublicGalleryEntryKey(nextEntry) === activeRecommendEntryKey
) {
return;
}
void selectRecommendRuntimeEntry(nextEntry);
},
[
activeRecommendEntryKey,
recommendRuntimeEntries,
selectRecommendRuntimeEntry,
],
);
const recommendRuntimeContent = useMemo(() => {
if (
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
!activeRecommendRuntimeKind
) {
return null;
}
const activeEntry =
recommendRuntimeEntries.find(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
) ?? null;
if (!activeEntry) {
return null;
}
if (activeRecommendRuntimeKind === 'big-fish') {
return (
<BigFishRuntimeShell
run={bigFishRun}
assetSlots={bigFishSession?.assetSlots ?? []}
shareTitle={bigFishRuntimeShare?.title ?? null}
sharePublicWorkCode={bigFishRuntimeShare?.publicWorkCode ?? null}
isBusy={isBigFishBusy}
error={bigFishError}
embedded
onBack={() => {
reportBigFishObservedPlayTime();
setActiveRecommendRuntimeKind(null);
}}
onRestart={() => {
reportBigFishObservedPlayTime();
void restartBigFishRun();
}}
onSubmitInput={submitBigFishInput}
/>
);
}
if (activeRecommendRuntimeKind === 'match3d') {
return (
<Match3DRuntimeShell
run={match3dRun}
isBusy={isMatch3DBusy}
error={match3dError}
embedded
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
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,
'同步抓大鹅倒计时失败。',
),
);
});
}}
/>
);
}
if (activeRecommendRuntimeKind === 'puzzle') {
return (
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={
isPuzzleBusy ||
isPuzzleNextLevelGenerating ||
isPuzzleLeaderboardBusy
}
error={puzzleError}
embedded
hideBackButton
hideExitControls
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onRemodelWork={
selectedPuzzleDetail?.publicationStatus === 'published'
? remodelCurrentPuzzleRuntimeWork
: undefined
}
onSwapPieces={(payload) => {
void swapPuzzlePiecesInRun(payload);
}}
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={(target) => {
void advancePuzzleLevel(target);
}}
onRestartLevel={() => {
void restartPuzzleCurrentLevel();
}}
onPauseChange={setPuzzleRuntimePaused}
onUseProp={usePuzzleProp}
onTimeExpired={syncPuzzleRuntimeTimeout}
/>
);
}
if (activeRecommendRuntimeKind === 'square-hole') {
return (
<SquareHoleRuntimeShell
run={squareHoleRun}
isBusy={isSquareHoleBusy}
error={squareHoleError}
embedded
onBack={() => {
if (
squareHoleRun?.runId &&
squareHoleRun.status.toLowerCase() === 'running'
) {
void stopSquareHoleRun(squareHoleRun.runId).catch(
() => undefined,
);
}
setActiveRecommendRuntimeKind(null);
}}
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,
'同步方洞挑战倒计时失败。',
),
);
});
}}
/>
);
}
if (activeRecommendRuntimeKind === 'visual-novel') {
return (
<VisualNovelRuntimeShell
draft={visualNovelWork?.draft ?? visualNovelSession?.draft}
run={visualNovelRun}
isBusy={isVisualNovelBusy}
error={visualNovelError}
embedded
onBack={() => {
setActiveRecommendRuntimeKind(null);
}}
onSubmitAction={(payload) => {
void submitVisualNovelRuntimeAction(payload);
}}
/>
);
}
return (
<div className={`platform-theme ${platformThemeClass} flex h-full min-h-0 items-center justify-center bg-[var(--platform-recommend-runtime-state-fill)] px-5 text-center text-sm font-semibold leading-6 text-[var(--platform-recommend-runtime-state-text)]`}>
</div>
);
}, [
activeRecommendEntryKey,
activeRecommendRuntimeKind,
bigFishError,
bigFishRun,
bigFishRuntimeShare,
bigFishSession?.assetSlots,
isBigFishBusy,
isMatch3DBusy,
isPuzzleBusy,
isPuzzleLeaderboardBusy,
isPuzzleNextLevelGenerating,
isSquareHoleBusy,
isVisualNovelBusy,
match3dError,
match3dFlow,
match3dRun,
platformThemeClass,
puzzleError,
puzzleRun,
recommendRuntimeEntries,
remodelCurrentPuzzleRuntimeWork,
reportBigFishObservedPlayTime,
restartBigFishRun,
selectedPuzzleDetail,
selectionStage,
setMatch3DError,
setMatch3DRun,
setPuzzleRuntimePaused,
setSquareHoleRun,
squareHoleError,
squareHoleFlow,
squareHoleRun,
submitBigFishInput,
submitVisualNovelRuntimeAction,
advancePuzzleLevel,
dragPuzzlePiece,
restartPuzzleCurrentLevel,
setSquareHoleError,
swapPuzzlePiecesInRun,
syncPuzzleRuntimeTimeout,
usePuzzleProp,
visualNovelError,
visualNovelRun,
visualNovelSession,
visualNovelWork,
]);
useEffect(() => {
if (
selectionStage !== 'platform' ||
platformBootstrap.platformTab !== 'home' ||
!platformBootstrap.isAuthenticated ||
!platformBootstrap.canReadProtectedData ||
platformBootstrap.isLoadingPlatform
) {
return;
}
if (recommendRuntimeEntries.length === 0) {
setActiveRecommendEntryKey(null);
setActiveRecommendRuntimeKind(null);
setActiveRecommendRuntimeError(null);
return;
}
const hasActiveEntry =
activeRecommendEntryKey &&
recommendRuntimeEntries.some(
(entry) =>
getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey,
);
if (hasActiveEntry || isStartingRecommendEntry) {
return;
}
const firstRecommendEntry = recommendRuntimeEntries[0];
if (firstRecommendEntry) {
void selectRecommendRuntimeEntry(firstRecommendEntry);
}
}, [
activeRecommendEntryKey,
isStartingRecommendEntry,
platformBootstrap.canReadProtectedData,
platformBootstrap.isLoadingPlatform,
platformBootstrap.isAuthenticated,
platformBootstrap.platformTab,
recommendRuntimeEntries,
selectRecommendRuntimeEntry,
selectionStage,
]);
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;
}
if (isVisualNovelGalleryEntry(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;
}
if (isVisualNovelGalleryEntry(entry)) {
const matchedWork = visualNovelWorks.find(
(work) => work.profileId === entry.profileId,
);
if (!matchedWork) {
setPublicWorkDetailError('这份视觉小说缺少可编辑草稿。');
return;
}
void openVisualNovelDraft(matchedWork, { 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,
openVisualNovelDraft,
runProtectedAction,
selectedDetailEntry,
selectedPuzzleDetail,
visualNovelWorks,
],
);
const remixSelectedPublicWork = useCallback(() => {
if (!selectedPublicWorkDetail) {
return;
}
if (isSelectedPublicWorkOwned) {
editOwnedPublicWork(selectedPublicWorkDetail);
return;
}
remixPublicWork(selectedPublicWorkDetail);
}, [
editOwnedPublicWork,
isSelectedPublicWorkOwned,
remixPublicWork,
selectedPublicWorkDetail,
]);
const handlePublicCodeSearch = useCallback(
async (keyword: string) => {
const 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 shouldSearchVisualNovelFirst = upperKeyword.startsWith('VN');
const shouldSearchWorkFirst =
!shouldSearchUserIdFirst &&
!shouldSearchBigFishFirst &&
!shouldSearchMatch3DFirst &&
!shouldSearchPuzzleFirst &&
!shouldSearchSquareHoleFirst &&
!shouldSearchVisualNovelFirst &&
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
const shouldSearchUserFirst =
shouldSearchUserIdFirst ||
upperKeyword.startsWith('SY') ||
(!shouldSearchWorkFirst &&
!shouldSearchBigFishFirst &&
!shouldSearchMatch3DFirst &&
!shouldSearchPuzzleFirst &&
!shouldSearchSquareHoleFirst &&
!shouldSearchVisualNovelFirst);
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));
};
const tryOpenVisualNovelGalleryEntry = async () => {
const entries =
visualNovelGalleryEntries.length > 0
? visualNovelGalleryEntries
: await refreshVisualNovelGallery();
const matchedEntry = entries.find((entry) =>
isSameVisualNovelPublicWorkCode(normalizedKeyword, entry.profileId),
);
if (!matchedEntry) {
throw new Error('未找到视觉小说作品。');
}
openPublicWorkDetail(mapVisualNovelWorkToPublicWorkDetail(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 (shouldSearchVisualNovelFirst) {
await tryOpenVisualNovelGalleryEntry();
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,
refreshVisualNovelGallery,
squareHoleGalleryEntries,
visualNovelGalleryEntries,
],
);
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,
setBigFishError,
],
);
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();
void refreshVisualNovelGallery();
}
}, [
isBigFishCreationVisible,
refreshBigFishGallery,
refreshMatch3DGallery,
refreshPuzzleGallery,
refreshSquareHoleGallery,
refreshVisualNovelGallery,
selectionStage,
]);
useEffect(() => {
if (
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshPuzzleShelf();
void refreshMatch3DShelf();
void refreshSquareHoleShelf();
void refreshVisualNovelShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshMatch3DShelf,
refreshPuzzleShelf,
refreshSquareHoleShelf,
refreshVisualNovelShelf,
selectionStage,
]);
useEffect(() => {
if (
isBigFishCreationVisible &&
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshBigFishShelf();
}
}, [
isBigFishCreationVisible,
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshBigFishShelf,
selectionStage,
]);
const renderCreationHubContent = (
mode: 'start-only' | 'works-only',
fallbackLabel: string,
) => (
<Suspense fallback={<LazyPanelFallback label={fallbackLabel} />}>
<CustomWorldCreationHub
mode={mode}
items={creationHubItems}
loading={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isMatch3DLoadingLibrary ||
isSquareHoleLoadingLibrary ||
isPuzzleLoadingLibrary ||
isVisualNovelLoadingLibrary
}
error={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isMatch3DLoadingLibrary ||
isSquareHoleLoadingLibrary ||
isPuzzleLoadingLibrary ||
isVisualNovelLoadingLibrary
? null
: (platformBootstrap.platformError ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
match3dError ??
squareHoleError ??
puzzleShelfError ??
puzzleError ??
visualNovelError)
}
onRetry={() => {
platformBootstrap.setPlatformError(null);
setBigFishError(null);
setMatch3DError(null);
setSquareHoleError(null);
setPuzzleShelfError(null);
setPuzzleCreationError(null);
setPuzzleError(null);
setVisualNovelError(null);
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
if (isBigFishCreationVisible) {
void refreshBigFishShelf();
}
void refreshMatch3DShelf();
void refreshSquareHoleShelf();
void refreshPuzzleShelf();
void refreshVisualNovelShelf();
}}
createError={
sessionController.creationTypeError ??
bigFishError ??
match3dError ??
squareHoleError ??
puzzleCreationError ??
puzzleError ??
visualNovelError
}
createBusy={
sessionController.isCreatingAgentSession ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isPuzzleBusy ||
isVisualNovelBusy ||
isVisualNovelStreamingReply
}
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}
visualNovelItems={visualNovelWorks}
onOpenVisualNovelDetail={(item) => {
runProtectedAction(() => {
void openVisualNovelDraft(item);
});
}}
onDeleteVisualNovel={(item) => {
handleDeleteVisualNovelWork(item);
}}
/>
</Suspense>
);
const creationStartContent = (
<div className="platform-page-stage platform-remap-surface flex h-full min-h-0 flex-col overflow-hidden px-3 pb-3 pt-3 sm:px-4 sm:pt-4 xl:px-5 xl:pb-4 xl:pt-4">
<div className="mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col">
<div className="shrink-0">
<div
className="flex snap-x gap-2 overflow-x-auto overscroll-x-contain pb-1 scrollbar-hide touch-pan-x [-webkit-overflow-scrolling:touch] sm:gap-3"
role="tablist"
aria-label="选择模板"
>
{getVisiblePlatformCreationTypes().map((item) => {
const selected = item.id === 'puzzle';
const disabled =
item.locked ||
sessionController.isCreatingAgentSession ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isPuzzleBusy ||
isVisualNovelBusy ||
isVisualNovelStreamingReply;
return (
<button
key={item.id}
type="button"
role="tab"
aria-label={item.title}
aria-selected={selected}
disabled={disabled}
onClick={() => {
if (item.id === 'puzzle') {
return;
}
handleCreationHubCreateType(item.id);
}}
className={`platform-creation-reference-card platform-interactive-card relative flex min-h-[4.75rem] w-[10.75rem] shrink-0 snap-start flex-col overflow-hidden rounded-[1.1rem] border p-0 text-left transition sm:min-h-[6.85rem] sm:w-[13.25rem] sm:rounded-[1.28rem] ${
selected
? 'border-white/48 bg-black/12 text-white shadow-none ring-1 ring-inset ring-white/34'
: 'border-[var(--platform-subpanel-border)] bg-[var(--platform-subpanel-fill)] text-white hover:border-[var(--platform-surface-hover-border)]'
} ${disabled ? 'cursor-not-allowed opacity-55' : ''}`}
>
<img
src={item.imageSrc}
alt=""
className="absolute inset-0 h-full w-full object-cover"
loading="lazy"
/>
<span
className={`absolute inset-0 ${
selected
? 'bg-[linear-gradient(90deg,rgba(3,7,18,0.58),rgba(3,7,18,0.08)),linear-gradient(180deg,rgba(3,7,18,0.02)_0%,rgba(3,7,18,0.16)_42%,rgba(3,7,18,0.84)_100%)]'
: item.locked
? 'bg-[linear-gradient(90deg,rgba(3,7,18,0.58),rgba(3,7,18,0.16)),linear-gradient(180deg,rgba(3,7,18,0.08)_0%,rgba(3,7,18,0.2)_42%,rgba(3,7,18,0.84)_100%)]'
: 'bg-[linear-gradient(90deg,rgba(3,7,18,0.5),rgba(3,7,18,0.04)),linear-gradient(180deg,rgba(3,7,18,0.04)_0%,rgba(3,7,18,0.16)_42%,rgba(3,7,18,0.78)_100%)]'
}`}
/>
<span className="relative z-10 flex min-h-5 items-center justify-end gap-2 px-3 pt-2.5 sm:px-4 sm:pt-3">
{item.locked ? (
<span className="platform-pill platform-pill--neutral border-white/30 bg-white/88 px-2.5 text-[10px] text-[var(--platform-text-soft)] sm:text-xs">
{item.badge}
</span>
) : (
<ArrowRight className="h-4 w-4 text-white/82" />
)}
</span>
<span className="relative z-10 mt-auto block min-w-0 px-3 pb-2.5 pt-1 text-white [text-shadow:0_1px_8px_rgba(0,0,0,0.75)] sm:px-4 sm:pb-3 sm:pt-2.5">
<span className="block truncate text-base font-black leading-tight text-white sm:text-lg">
{item.title}
</span>
<span className="mt-0.5 block truncate text-[11px] font-semibold leading-4 text-white/90 sm:mt-1 sm:text-xs">
{item.subtitle}
</span>
</span>
</button>
);
})}
</div>
</div>
<div className="mt-3 min-h-0 flex-1 overflow-hidden">
<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) => {
runProtectedAction(() => {
void createPuzzleDraftFromForm(payload);
});
}}
onAutoSaveForm={(payload) => {
void savePuzzleFormDraft(payload);
}}
showBackButton={false}
title={null}
/>
</Suspense>
</div>
</div>
</div>
);
const draftHubContent = renderCreationHubContent(
'works-only',
'正在加载草稿列表...',
);
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={creationStartContent}
draftTabContent={draftHubContent}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
if (
(entry.worldType ?? '').toLowerCase() === 'puzzle' ||
entry.worldKey.startsWith('puzzle:')
) {
void resumePuzzleSaveArchive(entry);
return;
}
void platformBootstrap.handleResumeSaveEntry(entry);
}}
onOpenCreateWorld={openCreationTypePicker}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={openPublicGalleryDetail}
onOpenRecommendGalleryDetail={openRecommendGalleryDetail}
recommendRuntimeContent={recommendRuntimeContent}
activeRecommendEntryKey={activeRecommendEntryKey}
isStartingRecommendEntry={
isStartingRecommendEntry ||
isBigFishBusy ||
isPuzzleBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isVisualNovelBusy
}
recommendRuntimeError={activeRecommendRuntimeError}
onSelectNextRecommendEntry={() =>
selectAdjacentRecommendRuntimeEntry(1)
}
onSelectPreviousRecommendEntry={() =>
selectAdjacentRecommendRuntimeEntry(-1)
}
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');
}}
onSubmit={submitRpgProfileFeedback}
/>
</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 ||
isVisualNovelBusy
}
error={publicWorkDetailError}
actionMode={selectedPublicWorkActionMode}
visibleCoverCount={resolveVisiblePuzzleDetailCoverCount(
selectedPublicWorkDetail,
puzzleRun,
)}
onBack={() => {
setPublicWorkDetailError(null);
clearSelectedPublicWorkAuthor();
setSelectionStage('platform');
}}
onLike={() => {
likePublicWork(selectedPublicWorkDetail);
}}
onStart={startSelectedPublicWork}
onRemix={remixSelectedPublicWork}
/>
</motion.div>
)}
{selectionStage === 'detail' && (
<motion.div
key="platform-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
{detailNavigation.isDetailLoading || !selectedDetailEntry ? (
<div className="flex h-full items-center justify-center">
<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 === 'creative-agent-workspace' && (
<motion.div
key="creative-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载智能创作..." />}
>
<CreativeAgentWorkspace
session={creativeAgentSession}
isBusy={isCreativeAgentBusy || isPuzzleBusy}
isStreaming={isCreativeAgentStreaming}
error={creativeAgentError}
eventLog={creativeAgentEvents}
onBack={leaveCreativeAgentWorkspace}
onSubmitMessage={(payload) => {
void submitCreativeAgentMessage(payload);
}}
onConfirmTemplate={(selection) => {
void confirmCreativeTemplateSelection(selection);
}}
onOpenTarget={() => {
void openCreativeAgentTarget();
}}
/>
</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();
}}
onSkip={skipPuzzleOnboarding}
/>
</motion.div>
)}
{selectionStage === 'puzzle-generating' && (
<motion.div
key="puzzle-generating"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图生成面板..." />}
>
<CustomWorldGenerationView
settingText={
puzzleSession?.lastAssistantReply ?? '正在整理当前拼图草稿。'
}
anchorEntries={buildPuzzleGenerationAnchorEntries(
puzzleSession,
puzzleFormDraftPayload,
)}
progress={buildMiniGameDraftGenerationProgress(
puzzleGenerationState,
puzzleGenerationProgressNowMs,
)}
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}
creativeDraftEdit={
activeCreativeAgentSessionId
? {
isBusy: isCreativeDraftEditBusy,
error: creativeDraftEditError,
onSubmit: submitCreativeDraftEdit,
}
: null
}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-agent-workspace' && (
<motion.div
key="visual-novel-agent-workspace"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说创作..." />}
>
<VisualNovelAgentWorkspace
session={visualNovelSession}
isBusy={isVisualNovelBusy || isVisualNovelStreamingReply}
error={visualNovelError}
streamingReplyText={visualNovelStreamingReplyText}
onBack={leaveVisualNovelFlow}
onCreateSession={(payload) => {
void visualNovelFlow.openWorkspace(payload);
}}
onSubmitMessage={(payload) => {
void visualNovelFlow.submitMessage(payload);
}}
onExecuteAction={(payload) => {
void visualNovelFlow.executeAction(payload);
}}
onOpenResult={openVisualNovelResult}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-result' &&
visualNovelSession?.draft && (
<motion.div
key="visual-novel-result"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说结果..." />}
>
<VisualNovelResultView
draft={visualNovelWork?.draft ?? visualNovelSession.draft}
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
setSelectionStage('visual-novel-agent-workspace');
}}
onSaveDraft={(draft) => {
void saveVisualNovelDraft(draft);
}}
onCompileWorkProfile={(draft) => {
void compileVisualNovelDraft(draft);
}}
onStartTestRun={(draft) => {
void startVisualNovelTestRunFromDraft(draft);
}}
onPublish={(draft) => {
void publishVisualNovelDraft(draft);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-runtime' && (
<motion.div
key="visual-novel-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说试玩..." />}
>
<VisualNovelRuntimeShell
draft={visualNovelWork?.draft ?? visualNovelSession?.draft}
run={visualNovelRun}
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
setSelectionStage(visualNovelRuntimeReturnStage);
}}
onSubmitAction={(payload) => {
void submitVisualNovelRuntimeAction(payload);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'visual-novel-gallery-detail' &&
visualNovelWork && (
<motion.div
key="visual-novel-gallery-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说详情..." />}
>
<VisualNovelResultView
draft={visualNovelWork.draft}
isBusy={isVisualNovelBusy}
error={visualNovelError}
onBack={() => {
setSelectionStage('platform');
}}
onStartTestRun={(draft) => {
setVisualNovelRuntimeReturnStage(
'visual-novel-gallery-detail',
);
void startVisualNovelTestRunFromDraft(draft);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-gallery-detail' && selectedPuzzleDetail && (
<motion.div
key="puzzle-gallery-detail"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图详情..." />}
>
<PuzzleGalleryDetailView
item={selectedPuzzleDetail}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
platformBootstrap.setPlatformTab(
puzzleDetailReturnTarget?.tab ?? 'home',
);
setPuzzleDetailReturnTarget(null);
setSelectionStage('platform');
}}
onEdit={
selectedPuzzleDetail.ownerUserId === authUi?.user?.id &&
Boolean(selectedPuzzleDetail.sourceSessionId?.trim())
? () => {
runProtectedAction(() => {
void openPuzzleDraft(selectedPuzzleDetail);
});
}
: null
}
onStartGame={() => {
void startPuzzleRunFromProfile(
selectedPuzzleDetail.profileId,
'puzzle-gallery-detail',
selectedPuzzleDetail,
false,
null,
{ authMode: 'isolated' },
);
}}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-runtime' && (
<motion.div
key="puzzle-runtime"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}
>
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={
isPuzzleBusy ||
isPuzzleNextLevelGenerating ||
isPuzzleLeaderboardBusy
}
error={puzzleError}
hideBackButton={Boolean(puzzleOnboardingDraft)}
onBack={() => {
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 ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isPuzzleBusy ||
isVisualNovelBusy ||
isVisualNovelStreamingReply
}
error={
bigFishError ??
creativeAgentError ??
match3dError ??
squareHoleError ??
puzzleCreationError ??
visualNovelError ??
puzzleError ??
sessionController.creationTypeError
}
onClose={() => {
if (
sessionController.isCreatingAgentSession ||
isCreativeAgentBusy ||
isCreativeAgentStreaming ||
isBigFishBusy ||
isMatch3DBusy ||
isSquareHoleBusy ||
isPuzzleBusy ||
isVisualNovelBusy ||
isVisualNovelStreamingReply
) {
return;
}
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
}}
onSelectBigFish={() => {
runProtectedAction(() => {
void openBigFishAgentWorkspace();
});
}}
onSelectMatch3D={() => {
runProtectedAction(() => {
void openMatch3DAgentWorkspace();
});
}}
onSelectSquareHole={() => {
runProtectedAction(() => {
void openSquareHoleAgentWorkspace();
});
}}
onSelectPuzzle={() => {
runProtectedAction(() => {
void openPuzzleAgentWorkspace();
});
}}
onSelectCreativeAgent={() => {
runProtectedAction(() => {
void openCreativeAgentWorkspace();
});
}}
onSelectVisualNovel={() => {
runProtectedAction(() => {
openVisualNovelAgentWorkspace();
});
}}
/>
<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;