Files
Genarrative/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx

2550 lines
86 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Loader2 } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import {
lazy,
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
import type {
BigFishRuntimeSnapshotResponse,
BigFishSessionSnapshotResponse,
ExecuteBigFishActionRequest,
SendBigFishMessageRequest,
SubmitBigFishInputRequest,
} from '../../../packages/shared/src/contracts/bigFish';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type {
PuzzleAgentActionRequest,
PuzzleAgentOperationRecord,
} from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
getPublicAuthUserByCode,
getPublicAuthUserById,
} from '../../services/authService';
import {
createBigFishCreationSession,
executeBigFishCreationAction,
getBigFishCreationSession,
streamBigFishCreationMessage,
} from '../../services/big-fish-creation';
import {
startBigFishRuntimeRun,
submitBigFishRuntimeInput,
} from '../../services/big-fish-runtime';
import {
deleteBigFishWork,
listBigFishWorks,
} from '../../services/big-fish-works';
import { readCustomWorldAgentUiState } from '../../services/customWorldAgentUiState';
import {
buildBigFishGenerationAnchorEntries,
buildMiniGameDraftGenerationProgress,
buildPuzzleGenerationAnchorEntries,
createMiniGameDraftGenerationState,
type MiniGameDraftGenerationState,
} from '../../services/miniGameDraftGenerationProgress';
import { getPlatformProfileDashboard } from '../../services/platform-entry';
import {
createPuzzleAgentSession,
executePuzzleAgentAction,
getPuzzleAgentSession,
streamPuzzleAgentMessage,
} from '../../services/puzzle-agent';
import {
getPuzzleGalleryDetail,
listPuzzleGallery,
} from '../../services/puzzle-gallery';
import { advanceLocalPuzzleNextLevel } from '../../services/puzzle-runtime';
import {
dragLocalPuzzlePiece,
startLocalPuzzleRun,
swapLocalPuzzlePieces,
} from '../../services/puzzle-runtime/puzzleLocalRuntime';
import { deletePuzzleWork, listPuzzleWorks } from '../../services/puzzle-works';
import { isSamePuzzlePublicWorkCode } from '../../services/publicWorkCode';
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry';
import { getRpgEntryWorldGalleryDetailByCode } from '../../services/rpg-entry/rpgEntryLibraryClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { CustomWorldCreationHub } from '../custom-world-home/CustomWorldCreationHub';
import { PuzzleAgentWorkspace } from '../puzzle-agent/PuzzleAgentWorkspace';
import { PuzzleGalleryDetailView } from '../puzzle-gallery/PuzzleGalleryDetailView';
import { PuzzleResultView } from '../puzzle-result/PuzzleResultView';
import { PuzzleRuntimeShell } from '../puzzle-runtime/PuzzleRuntimeShell';
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 {
isPuzzleGalleryEntry,
mapPuzzleWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
} from '../rpg-entry/rpgEntryWorldPresentation';
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
import {
PlatformEntryHomeView,
type PlatformHomeTab,
} from './PlatformEntryHomeView';
import {
buildCreationHubFallbackItems,
normalizeAgentBackedProfile,
resolveRpgCreationErrorMessage,
} from './platformEntryShared';
import type { PlatformEntryFlowShellProps } from './platformEntryTypes';
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
type AgentResultPublishGateView = {
blockers: string[];
publishReady: boolean;
};
type PuzzleDetailReturnTarget = {
tab: PlatformHomeTab;
};
type AgentResultBlockerView = {
code?: string;
message: string;
};
const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
'publish_missing_world_hook',
'publish_missing_player_premise',
'publish_missing_core_conflict',
'publish_missing_main_chapter',
'publish_missing_first_act',
]);
function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
const rawTime = entry.publishedAt ?? entry.updatedAt;
const timestamp = new Date(rawTime).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
return `${isPuzzleGalleryEntry(entry) ? 'puzzle' : 'rpg'}:${entry.ownerUserId}:${entry.profileId}`;
}
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 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.hook',
'settingText',
]),
);
}
if (code === 'publish_missing_player_premise') {
return Boolean(
readProfileTextField(profile, [
'playerPremise',
'creatorIntent.playerPremise',
'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,
};
}
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,
};
});
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>
);
}
export function PlatformEntryFlowShellImpl({
selectionStage,
setSelectionStage,
hasSavedGame,
savedSnapshot,
handleContinueGame,
handleStartNewGame,
handleCustomWorldSelect,
}: PlatformEntryFlowShellProps) {
const authUi = useAuthUi();
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
const [bigFishWorks, setBigFishWorks] = useState<BigFishWorkSummary[]>([]);
const [bigFishRun, setBigFishRun] =
useState<BigFishRuntimeSnapshotResponse | null>(null);
const [isBigFishLoadingLibrary, setIsBigFishLoadingLibrary] = useState(false);
const [bigFishGenerationState, setBigFishGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
const bigFishInputInFlightRef = useRef(false);
const [puzzleOperation, 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 [puzzleRun, setPuzzleRun] = useState<PuzzleRunSnapshot | null>(null);
const [isPuzzleLoadingLibrary, setIsPuzzleLoadingLibrary] = useState(false);
const [puzzleGenerationState, setPuzzleGenerationState] =
useState<MiniGameDraftGenerationState | null>(null);
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 hadReadableProtectedDataRef = useRef(false);
const hasInitialAgentSession = Boolean(
readCustomWorldAgentUiState().activeSessionId,
);
const platformBootstrap = usePlatformEntryBootstrap({
user: authUi?.user,
canAccessProtectedData: authUi?.canAccessProtectedData,
getProfileDashboard: getPlatformProfileDashboard,
handleContinueGame,
hasInitialAgentSession,
});
const entryNavigation = usePlatformEntryNavigation({
setSelectionStage,
setSelectedDetailEntry,
});
const { setPlatformTab } = platformBootstrap;
const enterCreateTab = useCallback(() => {
// 只依赖稳定的 setter避免把 bootstrap 对象的 render 级引用变化
// 传导成 Agent session 恢复 effect 的重复触发。
setPlatformTab('create');
}, [setPlatformTab]);
const resolveBigFishErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const resolvePuzzleErrorMessage = useCallback(
(error: unknown, fallback: string) =>
resolveRpgCreationErrorMessage(error, fallback),
[],
);
const 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,
agentSession: sessionController.agentSession,
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,
buildDraftResultProfile: (session) =>
rpgCreationPreviewAdapter.buildPreviewFromSession(session),
});
const detailNavigation = usePlatformEntryLibraryDetail({
userId: authUi?.user?.id,
selectedDetailEntry,
setSelectedDetailEntry,
savedCustomWorldEntries: platformBootstrap.savedCustomWorldEntries,
setSavedCustomWorldEntries: platformBootstrap.setSavedCustomWorldEntries,
setGeneratedCustomWorldProfile:
sessionController.setGeneratedCustomWorldProfile,
setCustomWorldError: sessionController.setCustomWorldError,
setCustomWorldAutoSaveError:
autosaveCoordinator.setCustomWorldAutoSaveError,
setCustomWorldAutoSaveState:
autosaveCoordinator.setCustomWorldAutoSaveState,
setCustomWorldGenerationViewSource:
sessionController.setCustomWorldGenerationViewSource,
setCustomWorldResultViewSource:
sessionController.setCustomWorldResultViewSource,
setSelectionStage,
setPlatformTabToCreate: enterCreateTab,
setPlatformError: platformBootstrap.setPlatformError,
appendBrowseHistoryEntry: platformBootstrap.appendBrowseHistoryEntry,
refreshCustomWorldWorks: platformBootstrap.refreshCustomWorldWorks,
refreshPublishedGallery: platformBootstrap.refreshPublishedGallery,
persistAgentUiState: sessionController.persistAgentUiState,
syncAgentSessionSnapshot: sessionController.syncAgentSessionSnapshot,
buildDraftResultProfile: (session) =>
rpgCreationPreviewAdapter.buildPreviewFromSession(session),
suppressAgentDraftResultAutoOpen:
sessionController.suppressAgentDraftResultAutoOpen,
releaseAgentDraftResultAutoOpenSuppression:
sessionController.releaseAgentDraftResultAutoOpenSuppression,
resetAutoSaveTrackingToIdle:
autosaveCoordinator.resetAutoSaveTrackingToIdle,
markAutoSavedProfile: autosaveCoordinator.markAutoSavedProfile,
});
const enterWorldCoordinator = useRpgCreationEnterWorld({
isAgentDraftResultView: sessionController.isAgentDraftResultView,
activeAgentSessionId: sessionController.activeAgentSessionId,
generatedCustomWorldProfile: sessionController.generatedCustomWorldProfile,
agentSessionProfile: sessionController.agentDraftResultProfile,
agentSession: sessionController.agentSession,
handleCustomWorldSelect,
executePublishWorld: async () => {
const latestSession = await autosaveCoordinator.executeAgentActionAndWait(
{
action: 'publish_world',
},
);
// 发布动作会在后端同步 gallery 投影;前端发布完成后立即刷新首页/分类页共用的公开作品列表。
await Promise.allSettled([
platformBootstrap.refreshPublishedGallery(),
platformBootstrap.refreshCustomWorldWorks(),
refreshPuzzleGallery(),
]);
return latestSession;
},
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 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 puzzlePublicEntries = puzzleGalleryEntries.map(
mapPuzzleWorkToPlatformGalleryCard,
);
return mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
puzzlePublicEntries,
).slice(0, 6);
}, [platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries]);
const latestGalleryEntries = useMemo(
() =>
mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
puzzleGalleryEntries.map(mapPuzzleWorkToPlatformGalleryCard),
),
[platformBootstrap.publishedGalleryEntries, puzzleGalleryEntries],
);
const creationHubItems =
platformBootstrap.customWorldWorkEntries.length > 0
? platformBootstrap.customWorldWorkEntries
: buildCreationHubFallbackItems(
platformBootstrap.savedCustomWorldEntries,
);
const resultViewError =
autosaveCoordinator.customWorldAutoSaveError ??
sessionController.customWorldError;
useEffect(() => {
if (
selectionStage === 'custom-world-result' &&
!sessionController.generatedCustomWorldProfile
) {
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
}
}, [
selectedDetailEntry,
selectionStage,
sessionController.generatedCustomWorldProfile,
setSelectionStage,
]);
const runProtectedAction = useCallback(
(action: () => void) => {
if (!authUi?.requireAuth) {
action();
return;
}
authUi.requireAuth(action);
},
[authUi],
);
const 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 refreshBigFishShelf = useCallback(async () => {
setIsBigFishLoadingLibrary(true);
try {
const worksResponse = await listBigFishWorks();
setBigFishWorks(worksResponse.items);
setBigFishError(null);
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '读取大鱼吃小鱼作品列表失败。'),
);
} finally {
setIsBigFishLoadingLibrary(false);
}
}, [resolveBigFishErrorMessage]);
const refreshPuzzleShelf = useCallback(async () => {
setIsPuzzleLoadingLibrary(true);
try {
const worksResponse = await listPuzzleWorks();
setPuzzleWorks(worksResponse.items);
setPuzzleError(null);
} catch (error) {
setPuzzleError(
resolvePuzzleErrorMessage(error, '读取拼图作品列表失败。'),
);
} finally {
setIsPuzzleLoadingLibrary(false);
}
}, [resolvePuzzleErrorMessage]);
const refreshPuzzleGallery = useCallback(async () => {
try {
const galleryResponse = await listPuzzleGallery();
setPuzzleGalleryEntries(galleryResponse.items);
return galleryResponse.items;
} catch (error) {
setPuzzleGalleryEntries([]);
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图广场失败。'));
return [];
}
}, [resolvePuzzleErrorMessage]);
const 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_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 puzzleFlow = usePlatformCreationAgentFlowController<
PuzzleAgentSessionSnapshot,
Record<string, never>,
{ 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: () => {
setShowCreationTypeModal(false);
},
onActionComplete: async ({ payload, response, setSession }) => {
setPuzzleOperation(response.operation);
setSession(response.session);
if (payload.action === 'publish_puzzle_work') {
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);
setSelectionStage('puzzle-gallery-detail');
}
},
beforeExecuteAction: ({ payload }) => {
if (payload.action !== 'compile_puzzle_draft') {
return;
}
setSelectionStage('puzzle-generating');
setPuzzleGenerationState(createMiniGameDraftGenerationState('puzzle'));
},
onActionError: ({ payload, errorMessage }) => {
if (payload.action !== 'compile_puzzle_draft') {
return;
}
setPuzzleGenerationState((current) =>
current
? {
...current,
phase: 'failed',
error: errorMessage,
}
: current,
);
},
});
const bigFishSession = bigFishFlow.session;
const bigFishError = bigFishFlow.error;
const setBigFishError = bigFishFlow.setError;
const isBigFishBusy = bigFishFlow.isBusy;
const setIsBigFishBusy = bigFishFlow.setIsBusy;
const streamingBigFishReplyText = bigFishFlow.streamingReplyText;
const isStreamingBigFishReply = bigFishFlow.isStreamingReply;
const puzzleSession = puzzleFlow.session;
const puzzleError = puzzleFlow.error;
const setPuzzleError = puzzleFlow.setError;
const isPuzzleBusy = puzzleFlow.isBusy;
const setIsPuzzleBusy = puzzleFlow.setIsBusy;
const streamingPuzzleReplyText = puzzleFlow.streamingReplyText;
const isStreamingPuzzleReply = puzzleFlow.isStreamingReply;
const resetRpgSessionViewState = sessionController.resetSessionViewState;
const setRpgGeneratedCustomWorldProfile =
sessionController.setGeneratedCustomWorldProfile;
const setRpgCustomWorldError = sessionController.setCustomWorldError;
const persistRpgAgentUiState = sessionController.persistAgentUiState;
const resetAutoSaveTrackingToIdle =
autosaveCoordinator.resetAutoSaveTrackingToIdle;
const openBigFishAgentWorkspace = useCallback(async () => {
setBigFishRun(null);
await bigFishFlow.openWorkspace();
}, [bigFishFlow]);
const openPuzzleAgentWorkspace = useCallback(async () => {
setPuzzleRun(null);
setPuzzleOperation(null);
await puzzleFlow.openWorkspace();
}, [puzzleFlow]);
useEffect(() => {
if (platformBootstrap.canReadProtectedData) {
hadReadableProtectedDataRef.current = true;
return;
}
if (authUi?.user || !hadReadableProtectedDataRef.current) {
return;
}
hadReadableProtectedDataRef.current = false;
// 创作中心只展示当前登录用户的私有作品。
// 一旦退出登录或鉴权上下文被收回,三类作品缓存必须同步清空,不能等刷新页面。
setShowCreationTypeModal(false);
setSelectedDetailEntry(null);
setBigFishWorks([]);
setBigFishRun(null);
setBigFishGenerationState(null);
setBigFishError(null);
setPuzzleOperation(null);
setPuzzleWorks([]);
setSelectedPuzzleDetail(null);
setPuzzleRun(null);
setPuzzleGenerationState(null);
setIsPuzzleNextLevelGenerating(false);
setPuzzleError(null);
setDeletingCreationWorkId(null);
resetRpgSessionViewState();
setRpgGeneratedCustomWorldProfile(null);
setRpgCustomWorldError(null);
persistRpgAgentUiState(null, null);
resetAutoSaveTrackingToIdle();
if (
selectionStage !== 'platform' &&
selectionStage !== 'detail' &&
selectionStage !== 'puzzle-gallery-detail'
) {
setSelectionStage('platform');
}
}, [
authUi?.user,
platformBootstrap.canReadProtectedData,
persistRpgAgentUiState,
resetAutoSaveTrackingToIdle,
resetRpgSessionViewState,
selectionStage,
setBigFishError,
setPuzzleError,
setRpgCustomWorldError,
setRpgGeneratedCustomWorldProfile,
setSelectionStage,
]);
const handleCreationHubCreateType = useCallback(
(type: PlatformCreationTypeId) => {
if (type === 'airp' || type === 'visual-novel') {
return;
}
if (!prepareCreationLaunch()) {
return;
}
if (type === 'rpg') {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
return;
}
if (type === 'big-fish') {
runProtectedAction(() => {
void openBigFishAgentWorkspace();
});
return;
}
if (type === 'puzzle') {
runProtectedAction(() => {
void openPuzzleAgentWorkspace();
});
}
},
[
openBigFishAgentWorkspace,
openPuzzleAgentWorkspace,
prepareCreationLaunch,
runProtectedAction,
sessionController,
],
);
const leaveBigFishFlow = useCallback(() => {
setBigFishRun(null);
setBigFishGenerationState(null);
bigFishFlow.leaveFlow();
}, [bigFishFlow]);
const leavePuzzleFlow = useCallback(() => {
setPuzzleOperation(null);
setPuzzleRun(null);
setPuzzleGenerationState(null);
setIsPuzzleNextLevelGenerating(false);
puzzleFlow.leaveFlow();
}, [puzzleFlow]);
const submitBigFishMessage = bigFishFlow.submitMessage;
const submitPuzzleMessage = puzzleFlow.submitMessage;
const executeBigFishAction = bigFishFlow.executeAction;
const executePuzzleAction = puzzleFlow.executeAction;
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]);
const startBigFishRun = useCallback(async () => {
if (!bigFishSession || isBigFishBusy) {
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
try {
const { run } = await startBigFishRuntimeRun(bigFishSession.sessionId);
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼测试玩法失败。'),
);
} finally {
setIsBigFishBusy(false);
}
}, [
bigFishSession,
isBigFishBusy,
resolveBigFishErrorMessage,
setSelectionStage,
]);
const startPuzzleRunFromProfile = useCallback(
async (profileId: string) => {
if (isPuzzleBusy) {
return;
}
setIsPuzzleBusy(true);
setPuzzleError(null);
try {
const { item } = await getPuzzleGalleryDetail(profileId);
setSelectedPuzzleDetail(item);
setPuzzleRun(startLocalPuzzleRun(item));
setSelectionStage('puzzle-runtime');
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '启动拼图玩法失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[isPuzzleBusy, resolvePuzzleErrorMessage, setSelectionStage],
);
const submitBigFishInput = useCallback(
(payload: SubmitBigFishInputRequest) => {
if (!bigFishRun || bigFishInputInFlightRef.current) {
return;
}
bigFishInputInFlightRef.current = true;
void submitBigFishRuntimeInput(bigFishRun.runId, payload)
.then(({ run }) => {
setBigFishRun(run);
})
.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '同步大鱼吃小鱼输入失败。'),
);
})
.finally(() => {
bigFishInputInFlightRef.current = false;
});
},
[bigFishRun, resolveBigFishErrorMessage],
);
const swapPuzzlePiecesInRun = useCallback(
(payload: { firstPieceId: string; secondPieceId: string }) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setPuzzleError(null);
setPuzzleRun(swapLocalPuzzlePieces(puzzleRun, payload));
},
[isPuzzleBusy, puzzleRun],
);
const dragPuzzlePiece = useCallback(
(payload: { pieceId: string; targetRow: number; targetCol: number }) => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
setPuzzleError(null);
setPuzzleRun(dragLocalPuzzlePiece(puzzleRun, payload));
},
[isPuzzleBusy, puzzleRun],
);
const advancePuzzleLevel = useCallback(async () => {
if (!puzzleRun || isPuzzleBusy) {
return;
}
const currentLevel = puzzleRun.currentLevel;
if (!currentLevel || currentLevel.status !== 'cleared') {
return;
}
setIsPuzzleBusy(true);
setIsPuzzleNextLevelGenerating(true);
setPuzzleError(null);
try {
const { run } = await advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ??
puzzleSession?.sessionId ??
null,
});
setPuzzleRun(run);
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {
setIsPuzzleNextLevelGenerating(false);
setIsPuzzleBusy(false);
}
}, [
isPuzzleBusy,
puzzleRun,
puzzleSession,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
]);
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 handleExperienceRpgWork = useCallback(
(work: (typeof creationHubItems)[number]) => {
if (!work.profileId) {
return;
}
runProtectedAction(() => {
const matchedEntry = platformBootstrap.savedCustomWorldEntries.find(
(entry) => entry.profileId === work.profileId,
);
if (!matchedEntry) {
platformBootstrap.setPlatformError(
'未找到可体验的作品,请刷新后重试。',
);
return;
}
handleCustomWorldSelect(matchedEntry.profile);
});
},
[
handleCustomWorldSelect,
platformBootstrap,
platformBootstrap.savedCustomWorldEntries,
runProtectedAction,
],
);
const handleDeleteLibraryEntry = useCallback(
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => {
if (!entry.profileId || deletingCreationWorkId) {
return;
}
runProtectedAction(() => {
const confirmed = window.confirm(
`确认删除作品《${entry.worldName}》吗?删除后会从你的作品列表和公开广场中移除。`,
);
if (!confirmed) {
return;
}
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, runProtectedAction],
);
const handleDeletePublishedWork = useCallback(
(work: (typeof creationHubItems)[number]) => {
if (deletingCreationWorkId) {
return;
}
runProtectedAction(() => {
const confirmed = window.confirm(
`确认删除作品《${work.title}》吗?删除后会从你的作品列表和公开广场中移除。`,
);
if (!confirmed) {
return;
}
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, runProtectedAction],
);
const handleDeleteBigFishWork = useCallback(
(work: BigFishWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
runProtectedAction(() => {
const confirmed = window.confirm(
`确认删除作品《${work.title}》吗?删除后会从你的作品列表中移除。`,
);
if (!confirmed) {
return;
}
setDeletingCreationWorkId(work.workId);
setBigFishError(null);
void deleteBigFishWork(work.sourceSessionId)
.then((response) => {
setBigFishWorks(response.items);
})
.catch((error) => {
setBigFishError(
resolveBigFishErrorMessage(error, '删除大鱼吃小鱼作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
});
},
[deletingCreationWorkId, resolveBigFishErrorMessage, runProtectedAction],
);
const handleDeletePuzzleWork = useCallback(
(work: PuzzleWorkSummary) => {
if (deletingCreationWorkId) {
return;
}
runProtectedAction(() => {
const confirmed = window.confirm(
`确认删除作品《${work.levelName}》吗?删除后会从你的作品列表和公开广场中移除。`,
);
if (!confirmed) {
return;
}
setDeletingCreationWorkId(work.workId);
setPuzzleError(null);
void deletePuzzleWork(work.profileId)
.then((response) => {
setPuzzleWorks(response.items);
void refreshPuzzleGallery();
})
.catch((error) => {
setPuzzleError(
resolvePuzzleErrorMessage(error, '删除拼图作品失败。'),
);
})
.finally(() => {
setDeletingCreationWorkId(null);
});
});
},
[
deletingCreationWorkId,
refreshPuzzleGallery,
resolvePuzzleErrorMessage,
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');
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '读取拼图详情失败。'));
} finally {
setIsPuzzleBusy(false);
}
},
[
platformBootstrap.platformTab,
resolvePuzzleErrorMessage,
setSelectionStage,
],
);
const openPuzzleDraft = useCallback(
async (item: PuzzleWorkSummary) => {
setPuzzleOperation(null);
setPuzzleRun(null);
setSelectedPuzzleDetail(null);
if (!item.sourceSessionId?.trim()) {
if (item.publicationStatus === 'published') {
await openPuzzleDetail(item.profileId, { tab: 'create' });
return;
}
setPuzzleError('这份拼图草稿缺少会话信息,请重新开始创作。');
return;
}
const restoredSession = await puzzleFlow.restoreDraft(
item.sourceSessionId,
);
if (!restoredSession) {
await refreshPuzzleShelf().catch(() => undefined);
}
},
[openPuzzleDetail, puzzleFlow, refreshPuzzleShelf, setPuzzleError],
);
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 shouldSearchPuzzleFirst = upperKeyword.startsWith('PZ');
const shouldSearchWorkFirst =
!shouldSearchUserIdFirst &&
!shouldSearchPuzzleFirst &&
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
const shouldSearchUserFirst =
shouldSearchUserIdFirst ||
upperKeyword.startsWith('SY') ||
(!shouldSearchWorkFirst && !shouldSearchPuzzleFirst);
const tryOpenGalleryEntry = async () => {
const entry =
await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
await detailNavigation.openGalleryDetail({
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,
} satisfies CustomWorldGalleryCard);
};
const tryOpenPuzzleGalleryEntry = async () => {
const entries =
puzzleGalleryEntries.length > 0
? puzzleGalleryEntries
: await refreshPuzzleGallery();
const matchedEntry = entries.find((entry) =>
isSamePuzzlePublicWorkCode(normalizedKeyword, entry.profileId),
);
if (!matchedEntry) {
throw new Error('未找到拼图作品。');
}
await openPuzzleDetail(matchedEntry.profileId, {
tab: platformBootstrap.platformTab,
});
};
try {
if (shouldSearchUserIdFirst) {
const user = await getPublicAuthUserById(normalizedKeyword);
setSearchedPublicUser(user);
return;
}
if (shouldSearchPuzzleFirst) {
await tryOpenPuzzleGalleryEntry();
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);
}
},
[
detailNavigation,
openPuzzleDetail,
platformBootstrap.platformTab,
puzzleGalleryEntries,
refreshPuzzleGallery,
],
);
const openBigFishDraft = useCallback(
async (item: BigFishWorkSummary) => {
setBigFishRun(null);
const restoredSession = await bigFishFlow.restoreDraft(
item.sourceSessionId,
);
if (!restoredSession) {
await refreshBigFishShelf().catch(() => undefined);
}
},
[bigFishFlow, refreshBigFishShelf],
);
const startBigFishRunFromWork = useCallback(
async (item: BigFishWorkSummary) => {
const sessionId = item.sourceSessionId?.trim();
if (!sessionId) {
setBigFishError('当前作品缺少会话信息,暂时无法进入玩法。');
return;
}
setIsBigFishBusy(true);
setBigFishError(null);
try {
const { session } = await getBigFishCreationSession(sessionId);
const { run } = await startBigFishRuntimeRun(sessionId);
bigFishFlow.setSession(session);
setBigFishRun(run);
setSelectionStage('big-fish-runtime');
} catch (error) {
setBigFishError(
resolveBigFishErrorMessage(error, '启动大鱼吃小鱼玩法失败。'),
);
} finally {
setIsBigFishBusy(false);
}
},
[bigFishFlow, resolveBigFishErrorMessage, setSelectionStage],
);
useEffect(() => {
if (selectionStage === 'platform') {
void refreshPuzzleGallery();
}
}, [refreshPuzzleGallery, selectionStage]);
useEffect(() => {
if (
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshPuzzleShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshPuzzleShelf,
selectionStage,
]);
useEffect(() => {
if (
(platformBootstrap.platformTab === 'create' ||
selectionStage === 'platform') &&
platformBootstrap.canReadProtectedData
) {
void refreshBigFishShelf();
}
}, [
platformBootstrap.canReadProtectedData,
platformBootstrap.platformTab,
refreshBigFishShelf,
selectionStage,
]);
const creationHubContent = (
<CustomWorldCreationHub
items={creationHubItems}
loading={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
}
error={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
? null
: (platformBootstrap.platformError ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
puzzleError)
}
onRetry={() => {
platformBootstrap.setPlatformError(null);
setBigFishError(null);
setPuzzleError(null);
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
void refreshBigFishShelf();
void refreshPuzzleShelf();
}}
createError={
sessionController.creationTypeError ?? bigFishError ?? puzzleError
}
createBusy={
sessionController.isCreatingAgentSession ||
isBigFishBusy ||
isPuzzleBusy
}
onCreateType={handleCreationHubCreateType}
onOpenDraft={(item) => {
runProtectedAction(() => {
void detailNavigation.handleOpenCreationWork(item);
});
}}
onEnterPublished={(profileId) => {
runProtectedAction(() => {
const matchedWork = creationHubItems.find(
(entry) => entry.profileId === profileId,
);
if (!matchedWork) {
return;
}
void detailNavigation.handleOpenCreationWork(matchedWork);
});
}}
onDeletePublished={(item) => {
handleDeletePublishedWork(item);
}}
deletingWorkId={deletingCreationWorkId}
onExperienceRpg={(item) => {
handleExperienceRpgWork(item);
}}
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
bigFishItems={bigFishWorks}
onOpenBigFishDetail={(item) => {
runProtectedAction(() => {
void openBigFishDraft(item);
});
}}
onExperienceBigFish={(item) => {
runProtectedAction(() => {
void startBigFishRunFromWork(item);
});
}}
onDeleteBigFish={(item) => {
handleDeleteBigFishWork(item);
}}
puzzleItems={puzzleWorks}
onOpenPuzzleDetail={(item) => {
runProtectedAction(() => {
void openPuzzleDraft(item);
});
}}
onExperiencePuzzle={(profileId) => {
runProtectedAction(() => {
void startPuzzleRunFromProfile(profileId);
});
}}
onDeletePuzzle={(item) => {
handleDeletePuzzleWork(item);
}}
/>
);
return (
<>
<AnimatePresence mode="wait">
{selectionStage === 'platform' && (
<motion.div
key="platform-home"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
<PlatformEntryHomeView
activeTab={platformBootstrap.platformTab}
onTabChange={platformBootstrap.setPlatformTab}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
saveEntries={platformBootstrap.saveEntries}
saveError={platformBootstrap.saveError}
featuredEntries={featuredGalleryEntries}
latestEntries={latestGalleryEntries}
myEntries={platformBootstrap.savedCustomWorldEntries}
historyEntries={platformBootstrap.historyEntries}
profileDashboard={platformBootstrap.profileDashboard}
isLoadingPlatform={platformBootstrap.isLoadingPlatform}
isLoadingDashboard={platformBootstrap.isLoadingDashboard}
isResumingSaveWorldKey={platformBootstrap.isResumingSaveWorldKey}
platformError={
platformBootstrap.isLoadingPlatform
? null
: (platformBootstrap.platformError ??
sessionController.agentWorkspaceRestoreError)
}
dashboardError={
platformBootstrap.isLoadingDashboard
? null
: platformBootstrap.dashboardError
}
createTabContent={creationHubContent}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
void platformBootstrap.handleResumeSaveEntry(entry);
}}
onOpenCreateWorld={openCreationTypePicker}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={(entry) => {
if (isPuzzleGalleryEntry(entry)) {
void openPuzzleDetail(entry.profileId, {
tab: platformBootstrap.platformTab,
});
return;
}
runProtectedAction(() => {
void detailNavigation.openGalleryDetail(entry);
});
}}
onOpenLibraryDetail={(entry) => {
runProtectedAction(() => {
detailNavigation.openLibraryDetail(entry);
});
}}
onDeleteLibraryEntry={(entry) => {
handleDeleteLibraryEntry(entry);
}}
deletingLibraryEntryId={deletingCreationWorkId}
onSearchPublicCode={(keyword) => {
void handlePublicCodeSearch(keyword);
}}
isSearchingPublicCode={isSearchingPublicCode}
onOpenProfileDashboardCard={() => {
if (platformBootstrap.dashboardError) {
void platformBootstrap.refreshProfileDashboard();
}
}}
onRechargeSuccess={platformBootstrap.refreshProfileDashboard}
/>
</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>
) : (
<PlatformEntryWorldDetailView
entry={selectedDetailEntry}
isMutating={detailNavigation.isMutatingDetail}
error={detailNavigation.detailError}
onBack={() => {
detailNavigation.setDetailError(null);
entryNavigation.backToPlatformHome();
}}
onStartGame={handleStartSelectedWorld}
onContinueEdit={
detailNavigation.isSelectedWorldOwned
? () => {
runProtectedAction(() => {
detailNavigation.openSavedCustomWorldEditor(
selectedDetailEntry,
);
});
}
: null
}
onPublish={
selectedDetailEntry.visibility === 'draft' &&
detailNavigation.isSelectedWorldOwned
? () => {
runProtectedAction(() => {
void detailNavigation.handlePublishSelectedWorld();
});
}
: null
}
onUnpublish={
selectedDetailEntry.visibility === 'published' &&
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 ?? []}
isBusy={isBigFishBusy}
error={bigFishError}
onBack={() => {
setSelectionStage('big-fish-result');
}}
onSubmitInput={submitBigFishInput}
/>
</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"
>
<PuzzleAgentWorkspace
session={puzzleSession}
activeOperation={puzzleOperation}
streamingReplyText={streamingPuzzleReplyText}
isStreamingReply={isStreamingPuzzleReply}
isBusy={isPuzzleBusy || isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
onSubmitMessage={(payload) => {
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
/>
</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,
)}
progress={buildMiniGameDraftGenerationProgress(
puzzleGenerationState,
)}
isGenerating={isPuzzleBusy}
error={puzzleError}
onBack={leavePuzzleFlow}
onEditSetting={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onRetry={() => {
void executePuzzleAction({ action: 'compile_puzzle_draft' });
}}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成草稿"
settingTitle="当前拼图信息"
settingDescription={null}
progressTitle="拼图草稿生成进度"
activeBadgeLabel="草稿生成中"
pausedBadgeLabel="草稿生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'puzzle-result' && puzzleSession?.draft && (
<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"
>
<PuzzleResultView
session={puzzleSession}
author={authUi?.user ?? null}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
/>
</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"
>
<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);
}}
/>
</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]"
>
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={isPuzzleBusy || isPuzzleNextLevelGenerating}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-gallery-detail');
}}
onSwapPieces={(payload) => {
void swapPuzzlePiecesInRun(payload);
}}
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={() => {
void advancePuzzleLevel();
}}
/>
{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}
</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(
normalizeAgentBackedProfile(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 {
await enterWorldCoordinator.publishCurrentResult();
} catch (error) {
sessionController.setCustomWorldError(
resolveRpgCreationErrorMessage(
error,
'发布到广场失败。',
),
);
throw error;
}
}
: undefined
}
onGenerateEntity={
sessionController.isAgentDraftResultView
? async (kind) => {
const action =
kind === 'landmark'
? 'generate_landmarks'
: 'generate_characters';
const latestSession =
await autosaveCoordinator.executeAgentActionAndWait(
{
action,
count: 1,
...(kind === 'playable'
? { roleType: 'playable' as const }
: kind === 'story'
? { roleType: 'story' as const }
: {}),
},
);
const latestProfile = latestSession
? rpgCreationPreviewAdapter.buildPreviewFromSession(
latestSession,
)
: null;
if (latestProfile) {
sessionController.setGeneratedCustomWorldProfile(
latestProfile,
);
}
return { profile: latestProfile };
}
: undefined
}
onDeleteEntities={
sessionController.isAgentDraftResultView
? async (kind, ids) => {
if (ids.length === 0) return;
const latestSession =
await autosaveCoordinator.executeAgentActionAndWait(
kind === 'story'
? { action: 'delete_characters', roleIds: ids }
: { action: 'delete_landmarks', sceneIds: ids },
);
const latestProfile = latestSession
? rpgCreationPreviewAdapter.buildPreviewFromSession(
latestSession,
)
: null;
if (latestProfile) {
sessionController.setGeneratedCustomWorldProfile(
latestProfile,
);
}
}
: undefined
}
readOnly={false}
compactAgentResultMode={
sessionController.isAgentDraftResultView
}
backLabel={
sessionController.isAgentDraftResultView
? '返回创作'
: undefined
}
editActionLabel="继续调整设定"
enterWorldActionLabel={
sessionController.isAgentDraftResultView &&
sessionController.agentSession?.stage !== 'published'
? '发布并进入世界'
: '进入世界'
}
publishReady={
sessionController.isAgentDraftResultView
? agentResultPublishGateView.publishReady
: true
}
publishBlockers={
sessionController.isAgentDraftResultView
? agentResultPublishGateView.blockers
: []
}
qualityFindings={
sessionController.isAgentDraftResultView
? agentResultPreviewQualityFindings
: []
}
previewSourceLabel={
sessionController.isAgentDraftResultView
? agentResultPreviewSourceLabel
: null
}
autoSaveState={autosaveCoordinator.customWorldAutoSaveState}
/>
</Suspense>
</motion.div>
)}
</AnimatePresence>
<PlatformEntryCreationTypeModal
isOpen={showCreationTypeModal}
isBusy={
sessionController.isCreatingAgentSession ||
isBigFishBusy ||
isPuzzleBusy
}
error={
bigFishError ?? puzzleError ?? sessionController.creationTypeError
}
onClose={() => {
if (
sessionController.isCreatingAgentSession ||
isBigFishBusy ||
isPuzzleBusy
) {
return;
}
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
}}
onSelectBigFish={() => {
runProtectedAction(() => {
void openBigFishAgentWorkspace();
});
}}
onSelectPuzzle={() => {
runProtectedAction(() => {
void openPuzzleAgentWorkspace();
});
}}
/>
<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;