This commit is contained in:
2026-04-19 20:33:18 +08:00
parent 692643136f
commit 67c584b4df
123 changed files with 11898 additions and 4082 deletions

View File

@@ -10,8 +10,8 @@ import {
} from 'react';
import type {
CustomWorldAgentMessage,
CustomWorldAgentActionRequest,
CustomWorldAgentMessage,
CustomWorldAgentOperationRecord,
CustomWorldAgentSessionSnapshot,
SendCustomWorldAgentMessageRequest,
@@ -20,6 +20,7 @@ import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
ProfileDashboardSummary,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
@@ -62,7 +63,9 @@ import {
listCustomWorldGallery,
listCustomWorldLibrary,
listProfileBrowseHistory,
listProfileSaveArchives,
publishCustomWorldProfile,
resumeProfileSaveArchive,
syncProfileBrowseHistory,
unpublishCustomWorldProfile,
upsertCustomWorldProfile,
@@ -115,7 +118,7 @@ type PreGameSelectionFlowProps = {
gameState: GameState;
hasSavedGame: boolean;
savedSnapshot: HydratedSavedGameSnapshot | null;
handleContinueGame: () => void;
handleContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
handleStartNewGame: () => void;
handleCustomWorldSelect: (customWorldProfile: CustomWorldProfile) => void;
};
@@ -198,6 +201,9 @@ export function PreGameSelectionFlow({
const [historyEntries, setHistoryEntries] = useState<
PlatformBrowseHistoryEntry[]
>([]);
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>(
[],
);
const [platformTab, setPlatformTab] = useState<PlatformHomeTab>('home');
const [selectedDetailEntry, setSelectedDetailEntry] =
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
@@ -225,10 +231,14 @@ export function PreGameSelectionFlow({
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 [isClearingHistory, setIsClearingHistory] = useState(false);
const [isResumingSaveWorldKey, setIsResumingSaveWorldKey] = useState<
string | null
>(null);
const [isDetailLoading, setIsDetailLoading] = useState(false);
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
const [customWorldAutoSaveState, setCustomWorldAutoSaveState] =
@@ -245,6 +255,9 @@ export function PreGameSelectionFlow({
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(
() =>
@@ -258,6 +271,19 @@ export function PreGameSelectionFlow({
() => 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) => {
@@ -278,6 +304,13 @@ export function PreGameSelectionFlow({
}, []);
const refreshProfileDashboard = useCallback(async () => {
if (!authUi?.user) {
setProfileDashboard(null);
setDashboardError(null);
setIsLoadingDashboard(false);
return;
}
setIsLoadingDashboard(true);
setDashboardError(null);
@@ -288,7 +321,7 @@ export function PreGameSelectionFlow({
} finally {
setIsLoadingDashboard(false);
}
}, []);
}, [authUi?.user]);
const appendBrowseHistoryEntry = useCallback(
async (entry: PlatformBrowseHistoryWriteEntry) => {
@@ -296,6 +329,10 @@ export function PreGameSelectionFlow({
setHistoryEntries(nextEntries);
setHistoryError(null);
if (!authUi?.user) {
return;
}
try {
const syncedEntries = await upsertProfileBrowseHistory(entry);
setHistoryEntries(syncedEntries);
@@ -341,10 +378,16 @@ export function PreGameSelectionFlow({
const localHistoryEntries = readPlatformBrowseHistory(authUi?.user);
setHistoryEntries(localHistoryEntries);
setHistoryError(null);
setSaveError(null);
setIsLoadingPlatform(true);
setPlatformError(null);
setIsLoadingDashboard(true);
setIsLoadingDashboard(isAuthenticated);
setDashboardError(null);
if (!isAuthenticated) {
setSavedCustomWorldEntries([]);
setSaveEntries([]);
setProfileDashboard(null);
}
try {
const [
@@ -352,23 +395,29 @@ export function PreGameSelectionFlow({
galleryEntriesResult,
dashboardResult,
historyResult,
saveArchivesResult,
] = await Promise.allSettled([
listCustomWorldLibrary(),
isAuthenticated ? listCustomWorldLibrary() : Promise.resolve([]),
listCustomWorldGallery(),
getProfileDashboard(),
(async () => {
let nextEntries = await listProfileBrowseHistory();
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);
}
if (
hasPendingPlatformBrowseHistoryMigration(authUi?.user) &&
localHistoryEntries.length > 0
) {
nextEntries = await syncProfileBrowseHistory(
localHistoryEntries,
);
markPlatformBrowseHistoryMigrated(authUi?.user);
}
return nextEntries;
})(),
return nextEntries;
})()
: Promise.resolve(localHistoryEntries),
isAuthenticated ? listProfileSaveArchives() : Promise.resolve([]),
]);
if (!isActive) {
return;
@@ -387,7 +436,7 @@ export function PreGameSelectionFlow({
}
if (
libraryEntriesResult.status === 'rejected' ||
(isAuthenticated && libraryEntriesResult.status === 'rejected') ||
galleryEntriesResult.status === 'rejected'
) {
const platformFailure =
@@ -403,7 +452,7 @@ export function PreGameSelectionFlow({
if (dashboardResult.status === 'fulfilled') {
setProfileDashboard(dashboardResult.value);
} else {
} else if (isAuthenticated) {
setProfileDashboard(null);
setDashboardError(
resolveErrorMessage(
@@ -415,11 +464,34 @@ export function PreGameSelectionFlow({
if (historyResult.status === 'fulfilled') {
setHistoryEntries(historyResult.value);
} else {
} 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);
@@ -431,7 +503,7 @@ export function PreGameSelectionFlow({
return () => {
isActive = false;
};
}, [authUi?.user]);
}, [authUi?.user, isAuthenticated]);
useEffect(() => {
if (
@@ -990,8 +1062,10 @@ export function PreGameSelectionFlow({
setIsClearingHistory(true);
setHistoryError(null);
try {
await clearProfileBrowseHistory();
clearPlatformBrowseHistory(authUi?.user);
if (authUi?.user) {
await clearProfileBrowseHistory();
}
setHistoryEntries([]);
} catch (error) {
setHistoryError(resolveErrorMessage(error, '清空浏览历史失败。'));
@@ -1000,6 +1074,34 @@ export function PreGameSelectionFlow({
}
};
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) {
@@ -1107,7 +1209,9 @@ export function PreGameSelectionFlow({
return;
}
handleCustomWorldSelect(selectedDetailEntry.profile);
runProtectedAction(() => {
handleCustomWorldSelect(selectedDetailEntry.profile);
});
};
const handlePublishSelectedWorld = async () => {
@@ -1208,6 +1312,8 @@ export function PreGameSelectionFlow({
onTabChange={setPlatformTab}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
saveEntries={saveEntries}
saveError={saveError}
featuredEntries={featuredGalleryEntries}
latestEntries={publishedGalleryEntries}
myEntries={savedCustomWorldEntries}
@@ -1217,20 +1323,30 @@ export function PreGameSelectionFlow({
isLoadingPlatform={isLoadingPlatform}
isLoadingDashboard={isLoadingDashboard}
isClearingHistory={isClearingHistory}
isResumingSaveWorldKey={isResumingSaveWorldKey}
platformError={
isLoadingPlatform ? null : (platformError ?? creationTypeError)
}
dashboardError={isLoadingDashboard ? null : dashboardError}
onContinueGame={handleContinueGame}
onResumeSave={(entry) => {
void handleResumeSaveEntry(entry);
}}
onClearHistory={() => {
void handleClearBrowseHistory();
}}
onOpenCreateWorld={openCustomWorldCreator}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={(entry) => {
void openGalleryDetail(entry);
runProtectedAction(() => {
void openGalleryDetail(entry);
});
}}
onOpenLibraryDetail={(entry) => {
runProtectedAction(() => {
openLibraryDetail(entry);
});
}}
onOpenLibraryDetail={openLibraryDetail}
onOpenProfileDashboardCard={() => {
if (dashboardError) {
void refreshProfileDashboard();
@@ -1266,23 +1382,41 @@ export function PreGameSelectionFlow({
onStartGame={handleStartSelectedWorld}
onContinueEdit={
isSelectedWorldOwned
? () => openSavedCustomWorldEditor(selectedDetailEntry)
? () => {
runProtectedAction(() => {
openSavedCustomWorldEditor(selectedDetailEntry);
});
}
: null
}
onPublish={
selectedDetailEntry.visibility === 'draft' &&
isSelectedWorldOwned
? handlePublishSelectedWorld
? () => {
runProtectedAction(() => {
void handlePublishSelectedWorld();
});
}
: null
}
onUnpublish={
selectedDetailEntry.visibility === 'published' &&
isSelectedWorldOwned
? handleUnpublishSelectedWorld
? () => {
runProtectedAction(() => {
void handleUnpublishSelectedWorld();
});
}
: null
}
onDelete={
isSelectedWorldOwned ? handleDeleteSelectedWorld : null
isSelectedWorldOwned
? () => {
runProtectedAction(() => {
void handleDeleteSelectedWorld();
});
}
: null
}
/>
)}
@@ -1409,7 +1543,9 @@ export function PreGameSelectionFlow({
onRegenerate={undefined}
onContinueExpand={undefined}
onEnterWorld={() => {
handleCustomWorldSelect(generatedCustomWorldProfile);
runProtectedAction(() => {
handleCustomWorldSelect(generatedCustomWorldProfile);
});
}}
readOnly={false}
backLabel={isAgentDraftResultView ? '返回创作' : undefined}
@@ -1433,7 +1569,9 @@ export function PreGameSelectionFlow({
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
void openRpgAgentWorkspace();
runProtectedAction(() => {
void openRpgAgentWorkspace();
});
}}
/>
</>