1
This commit is contained in:
@@ -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();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user