1
This commit is contained in:
@@ -70,7 +70,9 @@ function WorldCard({
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const leadPortrait = resolvePlatformWorldLeadPortrait(entry);
|
||||
const tags = buildPlatformWorldTags(entry);
|
||||
const tags = [
|
||||
...new Set(buildPlatformWorldTags(entry).map((tag) => tag.trim()).filter(Boolean)),
|
||||
].slice(0, 3);
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -120,9 +122,9 @@ function WorldCard({
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{tags.length > 0 ? (
|
||||
tags.map((tag) => (
|
||||
tags.map((tag, index) => (
|
||||
<span
|
||||
key={tag}
|
||||
key={`world-tag-${index}-${tag || 'empty'}`}
|
||||
className="rounded-full border border-white/10 bg-black/24 px-2.5 py-1 text-[10px] text-zinc-100"
|
||||
>
|
||||
{tag}
|
||||
|
||||
@@ -66,7 +66,9 @@ export function PlatformWorldDetailView({
|
||||
3,
|
||||
);
|
||||
const previewLandmarks = entry.profile.landmarks.slice(0, 3);
|
||||
const tags = buildPlatformWorldTags(entry);
|
||||
const tags = [
|
||||
...new Set(buildPlatformWorldTags(entry).map((tag) => tag.trim()).filter(Boolean)),
|
||||
].slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
@@ -133,9 +135,9 @@ export function PlatformWorldDetailView({
|
||||
{entry.summaryText || '等待补充世界摘要。'}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{tags.map((tag) => (
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={tag}
|
||||
key={`world-detail-tag-${index}-${tag || 'empty'}`}
|
||||
className="rounded-full border border-white/10 bg-black/24 px-3 py-1 text-[10px] text-zinc-100"
|
||||
>
|
||||
{tag}
|
||||
@@ -189,9 +191,9 @@ export function PlatformWorldDetailView({
|
||||
关键角色
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-3">
|
||||
{previewCharacters.map((character) => (
|
||||
{previewCharacters.map((character, index) => (
|
||||
<div
|
||||
key={character.id}
|
||||
key={character.id || `preview-character-${index}`}
|
||||
className="rounded-2xl border border-white/10 bg-black/20 px-3 py-3"
|
||||
>
|
||||
<div className="line-clamp-1 text-sm font-bold text-white">
|
||||
@@ -210,9 +212,9 @@ export function PlatformWorldDetailView({
|
||||
关键场景
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-3">
|
||||
{previewLandmarks.map((landmark) => (
|
||||
{previewLandmarks.map((landmark, index) => (
|
||||
<div
|
||||
key={landmark.id}
|
||||
key={landmark.id || `preview-landmark-${index}`}
|
||||
className="rounded-2xl border border-white/10 bg-black/20 px-3 py-3"
|
||||
>
|
||||
<div className="line-clamp-1 text-sm font-bold text-white">
|
||||
|
||||
@@ -8,11 +8,14 @@ import { beforeEach, expect, test, vi } from 'vitest';
|
||||
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
|
||||
import {
|
||||
createCustomWorldAgentSession,
|
||||
executeCustomWorldAgentAction,
|
||||
getCustomWorldAgentOperation,
|
||||
getCustomWorldAgentSession,
|
||||
} from '../../services/aiService';
|
||||
import {
|
||||
listCustomWorldGallery,
|
||||
listCustomWorldLibrary,
|
||||
upsertCustomWorldProfile,
|
||||
} from '../../services/storageService';
|
||||
import type { GameState } from '../../types';
|
||||
import {
|
||||
@@ -41,11 +44,23 @@ vi.mock('../../services/storageService', () => ({
|
||||
vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
|
||||
CustomWorldAgentWorkspace: ({
|
||||
session,
|
||||
onExecuteAction,
|
||||
}: {
|
||||
session: CustomWorldAgentSessionSnapshot | null;
|
||||
onExecuteAction: (payload: { action: string }) => void;
|
||||
}) => (
|
||||
<div className="agent-workspace-mock">
|
||||
Agent工作区:{session?.sessionId ?? 'missing-session'}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onExecuteAction({
|
||||
action: 'draft_foundation',
|
||||
});
|
||||
}}
|
||||
>
|
||||
开始生成草稿
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
@@ -117,9 +132,51 @@ beforeEach(() => {
|
||||
window.sessionStorage.clear();
|
||||
vi.mocked(listCustomWorldLibrary).mockResolvedValue([]);
|
||||
vi.mocked(listCustomWorldGallery).mockResolvedValue([]);
|
||||
vi.mocked(upsertCustomWorldProfile).mockResolvedValue({
|
||||
entry: {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'agent-draft-custom-world-agent-session-1',
|
||||
profile: {
|
||||
id: 'agent-draft-custom-world-agent-session-1',
|
||||
name: '潮雾列岛',
|
||||
} as never,
|
||||
visibility: 'draft',
|
||||
publishedAt: null,
|
||||
updatedAt: '2026-04-14T12:00:00.000Z',
|
||||
authorDisplayName: '玩家',
|
||||
worldName: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summaryText: '第一版世界底稿已经整理完成。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'tide',
|
||||
playableNpcCount: 1,
|
||||
landmarkCount: 1,
|
||||
},
|
||||
entries: [],
|
||||
});
|
||||
vi.mocked(createCustomWorldAgentSession).mockResolvedValue({
|
||||
session: mockSession,
|
||||
});
|
||||
vi.mocked(executeCustomWorldAgentAction).mockResolvedValue({
|
||||
operation: {
|
||||
operationId: 'operation-draft-foundation-1',
|
||||
type: 'draft_foundation',
|
||||
status: 'queued',
|
||||
phaseLabel: '已接收请求',
|
||||
phaseDetail: '正在准备生成世界底稿。',
|
||||
progress: 10,
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
|
||||
operationId: 'operation-draft-foundation-1',
|
||||
type: 'draft_foundation',
|
||||
status: 'running',
|
||||
phaseLabel: '生成世界底稿',
|
||||
phaseDetail: '正在根据已确认锚点编译第一版世界结构。',
|
||||
progress: 38,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(mockSession);
|
||||
});
|
||||
|
||||
@@ -151,3 +208,168 @@ test('create tab opens game type modal, keeps AIRP and visual novel locked, and
|
||||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('starting draft generation leaves the agent workspace and shows the generation progress view', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '创作' }));
|
||||
await user.click(screen.getByRole('button', { name: /开启新的创作/u }));
|
||||
await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('Agent工作区:custom-world-agent-session-1'),
|
||||
).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '开始生成草稿' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(executeCustomWorldAgentAction).toHaveBeenCalledWith(
|
||||
'custom-world-agent-session-1',
|
||||
{
|
||||
action: 'draft_foundation',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(await screen.findByText('世界草稿生成进度')).toBeTruthy();
|
||||
expect(screen.queryByText(/Agent工作区/u)).toBeNull();
|
||||
expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('existing draft sessions enter the legacy result layout directly', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(getCustomWorldAgentOperation).mockResolvedValue({
|
||||
operationId: 'operation-draft-foundation-1',
|
||||
type: 'draft_foundation',
|
||||
status: 'completed',
|
||||
phaseLabel: '世界底稿已生成',
|
||||
phaseDetail: '第一版世界底稿和 4 张草稿卡已经整理完成。',
|
||||
progress: 100,
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue({
|
||||
...mockSession,
|
||||
stage: 'object_refining',
|
||||
creatorIntent: {
|
||||
sourceMode: 'card',
|
||||
worldHook: '被海雾吞没的旧航路群岛',
|
||||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||||
themeKeywords: ['海雾', '旧航路'],
|
||||
toneDirectives: ['压抑', '悬疑'],
|
||||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||||
coreConflicts: ['航运公会与守灯会争夺航路控制权'],
|
||||
keyFactions: [],
|
||||
keyCharacters: [],
|
||||
keyLandmarks: [],
|
||||
iconicElements: ['会移动的海雾'],
|
||||
forbiddenDirectives: [],
|
||||
rawSettingText: '',
|
||||
},
|
||||
draftProfile: {
|
||||
name: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '第一版世界底稿已经整理完成。',
|
||||
tone: '压抑、潮湿、悬疑',
|
||||
playerGoal: '查清沉船与禁航区异动的真相。',
|
||||
majorFactions: ['守灯会', '航运公会'],
|
||||
coreConflicts: ['守灯会与航运公会争夺旧航路控制权'],
|
||||
playableNpcs: [
|
||||
{
|
||||
id: 'playable-1',
|
||||
name: '沈砺',
|
||||
title: '旧航路引路人',
|
||||
role: '关键同行者',
|
||||
publicIdentity: '最熟悉旧航路的人。',
|
||||
publicMask: '看上去像可靠旧友。',
|
||||
currentPressure: '他必须在两股势力间站队。',
|
||||
hiddenHook: '暗中替沉船商盟引路。',
|
||||
relationToPlayer: '旧友兼潜在背叛者',
|
||||
threadIds: ['thread-1'],
|
||||
summary: '他像旧友,但也像一把始终没收回鞘的刀。',
|
||||
},
|
||||
],
|
||||
storyNpcs: [
|
||||
{
|
||||
id: 'story-1',
|
||||
name: '顾潮音',
|
||||
title: '守灯会值夜人',
|
||||
role: '场景关键角色',
|
||||
publicIdentity: '负责夜间巡灯与封锁。',
|
||||
publicMask: '对外一直冷静克制。',
|
||||
currentPressure: '她知道更多禁航区真相。',
|
||||
hiddenHook: '曾亲眼见过失控海雾吞船。',
|
||||
relationToPlayer: '最早愿意交换线索的人',
|
||||
threadIds: ['thread-1'],
|
||||
summary: '她总像比所有人更早知道海雾会往哪一侧压下来。',
|
||||
},
|
||||
],
|
||||
landmarks: [
|
||||
{
|
||||
id: 'landmark-1',
|
||||
name: '回潮旧灯塔',
|
||||
purpose: '观察雾潮与往来船只',
|
||||
mood: '潮湿、压抑、风声不止',
|
||||
importance: '开局核心场景',
|
||||
characterIds: ['story-1'],
|
||||
threadIds: ['thread-1'],
|
||||
summary: '旧灯塔是整片群岛最先看见异动的地方。',
|
||||
},
|
||||
],
|
||||
factions: [],
|
||||
threads: [],
|
||||
chapters: [],
|
||||
worldHook: '被海雾吞没的旧航路群岛',
|
||||
playerPremise: '玩家回到群岛调查沉船真相。',
|
||||
openingSituation: '首夜就有陌生船只闯入禁航区。',
|
||||
iconicElements: ['会移动的海雾'],
|
||||
sourceAnchorSummary: '海雾、旧灯塔、失控航路。',
|
||||
},
|
||||
draftCards: [
|
||||
{
|
||||
id: 'world-foundation',
|
||||
kind: 'world',
|
||||
title: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '第一版世界底稿已经整理完成。',
|
||||
status: 'warning',
|
||||
linkedIds: ['playable-1', 'story-1', 'landmark-1'],
|
||||
warningCount: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<TestWrapper />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '创作' }));
|
||||
await user.click(screen.getByRole('button', { name: /开启新的创作/u }));
|
||||
await user.click(screen.getByRole('button', { name: /角色扮演 RPG/u }));
|
||||
|
||||
await waitFor(
|
||||
async () => {
|
||||
expect(await screen.findByText('世界档案')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: /保存到我的作品|自动保存中|已保存到我的作品/u,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
},
|
||||
{ timeout: 2500 },
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/Agent工作区/u)).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /^锚点/u })).toBeNull();
|
||||
expect(screen.getByText(/原始设定/u)).toBeTruthy();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /场景角色/u }));
|
||||
await user.click(screen.getByRole('button', { name: /顾潮音/u }));
|
||||
|
||||
expect(await screen.findByText(/编辑场景角色:顾潮音/u)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /AI生成形象与动作/u }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('技能')).toBeTruthy();
|
||||
|
||||
});
|
||||
|
||||
@@ -31,16 +31,17 @@ import {
|
||||
getCustomWorldAgentSession,
|
||||
sendCustomWorldAgentMessage,
|
||||
} from '../../services/aiService';
|
||||
import {
|
||||
readCustomWorldAgentUiState,
|
||||
writeCustomWorldAgentUiState,
|
||||
} from '../../services/customWorldAgentUiState';
|
||||
import { buildCustomWorldProfileFromAgentDraft } from '../../services/customWorldAgentDraftResult';
|
||||
import {
|
||||
buildAgentDraftFoundationGenerationProgress,
|
||||
buildAgentDraftFoundationSettingText,
|
||||
isDraftFoundationOperation,
|
||||
isDraftFoundationOperationRunning,
|
||||
} from '../../services/customWorldAgentGenerationProgress';
|
||||
import {
|
||||
readCustomWorldAgentUiState,
|
||||
writeCustomWorldAgentUiState,
|
||||
} from '../../services/customWorldAgentUiState';
|
||||
import {
|
||||
buildCustomWorldCreatorIntentDisplayText,
|
||||
buildCustomWorldCreatorIntentGenerationText,
|
||||
@@ -61,7 +62,7 @@ import {
|
||||
type GameState,
|
||||
} from '../../types';
|
||||
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
|
||||
import { type PlatformHomeTab,PlatformHomeView } from './PlatformHomeView';
|
||||
import { type PlatformHomeTab, PlatformHomeView } from './PlatformHomeView';
|
||||
import { PlatformWorldDetailView } from './PlatformWorldDetailView';
|
||||
|
||||
const CustomWorldGenerationView = lazy(async () => {
|
||||
@@ -106,6 +107,9 @@ type CustomWorldGenerationViewSource =
|
||||
| 'agent-draft-foundation'
|
||||
| null;
|
||||
|
||||
type CustomWorldResultViewSource = 'classic' | 'agent-draft' | null;
|
||||
type CustomWorldAutoSaveState = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
type PreGameSelectionFlowProps = {
|
||||
selectionStage: SelectionStage;
|
||||
setSelectionStage: (stage: SelectionStage) => void;
|
||||
@@ -270,11 +274,21 @@ export function PreGameSelectionFlow({
|
||||
const [isMutatingDetail, setIsMutatingDetail] = useState(false);
|
||||
const [customWorldProgress, setCustomWorldProgress] =
|
||||
useState<CustomWorldGenerationProgress | null>(null);
|
||||
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 customWorldAbortControllerRef = useRef<AbortController | null>(null);
|
||||
const customWorldAutoSaveTimeoutRef = useRef<number | null>(null);
|
||||
const lastAutoSavedProfileSignatureRef = useRef<string | null>(null);
|
||||
const latestAutoSaveRequestIdRef = useRef(0);
|
||||
|
||||
const previewCustomWorldCharacters = useMemo(
|
||||
() =>
|
||||
@@ -307,34 +321,6 @@ export function PreGameSelectionFlow({
|
||||
return nextSession;
|
||||
}, []);
|
||||
|
||||
const refreshPlatformData = useCallback(async () => {
|
||||
setIsLoadingPlatform(true);
|
||||
setPlatformError(null);
|
||||
|
||||
try {
|
||||
const [libraryEntries, galleryEntries] = await Promise.all([
|
||||
listCustomWorldLibrary(),
|
||||
listCustomWorldGallery(),
|
||||
]);
|
||||
setSavedCustomWorldEntries(libraryEntries);
|
||||
setPublishedGalleryEntries(galleryEntries);
|
||||
if (selectedDetailEntry) {
|
||||
const nextOwnedEntry = libraryEntries.find(
|
||||
(entry) =>
|
||||
entry.ownerUserId === selectedDetailEntry.ownerUserId &&
|
||||
entry.profileId === selectedDetailEntry.profileId,
|
||||
);
|
||||
if (nextOwnedEntry) {
|
||||
setSelectedDetailEntry(nextOwnedEntry);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setPlatformError(resolveErrorMessage(error, '读取平台数据失败。'));
|
||||
} finally {
|
||||
setIsLoadingPlatform(false);
|
||||
}
|
||||
}, [selectedDetailEntry]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAppliedInitialAgentWorkspaceRef.current) {
|
||||
return;
|
||||
@@ -397,6 +383,9 @@ export function PreGameSelectionFlow({
|
||||
useEffect(
|
||||
() => () => {
|
||||
customWorldAbortControllerRef.current?.abort();
|
||||
if (customWorldAutoSaveTimeoutRef.current !== null) {
|
||||
window.clearTimeout(customWorldAutoSaveTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -512,6 +501,72 @@ export function PreGameSelectionFlow({
|
||||
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(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 customWorldSettingPreview = useMemo(() => {
|
||||
if (customWorldCreatorIntent.sourceMode === 'freeform') {
|
||||
return customWorldCreatorIntent.rawSettingText.trim();
|
||||
@@ -531,6 +586,51 @@ export function PreGameSelectionFlow({
|
||||
() => buildAgentDraftFoundationSettingText(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(
|
||||
() =>
|
||||
@@ -543,25 +643,38 @@ export function PreGameSelectionFlow({
|
||||
|
||||
const isAgentDraftGenerationView =
|
||||
customWorldGenerationViewSource === 'agent-draft-foundation';
|
||||
const isAgentDraftResultView = customWorldResultViewSource === 'agent-draft';
|
||||
const activeGenerationSettingText = isAgentDraftGenerationView
|
||||
? agentDraftSettingPreview
|
||||
: customWorldSettingPreview;
|
||||
const activeGenerationProgress = isAgentDraftGenerationView
|
||||
? agentDraftGenerationProgress
|
||||
: customWorldProgress;
|
||||
const isActiveGenerationRunning = isAgentDraftGenerationView
|
||||
? isDraftFoundationOperationRunning(agentOperation)
|
||||
: isGeneratingCustomWorld;
|
||||
const activeGenerationError =
|
||||
isAgentDraftGenerationView &&
|
||||
isDraftFoundationOperation(agentOperation) &&
|
||||
agentOperation.status === 'failed'
|
||||
const activeGenerationError = isAgentDraftGenerationView
|
||||
? isDraftFoundationOperation(agentOperation) &&
|
||||
agentOperation.status === 'failed'
|
||||
? agentOperation.error || agentOperation.phaseDetail
|
||||
: customWorldError;
|
||||
: null
|
||||
: customWorldError;
|
||||
|
||||
const leaveCustomWorldResult = () => {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setSelectionStage(selectedDetailEntry ? 'detail' : 'platform');
|
||||
setCustomWorldResultViewSource(null);
|
||||
setSelectionStage(
|
||||
isAgentDraftResultView
|
||||
? 'agent-workspace'
|
||||
: selectedDetailEntry
|
||||
? 'detail'
|
||||
: 'platform',
|
||||
);
|
||||
};
|
||||
|
||||
const leaveCustomWorldGeneration = () => {
|
||||
@@ -570,8 +683,11 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setSelectionStage('platform');
|
||||
};
|
||||
|
||||
@@ -596,6 +712,12 @@ export function PreGameSelectionFlow({
|
||||
const { session } = await createCustomWorldAgentSession({});
|
||||
setAgentSession(session);
|
||||
setAgentOperation(null);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setAgentDraftGenerationStartedAt(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
persistAgentUiState(session.sessionId, null);
|
||||
setShowCreationTypeModal(false);
|
||||
setPlatformTab('create');
|
||||
@@ -639,6 +761,20 @@ export function PreGameSelectionFlow({
|
||||
return;
|
||||
}
|
||||
|
||||
const isDraftFoundationAction = payload.action === 'draft_foundation';
|
||||
|
||||
if (isDraftFoundationAction) {
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource('agent-draft-foundation');
|
||||
setCustomWorldResultViewSource(null);
|
||||
setAgentDraftGenerationStartedAt(Date.now());
|
||||
setSelectionStage('custom-world-generating');
|
||||
}
|
||||
|
||||
try {
|
||||
const { operation } = await executeCustomWorldAgentAction(
|
||||
activeAgentSessionId,
|
||||
@@ -665,10 +801,44 @@ export function PreGameSelectionFlow({
|
||||
const leaveAgentWorkspace = () => {
|
||||
setPlatformTab('create');
|
||||
setAgentOperation(null);
|
||||
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');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setPlatformTab('create');
|
||||
setSelectionStage('platform');
|
||||
};
|
||||
|
||||
const retryAgentDraftGeneration = () => {
|
||||
void executeAgentAction({
|
||||
action: 'draft_foundation',
|
||||
});
|
||||
};
|
||||
|
||||
const openCustomWorldCreator = () => {
|
||||
if (isGeneratingCustomWorld) {
|
||||
return;
|
||||
@@ -683,7 +853,11 @@ export function PreGameSelectionFlow({
|
||||
setPlatformError(null);
|
||||
setDetailError(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setCustomWorldCreatorIntent(
|
||||
createEmptyCustomWorldCreatorIntent('freeform'),
|
||||
);
|
||||
@@ -710,7 +884,11 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource(null);
|
||||
setShowCustomWorldModal(true);
|
||||
};
|
||||
|
||||
@@ -740,26 +918,98 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
};
|
||||
|
||||
const saveGeneratedCustomWorld = async () => {
|
||||
const saveGeneratedCustomWorld = useCallback(
|
||||
async (profile = generatedCustomWorldProfile) => {
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileSignature = JSON.stringify(profile);
|
||||
const requestId = latestAutoSaveRequestIdRef.current + 1;
|
||||
latestAutoSaveRequestIdRef.current = requestId;
|
||||
setCustomWorldAutoSaveState('saving');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
|
||||
try {
|
||||
const mutation = await upsertCustomWorldProfile(profile);
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
const mutation = await upsertCustomWorldProfile(
|
||||
generatedCustomWorldProfile,
|
||||
);
|
||||
setSavedCustomWorldEntries(mutation.entries);
|
||||
setSelectedDetailEntry(mutation.entry);
|
||||
await refreshPlatformData();
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setSelectionStage('platform');
|
||||
} catch (error) {
|
||||
setCustomWorldError(resolveErrorMessage(error, '保存自定义世界失败。'));
|
||||
if (
|
||||
selectionStage !== 'custom-world-result' ||
|
||||
isGeneratingCustomWorld
|
||||
) {
|
||||
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,
|
||||
isGeneratingCustomWorld,
|
||||
saveGeneratedCustomWorld,
|
||||
selectionStage,
|
||||
]);
|
||||
|
||||
const openSavedCustomWorldEditor = (
|
||||
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
||||
@@ -770,6 +1020,9 @@ export function PreGameSelectionFlow({
|
||||
|
||||
setSelectedDetailEntry(entry);
|
||||
setGeneratedCustomWorldProfile(entry.profile);
|
||||
lastAutoSavedProfileSignatureRef.current = JSON.stringify(entry.profile);
|
||||
setCustomWorldAutoSaveState('saved');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldCreatorIntent(
|
||||
entry.profile.creatorIntent ??
|
||||
({
|
||||
@@ -780,6 +1033,8 @@ export function PreGameSelectionFlow({
|
||||
setCustomWorldGenerationMode(entry.profile.generationMode ?? 'full');
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource('classic');
|
||||
setSelectionStage('custom-world-result');
|
||||
};
|
||||
|
||||
@@ -844,6 +1099,9 @@ export function PreGameSelectionFlow({
|
||||
...mergedProfile,
|
||||
id: generatedCustomWorldProfile.id,
|
||||
});
|
||||
lastAutoSavedProfileSignatureRef.current = null;
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldError(null);
|
||||
} catch (error) {
|
||||
@@ -899,7 +1157,11 @@ export function PreGameSelectionFlow({
|
||||
customWorldAbortControllerRef.current?.abort();
|
||||
customWorldAbortControllerRef.current = abortController;
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldGenerationViewSource('classic');
|
||||
setCustomWorldResultViewSource(null);
|
||||
setShowCustomWorldModal(false);
|
||||
setSelectionStage('custom-world-generating');
|
||||
setIsGeneratingCustomWorld(true);
|
||||
@@ -929,8 +1191,13 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
: profile,
|
||||
);
|
||||
lastAutoSavedProfileSignatureRef.current = null;
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldProgress(null);
|
||||
setCustomWorldError(null);
|
||||
setCustomWorldGenerationViewSource(null);
|
||||
setCustomWorldResultViewSource('classic');
|
||||
setSelectionStage('custom-world-result');
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
@@ -1019,6 +1286,17 @@ export function PreGameSelectionFlow({
|
||||
entry.profileId === selectedDetailEntry.profileId,
|
||||
),
|
||||
);
|
||||
const resultViewSaveActionLabel =
|
||||
customWorldAutoSaveState === 'saving'
|
||||
? '自动保存中'
|
||||
: customWorldAutoSaveState === 'saved'
|
||||
? '已保存到我的作品'
|
||||
: customWorldAutoSaveState === 'error'
|
||||
? '重新保存到我的作品'
|
||||
: '保存到我的作品';
|
||||
const resultViewError =
|
||||
customWorldAutoSaveError ??
|
||||
(isAgentDraftResultView ? null : customWorldError);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1156,16 +1434,61 @@ export function PreGameSelectionFlow({
|
||||
fallback={<LazyPanelFallback label="正在加载世界生成面板..." />}
|
||||
>
|
||||
<CustomWorldGenerationView
|
||||
settingText={customWorldSettingPreview}
|
||||
progress={customWorldProgress}
|
||||
isGenerating={isGeneratingCustomWorld}
|
||||
error={customWorldError}
|
||||
onBack={leaveCustomWorldGeneration}
|
||||
onEditSetting={editCustomWorldSetting}
|
||||
onRetry={() => {
|
||||
void createCustomWorld();
|
||||
}}
|
||||
onInterrupt={interruptCustomWorldGeneration}
|
||||
settingText={activeGenerationSettingText}
|
||||
progress={activeGenerationProgress}
|
||||
isGenerating={isActiveGenerationRunning}
|
||||
error={activeGenerationError}
|
||||
onBack={
|
||||
isAgentDraftGenerationView
|
||||
? leaveAgentDraftGeneration
|
||||
: leaveCustomWorldGeneration
|
||||
}
|
||||
onEditSetting={
|
||||
isAgentDraftGenerationView
|
||||
? leaveAgentDraftGeneration
|
||||
: editCustomWorldSetting
|
||||
}
|
||||
onRetry={
|
||||
isAgentDraftGenerationView
|
||||
? retryAgentDraftGeneration
|
||||
: () => {
|
||||
void createCustomWorld();
|
||||
}
|
||||
}
|
||||
onInterrupt={
|
||||
isAgentDraftGenerationView
|
||||
? undefined
|
||||
: interruptCustomWorldGeneration
|
||||
}
|
||||
backLabel={
|
||||
isAgentDraftGenerationView ? '返回工作区' : undefined
|
||||
}
|
||||
settingActionLabel={
|
||||
isAgentDraftGenerationView ? '回到工作区' : undefined
|
||||
}
|
||||
retryLabel={
|
||||
isAgentDraftGenerationView ? '重新生成草稿' : undefined
|
||||
}
|
||||
settingTitle={
|
||||
isAgentDraftGenerationView ? '当前共创设定' : undefined
|
||||
}
|
||||
settingDescription={
|
||||
isAgentDraftGenerationView
|
||||
? '这批锚点会被整理成第一版世界底稿与草稿卡。'
|
||||
: undefined
|
||||
}
|
||||
progressTitle={
|
||||
isAgentDraftGenerationView ? '世界草稿生成进度' : undefined
|
||||
}
|
||||
activeBadgeLabel={
|
||||
isAgentDraftGenerationView ? '草稿编译中' : undefined
|
||||
}
|
||||
pausedBadgeLabel={
|
||||
isAgentDraftGenerationView ? '草稿生成已暂停' : undefined
|
||||
}
|
||||
idleBadgeLabel={
|
||||
isAgentDraftGenerationView ? '等待返回工作区' : undefined
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
@@ -1186,22 +1509,52 @@ export function PreGameSelectionFlow({
|
||||
<CustomWorldResultView
|
||||
profile={generatedCustomWorldProfile}
|
||||
previewCharacters={previewCustomWorldCharacters}
|
||||
isGenerating={isGeneratingCustomWorld}
|
||||
progress={customWorldProgress?.overallProgress ?? 0}
|
||||
progressLabel={customWorldProgress?.phaseLabel ?? ''}
|
||||
error={customWorldError}
|
||||
isGenerating={
|
||||
isAgentDraftResultView ? false : isGeneratingCustomWorld
|
||||
}
|
||||
progress={
|
||||
isAgentDraftResultView
|
||||
? 0
|
||||
: (customWorldProgress?.overallProgress ?? 0)
|
||||
}
|
||||
progressLabel={
|
||||
isAgentDraftResultView
|
||||
? ''
|
||||
: (customWorldProgress?.phaseLabel ?? '')
|
||||
}
|
||||
error={resultViewError}
|
||||
onProfileChange={setGeneratedCustomWorldProfile}
|
||||
onBack={leaveCustomWorldResult}
|
||||
onEditSetting={editCustomWorldSetting}
|
||||
onRegenerate={() => {
|
||||
void createCustomWorld();
|
||||
}}
|
||||
onContinueExpand={() => {
|
||||
void continueExpandCustomWorld();
|
||||
}}
|
||||
onBack={
|
||||
isAgentDraftResultView
|
||||
? leaveAgentDraftResult
|
||||
: leaveCustomWorldResult
|
||||
}
|
||||
onEditSetting={
|
||||
isAgentDraftResultView ? undefined : editCustomWorldSetting
|
||||
}
|
||||
onRegenerate={
|
||||
isAgentDraftResultView
|
||||
? retryAgentDraftGeneration
|
||||
: () => {
|
||||
void createCustomWorld();
|
||||
}
|
||||
}
|
||||
onContinueExpand={
|
||||
isAgentDraftResultView
|
||||
? undefined
|
||||
: () => {
|
||||
void continueExpandCustomWorld();
|
||||
}
|
||||
}
|
||||
onSave={() => {
|
||||
void saveGeneratedCustomWorld();
|
||||
}}
|
||||
readOnly={false}
|
||||
backLabel={isAgentDraftResultView ? '返回创作' : undefined}
|
||||
regenerateActionLabel={
|
||||
isAgentDraftResultView ? '重新生成草稿' : undefined
|
||||
}
|
||||
saveActionLabel={resultViewSaveActionLabel}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user