@@ -10,6 +10,12 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import type { JsonObject } from '../../../packages/shared/src/contracts/common';
|
||||
import type {
|
||||
CustomWorldAgentActionRequest,
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
SendCustomWorldAgentMessageRequest,
|
||||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldGenerationProgress,
|
||||
@@ -17,7 +23,24 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { generateCustomWorldProfile } from '../../services/aiService';
|
||||
import {
|
||||
createCustomWorldAgentSession,
|
||||
executeCustomWorldAgentAction,
|
||||
generateCustomWorldProfile,
|
||||
getCustomWorldAgentOperation,
|
||||
getCustomWorldAgentSession,
|
||||
sendCustomWorldAgentMessage,
|
||||
} from '../../services/aiService';
|
||||
import {
|
||||
readCustomWorldAgentUiState,
|
||||
writeCustomWorldAgentUiState,
|
||||
} from '../../services/customWorldAgentUiState';
|
||||
import {
|
||||
buildAgentDraftFoundationGenerationProgress,
|
||||
buildAgentDraftFoundationSettingText,
|
||||
isDraftFoundationOperation,
|
||||
isDraftFoundationOperationRunning,
|
||||
} from '../../services/customWorldAgentGenerationProgress';
|
||||
import {
|
||||
buildCustomWorldCreatorIntentDisplayText,
|
||||
buildCustomWorldCreatorIntentGenerationText,
|
||||
@@ -37,7 +60,8 @@ import {
|
||||
type CustomWorldProfile,
|
||||
type GameState,
|
||||
} from '../../types';
|
||||
import { PlatformHomeView } from './PlatformHomeView';
|
||||
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
|
||||
import { type PlatformHomeTab,PlatformHomeView } from './PlatformHomeView';
|
||||
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
|
||||
|
||||
const CustomWorldGenerationView = lazy(async () => {
|
||||
@@ -61,12 +85,27 @@ const CustomWorldCreatorModal = lazy(async () => {
|
||||
};
|
||||
});
|
||||
|
||||
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 =
|
||||
| 'classic'
|
||||
| 'agent-draft-foundation'
|
||||
| null;
|
||||
|
||||
type PreGameSelectionFlowProps = {
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
@@ -151,6 +190,22 @@ 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 LazyPanelFallback({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 items-center justify-center">
|
||||
@@ -170,6 +225,8 @@ export function PreGameSelectionFlow({
|
||||
handleStartNewGame,
|
||||
handleCustomWorldSelect,
|
||||
}: PreGameSelectionFlowProps) {
|
||||
const initialAgentUiStateRef = useRef(readCustomWorldAgentUiState());
|
||||
const hasAppliedInitialAgentWorkspaceRef = useRef(false);
|
||||
const [generatedCustomWorldProfile, setGeneratedCustomWorldProfile] =
|
||||
useState<CustomWorldProfile | null>(null);
|
||||
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
|
||||
@@ -178,8 +235,25 @@ export function PreGameSelectionFlow({
|
||||
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
|
||||
CustomWorldGalleryCard[]
|
||||
>([]);
|
||||
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 [isLoadingAgentSession, setIsLoadingAgentSession] = useState(false);
|
||||
const [showCustomWorldModal, setShowCustomWorldModal] = useState(false);
|
||||
const [customWorldCreatorIntent, setCustomWorldCreatorIntent] =
|
||||
useState<CustomWorldCreatorIntent>(() =>
|
||||
@@ -196,6 +270,10 @@ export function PreGameSelectionFlow({
|
||||
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
|
||||
const [customWorldProgress, setCustomWorldProgress] =
|
||||
useState<CustomWorldGenerationProgress | null>(null);
|
||||
const [customWorldGenerationViewSource, setCustomWorldGenerationViewSource] =
|
||||
useState<CustomWorldGenerationViewSource>(null);
|
||||
const [agentDraftGenerationStartedAt, setAgentDraftGenerationStartedAt] =
|
||||
useState<number | null>(null);
|
||||
const customWorldAbortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const previewCustomWorldCharacters = useMemo(
|
||||
@@ -211,6 +289,24 @@ export function PreGameSelectionFlow({
|
||||
[publishedGalleryEntries],
|
||||
);
|
||||
|
||||
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 refreshPlatformData = useCallback(async () => {
|
||||
setIsLoadingPlatform(true);
|
||||
setPlatformError(null);
|
||||
@@ -239,6 +335,18 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
}, [selectedDetailEntry]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAppliedInitialAgentWorkspaceRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasAppliedInitialAgentWorkspaceRef.current = true;
|
||||
if (initialAgentUiStateRef.current.activeSessionId) {
|
||||
setPlatformTab('create');
|
||||
setSelectionStage('agent-workspace');
|
||||
}
|
||||
}, [setSelectionStage]);
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
|
||||
@@ -293,6 +401,117 @@ export function PreGameSelectionFlow({
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAgentSessionId) {
|
||||
setAgentSession(null);
|
||||
setIsLoadingAgentSession(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);
|
||||
persistAgentUiState(null, null);
|
||||
setPlatformTab('create');
|
||||
setSelectionStage('platform');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) {
|
||||
setIsLoadingAgentSession(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
activeAgentSessionId,
|
||||
persistAgentUiState,
|
||||
setSelectionStage,
|
||||
syncAgentSessionSnapshot,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeAgentSessionId || !activeAgentOperationId) {
|
||||
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,
|
||||
persistAgentUiState,
|
||||
syncAgentSessionSnapshot,
|
||||
]);
|
||||
|
||||
const customWorldSettingPreview = useMemo(() => {
|
||||
if (customWorldCreatorIntent.sourceMode === 'freeform') {
|
||||
return customWorldCreatorIntent.rawSettingText.trim();
|
||||
@@ -308,10 +527,40 @@ export function PreGameSelectionFlow({
|
||||
return customWorldCreatorIntent.rawSettingText.trim();
|
||||
}, [customWorldCreatorIntent]);
|
||||
|
||||
const agentDraftSettingPreview = useMemo(
|
||||
() => buildAgentDraftFoundationSettingText(agentSession),
|
||||
[agentSession],
|
||||
);
|
||||
|
||||
const agentDraftGenerationProgress = useMemo(
|
||||
() =>
|
||||
buildAgentDraftFoundationGenerationProgress(
|
||||
agentOperation,
|
||||
agentDraftGenerationStartedAt,
|
||||
),
|
||||
[agentDraftGenerationStartedAt, agentOperation],
|
||||
);
|
||||
|
||||
const isAgentDraftGenerationView =
|
||||
customWorldGenerationViewSource === 'agent-draft-foundation';
|
||||
const activeGenerationProgress = isAgentDraftGenerationView
|
||||
? agentDraftGenerationProgress
|
||||
: customWorldProgress;
|
||||
const isActiveGenerationRunning = isAgentDraftGenerationView
|
||||
? isDraftFoundationOperationRunning(agentOperation)
|
||||
: isGeneratingCustomWorld;
|
||||
const activeGenerationError =
|
||||
isAgentDraftGenerationView &&
|
||||
isDraftFoundationOperation(agentOperation) &&
|
||||
agentOperation.status === 'failed'
|
||||
? agentOperation.error || agentOperation.phaseDetail
|
||||
: customWorldError;
|
||||
|
||||
const leaveCustomWorldResult = () => {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
|
||||
};
|
||||
|
||||
@@ -322,6 +571,101 @@ export function PreGameSelectionFlow({
|
||||
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setSelectionStage('platform');
|
||||
};
|
||||
|
||||
const openCreationTypePicker = () => {
|
||||
if (isCreatingAgentSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCreationTypeError(null);
|
||||
setShowCreationTypeModal(true);
|
||||
};
|
||||
|
||||
const openRpgAgentWorkspace = async () => {
|
||||
if (isCreatingAgentSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreatingAgentSession(true);
|
||||
setCreationTypeError(null);
|
||||
|
||||
try {
|
||||
const { session } = await createCustomWorldAgentSession({});
|
||||
setAgentSession(session);
|
||||
setAgentOperation(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;
|
||||
}
|
||||
|
||||
try {
|
||||
const { operation } = await sendCustomWorldAgentMessage(
|
||||
activeAgentSessionId,
|
||||
payload,
|
||||
);
|
||||
setAgentOperation(operation);
|
||||
persistAgentUiState(activeAgentSessionId, operation.operationId);
|
||||
} catch (error) {
|
||||
const errorMessage = resolveErrorMessage(error, '发送共创消息失败。');
|
||||
setAgentOperation(
|
||||
createFailedAgentOperation({
|
||||
type: 'process_message',
|
||||
phaseLabel: '发送消息失败',
|
||||
error: errorMessage,
|
||||
}),
|
||||
);
|
||||
persistAgentUiState(activeAgentSessionId, null);
|
||||
}
|
||||
};
|
||||
|
||||
const executeAgentAction = async (payload: CustomWorldAgentActionRequest) => {
|
||||
if (!activeAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
persistAgentUiState(activeAgentSessionId, null);
|
||||
setSelectionStage('platform');
|
||||
};
|
||||
|
||||
@@ -340,7 +684,9 @@ export function PreGameSelectionFlow({
|
||||
setDetailError(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldCreatorIntent(createEmptyCustomWorldCreatorIntent('freeform'));
|
||||
setCustomWorldCreatorIntent(
|
||||
createEmptyCustomWorldCreatorIntent('freeform'),
|
||||
);
|
||||
setCustomWorldGenerationMode('fast');
|
||||
setShowCustomWorldModal(true);
|
||||
};
|
||||
@@ -400,7 +746,9 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
|
||||
try {
|
||||
const mutation = await upsertCustomWorldProfile(generatedCustomWorldProfile);
|
||||
const mutation = await upsertCustomWorldProfile(
|
||||
generatedCustomWorldProfile,
|
||||
);
|
||||
setSavedCustomWorldEntries(mutation.entries);
|
||||
setSelectedDetailEntry(mutation.entry);
|
||||
await refreshPlatformData();
|
||||
@@ -684,18 +1032,20 @@ export function PreGameSelectionFlow({
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<PlatformHomeView
|
||||
activeTab={platformTab}
|
||||
onTabChange={setPlatformTab}
|
||||
hasSavedGame={hasSavedGame}
|
||||
savedSnapshot={savedSnapshot}
|
||||
featuredEntries={featuredGalleryEntries}
|
||||
latestEntries={publishedGalleryEntries}
|
||||
myEntries={savedCustomWorldEntries}
|
||||
isLoadingPlatform={isLoadingPlatform}
|
||||
platformError={isLoadingPlatform ? null : platformError}
|
||||
platformError={
|
||||
isLoadingPlatform ? null : (platformError ?? creationTypeError)
|
||||
}
|
||||
onContinueGame={handleContinueGame}
|
||||
onRefresh={() => {
|
||||
void refreshPlatformData();
|
||||
}}
|
||||
onOpenCreateWorld={openCustomWorldCreator}
|
||||
onOpenCreateTypePicker={openCreationTypePicker}
|
||||
onOpenGalleryDetail={(entry) => {
|
||||
void openGalleryDetail(entry);
|
||||
}}
|
||||
@@ -750,6 +1100,50 @@ export function PreGameSelectionFlow({
|
||||
</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}
|
||||
onBack={leaveAgentWorkspace}
|
||||
onRefresh={() => {
|
||||
if (!activeAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
void syncAgentSessionSnapshot(activeAgentSessionId);
|
||||
}}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitAgentMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void executeAgentAction(payload);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="rounded-2xl border border-white/10 bg-black/30 px-5 py-4 text-sm text-zinc-300">
|
||||
{isLoadingAgentSession
|
||||
? '正在准备 Agent 共创工作区...'
|
||||
: creationTypeError || '正在恢复创作工作区...'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'custom-world-generating' && (
|
||||
<motion.div
|
||||
key="custom-world-generating"
|
||||
@@ -759,9 +1153,7 @@ export function PreGameSelectionFlow({
|
||||
className="flex h-full min-h-0 flex-col"
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<LazyPanelFallback label="正在加载世界生成面板..." />
|
||||
}
|
||||
fallback={<LazyPanelFallback label="正在加载世界生成面板..." />}
|
||||
>
|
||||
<CustomWorldGenerationView
|
||||
settingText={customWorldSettingPreview}
|
||||
@@ -816,6 +1208,21 @@ export function PreGameSelectionFlow({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<PlatformCreationTypeModal
|
||||
isOpen={showCreationTypeModal}
|
||||
isBusy={isCreatingAgentSession}
|
||||
error={creationTypeError}
|
||||
onClose={() => {
|
||||
if (isCreatingAgentSession) {
|
||||
return;
|
||||
}
|
||||
setShowCreationTypeModal(false);
|
||||
}}
|
||||
onSelectRpg={() => {
|
||||
void openRpgAgentWorkspace();
|
||||
}}
|
||||
/>
|
||||
|
||||
{showCustomWorldModal ? (
|
||||
<Suspense fallback={null}>
|
||||
<CustomWorldCreatorModal
|
||||
|
||||
Reference in New Issue
Block a user