1571 lines
49 KiB
TypeScript
1571 lines
49 KiB
TypeScript
import { AnimatePresence, motion } from 'motion/react';
|
|
import {
|
|
lazy,
|
|
Suspense,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import type {
|
|
CustomWorldAgentActionRequest,
|
|
CustomWorldAgentMessage,
|
|
CustomWorldAgentOperationRecord,
|
|
CustomWorldAgentSessionSnapshot,
|
|
SendCustomWorldAgentMessageRequest,
|
|
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
|
import type {
|
|
CustomWorldGalleryCard,
|
|
CustomWorldLibraryEntry,
|
|
ProfileDashboardSummary,
|
|
ProfileSaveArchiveSummary,
|
|
} from '../../../packages/shared/src/contracts/runtime';
|
|
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
|
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
|
import {
|
|
createCustomWorldAgentSession,
|
|
executeCustomWorldAgentAction,
|
|
getCustomWorldAgentOperation,
|
|
getCustomWorldAgentSession,
|
|
streamCustomWorldAgentMessage,
|
|
} from '../../services/aiService';
|
|
import { buildCustomWorldProfileFromAgentDraft } from '../../services/customWorldAgentDraftResult';
|
|
import {
|
|
buildAgentDraftFoundationAnchorEntries,
|
|
buildAgentDraftFoundationGenerationProgress,
|
|
buildAgentDraftFoundationSettingText,
|
|
isDraftFoundationOperation,
|
|
isDraftFoundationOperationRunning,
|
|
} from '../../services/customWorldAgentGenerationProgress';
|
|
import {
|
|
readCustomWorldAgentUiState,
|
|
writeCustomWorldAgentUiState,
|
|
} from '../../services/customWorldAgentUiState';
|
|
import { buildCustomWorldCreatorIntentFoundationText } from '../../services/customWorldCreatorIntent';
|
|
import {
|
|
hasPendingPlatformBrowseHistoryMigration,
|
|
markPlatformBrowseHistoryMigrated,
|
|
type PlatformBrowseHistoryEntry,
|
|
type PlatformBrowseHistoryWriteEntry,
|
|
readPlatformBrowseHistory,
|
|
writePlatformBrowseHistory,
|
|
} from '../../services/platformBrowseHistory';
|
|
import {
|
|
deleteCustomWorldProfile,
|
|
getCustomWorldGalleryDetail,
|
|
getProfileDashboard,
|
|
listCustomWorldGallery,
|
|
listCustomWorldLibrary,
|
|
listProfileBrowseHistory,
|
|
listProfileSaveArchives,
|
|
publishCustomWorldProfile,
|
|
resumeProfileSaveArchive,
|
|
syncProfileBrowseHistory,
|
|
unpublishCustomWorldProfile,
|
|
upsertCustomWorldProfile,
|
|
upsertProfileBrowseHistory,
|
|
} from '../../services/storageService';
|
|
import { type CustomWorldProfile, type GameState } from '../../types';
|
|
import { useAuthUi } from '../auth/AuthUiContext';
|
|
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
|
|
import { type PlatformHomeTab, PlatformHomeView } from './PlatformHomeView';
|
|
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
|
|
|
|
const CustomWorldGenerationView = lazy(async () => {
|
|
const module = await import('../CustomWorldGenerationView');
|
|
return {
|
|
default: module.CustomWorldGenerationView,
|
|
};
|
|
});
|
|
|
|
const CustomWorldResultView = lazy(async () => {
|
|
const module = await import('../CustomWorldResultView');
|
|
return {
|
|
default: module.CustomWorldResultView,
|
|
};
|
|
});
|
|
|
|
const CustomWorldAgentWorkspace = lazy(async () => {
|
|
const module = await import(
|
|
'../custom-world-agent/CustomWorldAgentWorkspace'
|
|
);
|
|
return {
|
|
default: module.CustomWorldAgentWorkspace,
|
|
};
|
|
});
|
|
|
|
export type SelectionStage =
|
|
| 'platform'
|
|
| 'detail'
|
|
| 'agent-workspace'
|
|
| 'custom-world-generating'
|
|
| 'custom-world-result';
|
|
|
|
type CustomWorldGenerationViewSource = 'agent-draft-foundation' | null;
|
|
|
|
type CustomWorldResultViewSource = 'saved-profile' | 'agent-draft' | null;
|
|
type CustomWorldAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
|
|
|
|
type PreGameSelectionFlowProps = {
|
|
selectionStage: SelectionStage;
|
|
setSelectionStage: (stage: SelectionStage) => void;
|
|
gameState: GameState;
|
|
hasSavedGame: boolean;
|
|
savedSnapshot: HydratedSavedGameSnapshot | null;
|
|
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
|
|
handleStartNewGame: () => void;
|
|
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
|
|
};
|
|
|
|
function resolveErrorMessage(error: unknown, fallback: string) {
|
|
return error instanceof Error ? error.message : fallback;
|
|
}
|
|
|
|
function createFailedAgentOperation(params: {
|
|
type: CustomWorldAgentOperationRecord['type'];
|
|
phaseLabel: string;
|
|
error: string;
|
|
}): CustomWorldAgentOperationRecord {
|
|
return {
|
|
operationId: `local-failed-${Date.now()}`,
|
|
type: params.type,
|
|
status: 'failed',
|
|
phaseLabel: params.phaseLabel,
|
|
phaseDetail: params.error,
|
|
progress: 100,
|
|
error: params.error,
|
|
};
|
|
}
|
|
|
|
function buildOptimisticAgentMessage(
|
|
payload: Pick<CustomWorldAgentMessage, 'id' | 'role' | 'kind' | 'text'>,
|
|
): CustomWorldAgentMessage {
|
|
return {
|
|
...payload,
|
|
createdAt: new Date().toISOString(),
|
|
relatedOperationId: null,
|
|
};
|
|
}
|
|
|
|
function normalizeAgentBackedProfile(profile: CustomWorldProfile) {
|
|
const foundationText = buildCustomWorldCreatorIntentFoundationText(
|
|
profile.creatorIntent,
|
|
).trim();
|
|
|
|
if (!foundationText || foundationText === profile.settingText.trim()) {
|
|
return profile;
|
|
}
|
|
|
|
return {
|
|
...profile,
|
|
settingText: foundationText,
|
|
} satisfies CustomWorldProfile;
|
|
}
|
|
|
|
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 PreGameSelectionFlow({
|
|
selectionStage,
|
|
setSelectionStage,
|
|
hasSavedGame,
|
|
savedSnapshot,
|
|
handleContinueGame,
|
|
handleStartNewGame,
|
|
handleCustomWorldSelect,
|
|
}: PreGameSelectionFlowProps) {
|
|
const authUi = useAuthUi();
|
|
const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState());
|
|
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
|
|
const hasRequestedInitialAgentWorkspaceAuthRef = useRef(false);
|
|
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
|
useState<CustomWorldProfile | null>(null);
|
|
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
|
|
CustomWorldLibraryEntry<CustomWorldProfile>[]
|
|
>([]);
|
|
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
|
|
CustomWorldGalleryCard[]
|
|
>([]);
|
|
const [historyEntries, setHistoryEntries] = useState<
|
|
PlatformBrowseHistoryEntry[]
|
|
>([]);
|
|
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>(
|
|
[],
|
|
);
|
|
const [platformTab, setPlatformTab] = useState<PlatformHomeTab>('home');
|
|
const [selectedDetailEntry, setSelectedDetailEntry] =
|
|
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
|
|
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
|
|
const [creationTypeError, setCreationTypeError] = useState<string | null>(
|
|
null,
|
|
);
|
|
const [isCreatingAgentSession, setIsCreatingAgentSession] = useState(false);
|
|
const [activeAgentSessionId, setActiveAgentSessionId] = useState<
|
|
string | null
|
|
>(() => initialAgentUiStateRef.current.activeSessionId ?? null);
|
|
const [activeAgentOperationId, setActiveAgentOperationId] = useState<
|
|
string | null
|
|
>(() => initialAgentUiStateRef.current.activeOperationId ?? null);
|
|
const [agentSession, setAgentSession] =
|
|
useState<CustomWorldAgentSessionSnapshot | null>(null);
|
|
const [agentOperation, setAgentOperation] =
|
|
useState<CustomWorldAgentOperationRecord | null>(null);
|
|
const [streamingAgentReplyText, setStreamingAgentReplyText] = useState('');
|
|
const [isStreamingAgentReply, setIsStreamingAgentReply] = useState(false);
|
|
const [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
|
|
const [customWorldError, setCustomWorldError] = useState<string | null>(null);
|
|
const [platformError, setPlatformError] = useState<string | null>(null);
|
|
const [profileDashboard, setProfileDashboard] =
|
|
useState<ProfileDashboardSummary | null>(null);
|
|
const [dashboardError, setDashboardError] = useState<string | null>(null);
|
|
const [_historyError, setHistoryError] = useState<string | null>(null);
|
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
const [detailError, setDetailError] = useState<string | null>(null);
|
|
const [isLoadingPlatform, setIsLoadingPlatform] = useState(false);
|
|
const [isLoadingDashboard, setIsLoadingDashboard] = useState(false);
|
|
const [isResumingSaveWorldKey, setIsResumingSaveWorldKey] = useState<
|
|
string | null
|
|
>(null);
|
|
const [isDetailLoading, setIsDetailLoading] = useState(false);
|
|
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
|
|
const [customWorldAutoSaveState, setCustomWorldAutoSaveState] =
|
|
useState<CustomWorldAutoSaveState>('idle');
|
|
const [customWorldAutoSaveError, setCustomWorldAutoSaveError] = useState<
|
|
string | null
|
|
>(null);
|
|
const [customWorldGenerationViewSource, setCustomWorldGenerationViewSource] =
|
|
useState<CustomWorldGenerationViewSource>(null);
|
|
const [customWorldResultViewSource, setCustomWorldResultViewSource] =
|
|
useState<CustomWorldResultViewSource>(null);
|
|
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
|
|
useState<number | null>(null);
|
|
const customWorldAutoSaveTimeoutRef = useRef<number | null>(null);
|
|
const lastAutoSavedProfileSignatureRef = useRef<string | null>(null);
|
|
const latestAutoSaveRequestIdRef = useRef(0);
|
|
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
|
|
undefined,
|
|
);
|
|
|
|
const previewCustomWorldCharacters = useMemo(
|
|
() =>
|
|
generatedCustomWorldProfile
|
|
? buildCustomWorldPlayableCharacters(generatedCustomWorldProfile)
|
|
: [],
|
|
[generatedCustomWorldProfile],
|
|
);
|
|
|
|
const featuredGalleryEntries = useMemo(
|
|
() => publishedGalleryEntries.slice(0, 6),
|
|
[publishedGalleryEntries],
|
|
);
|
|
const isAuthenticated = Boolean(authUi?.user);
|
|
|
|
const runProtectedAction = useCallback(
|
|
(action: () => void) => {
|
|
if (!authUi?.requireAuth) {
|
|
action();
|
|
return;
|
|
}
|
|
|
|
authUi.requireAuth(action);
|
|
},
|
|
[authUi],
|
|
);
|
|
|
|
const persistAgentUiState = useCallback(
|
|
(nextSessionId: string | null, nextOperationId: string | null) => {
|
|
setActiveAgentSessionId(nextSessionId);
|
|
setActiveAgentOperationId(nextOperationId);
|
|
writeCustomWorldAgentUiState({
|
|
activeSessionId: nextSessionId,
|
|
activeOperationId: nextOperationId,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const syncAgentSessionSnapshot = useCallback(async (sessionId: string) => {
|
|
const nextSession = await getCustomWorldAgentSession(sessionId);
|
|
setAgentSession(nextSession);
|
|
return nextSession;
|
|
}, []);
|
|
|
|
const refreshProfileDashboard = useCallback(async () => {
|
|
if (!authUi?.user) {
|
|
setProfileDashboard(null);
|
|
setDashboardError(null);
|
|
setIsLoadingDashboard(false);
|
|
return;
|
|
}
|
|
|
|
setIsLoadingDashboard(true);
|
|
setDashboardError(null);
|
|
|
|
try {
|
|
setProfileDashboard(await getProfileDashboard());
|
|
} catch (error) {
|
|
setDashboardError(resolveErrorMessage(error, '读取个人数据看板失败。'));
|
|
} finally {
|
|
setIsLoadingDashboard(false);
|
|
}
|
|
}, [authUi?.user]);
|
|
|
|
const appendBrowseHistoryEntry = useCallback(
|
|
async (entry: PlatformBrowseHistoryWriteEntry) => {
|
|
const nextEntries = writePlatformBrowseHistory(authUi?.user, entry);
|
|
setHistoryEntries(nextEntries);
|
|
setHistoryError(null);
|
|
|
|
if (!authUi?.user) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const syncedEntries = await upsertProfileBrowseHistory(entry);
|
|
setHistoryEntries(syncedEntries);
|
|
markPlatformBrowseHistoryMigrated(authUi?.user);
|
|
} catch (error) {
|
|
setHistoryError(resolveErrorMessage(error, '写入浏览历史失败。'));
|
|
}
|
|
},
|
|
[authUi?.user],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const initialAgentSessionId = initialAgentUiStateRef.current.activeSessionId;
|
|
|
|
if (!initialAgentSessionId || hasAppliedInitialAgentWorkspaceRef.current) {
|
|
return;
|
|
}
|
|
|
|
setPlatformTab('create');
|
|
|
|
// URL 或 sessionStorage 中残留的共创工作区属于受保护入口,
|
|
// 未登录时只允许先唤起登录弹窗,不能直接恢复会话请求。
|
|
if (!authUi?.user) {
|
|
if (!hasRequestedInitialAgentWorkspaceAuthRef.current) {
|
|
hasRequestedInitialAgentWorkspaceAuthRef.current = true;
|
|
authUi?.openLoginModal?.(() => {
|
|
setSelectionStage('agent-workspace');
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
hasAppliedInitialAgentWorkspaceRef.current = true;
|
|
setSelectionStage('agent-workspace');
|
|
}, [authUi?.openLoginModal, authUi?.user, setSelectionStage]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedDetailEntry) {
|
|
return;
|
|
}
|
|
|
|
const nextOwnedEntry = savedCustomWorldEntries.find(
|
|
(entry) =>
|
|
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
|
entry.profileId === selectedDetailEntry.profileId,
|
|
);
|
|
if (nextOwnedEntry && nextOwnedEntry !== selectedDetailEntry) {
|
|
setSelectedDetailEntry(nextOwnedEntry);
|
|
}
|
|
}, [savedCustomWorldEntries, selectedDetailEntry]);
|
|
|
|
useEffect(() => {
|
|
let isActive = true;
|
|
|
|
void (async () => {
|
|
const localHistoryEntries = readPlatformBrowseHistory(authUi?.user);
|
|
setHistoryEntries(localHistoryEntries);
|
|
setHistoryError(null);
|
|
setSaveError(null);
|
|
setIsLoadingPlatform(true);
|
|
setPlatformError(null);
|
|
setIsLoadingDashboard(isAuthenticated);
|
|
setDashboardError(null);
|
|
if (!isAuthenticated) {
|
|
setSavedCustomWorldEntries([]);
|
|
setSaveEntries([]);
|
|
setProfileDashboard(null);
|
|
}
|
|
|
|
try {
|
|
const [
|
|
libraryEntriesResult,
|
|
galleryEntriesResult,
|
|
dashboardResult,
|
|
historyResult,
|
|
saveArchivesResult,
|
|
] = await Promise.allSettled([
|
|
isAuthenticated ? listCustomWorldLibrary() : Promise.resolve([]),
|
|
listCustomWorldGallery(),
|
|
isAuthenticated ? getProfileDashboard() : Promise.resolve(null),
|
|
isAuthenticated
|
|
? (async () => {
|
|
let nextEntries = await listProfileBrowseHistory();
|
|
|
|
if (
|
|
hasPendingPlatformBrowseHistoryMigration(authUi?.user) &&
|
|
localHistoryEntries.length > 0
|
|
) {
|
|
nextEntries =
|
|
await syncProfileBrowseHistory(localHistoryEntries);
|
|
markPlatformBrowseHistoryMigrated(authUi?.user);
|
|
}
|
|
|
|
return nextEntries;
|
|
})()
|
|
: Promise.resolve(localHistoryEntries),
|
|
isAuthenticated ? listProfileSaveArchives() : Promise.resolve([]),
|
|
]);
|
|
if (!isActive) {
|
|
return;
|
|
}
|
|
|
|
if (libraryEntriesResult.status === 'fulfilled') {
|
|
setSavedCustomWorldEntries(libraryEntriesResult.value);
|
|
} else {
|
|
setSavedCustomWorldEntries([]);
|
|
}
|
|
|
|
if (galleryEntriesResult.status === 'fulfilled') {
|
|
setPublishedGalleryEntries(galleryEntriesResult.value);
|
|
} else {
|
|
setPublishedGalleryEntries([]);
|
|
}
|
|
|
|
if (
|
|
(isAuthenticated && libraryEntriesResult.status === 'rejected') ||
|
|
galleryEntriesResult.status === 'rejected'
|
|
) {
|
|
const platformFailure =
|
|
libraryEntriesResult.status === 'rejected'
|
|
? libraryEntriesResult.reason
|
|
: galleryEntriesResult.status === 'rejected'
|
|
? galleryEntriesResult.reason
|
|
: null;
|
|
setPlatformError(
|
|
resolveErrorMessage(platformFailure, '读取平台数据失败。'),
|
|
);
|
|
}
|
|
|
|
if (dashboardResult.status === 'fulfilled') {
|
|
setProfileDashboard(dashboardResult.value);
|
|
} else if (isAuthenticated) {
|
|
setProfileDashboard(null);
|
|
setDashboardError(
|
|
resolveErrorMessage(
|
|
dashboardResult.reason,
|
|
'读取个人数据看板失败。',
|
|
),
|
|
);
|
|
}
|
|
|
|
if (historyResult.status === 'fulfilled') {
|
|
setHistoryEntries(historyResult.value);
|
|
} else if (isAuthenticated) {
|
|
setHistoryError(
|
|
resolveErrorMessage(historyResult.reason, '读取浏览历史失败。'),
|
|
);
|
|
}
|
|
|
|
if (saveArchivesResult.status === 'fulfilled') {
|
|
setSaveEntries(saveArchivesResult.value);
|
|
} else if (isAuthenticated) {
|
|
setSaveEntries([]);
|
|
setSaveError(
|
|
resolveErrorMessage(
|
|
saveArchivesResult.reason,
|
|
'读取存档列表失败。',
|
|
),
|
|
);
|
|
}
|
|
|
|
const nextPlatformBootstrapUserId = authUi?.user?.id ?? null;
|
|
if (
|
|
platformTabBootstrapUserIdRef.current !== nextPlatformBootstrapUserId
|
|
) {
|
|
platformTabBootstrapUserIdRef.current = nextPlatformBootstrapUserId;
|
|
if (!initialAgentUiStateRef.current.activeSessionId) {
|
|
setPlatformTab(
|
|
isAuthenticated &&
|
|
saveArchivesResult.status === 'fulfilled' &&
|
|
saveArchivesResult.value.length > 0
|
|
? 'saves'
|
|
: 'home',
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
if (isActive) {
|
|
setIsLoadingPlatform(false);
|
|
setIsLoadingDashboard(false);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
isActive = false;
|
|
};
|
|
}, [authUi?.user, isAuthenticated]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
selectionStage === 'custom-world-result' &&
|
|
!generatedCustomWorldProfile
|
|
) {
|
|
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
|
|
}
|
|
}, [
|
|
generatedCustomWorldProfile,
|
|
selectedDetailEntry,
|
|
selectionStage,
|
|
setSelectionStage,
|
|
]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
if (customWorldAutoSaveTimeoutRef.current !== null) {
|
|
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
|
|
}
|
|
},
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!activeAgentSessionId) {
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setIsLoadingAgentSession(false);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
return;
|
|
}
|
|
|
|
if (!authUi?.user) {
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setIsLoadingAgentSession(false);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
setIsLoadingAgentSession(true);
|
|
|
|
void syncAgentSessionSnapshot(activeAgentSessionId)
|
|
.then(() => {
|
|
if (!cancelled) {
|
|
setCreationTypeError(null);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
setCreationTypeError(
|
|
resolveErrorMessage(error, '读取 Agent 共创工作区失败。'),
|
|
);
|
|
setAgentSession(null);
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
persistAgentUiState(null, null);
|
|
setPlatformTab('create');
|
|
setSelectionStage('platform');
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) {
|
|
setIsLoadingAgentSession(false);
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
activeAgentSessionId,
|
|
authUi?.user,
|
|
persistAgentUiState,
|
|
setSelectionStage,
|
|
syncAgentSessionSnapshot,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!activeAgentSessionId || !activeAgentOperationId || !authUi?.user) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
|
|
const pollOperation = async () => {
|
|
try {
|
|
const nextOperation = await getCustomWorldAgentOperation(
|
|
activeAgentSessionId,
|
|
activeAgentOperationId,
|
|
);
|
|
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
setAgentOperation(nextOperation);
|
|
|
|
if (
|
|
nextOperation.status === 'completed' ||
|
|
nextOperation.status === 'failed'
|
|
) {
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
await syncAgentSessionSnapshot(activeAgentSessionId).catch(
|
|
() => null,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
const errorMessage = resolveErrorMessage(
|
|
error,
|
|
'读取共创操作状态失败。',
|
|
);
|
|
setAgentOperation(
|
|
createFailedAgentOperation({
|
|
type: 'process_message',
|
|
phaseLabel: '读取操作状态失败',
|
|
error: errorMessage,
|
|
}),
|
|
);
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
}
|
|
};
|
|
|
|
void pollOperation();
|
|
const intervalId = window.setInterval(() => {
|
|
void pollOperation();
|
|
}, 1200);
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
window.clearInterval(intervalId);
|
|
};
|
|
}, [
|
|
activeAgentOperationId,
|
|
activeAgentSessionId,
|
|
authUi?.user,
|
|
persistAgentUiState,
|
|
syncAgentSessionSnapshot,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!isDraftFoundationOperationRunning(agentOperation) ||
|
|
agentDraftGenerationStartedAt
|
|
) {
|
|
return;
|
|
}
|
|
|
|
setAgentDraftGenerationStartedAt(Date.now());
|
|
}, [agentDraftGenerationStartedAt, agentOperation]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
selectionStage !== 'custom-world-generating' ||
|
|
customWorldGenerationViewSource !== 'agent-draft-foundation' ||
|
|
!isDraftFoundationOperation(agentOperation) ||
|
|
agentOperation.status !== 'completed'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
const timeoutId = window.setTimeout(() => {
|
|
void (async () => {
|
|
const latestSession = activeAgentSessionId
|
|
? await syncAgentSessionSnapshot(activeAgentSessionId).catch(
|
|
() => null,
|
|
)
|
|
: agentSession;
|
|
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
const draftResultProfile = buildCustomWorldProfileFromAgentDraft(
|
|
latestSession ?? agentSession,
|
|
);
|
|
if (!draftResultProfile) {
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setSelectionStage('agent-workspace');
|
|
return;
|
|
}
|
|
|
|
setGeneratedCustomWorldProfile(
|
|
normalizeAgentBackedProfile(draftResultProfile),
|
|
);
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource('agent-draft');
|
|
setSelectionStage('custom-world-result');
|
|
})();
|
|
}, 900);
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
window.clearTimeout(timeoutId);
|
|
};
|
|
}, [
|
|
activeAgentSessionId,
|
|
agentOperation,
|
|
customWorldGenerationViewSource,
|
|
agentSession,
|
|
selectionStage,
|
|
setSelectionStage,
|
|
syncAgentSessionSnapshot,
|
|
]);
|
|
|
|
const agentDraftSettingPreview = useMemo(
|
|
() => buildAgentDraftFoundationSettingText(agentSession),
|
|
[agentSession],
|
|
);
|
|
const agentDraftAnchorPreviewEntries = useMemo(
|
|
() => buildAgentDraftFoundationAnchorEntries(agentSession),
|
|
[agentSession],
|
|
);
|
|
const agentDraftResultProfile = useMemo(
|
|
() => buildCustomWorldProfileFromAgentDraft(agentSession),
|
|
[agentSession],
|
|
);
|
|
const shouldAutoOpenAgentDraftResult = useMemo(
|
|
() =>
|
|
Boolean(
|
|
agentDraftResultProfile &&
|
|
agentSession &&
|
|
(agentSession.stage === 'object_refining' ||
|
|
agentSession.stage === 'visual_refining' ||
|
|
agentSession.stage === 'long_tail_review' ||
|
|
agentSession.stage === 'ready_to_publish' ||
|
|
agentSession.stage === 'published') &&
|
|
agentSession.draftCards.length > 0,
|
|
),
|
|
[agentDraftResultProfile, agentSession],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!shouldAutoOpenAgentDraftResult || !agentDraftResultProfile) {
|
|
return;
|
|
}
|
|
|
|
if (selectionStage === 'agent-workspace') {
|
|
setGeneratedCustomWorldProfile(agentDraftResultProfile);
|
|
setCustomWorldResultViewSource('agent-draft');
|
|
setSelectionStage('custom-world-result');
|
|
return;
|
|
}
|
|
|
|
if (
|
|
selectionStage === 'custom-world-result' &&
|
|
!generatedCustomWorldProfile
|
|
) {
|
|
setGeneratedCustomWorldProfile(agentDraftResultProfile);
|
|
setCustomWorldResultViewSource('agent-draft');
|
|
}
|
|
}, [
|
|
agentDraftResultProfile,
|
|
generatedCustomWorldProfile,
|
|
selectionStage,
|
|
setSelectionStage,
|
|
shouldAutoOpenAgentDraftResult,
|
|
]);
|
|
|
|
const agentDraftGenerationProgress = useMemo(
|
|
() =>
|
|
buildAgentDraftFoundationGenerationProgress(
|
|
agentOperation,
|
|
agentDraftGenerationStartedAt,
|
|
),
|
|
[agentDraftGenerationStartedAt, agentOperation],
|
|
);
|
|
|
|
const isAgentDraftGenerationView =
|
|
customWorldGenerationViewSource === 'agent-draft-foundation';
|
|
const isAgentDraftResultView = customWorldResultViewSource === 'agent-draft';
|
|
const activeGenerationSettingText = agentDraftSettingPreview;
|
|
const activeGenerationProgress = agentDraftGenerationProgress;
|
|
const isActiveGenerationRunning =
|
|
isDraftFoundationOperationRunning(agentOperation);
|
|
const activeGenerationError =
|
|
isDraftFoundationOperation(agentOperation) &&
|
|
agentOperation.status === 'failed'
|
|
? agentOperation.error || agentOperation.phaseDetail
|
|
: null;
|
|
|
|
const leaveCustomWorldResult = () => {
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
setSelectionStage(
|
|
isAgentDraftResultView
|
|
? 'agent-workspace'
|
|
: selectedDetailEntry
|
|
? 'detail'
|
|
: 'platform',
|
|
);
|
|
};
|
|
|
|
const openCreationTypePicker = () => {
|
|
if (isCreatingAgentSession) {
|
|
return;
|
|
}
|
|
|
|
if (!hasSavedGame) {
|
|
handleStartNewGame();
|
|
}
|
|
|
|
setCreationTypeError(null);
|
|
setShowCreationTypeModal(true);
|
|
};
|
|
|
|
const openRpgAgentWorkspace = async (seedText = '') => {
|
|
if (isCreatingAgentSession) {
|
|
return;
|
|
}
|
|
|
|
setIsCreatingAgentSession(true);
|
|
setCreationTypeError(null);
|
|
|
|
try {
|
|
const { session } = await createCustomWorldAgentSession(
|
|
seedText ? { seedText } : {},
|
|
);
|
|
setAgentSession(session);
|
|
setAgentOperation(null);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
persistAgentUiState(session.sessionId, null);
|
|
setShowCreationTypeModal(false);
|
|
setPlatformTab('create');
|
|
setSelectionStage('agent-workspace');
|
|
} catch (error) {
|
|
setCreationTypeError(resolveErrorMessage(error, '开启共创工作台失败。'));
|
|
} finally {
|
|
setIsCreatingAgentSession(false);
|
|
}
|
|
};
|
|
|
|
const submitAgentMessage = async (
|
|
payload: SendCustomWorldAgentMessageRequest,
|
|
) => {
|
|
if (!activeAgentSessionId) {
|
|
return;
|
|
}
|
|
|
|
const optimisticUserMessage = buildOptimisticAgentMessage({
|
|
id: payload.clientMessageId,
|
|
role: 'user',
|
|
kind: 'chat',
|
|
text: payload.text.trim(),
|
|
});
|
|
|
|
setAgentOperation(null);
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(true);
|
|
setAgentSession((current) =>
|
|
current
|
|
? {
|
|
...current,
|
|
messages: [...current.messages, optimisticUserMessage],
|
|
updatedAt: optimisticUserMessage.createdAt,
|
|
}
|
|
: current,
|
|
);
|
|
|
|
try {
|
|
const nextSession = await streamCustomWorldAgentMessage(
|
|
activeAgentSessionId,
|
|
payload,
|
|
{
|
|
onUpdate: (text) => {
|
|
setStreamingAgentReplyText(text);
|
|
},
|
|
},
|
|
);
|
|
setAgentSession(nextSession);
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
} catch (error) {
|
|
const errorMessage = resolveErrorMessage(error, '发送共创消息失败。');
|
|
setAgentSession((current) =>
|
|
current
|
|
? {
|
|
...current,
|
|
messages: [
|
|
...current.messages,
|
|
buildOptimisticAgentMessage({
|
|
id: `message-error-${Date.now()}`,
|
|
role: 'assistant',
|
|
kind: 'warning',
|
|
text: errorMessage,
|
|
}),
|
|
],
|
|
updatedAt: new Date().toISOString(),
|
|
}
|
|
: current,
|
|
);
|
|
setStreamingAgentReplyText('');
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
} finally {
|
|
setIsStreamingAgentReply(false);
|
|
}
|
|
};
|
|
|
|
const executeAgentAction = async (payload: CustomWorldAgentActionRequest) => {
|
|
if (!activeAgentSessionId) {
|
|
return;
|
|
}
|
|
|
|
const isDraftFoundationAction = payload.action === 'draft_foundation';
|
|
|
|
if (isDraftFoundationAction) {
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
|
setCustomWorldResultViewSource(null);
|
|
setAgentDraftGenerationStartedAt(Date.now());
|
|
setSelectionStage('custom-world-generating');
|
|
}
|
|
|
|
try {
|
|
const { operation } = await executeCustomWorldAgentAction(
|
|
activeAgentSessionId,
|
|
payload,
|
|
);
|
|
setAgentOperation(operation);
|
|
persistAgentUiState(activeAgentSessionId, operation.operationId);
|
|
} catch (error) {
|
|
const errorMessage = resolveErrorMessage(error, '执行共创操作失败。');
|
|
setAgentOperation(
|
|
createFailedAgentOperation({
|
|
type:
|
|
payload.action === 'draft_foundation'
|
|
? 'draft_foundation'
|
|
: payload.action,
|
|
phaseLabel: '执行操作失败',
|
|
error: errorMessage,
|
|
}),
|
|
);
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
}
|
|
};
|
|
|
|
const leaveAgentWorkspace = () => {
|
|
setPlatformTab('create');
|
|
setAgentOperation(null);
|
|
setStreamingAgentReplyText('');
|
|
setIsStreamingAgentReply(false);
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
persistAgentUiState(activeAgentSessionId, null);
|
|
setSelectionStage('platform');
|
|
};
|
|
|
|
const leaveAgentDraftGeneration = () => {
|
|
if (isDraftFoundationOperationRunning(agentOperation)) {
|
|
return;
|
|
}
|
|
|
|
setAgentDraftGenerationStartedAt(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setSelectionStage('agent-workspace');
|
|
};
|
|
|
|
const leaveAgentDraftResult = () => {
|
|
setGeneratedCustomWorldProfile(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldAutoSaveState('idle');
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource(null);
|
|
setPlatformTab('create');
|
|
setSelectionStage('platform');
|
|
};
|
|
|
|
const retryAgentDraftGeneration = () => {
|
|
void executeAgentAction({
|
|
action: 'draft_foundation',
|
|
});
|
|
};
|
|
|
|
const openCustomWorldCreator = () => {
|
|
openCreationTypePicker();
|
|
};
|
|
|
|
const openLibraryDetail = (
|
|
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
|
) => {
|
|
if (entry.visibility === 'published') {
|
|
void appendBrowseHistoryEntry({
|
|
ownerUserId: entry.ownerUserId,
|
|
profileId: entry.profileId,
|
|
worldName: entry.worldName,
|
|
subtitle: entry.subtitle,
|
|
summaryText: entry.summaryText,
|
|
coverImageSrc: entry.coverImageSrc,
|
|
themeMode: entry.themeMode,
|
|
authorDisplayName: entry.authorDisplayName,
|
|
});
|
|
}
|
|
setSelectedDetailEntry(entry);
|
|
setDetailError(null);
|
|
setSelectionStage('detail');
|
|
};
|
|
|
|
const openGalleryDetail = async (entry: CustomWorldGalleryCard) => {
|
|
setSelectionStage('detail');
|
|
setIsDetailLoading(true);
|
|
setDetailError(null);
|
|
try {
|
|
const detailEntry = await getCustomWorldGalleryDetail(
|
|
entry.ownerUserId,
|
|
entry.profileId,
|
|
);
|
|
setSelectedDetailEntry(detailEntry);
|
|
void appendBrowseHistoryEntry({
|
|
ownerUserId: detailEntry.ownerUserId,
|
|
profileId: detailEntry.profileId,
|
|
worldName: detailEntry.worldName,
|
|
subtitle: detailEntry.subtitle,
|
|
summaryText: detailEntry.summaryText,
|
|
coverImageSrc: detailEntry.coverImageSrc,
|
|
themeMode: detailEntry.themeMode,
|
|
authorDisplayName: detailEntry.authorDisplayName,
|
|
});
|
|
} catch (error) {
|
|
setSelectedDetailEntry(null);
|
|
setDetailError(resolveErrorMessage(error, '读取作品详情失败。'));
|
|
} finally {
|
|
setIsDetailLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleResumeSaveEntry = useCallback(
|
|
async (entry: ProfileSaveArchiveSummary) => {
|
|
if (!authUi?.user || isResumingSaveWorldKey) {
|
|
return;
|
|
}
|
|
|
|
setIsResumingSaveWorldKey(entry.worldKey);
|
|
setSaveError(null);
|
|
|
|
try {
|
|
const resumedArchive = await resumeProfileSaveArchive(entry.worldKey);
|
|
setSaveEntries((currentEntries) =>
|
|
currentEntries.map((currentEntry) =>
|
|
currentEntry.worldKey === resumedArchive.entry.worldKey
|
|
? resumedArchive.entry
|
|
: currentEntry,
|
|
),
|
|
);
|
|
handleContinueGame(resumedArchive.snapshot);
|
|
} catch (error) {
|
|
setSaveError(resolveErrorMessage(error, '恢复存档失败。'));
|
|
} finally {
|
|
setIsResumingSaveWorldKey(null);
|
|
}
|
|
},
|
|
[authUi?.user, handleContinueGame, isResumingSaveWorldKey],
|
|
);
|
|
|
|
const saveGeneratedCustomWorld = useCallback(
|
|
async (profile = generatedCustomWorldProfile) => {
|
|
if (!profile) {
|
|
return null;
|
|
}
|
|
|
|
const normalizedProfile = normalizeAgentBackedProfile(profile);
|
|
const profileSignature = JSON.stringify(normalizedProfile);
|
|
const requestId = latestAutoSaveRequestIdRef.current + 1;
|
|
latestAutoSaveRequestIdRef.current = requestId;
|
|
setCustomWorldAutoSaveState('saving');
|
|
setCustomWorldAutoSaveError(null);
|
|
|
|
try {
|
|
const mutation = await upsertCustomWorldProfile(normalizedProfile);
|
|
if (latestAutoSaveRequestIdRef.current !== requestId) {
|
|
return mutation;
|
|
}
|
|
|
|
lastAutoSavedProfileSignatureRef.current = profileSignature;
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
setSelectedDetailEntry((current) => {
|
|
if (!current || current.profileId === mutation.entry.profileId) {
|
|
return mutation.entry;
|
|
}
|
|
|
|
return current;
|
|
});
|
|
setCustomWorldAutoSaveState('saved');
|
|
setCustomWorldAutoSaveError(null);
|
|
return mutation;
|
|
} catch (error) {
|
|
if (latestAutoSaveRequestIdRef.current !== requestId) {
|
|
return null;
|
|
}
|
|
|
|
setCustomWorldAutoSaveState('error');
|
|
setCustomWorldAutoSaveError(
|
|
resolveErrorMessage(error, '保存自定义世界失败。'),
|
|
);
|
|
return null;
|
|
}
|
|
},
|
|
[generatedCustomWorldProfile],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!generatedCustomWorldProfile) {
|
|
setCustomWorldAutoSaveState('idle');
|
|
setCustomWorldAutoSaveError(null);
|
|
lastAutoSavedProfileSignatureRef.current = null;
|
|
if (customWorldAutoSaveTimeoutRef.current !== null) {
|
|
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
|
|
customWorldAutoSaveTimeoutRef.current = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (selectionStage !== 'custom-world-result') {
|
|
return;
|
|
}
|
|
|
|
const nextSignature = JSON.stringify(generatedCustomWorldProfile);
|
|
if (nextSignature === lastAutoSavedProfileSignatureRef.current) {
|
|
return;
|
|
}
|
|
|
|
setCustomWorldAutoSaveState('saving');
|
|
if (customWorldAutoSaveTimeoutRef.current !== null) {
|
|
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
|
|
}
|
|
|
|
const profileToSave = generatedCustomWorldProfile;
|
|
customWorldAutoSaveTimeoutRef.current = window.setTimeout(() => {
|
|
void saveGeneratedCustomWorld(profileToSave);
|
|
customWorldAutoSaveTimeoutRef.current = null;
|
|
}, 600);
|
|
|
|
return () => {
|
|
if (customWorldAutoSaveTimeoutRef.current !== null) {
|
|
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
|
|
customWorldAutoSaveTimeoutRef.current = null;
|
|
}
|
|
};
|
|
}, [generatedCustomWorldProfile, saveGeneratedCustomWorld, selectionStage]);
|
|
|
|
const openSavedCustomWorldEditor = (
|
|
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
|
) => {
|
|
setSelectedDetailEntry(entry);
|
|
const normalizedProfile = normalizeAgentBackedProfile(entry.profile);
|
|
setGeneratedCustomWorldProfile(normalizedProfile);
|
|
lastAutoSavedProfileSignatureRef.current =
|
|
JSON.stringify(normalizedProfile);
|
|
setCustomWorldAutoSaveState('saved');
|
|
setCustomWorldAutoSaveError(null);
|
|
setCustomWorldError(null);
|
|
setCustomWorldGenerationViewSource(null);
|
|
setCustomWorldResultViewSource('saved-profile');
|
|
setSelectionStage('custom-world-result');
|
|
};
|
|
|
|
const handleStartSelectedWorld = () => {
|
|
if (!selectedDetailEntry) {
|
|
return;
|
|
}
|
|
|
|
runProtectedAction(() => {
|
|
handleCustomWorldSelect(selectedDetailEntry.profile);
|
|
});
|
|
};
|
|
|
|
const handlePublishSelectedWorld = async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const mutation = await publishCustomWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
setSelectedDetailEntry(mutation.entry);
|
|
setPublishedGalleryEntries(await listCustomWorldGallery());
|
|
} catch (error) {
|
|
setDetailError(resolveErrorMessage(error, '发布自定义世界失败。'));
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
};
|
|
|
|
const handleUnpublishSelectedWorld = async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const mutation = await unpublishCustomWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(mutation.entries);
|
|
setSelectedDetailEntry(mutation.entry);
|
|
setPublishedGalleryEntries(await listCustomWorldGallery());
|
|
} catch (error) {
|
|
setDetailError(resolveErrorMessage(error, '下架自定义世界失败。'));
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteSelectedWorld = async () => {
|
|
if (!selectedDetailEntry || isMutatingDetail) {
|
|
return;
|
|
}
|
|
|
|
const confirmed = window.confirm(
|
|
`确认删除作品《${selectedDetailEntry.worldName}》吗?删除后会从你的作品列表和公开广场中移除。`,
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
|
|
setIsMutatingDetail(true);
|
|
setDetailError(null);
|
|
try {
|
|
const entries = await deleteCustomWorldProfile(
|
|
selectedDetailEntry.profileId,
|
|
);
|
|
setSavedCustomWorldEntries(entries);
|
|
setSelectedDetailEntry(null);
|
|
setPlatformTab('create');
|
|
setSelectionStage('platform');
|
|
setPublishedGalleryEntries(await listCustomWorldGallery());
|
|
} catch (error) {
|
|
setDetailError(resolveErrorMessage(error, '删除自定义世界失败。'));
|
|
} finally {
|
|
setIsMutatingDetail(false);
|
|
}
|
|
};
|
|
|
|
const isSelectedWorldOwned = Boolean(
|
|
selectedDetailEntry &&
|
|
savedCustomWorldEntries.some(
|
|
(entry) =>
|
|
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
|
entry.profileId === selectedDetailEntry.profileId,
|
|
),
|
|
);
|
|
const resultViewError = customWorldAutoSaveError ?? customWorldError;
|
|
|
|
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 flex-col"
|
|
>
|
|
<PlatformHomeView
|
|
activeTab={platformTab}
|
|
onTabChange={setPlatformTab}
|
|
hasSavedGame={hasSavedGame}
|
|
savedSnapshot={savedSnapshot}
|
|
saveEntries={saveEntries}
|
|
saveError={saveError}
|
|
featuredEntries={featuredGalleryEntries}
|
|
latestEntries={publishedGalleryEntries}
|
|
myEntries={savedCustomWorldEntries}
|
|
historyEntries={historyEntries}
|
|
profileDashboard={profileDashboard}
|
|
isLoadingPlatform={isLoadingPlatform}
|
|
isLoadingDashboard={isLoadingDashboard}
|
|
isResumingSaveWorldKey={isResumingSaveWorldKey}
|
|
platformError={
|
|
isLoadingPlatform ? null : (platformError ?? creationTypeError)
|
|
}
|
|
dashboardError={isLoadingDashboard ? null : dashboardError}
|
|
onContinueGame={handleContinueGame}
|
|
onResumeSave={(entry) => {
|
|
void handleResumeSaveEntry(entry);
|
|
}}
|
|
onOpenCreateWorld={openCustomWorldCreator}
|
|
onOpenCreateTypePicker={openCreationTypePicker}
|
|
onOpenGalleryDetail={(entry) => {
|
|
runProtectedAction(() => {
|
|
void openGalleryDetail(entry);
|
|
});
|
|
}}
|
|
onOpenLibraryDetail={(entry) => {
|
|
runProtectedAction(() => {
|
|
openLibraryDetail(entry);
|
|
});
|
|
}}
|
|
onOpenProfileDashboardCard={() => {
|
|
if (dashboardError) {
|
|
void 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"
|
|
>
|
|
{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)]">
|
|
{detailError || '正在读取作品详情...'}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<PlatformWorldDetailView
|
|
entry={selectedDetailEntry}
|
|
isMutating={isMutatingDetail}
|
|
error={detailError}
|
|
onBack={() => {
|
|
setDetailError(null);
|
|
setSelectionStage('platform');
|
|
}}
|
|
onStartGame={handleStartSelectedWorld}
|
|
onContinueEdit={
|
|
isSelectedWorldOwned
|
|
? () => {
|
|
runProtectedAction(() => {
|
|
openSavedCustomWorldEditor(selectedDetailEntry);
|
|
});
|
|
}
|
|
: null
|
|
}
|
|
onPublish={
|
|
selectedDetailEntry.visibility === 'draft' &&
|
|
isSelectedWorldOwned
|
|
? () => {
|
|
runProtectedAction(() => {
|
|
void handlePublishSelectedWorld();
|
|
});
|
|
}
|
|
: null
|
|
}
|
|
onUnpublish={
|
|
selectedDetailEntry.visibility === 'published' &&
|
|
isSelectedWorldOwned
|
|
? () => {
|
|
runProtectedAction(() => {
|
|
void handleUnpublishSelectedWorld();
|
|
});
|
|
}
|
|
: null
|
|
}
|
|
onDelete={
|
|
isSelectedWorldOwned
|
|
? () => {
|
|
runProtectedAction(() => {
|
|
void 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 共创工作区..." />
|
|
}
|
|
>
|
|
{agentSession ? (
|
|
<CustomWorldAgentWorkspace
|
|
session={agentSession}
|
|
activeOperation={agentOperation}
|
|
streamingReplyText={streamingAgentReplyText}
|
|
isStreamingReply={isStreamingAgentReply}
|
|
onBack={leaveAgentWorkspace}
|
|
onSubmitMessage={(payload) => {
|
|
void submitAgentMessage(payload);
|
|
}}
|
|
onExecuteAction={(payload) => {
|
|
void 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)]">
|
|
{isLoadingAgentSession
|
|
? '正在准备 Agent 共创工作区...'
|
|
: creationTypeError || '正在恢复创作工作区...'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Suspense>
|
|
</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={activeGenerationSettingText}
|
|
anchorEntries={agentDraftAnchorPreviewEntries}
|
|
progress={activeGenerationProgress}
|
|
isGenerating={isActiveGenerationRunning}
|
|
error={activeGenerationError}
|
|
onBack={leaveAgentDraftGeneration}
|
|
onEditSetting={leaveAgentDraftGeneration}
|
|
onRetry={retryAgentDraftGeneration}
|
|
onInterrupt={undefined}
|
|
backLabel="返回工作区"
|
|
settingActionLabel={null}
|
|
retryLabel="重新生成草稿"
|
|
settingTitle="当前世界信息"
|
|
settingDescription={null}
|
|
progressTitle={
|
|
isAgentDraftGenerationView ? '世界草稿生成进度' : undefined
|
|
}
|
|
activeBadgeLabel={
|
|
isAgentDraftGenerationView ? '草稿编译中' : undefined
|
|
}
|
|
pausedBadgeLabel={
|
|
isAgentDraftGenerationView ? '草稿生成已暂停' : undefined
|
|
}
|
|
idleBadgeLabel={
|
|
isAgentDraftGenerationView ? '等待返回工作区' : undefined
|
|
}
|
|
/>
|
|
</Suspense>
|
|
</motion.div>
|
|
)}
|
|
|
|
{selectionStage === 'custom-world-result' &&
|
|
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="正在加载世界编辑器..." />}
|
|
>
|
|
<CustomWorldResultView
|
|
profile={generatedCustomWorldProfile}
|
|
previewCharacters={previewCustomWorldCharacters}
|
|
isGenerating={false}
|
|
progress={0}
|
|
progressLabel=""
|
|
error={resultViewError}
|
|
onProfileChange={(profile) => {
|
|
setGeneratedCustomWorldProfile(
|
|
normalizeAgentBackedProfile(profile),
|
|
);
|
|
}}
|
|
onBack={
|
|
isAgentDraftResultView
|
|
? leaveAgentDraftResult
|
|
: leaveCustomWorldResult
|
|
}
|
|
onEditSetting={undefined}
|
|
onRegenerate={undefined}
|
|
onContinueExpand={undefined}
|
|
onEnterWorld={() => {
|
|
runProtectedAction(() => {
|
|
handleCustomWorldSelect(generatedCustomWorldProfile);
|
|
});
|
|
}}
|
|
readOnly={false}
|
|
backLabel={isAgentDraftResultView ? '返回创作' : undefined}
|
|
editActionLabel="去Agent调整设定"
|
|
enterWorldActionLabel="进入世界"
|
|
autoSaveState={customWorldAutoSaveState}
|
|
/>
|
|
</Suspense>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<PlatformCreationTypeModal
|
|
isOpen={showCreationTypeModal}
|
|
isBusy={isCreatingAgentSession}
|
|
error={creationTypeError}
|
|
onClose={() => {
|
|
if (isCreatingAgentSession) {
|
|
return;
|
|
}
|
|
setShowCreationTypeModal(false);
|
|
}}
|
|
onSelectRpg={() => {
|
|
runProtectedAction(() => {
|
|
void openRpgAgentWorkspace();
|
|
});
|
|
}}
|
|
/>
|
|
</>
|
|
);
|
|
}
|