9692 lines
315 KiB
TypeScript
9692 lines
315 KiB
TypeScript
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 { resolveRuntimeNotFoundRecoveryAction } from '../../routing/runtimeNotFoundRecovery';
|
||
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 {
|
||
type CreationEntryConfig,
|
||
fetchCreationEntryConfig,
|
||
} from '../../services/creationEntryConfigService';
|
||
import {
|
||
cancelCreativeAgentSession,
|
||
confirmCreativePuzzleTemplate,
|
||
createCreativeAgentSession,
|
||
streamCreativeAgentMessage,
|
||
streamCreativeDraftEdit,
|
||
} from '../../services/creative-agent';
|
||
import {
|
||
readCustomWorldAgentUiState,
|
||
shouldRestoreCustomWorldAgentUiState,
|
||
} from '../../services/customWorldAgentUiState';
|
||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||
import {
|
||
deleteMatch3DWork,
|
||
getMatch3DWorkDetail,
|
||
listMatch3DGallery,
|
||
listMatch3DWorks,
|
||
} from '../../services/match3d-works';
|
||
import {
|
||
buildBigFishGenerationAnchorEntries,
|
||
buildMatch3DGenerationAnchorEntries,
|
||
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 {
|
||
buildVisualNovelEntryGenerationAnchorEntries,
|
||
buildVisualNovelEntryGenerationProgress,
|
||
type VisualNovelEntryFormPayload,
|
||
} from '../visual-novel-creation/VisualNovelAgentWorkspace';
|
||
import { createMockVisualNovelRunFromDraft } from '../visual-novel-runtime/visualNovelMockData';
|
||
import {
|
||
canExposePublicWork,
|
||
EDUTAINMENT_HIDDEN_MESSAGE,
|
||
filterGeneralPublicWorks,
|
||
} from './platformEdutainmentVisibility';
|
||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||
import {
|
||
derivePlatformCreationTypes,
|
||
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 { PlatformFeedbackView } from './PlatformFeedbackView';
|
||
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 VisualNovelEntryGenerationPhase = 'generating' | 'ready' | 'failed';
|
||
|
||
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),
|
||
generatedItemAssets: draft.generatedItemAssets,
|
||
};
|
||
}
|
||
|
||
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, '&')
|
||
.replace(/</gu, '<')
|
||
.replace(/>/gu, '>')
|
||
.replace(/"/gu, '"');
|
||
}
|
||
|
||
function buildPuzzleOnboardingFallbackImage(promptText: string) {
|
||
const trimmedPrompt = promptText.trim();
|
||
const displayPrompt = escapePuzzleOnboardingSvgText(
|
||
trimmedPrompt.slice(0, 12) || '百梦拼图',
|
||
);
|
||
return (
|
||
'data:image/svg+xml;utf8,' +
|
||
encodeURIComponent(`
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||
<defs>
|
||
<linearGradient id="sky" x1="0" y1="0" x2="1" y2="1">
|
||
<stop offset="0" stop-color="#fde68a" />
|
||
<stop offset="0.48" stop-color="#38bdf8" />
|
||
<stop offset="1" stop-color="#312e81" />
|
||
</linearGradient>
|
||
<radialGradient id="glow" cx="34%" cy="30%" r="42%">
|
||
<stop offset="0" stop-color="#fff7ed" stop-opacity="0.86" />
|
||
<stop offset="1" stop-color="#fff7ed" stop-opacity="0" />
|
||
</radialGradient>
|
||
</defs>
|
||
<rect width="1024" height="1024" fill="url(#sky)" />
|
||
<circle cx="352" cy="292" r="248" fill="url(#glow)" />
|
||
<path d="M0 654 C156 570 296 680 452 610 C626 532 782 598 1024 500 V1024 H0 Z" fill="#0f172a" opacity="0.88" />
|
||
<path d="M0 802 C196 710 332 838 540 750 C704 682 846 724 1024 670 V1024 H0 Z" fill="#111827" opacity="0.72" />
|
||
<path d="M160 344 C242 268 340 282 430 350 C532 428 646 388 752 322 C844 266 918 302 958 362" fill="none" stroke="#fff7ed" stroke-width="18" stroke-linecap="round" opacity="0.72" />
|
||
<path d="M188 538 H836" stroke="#ffffff" stroke-width="16" stroke-linecap="round" opacity="0.26" />
|
||
<path d="M260 620 H764" stroke="#ffffff" stroke-width="12" stroke-linecap="round" opacity="0.2" />
|
||
<text x="512" y="868" text-anchor="middle" font-family="sans-serif" font-size="54" font-weight="800" fill="#ffffff" opacity="0.84">${displayPrompt}</text>
|
||
</svg>`)
|
||
);
|
||
}
|
||
|
||
function buildPuzzleOnboardingFallbackWork(
|
||
promptText: string,
|
||
): PuzzleWorkSummary {
|
||
const now = new Date().toISOString();
|
||
const seed = Date.now();
|
||
const coverImageSrc = buildPuzzleOnboardingFallbackImage(promptText);
|
||
const level: PuzzleDraftLevel = {
|
||
levelId: 'onboarding-local-level-1',
|
||
levelName: '梦境拼图',
|
||
pictureDescription: promptText,
|
||
pictureReference: null,
|
||
candidates: [
|
||
{
|
||
candidateId: 'onboarding-local-candidate-1',
|
||
imageSrc: coverImageSrc,
|
||
assetId: 'onboarding-local-asset-1',
|
||
prompt: promptText,
|
||
actualPrompt: promptText,
|
||
sourceType: 'generated',
|
||
selected: true,
|
||
},
|
||
],
|
||
selectedCandidateId: 'onboarding-local-candidate-1',
|
||
coverImageSrc,
|
||
coverAssetId: 'onboarding-local-asset-1',
|
||
generationStatus: 'ready',
|
||
};
|
||
|
||
return {
|
||
workId: `onboarding-local-work-${seed}`,
|
||
profileId: `onboarding-local-profile-${seed}`,
|
||
ownerUserId: 'onboarding-guest',
|
||
sourceSessionId: null,
|
||
authorDisplayName: '百梦主',
|
||
workTitle: '梦境拼图',
|
||
workDescription: promptText,
|
||
levelName: level.levelName,
|
||
summary: promptText,
|
||
themeTags: ['新手引导', '拼图'],
|
||
coverImageSrc,
|
||
coverAssetId: level.coverAssetId,
|
||
publicationStatus: 'draft',
|
||
updatedAt: now,
|
||
publishedAt: null,
|
||
playCount: 0,
|
||
remixCount: 0,
|
||
likeCount: 0,
|
||
publishReady: true,
|
||
levels: [level],
|
||
};
|
||
}
|
||
|
||
function shouldUseLocalPuzzleOnboardingFallback(error: unknown) {
|
||
return (
|
||
error instanceof ApiClientError &&
|
||
error.status === 404 &&
|
||
(error.code === 'NOT_FOUND' || error.message.includes('资源不存在'))
|
||
);
|
||
}
|
||
|
||
function isMissingPuzzleWorkError(error: unknown) {
|
||
return (
|
||
(error instanceof ApiClientError &&
|
||
error.status === 404 &&
|
||
(error.code === 'NOT_FOUND' ||
|
||
error.message.includes('资源不存在') ||
|
||
error.message.includes('未找到'))) ||
|
||
(error instanceof Error &&
|
||
(error.message.includes('资源不存在') ||
|
||
error.message.includes('未找到拼图作品')))
|
||
);
|
||
}
|
||
|
||
function maybeAlertRuntimeNotFoundAndReturnHome() {
|
||
if (typeof window === 'undefined') {
|
||
return false;
|
||
}
|
||
|
||
const recoveryAction = resolveRuntimeNotFoundRecoveryAction(
|
||
window.location.pathname,
|
||
);
|
||
if (!recoveryAction) {
|
||
return false;
|
||
}
|
||
|
||
// 中文注释:直接 runtime 深链找不到作品时,弹窗确认后立刻回首页,避免保留空白运行态。
|
||
window.alert('作品不存在或已下架,将返回首页。');
|
||
pushAppHistoryPath(recoveryAction.nextPath);
|
||
return true;
|
||
}
|
||
|
||
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 [match3dFormDraftPayload, setMatch3DFormDraftPayload] =
|
||
useState<CreateMatch3DSessionRequest | null>(null);
|
||
const [match3dGenerationState, setMatch3DGenerationState] =
|
||
useState<MiniGameDraftGenerationState | null>(null);
|
||
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 [miniGameGenerationProgressNowMs, setMiniGameGenerationProgressNowMs] =
|
||
useState(() => Date.now());
|
||
const [puzzleFormDraftPayload, setPuzzleFormDraftPayload] =
|
||
useState<CreatePuzzleAgentSessionRequest | null>(null);
|
||
const [activeCreationFormType, setActiveCreationFormType] =
|
||
useState<PlatformCreationTypeId>('puzzle');
|
||
const [puzzleOnboardingPrompt, setPuzzleOnboardingPrompt] = useState('');
|
||
const [puzzleOnboardingPhase, setPuzzleOnboardingPhase] =
|
||
useState<PuzzleOnboardingPhase>('input');
|
||
const [puzzleOnboardingDraft, setPuzzleOnboardingDraft] =
|
||
useState<PuzzleOnboardingDraft | null>(null);
|
||
const [puzzleOnboardingError, setPuzzleOnboardingError] = useState<
|
||
string | null
|
||
>(null);
|
||
const [isPuzzleOnboardingSaving, setIsPuzzleOnboardingSaving] =
|
||
useState(false);
|
||
const [creativeAgentSession, setCreativeAgentSession] =
|
||
useState<CreativeAgentSessionSnapshot | null>(null);
|
||
const [creativeAgentError, setCreativeAgentError] = useState<string | null>(
|
||
null,
|
||
);
|
||
const [isCreativeAgentBusy, setIsCreativeAgentBusy] = useState(false);
|
||
const [isCreativeAgentStreaming, setIsCreativeAgentStreaming] =
|
||
useState(false);
|
||
const [creativeAgentEvents, setCreativeAgentEvents] = useState<
|
||
CreativeAgentSseEvent[]
|
||
>([]);
|
||
const [activeCreativeAgentSessionId, setActiveCreativeAgentSessionId] =
|
||
useState<string | null>(null);
|
||
const [creativeDraftEditError, setCreativeDraftEditError] = useState<
|
||
string | null
|
||
>(null);
|
||
const [isCreativeDraftEditBusy, setIsCreativeDraftEditBusy] = useState(false);
|
||
const [visualNovelWork, setVisualNovelWork] =
|
||
useState<VisualNovelWorkDetail | null>(null);
|
||
const [visualNovelWorks, setVisualNovelWorks] = useState<
|
||
VisualNovelWorkSummary[]
|
||
>([]);
|
||
const [visualNovelGalleryEntries, setVisualNovelGalleryEntries] = useState<
|
||
VisualNovelWorkSummary[]
|
||
>([]);
|
||
const [visualNovelRun, setVisualNovelRun] =
|
||
useState<VisualNovelRunSnapshot | null>(null);
|
||
const [visualNovelRuntimeReturnStage, setVisualNovelRuntimeReturnStage] =
|
||
useState<VisualNovelRuntimeReturnStage>('visual-novel-result');
|
||
const [visualNovelFormDraftPayload, setVisualNovelFormDraftPayload] =
|
||
useState<VisualNovelEntryFormPayload | null>(null);
|
||
const [visualNovelGenerationStartedAtMs, setVisualNovelGenerationStartedAtMs] =
|
||
useState<number | null>(null);
|
||
const [visualNovelGenerationPhase, setVisualNovelGenerationPhase] =
|
||
useState<VisualNovelEntryGenerationPhase>('generating');
|
||
const [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 [creationEntryConfig, setCreationEntryConfig] =
|
||
useState<CreationEntryConfig | null>(null);
|
||
const [creationEntryConfigError, setCreationEntryConfigError] = useState<
|
||
string | null
|
||
>(null);
|
||
const creationEntryTypes = useMemo(
|
||
() =>
|
||
creationEntryConfig
|
||
? derivePlatformCreationTypes(creationEntryConfig.creationTypes)
|
||
: [],
|
||
[creationEntryConfig],
|
||
);
|
||
const isBigFishCreationVisible = isPlatformCreationTypeVisible(
|
||
creationEntryTypes,
|
||
'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);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
setCreationEntryConfigError(null);
|
||
void fetchCreationEntryConfig()
|
||
.then((config) => {
|
||
if (!cancelled) {
|
||
setCreationEntryConfig(config);
|
||
}
|
||
})
|
||
.catch((error: unknown) => {
|
||
if (!cancelled) {
|
||
setCreationEntryConfigError(
|
||
error instanceof Error ? error.message : '读取创作入口配置失败。',
|
||
);
|
||
}
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, []);
|
||
|
||
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>();
|
||
filterGeneralPublicWorks([
|
||
...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 activeGenerationState =
|
||
selectionStage === 'puzzle-generating'
|
||
? puzzleGenerationState
|
||
: selectionStage === 'match3d-generating'
|
||
? match3dGenerationState
|
||
: null;
|
||
const shouldTickProgress =
|
||
selectionStage === 'visual-novel-generating'
|
||
? visualNovelGenerationStartedAtMs != null &&
|
||
visualNovelGenerationPhase !== 'ready' &&
|
||
visualNovelGenerationPhase !== 'failed'
|
||
: activeGenerationState != null &&
|
||
activeGenerationState.phase !== 'ready' &&
|
||
activeGenerationState.phase !== 'failed';
|
||
|
||
if (!shouldTickProgress) {
|
||
return undefined;
|
||
}
|
||
|
||
setMiniGameGenerationProgressNowMs(Date.now());
|
||
const timerId = window.setInterval(() => {
|
||
setMiniGameGenerationProgressNowMs(Date.now());
|
||
}, 500);
|
||
|
||
return () => window.clearInterval(timerId);
|
||
}, [
|
||
match3dGenerationState,
|
||
puzzleGenerationState,
|
||
selectionStage,
|
||
visualNovelGenerationPhase,
|
||
visualNovelGenerationStartedAtMs,
|
||
]);
|
||
|
||
const runProtectedAction = useCallback(
|
||
(action: () => void) => {
|
||
if (!authUi?.requireAuth) {
|
||
action();
|
||
return;
|
||
}
|
||
|
||
authUi.requireAuth(action);
|
||
},
|
||
[authUi],
|
||
);
|
||
|
||
const savePuzzleOnboardingDraft = useCallback(async () => {
|
||
if (!puzzleOnboardingDraft || isPuzzleOnboardingSaving) {
|
||
return;
|
||
}
|
||
|
||
setIsPuzzleOnboardingSaving(true);
|
||
setPuzzleOnboardingError(null);
|
||
try {
|
||
const response = await savePuzzleOnboardingWork({
|
||
promptText: puzzleOnboardingDraft.promptText,
|
||
item: puzzleOnboardingDraft.item,
|
||
});
|
||
setPuzzleWorks((current) => [response.item, ...current]);
|
||
setSelectedPuzzleDetail(null);
|
||
setPuzzleRun(null);
|
||
setPuzzleRuntimeAuthMode('default');
|
||
setPuzzleOnboardingDraft(null);
|
||
setPuzzleOnboardingPrompt('');
|
||
setPuzzleOnboardingPhase('input');
|
||
platformBootstrap.setPlatformTab('home');
|
||
setSelectionStage('platform');
|
||
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 match3dRuntimeAdapter = useMemo(
|
||
() => createServerMatch3DRuntimeAdapter(),
|
||
[],
|
||
);
|
||
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: () => {
|
||
setActiveCreationFormType('match3d');
|
||
setShowCreationTypeModal(false);
|
||
},
|
||
onActionComplete: async ({ payload, response, setSession }) => {
|
||
setSession(response.session);
|
||
if (payload.action !== 'match3d_compile_draft') {
|
||
return;
|
||
}
|
||
setMatch3DGenerationState((current) =>
|
||
current
|
||
? {
|
||
...current,
|
||
phase: 'ready',
|
||
completedAssetCount:
|
||
response.session.draft?.generatedItemAssets?.length ?? 3,
|
||
totalAssetCount:
|
||
response.session.draft?.generatedItemAssets?.length ?? 3,
|
||
}
|
||
: current,
|
||
);
|
||
|
||
const profileId = response.session.draft?.profileId;
|
||
if (!profileId) {
|
||
setMatch3DProfile(null);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const { item } = await getMatch3DWorkDetail(profileId);
|
||
setMatch3DProfile({
|
||
...item,
|
||
generatedItemAssets:
|
||
response.session.draft?.generatedItemAssets ??
|
||
item.generatedItemAssets,
|
||
});
|
||
await refreshMatch3DShelf().catch(() => undefined);
|
||
} catch {
|
||
setMatch3DProfile(buildMatch3DProfileFromSession(response.session));
|
||
}
|
||
},
|
||
beforeExecuteAction: ({ payload }) => {
|
||
if (payload.action !== 'match3d_compile_draft') {
|
||
return;
|
||
}
|
||
setSelectionStage('match3d-generating');
|
||
setMatch3DGenerationState(createMiniGameDraftGenerationState('match3d'));
|
||
},
|
||
onActionError: ({ payload, errorMessage }) => {
|
||
if (payload.action !== 'match3d_compile_draft') {
|
||
return;
|
||
}
|
||
setMatch3DGenerationState((current) =>
|
||
current
|
||
? {
|
||
...current,
|
||
phase: 'failed',
|
||
error: errorMessage,
|
||
}
|
||
: current,
|
||
);
|
||
},
|
||
});
|
||
|
||
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: () => {
|
||
setActiveCreationFormType('puzzle');
|
||
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 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 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 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 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 createMatch3DDraftFromForm = useCallback(
|
||
async (payload: CreateMatch3DSessionRequest) => {
|
||
setMatch3DFormDraftPayload(payload);
|
||
setMatch3DGenerationState(null);
|
||
setMatch3DSession(null);
|
||
setMatch3DProfile(null);
|
||
setMatch3DRun(null);
|
||
setMatch3DError(null);
|
||
setStreamingMatch3DReplyText('');
|
||
setIsStreamingMatch3DReply(false);
|
||
|
||
const nextSession = await match3dFlow.openWorkspace(payload);
|
||
if (!nextSession) {
|
||
return;
|
||
}
|
||
|
||
await match3dFlow.executeAction(
|
||
{ action: 'match3d_compile_draft' },
|
||
nextSession,
|
||
);
|
||
},
|
||
[
|
||
match3dFlow,
|
||
setIsStreamingMatch3DReply,
|
||
setMatch3DError,
|
||
setMatch3DProfile,
|
||
setMatch3DRun,
|
||
setMatch3DSession,
|
||
setStreamingMatch3DReplyText,
|
||
],
|
||
);
|
||
|
||
const createVisualNovelDraftFromForm = useCallback(
|
||
async (payload: VisualNovelEntryFormPayload) => {
|
||
setVisualNovelFormDraftPayload(payload);
|
||
setVisualNovelGenerationStartedAtMs(Date.now());
|
||
setVisualNovelGenerationPhase('generating');
|
||
setVisualNovelWork(null);
|
||
setVisualNovelRun(null);
|
||
setVisualNovelRuntimeReturnStage('visual-novel-result');
|
||
setVisualNovelError(null);
|
||
setIsVisualNovelBusy(true);
|
||
setSelectionStage('visual-novel-generating');
|
||
|
||
try {
|
||
const createResponse = await createVisualNovelSession({
|
||
sourceMode: payload.sourceMode,
|
||
seedText: payload.seedText,
|
||
sourceAssetIds: payload.sourceAssetIds,
|
||
});
|
||
setVisualNovelSession(createResponse.session);
|
||
const nextSession = await streamVisualNovelMessage(
|
||
createResponse.session.sessionId,
|
||
{
|
||
clientMessageId: `visual-novel-entry-${Date.now().toString(36)}`,
|
||
text: payload.seedText,
|
||
},
|
||
);
|
||
setVisualNovelSession(nextSession);
|
||
setVisualNovelGenerationPhase('ready');
|
||
setSelectionStage('visual-novel-result');
|
||
} catch (error) {
|
||
setVisualNovelGenerationPhase('failed');
|
||
setVisualNovelError(
|
||
resolvePuzzleErrorMessage(error, '生成视觉小说草稿失败。'),
|
||
);
|
||
} finally {
|
||
setIsVisualNovelBusy(false);
|
||
}
|
||
},
|
||
[
|
||
resolvePuzzleErrorMessage,
|
||
setIsVisualNovelBusy,
|
||
setSelectionStage,
|
||
setVisualNovelError,
|
||
setVisualNovelSession,
|
||
],
|
||
);
|
||
|
||
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);
|
||
setMatch3DFormDraftPayload(null);
|
||
setActiveCreationFormType('puzzle');
|
||
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');
|
||
setVisualNovelFormDraftPayload(null);
|
||
setVisualNovelGenerationStartedAtMs(null);
|
||
setVisualNovelGenerationPhase('generating');
|
||
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,
|
||
setActiveCreationFormType,
|
||
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') {
|
||
enterCreateTab();
|
||
setShowCreationTypeModal(false);
|
||
setActiveCreationFormType('match3d');
|
||
setMatch3DError(null);
|
||
return;
|
||
}
|
||
|
||
if (type === 'square-hole') {
|
||
runProtectedAction(() => {
|
||
void openSquareHoleAgentWorkspace();
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'puzzle') {
|
||
enterCreateTab();
|
||
setShowCreationTypeModal(false);
|
||
setActiveCreationFormType('puzzle');
|
||
setPuzzleCreationError(null);
|
||
setPuzzleError(null);
|
||
return;
|
||
}
|
||
|
||
if (type === 'visual-novel') {
|
||
enterCreateTab();
|
||
setShowCreationTypeModal(false);
|
||
setActiveCreationFormType('visual-novel');
|
||
setVisualNovelError(null);
|
||
return;
|
||
}
|
||
},
|
||
[
|
||
openBigFishAgentWorkspace,
|
||
enterCreateTab,
|
||
openSquareHoleAgentWorkspace,
|
||
prepareCreationLaunch,
|
||
runProtectedAction,
|
||
sessionController,
|
||
setActiveCreationFormType,
|
||
setMatch3DError,
|
||
setPuzzleCreationError,
|
||
setPuzzleError,
|
||
setVisualNovelError,
|
||
],
|
||
);
|
||
|
||
const leaveBigFishFlow = useCallback(() => {
|
||
setBigFishRun(null);
|
||
setBigFishRuntimeWork(null);
|
||
setBigFishRuntimeStartedAt(null);
|
||
setBigFishRuntimeReturnStage('platform');
|
||
setBigFishGenerationState(null);
|
||
bigFishFlow.leaveFlow();
|
||
}, [bigFishFlow]);
|
||
|
||
const leaveMatch3DFlow = useCallback(() => {
|
||
setMatch3DRun(null);
|
||
setMatch3DFormDraftPayload(null);
|
||
setMatch3DGenerationState(null);
|
||
setMatch3DRuntimeReturnStage('match3d-result');
|
||
match3dFlow.leaveFlow();
|
||
}, [match3dFlow, setMatch3DFormDraftPayload]);
|
||
|
||
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');
|
||
setVisualNovelFormDraftPayload(null);
|
||
setVisualNovelGenerationStartedAtMs(null);
|
||
setVisualNovelGenerationPhase('generating');
|
||
visualNovelFlow.leaveFlow();
|
||
}, [visualNovelFlow]);
|
||
|
||
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 submitSquareHoleMessage = squareHoleFlow.submitMessage;
|
||
|
||
const submitPuzzleMessage = puzzleFlow.submitMessage;
|
||
|
||
const executeBigFishAction = bigFishFlow.executeAction;
|
||
|
||
const executeMatch3DAction = match3dFlow.executeAction;
|
||
|
||
const executeSquareHoleAction = squareHoleFlow.executeAction;
|
||
|
||
const retryMatch3DDraftGeneration = useCallback(() => {
|
||
if (match3dFormDraftPayload) {
|
||
void createMatch3DDraftFromForm(match3dFormDraftPayload);
|
||
return;
|
||
}
|
||
|
||
void executeMatch3DAction({
|
||
action: 'match3d_compile_draft',
|
||
});
|
||
}, [
|
||
createMatch3DDraftFromForm,
|
||
executeMatch3DAction,
|
||
match3dFormDraftPayload,
|
||
]);
|
||
|
||
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 executePuzzleBackgroundAction = useCallback(
|
||
async (payload: PuzzleAgentActionRequest) => {
|
||
const targetSession = puzzleFlow.session;
|
||
if (!targetSession) {
|
||
return;
|
||
}
|
||
|
||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||
if (formPayload) {
|
||
setPuzzleFormDraftPayload(formPayload);
|
||
}
|
||
|
||
setPuzzleError(null);
|
||
try {
|
||
const response = await executePuzzleAgentAction(
|
||
targetSession.sessionId,
|
||
payload,
|
||
);
|
||
setPuzzleOperation(response.operation);
|
||
puzzleFlow.setSession(response.session);
|
||
} catch (error) {
|
||
setPuzzleError(
|
||
resolvePuzzleErrorMessage(error, '执行拼图操作失败。'),
|
||
);
|
||
}
|
||
},
|
||
[puzzleFlow, resolvePuzzleErrorMessage, setPuzzleError],
|
||
);
|
||
|
||
const retryPuzzleDraftGeneration = useCallback(() => {
|
||
if (puzzleFormDraftPayload) {
|
||
void createPuzzleDraftFromForm(puzzleFormDraftPayload);
|
||
return;
|
||
}
|
||
|
||
void executePuzzleAction(
|
||
buildPuzzleCompileActionFromFormPayload(puzzleFormDraftPayload),
|
||
);
|
||
}, [createPuzzleDraftFromForm, executePuzzleAction, puzzleFormDraftPayload]);
|
||
|
||
const retryVisualNovelDraftGeneration = useCallback(() => {
|
||
if (!visualNovelFormDraftPayload) {
|
||
setSelectionStage('visual-novel-agent-workspace');
|
||
return;
|
||
}
|
||
|
||
void createVisualNovelDraftFromForm(visualNovelFormDraftPayload);
|
||
}, [
|
||
createVisualNovelDraftFromForm,
|
||
setSelectionStage,
|
||
visualNovelFormDraftPayload,
|
||
]);
|
||
|
||
const executePuzzleWorkspaceAction = useCallback(
|
||
(payload: PuzzleAgentActionRequest) => {
|
||
if (
|
||
payload.action === 'compile_puzzle_draft' &&
|
||
isEmptyPuzzleFormOnlyDraft(puzzleFlow.session)
|
||
) {
|
||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||
if (formPayload) {
|
||
void createPuzzleDraftFromForm(formPayload);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (payload.action === 'generate_puzzle_images') {
|
||
void executePuzzleBackgroundAction(payload);
|
||
return;
|
||
}
|
||
|
||
void executePuzzleAction(payload);
|
||
},
|
||
[
|
||
createPuzzleDraftFromForm,
|
||
executePuzzleAction,
|
||
executePuzzleBackgroundAction,
|
||
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');
|
||
if (!maybeAlertRuntimeNotFoundAndReturnHome()) {
|
||
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 match3dRuntimeAdapter.startRun(
|
||
profile.profileId,
|
||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||
)
|
||
: await match3dRuntimeAdapter.startRun(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,
|
||
match3dRuntimeAdapter,
|
||
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);
|
||
setMatch3DFormDraftPayload(null);
|
||
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,
|
||
setMatch3DFormDraftPayload,
|
||
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) => {
|
||
if (!canExposePublicWork(entry)) {
|
||
setSelectedPublicWorkDetail(null);
|
||
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
|
||
setSelectionStage('platform');
|
||
return;
|
||
}
|
||
|
||
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) => {
|
||
if (!canExposePublicWork(entry)) {
|
||
setSelectedPublicWorkDetail(null);
|
||
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
|
||
setSelectionStage('platform');
|
||
return;
|
||
}
|
||
|
||
setIsPublicWorkDetailBusy(true);
|
||
setPublicWorkDetailError(null);
|
||
clearSelectedPublicWorkAuthor();
|
||
setSelectedPublicWorkDetail(entry);
|
||
setSelectionStage('work-detail');
|
||
|
||
try {
|
||
const detailEntry =
|
||
await detailNavigation.loadGalleryDetailEntry(entry);
|
||
setSelectedDetailEntry(detailEntry);
|
||
const detailCard = mapRpgGalleryCardToPublicWorkDetail(detailEntry);
|
||
if (!canExposePublicWork(detailCard)) {
|
||
setSelectedDetailEntry(null);
|
||
setSelectedPublicWorkDetail(null);
|
||
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
|
||
setSelectionStage('platform');
|
||
return;
|
||
}
|
||
|
||
setSelectedPublicWorkDetail(detailCard);
|
||
if (detailEntry.publicWorkCode?.trim()) {
|
||
pushAppHistoryPath(
|
||
buildPublicWorkStagePath('work-detail', detailEntry.publicWorkCode),
|
||
);
|
||
}
|
||
} catch (error) {
|
||
setSelectedPublicWorkDetail(entry);
|
||
setPublicWorkDetailError(
|
||
resolveRpgCreationErrorMessage(error, '读取作品详情失败。'),
|
||
);
|
||
} finally {
|
||
setIsPublicWorkDetailBusy(false);
|
||
}
|
||
},
|
||
[
|
||
clearSelectedPublicWorkAuthor,
|
||
detailNavigation,
|
||
setSelectedDetailEntry,
|
||
setSelectionStage,
|
||
],
|
||
);
|
||
|
||
const openPuzzlePublicWorkDetail = useCallback(
|
||
async (
|
||
profileId: string,
|
||
returnTarget: PuzzleDetailReturnTarget = {
|
||
tab: platformBootstrap.platformTab,
|
||
},
|
||
) => {
|
||
setIsPuzzleBusy(true);
|
||
setIsPublicWorkDetailBusy(true);
|
||
setPuzzleError(null);
|
||
setPublicWorkDetailError(null);
|
||
setSelectionStage('work-detail');
|
||
|
||
try {
|
||
const { item } = await getPuzzleGalleryDetail(profileId);
|
||
const detailEntry = mapPuzzleWorkToPublicWorkDetail(item);
|
||
if (!canExposePublicWork(detailEntry)) {
|
||
setSelectedPuzzleDetail(null);
|
||
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
|
||
setSelectionStage('platform');
|
||
return;
|
||
}
|
||
|
||
setSelectedPuzzleDetail(item);
|
||
setPuzzleDetailReturnTarget(returnTarget);
|
||
openPublicWorkDetail(detailEntry);
|
||
} catch (error) {
|
||
if (isMissingPuzzleWorkError(error)) {
|
||
setSelectedPuzzleDetail(null);
|
||
setPuzzleDetailReturnTarget(null);
|
||
setPuzzleRun(null);
|
||
setPuzzleRuntimeAuthMode('default');
|
||
setPuzzleError(null);
|
||
setPublicWorkDetailError(null);
|
||
setPlatformTab('home');
|
||
setSelectionStage('platform');
|
||
if (!maybeAlertRuntimeNotFoundAndReturnHome()) {
|
||
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');
|
||
if (!maybeAlertRuntimeNotFoundAndReturnHome()) {
|
||
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;
|
||
}
|
||
|
||
setMatch3DFormDraftPayload(null);
|
||
|
||
try {
|
||
const { item: profile } = await getMatch3DWorkDetail(item.profileId);
|
||
setMatch3DProfile(profile);
|
||
} catch (error) {
|
||
setMatch3DProfile(buildMatch3DProfileFromSession(restoredSession));
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(error, '读取抓大鹅作品详情失败。'),
|
||
);
|
||
}
|
||
},
|
||
[
|
||
match3dFlow,
|
||
openPublicWorkDetail,
|
||
refreshMatch3DShelf,
|
||
resolveMatch3DErrorMessage,
|
||
setMatch3DFormDraftPayload,
|
||
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 match3dRuntimeAdapter.restartRun(match3dRun.runId)
|
||
.then(({ run }) => {
|
||
setMatch3DRun(run);
|
||
})
|
||
.catch((error) => {
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(
|
||
error,
|
||
'重新开始抓大鹅玩法失败。',
|
||
),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
match3dFlow.setIsBusy(false);
|
||
});
|
||
}}
|
||
onOptimisticRunChange={setMatch3DRun}
|
||
onClickItem={(payload) => {
|
||
const runId = payload.runId ?? match3dRun?.runId;
|
||
if (!runId) {
|
||
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
|
||
}
|
||
return match3dRuntimeAdapter.clickItem(runId, payload);
|
||
}}
|
||
onTimeExpired={() => {
|
||
if (!match3dRun?.runId) {
|
||
return;
|
||
}
|
||
|
||
void match3dRuntimeAdapter.finishTimeUp(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,
|
||
match3dRuntimeAdapter,
|
||
platformBootstrap.platformTab,
|
||
platformThemeClass,
|
||
puzzleError,
|
||
puzzleRun,
|
||
recommendRuntimeEntries,
|
||
remodelCurrentPuzzleRuntimeWork,
|
||
resolveMatch3DErrorMessage,
|
||
resolveSquareHoleErrorMessage,
|
||
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;
|
||
if (!canExposePublicWork(card)) {
|
||
throw new Error(EDUTAINMENT_HIDDEN_MESSAGE);
|
||
}
|
||
|
||
setSelectedDetailEntry(entry);
|
||
openPublicWorkDetail(card);
|
||
};
|
||
const tryOpenPuzzleGalleryEntry = async () => {
|
||
const entries =
|
||
puzzleGalleryEntries.length > 0
|
||
? puzzleGalleryEntries
|
||
: await refreshPuzzleGallery();
|
||
const matchedEntry = entries
|
||
.map(mapPuzzleWorkToPublicWorkDetail)
|
||
.filter(canExposePublicWork)
|
||
.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) => {
|
||
const detailEntry = mapBigFishWorkToPublicWorkDetail(entry);
|
||
return (
|
||
canExposePublicWork(detailEntry) &&
|
||
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) => {
|
||
const detailEntry = mapMatch3DWorkToPublicWorkDetail(entry);
|
||
return (
|
||
canExposePublicWork(detailEntry) &&
|
||
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) => {
|
||
const detailEntry = mapSquareHoleWorkToPublicWorkDetail(entry);
|
||
return (
|
||
canExposePublicWork(detailEntry) &&
|
||
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) => {
|
||
const detailEntry = mapVisualNovelWorkToPublicWorkDetail(entry);
|
||
return (
|
||
canExposePublicWork(detailEntry) &&
|
||
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} />}>
|
||
{creationEntryConfig ? (
|
||
<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);
|
||
setCreationEntryConfigError(null);
|
||
void fetchCreationEntryConfig()
|
||
.then(setCreationEntryConfig)
|
||
.catch((error: unknown) => {
|
||
setCreationEntryConfigError(
|
||
error instanceof Error ? error.message : '读取创作入口配置失败。',
|
||
);
|
||
});
|
||
setBigFishError(null);
|
||
setMatch3DError(null);
|
||
setSquareHoleError(null);
|
||
setPuzzleShelfError(null);
|
||
setPuzzleCreationError(null);
|
||
setPuzzleError(null);
|
||
setVisualNovelError(null);
|
||
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
|
||
platformBootstrap.setPlatformError(
|
||
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
|
||
);
|
||
});
|
||
if (isBigFishCreationVisible) {
|
||
void refreshBigFishShelf();
|
||
}
|
||
void refreshMatch3DShelf();
|
||
void refreshSquareHoleShelf();
|
||
void refreshPuzzleShelf();
|
||
void refreshVisualNovelShelf();
|
||
}}
|
||
createError={
|
||
creationEntryConfigError ??
|
||
sessionController.creationTypeError ??
|
||
bigFishError ??
|
||
match3dError ??
|
||
squareHoleError ??
|
||
puzzleCreationError ??
|
||
puzzleError ??
|
||
visualNovelError
|
||
}
|
||
createBusy={
|
||
!creationEntryConfig ||
|
||
sessionController.isCreatingAgentSession ||
|
||
isCreativeAgentBusy ||
|
||
isCreativeAgentStreaming ||
|
||
isBigFishBusy ||
|
||
isMatch3DBusy ||
|
||
isSquareHoleBusy ||
|
||
isPuzzleBusy ||
|
||
isVisualNovelBusy ||
|
||
isVisualNovelStreamingReply
|
||
}
|
||
entryConfig={creationEntryConfig}
|
||
creationTypes={creationEntryTypes}
|
||
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);
|
||
}}
|
||
/>
|
||
) : null}
|
||
</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(creationEntryTypes).map((item) => {
|
||
const selected = item.id === activeCreationFormType;
|
||
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 === activeCreationFormType) {
|
||
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">
|
||
{activeCreationFormType === 'match3d' ? (
|
||
<Suspense
|
||
fallback={
|
||
<LazyPanelFallback label="正在加载抓大鹅创作..." />
|
||
}
|
||
>
|
||
<Match3DAgentWorkspace
|
||
session={match3dSession}
|
||
isBusy={isMatch3DBusy || isStreamingMatch3DReply}
|
||
error={match3dError}
|
||
onBack={leaveMatch3DFlow}
|
||
onExecuteAction={(payload) => {
|
||
void executeMatch3DAction(payload);
|
||
}}
|
||
initialFormPayload={match3dFormDraftPayload}
|
||
onCreateFromForm={(payload) => {
|
||
runProtectedAction(() => {
|
||
void createMatch3DDraftFromForm(payload);
|
||
});
|
||
}}
|
||
showBackButton={false}
|
||
title={null}
|
||
/>
|
||
</Suspense>
|
||
) : activeCreationFormType === 'visual-novel' ? (
|
||
<Suspense
|
||
fallback={<LazyPanelFallback label="正在加载视觉小说创作..." />}
|
||
>
|
||
<VisualNovelAgentWorkspace
|
||
session={null}
|
||
isBusy={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||
error={visualNovelError}
|
||
onBack={leaveVisualNovelFlow}
|
||
initialFormPayload={visualNovelFormDraftPayload}
|
||
onCreateFromForm={(payload) => {
|
||
runProtectedAction(() => {
|
||
void createVisualNovelDraftFromForm(payload);
|
||
});
|
||
}}
|
||
showBackButton={false}
|
||
title={null}
|
||
/>
|
||
</Suspense>
|
||
) : (
|
||
<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)
|
||
}
|
||
onLikeRecommendEntry={(entry) => {
|
||
likePublicWork(entry);
|
||
}}
|
||
onRemixRecommendEntry={(entry) => {
|
||
remixPublicWork(entry);
|
||
}}
|
||
onOpenLibraryDetail={(entry) => {
|
||
runProtectedAction(() => {
|
||
void detailNavigation.openLibraryDetail(entry);
|
||
});
|
||
}}
|
||
onDeleteLibraryEntry={(entry) => {
|
||
handleDeleteLibraryEntry(entry);
|
||
}}
|
||
deletingLibraryEntryId={deletingCreationWorkId}
|
||
onSearchPublicCode={(keyword) => {
|
||
void handlePublicCodeSearch(keyword);
|
||
}}
|
||
isSearchingPublicCode={isSearchingPublicCode}
|
||
profilePlayStats={profilePlayStats}
|
||
isProfilePlayStatsOpen={isProfilePlayStatsOpen}
|
||
isProfilePlayStatsLoading={isProfilePlayStatsLoading}
|
||
profilePlayStatsError={profilePlayStatsError}
|
||
onCloseProfilePlayStats={() => {
|
||
setIsProfilePlayStatsOpen(false);
|
||
}}
|
||
onOpenPlayedWork={openPlayedWork}
|
||
onOpenFeedback={openProfileFeedback}
|
||
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' && match3dSession && (
|
||
<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}
|
||
isBusy={isMatch3DBusy || isStreamingMatch3DReply}
|
||
error={match3dError}
|
||
onBack={leaveMatch3DFlow}
|
||
onExecuteAction={(payload) => {
|
||
void executeMatch3DAction(payload);
|
||
}}
|
||
/>
|
||
</Suspense>
|
||
</motion.div>
|
||
)}
|
||
|
||
{selectionStage === 'match3d-generating' && (
|
||
<motion.div
|
||
key="match3d-generating"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -12 }}
|
||
className="flex h-full min-h-0 flex-col"
|
||
>
|
||
<Suspense
|
||
fallback={<LazyPanelFallback label="正在加载抓大鹅生成面板..." />}
|
||
>
|
||
<CustomWorldGenerationView
|
||
settingText={
|
||
match3dSession?.lastAssistantReply ??
|
||
'正在生成本局抓大鹅物品素材。'
|
||
}
|
||
anchorEntries={buildMatch3DGenerationAnchorEntries(
|
||
match3dSession,
|
||
match3dFormDraftPayload,
|
||
)}
|
||
progress={buildMiniGameDraftGenerationProgress(
|
||
match3dGenerationState,
|
||
miniGameGenerationProgressNowMs,
|
||
)}
|
||
isGenerating={isMatch3DBusy}
|
||
error={match3dError}
|
||
onBack={leaveMatch3DFlow}
|
||
onEditSetting={() => {
|
||
setSelectionStage('match3d-agent-workspace');
|
||
}}
|
||
onRetry={retryMatch3DDraftGeneration}
|
||
onInterrupt={undefined}
|
||
backLabel="返回创作中心"
|
||
settingActionLabel={null}
|
||
retryLabel="重新生成草稿"
|
||
settingTitle="当前抓大鹅信息"
|
||
settingDescription={null}
|
||
progressTitle="抓大鹅草稿生成进度"
|
||
activeBadgeLabel="素材生成中"
|
||
pausedBadgeLabel="素材生成已暂停"
|
||
idleBadgeLabel="等待返回工作区"
|
||
/>
|
||
</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 match3dRuntimeAdapter.stopRun(match3dRun.runId).catch(
|
||
() => undefined,
|
||
);
|
||
}
|
||
setSelectionStage(match3dRuntimeReturnStage);
|
||
}}
|
||
onRestart={() => {
|
||
if (!match3dRun?.runId || isMatch3DBusy) {
|
||
return;
|
||
}
|
||
|
||
match3dFlow.setIsBusy(true);
|
||
setMatch3DError(null);
|
||
void match3dRuntimeAdapter.restartRun(match3dRun.runId)
|
||
.then(({ run }) => {
|
||
setMatch3DRun(run);
|
||
})
|
||
.catch((error) => {
|
||
setMatch3DError(
|
||
resolveMatch3DErrorMessage(
|
||
error,
|
||
'重新开始抓大鹅玩法失败。',
|
||
),
|
||
);
|
||
})
|
||
.finally(() => {
|
||
match3dFlow.setIsBusy(false);
|
||
});
|
||
}}
|
||
onOptimisticRunChange={setMatch3DRun}
|
||
onClickItem={(payload) => {
|
||
const runId = payload.runId ?? match3dRun?.runId;
|
||
if (!runId) {
|
||
return Promise.reject(
|
||
new Error('抓大鹅运行态缺少 runId。'),
|
||
);
|
||
}
|
||
return match3dRuntimeAdapter.clickItem(runId, payload);
|
||
}}
|
||
onTimeExpired={() => {
|
||
if (!match3dRun?.runId) {
|
||
return;
|
||
}
|
||
|
||
void match3dRuntimeAdapter.finishTimeUp(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,
|
||
miniGameGenerationProgressNowMs,
|
||
)}
|
||
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) => {
|
||
executePuzzleWorkspaceAction(payload);
|
||
}}
|
||
onStartTestRun={startPuzzleTestRunFromDraft}
|
||
creativeDraftEdit={
|
||
activeCreativeAgentSessionId
|
||
? {
|
||
isBusy: isCreativeDraftEditBusy,
|
||
error: creativeDraftEditError,
|
||
onSubmit: submitCreativeDraftEdit,
|
||
}
|
||
: null
|
||
}
|
||
/>
|
||
</Suspense>
|
||
</motion.div>
|
||
)}
|
||
|
||
{selectionStage === 'visual-novel-agent-workspace' && (
|
||
<motion.div
|
||
key="visual-novel-agent-workspace"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -12 }}
|
||
className="flex h-full min-h-0 flex-col"
|
||
>
|
||
<Suspense
|
||
fallback={<LazyPanelFallback label="正在加载视觉小说创作..." />}
|
||
>
|
||
<VisualNovelAgentWorkspace
|
||
session={visualNovelSession}
|
||
isBusy={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||
error={visualNovelError}
|
||
onBack={leaveVisualNovelFlow}
|
||
initialFormPayload={visualNovelFormDraftPayload}
|
||
onCreateFromForm={(payload) => {
|
||
void createVisualNovelDraftFromForm(payload);
|
||
}}
|
||
/>
|
||
</Suspense>
|
||
</motion.div>
|
||
)}
|
||
|
||
{selectionStage === 'visual-novel-generating' && (
|
||
<motion.div
|
||
key="visual-novel-generating"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -12 }}
|
||
className="flex h-full min-h-0 flex-col"
|
||
>
|
||
<Suspense
|
||
fallback={<LazyPanelFallback label="正在加载视觉小说生成面板..." />}
|
||
>
|
||
<CustomWorldGenerationView
|
||
settingText={
|
||
visualNovelFormDraftPayload?.seedText ??
|
||
visualNovelSession?.messages.find(
|
||
(message) => message.role === 'user',
|
||
)?.text ??
|
||
'正在整理当前视觉小说草稿。'
|
||
}
|
||
anchorEntries={buildVisualNovelEntryGenerationAnchorEntries(
|
||
visualNovelFormDraftPayload,
|
||
)}
|
||
progress={buildVisualNovelEntryGenerationProgress(
|
||
visualNovelGenerationStartedAtMs,
|
||
visualNovelGenerationPhase,
|
||
miniGameGenerationProgressNowMs,
|
||
)}
|
||
isGenerating={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||
error={visualNovelError}
|
||
onBack={leaveVisualNovelFlow}
|
||
onEditSetting={() => {
|
||
setSelectionStage('visual-novel-agent-workspace');
|
||
}}
|
||
onRetry={retryVisualNovelDraftGeneration}
|
||
onInterrupt={undefined}
|
||
backLabel="返回创作中心"
|
||
settingActionLabel={null}
|
||
retryLabel="重新生成草稿"
|
||
settingTitle="当前视觉小说信息"
|
||
settingDescription={null}
|
||
progressTitle="视觉小说草稿生成进度"
|
||
activeBadgeLabel="草稿生成中"
|
||
pausedBadgeLabel="草稿生成已暂停"
|
||
idleBadgeLabel="等待返回工作区"
|
||
/>
|
||
</Suspense>
|
||
</motion.div>
|
||
)}
|
||
|
||
{selectionStage === 'visual-novel-result' &&
|
||
(visualNovelSession?.draft || visualNovelWork?.draft) && (
|
||
<motion.div
|
||
key="visual-novel-result"
|
||
initial={{ opacity: 0, y: 12 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -12 }}
|
||
className="flex h-full min-h-0 flex-col"
|
||
>
|
||
<Suspense
|
||
fallback={<LazyPanelFallback label="正在加载视觉小说结果..." />}
|
||
>
|
||
<VisualNovelResultView
|
||
draft={visualNovelWork?.draft ?? visualNovelSession?.draft}
|
||
isBusy={isVisualNovelBusy}
|
||
error={visualNovelError}
|
||
onBack={() => {
|
||
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>
|
||
|
||
{creationEntryConfig ? (
|
||
<PlatformEntryCreationTypeModal
|
||
isOpen={showCreationTypeModal}
|
||
isBusy={
|
||
sessionController.isCreatingAgentSession ||
|
||
isCreativeAgentBusy ||
|
||
isCreativeAgentStreaming ||
|
||
isBigFishBusy ||
|
||
isMatch3DBusy ||
|
||
isSquareHoleBusy ||
|
||
isPuzzleBusy ||
|
||
isVisualNovelBusy ||
|
||
isVisualNovelStreamingReply
|
||
}
|
||
error={
|
||
creationEntryConfigError ??
|
||
bigFishError ??
|
||
creativeAgentError ??
|
||
match3dError ??
|
||
squareHoleError ??
|
||
puzzleCreationError ??
|
||
visualNovelError ??
|
||
puzzleError ??
|
||
sessionController.creationTypeError
|
||
}
|
||
entryConfig={creationEntryConfig}
|
||
creationTypes={creationEntryTypes}
|
||
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={() => {
|
||
handleCreationHubCreateType('match3d');
|
||
}}
|
||
onSelectSquareHole={() => {
|
||
runProtectedAction(() => {
|
||
void openSquareHoleAgentWorkspace();
|
||
});
|
||
}}
|
||
onSelectPuzzle={() => {
|
||
handleCreationHubCreateType('puzzle');
|
||
}}
|
||
onSelectCreativeAgent={() => {
|
||
runProtectedAction(() => {
|
||
void openCreativeAgentWorkspace();
|
||
});
|
||
}}
|
||
onSelectVisualNovel={() => {
|
||
handleCreationHubCreateType('visual-novel');
|
||
}}
|
||
/>
|
||
) : null}
|
||
<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;
|