1
This commit is contained in:
@@ -36,7 +36,7 @@ import {
|
||||
resolvePlatformWorldLeadPortrait,
|
||||
} from './platformWorldPresentation';
|
||||
|
||||
export type PlatformHomeTab = 'home' | 'create' | 'discover' | 'profile';
|
||||
export type PlatformHomeTab = 'home' | 'create' | 'profile';
|
||||
|
||||
function SectionHeader({ title, detail }: { title: string; detail: string }) {
|
||||
return (
|
||||
@@ -443,8 +443,6 @@ export function PlatformHomeView({
|
||||
const tabIcons = {
|
||||
home: "/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/192_RustyTrinket_House.png",
|
||||
create: '/Icons/01_Scroll.png',
|
||||
discover:
|
||||
"/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/321_Compass.png",
|
||||
profile: '/UI/Icon_Eq_Head.png',
|
||||
} as const;
|
||||
const recentPlayItems = savedSnapshot
|
||||
@@ -599,49 +597,6 @@ export function PlatformHomeView({
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === 'discover') {
|
||||
content = (
|
||||
<div className="space-y-4 pb-2">
|
||||
<section
|
||||
className="pixel-nine-slice"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
<div className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-[10px] tracking-[0.2em] text-zinc-100">
|
||||
DISCOVER
|
||||
</div>
|
||||
<div className="mt-4 text-3xl font-black text-white">发现频道</div>
|
||||
<div className="mt-2 max-w-[28rem] text-sm leading-6 text-zinc-300">
|
||||
这里会放后续的专题策展、内容聚合和更多平台频道。首版先保留一个干净的发现位,方便后续扩展。
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionHeader title="最近上新" detail="先看广场里的新内容" />
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取推荐内容..." />
|
||||
) : latestEntries.length > 0 ? (
|
||||
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{latestEntries.map((entry: CustomWorldGalleryCard) => (
|
||||
<WorldCard
|
||||
key={`${entry.ownerUserId}:${entry.profileId}:discover`}
|
||||
entry={entry}
|
||||
badge={formatPlatformWorldTime(entry.publishedAt)}
|
||||
metaLabel={describePlatformThemeLabel(entry.themeMode)}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="发现频道暂时还没有可展示的内容。" />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (activeTab === 'profile') {
|
||||
content = (
|
||||
<div className="space-y-4 pb-2">
|
||||
@@ -918,7 +873,7 @@ export function PlatformHomeView({
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="你最近还没有浏览过作品详情,去首页或发现逛一逛吧。" />
|
||||
<EmptyShelf text="你最近还没有浏览过作品详情,去首页逛一逛吧。" />
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -988,7 +943,7 @@ export function PlatformHomeView({
|
||||
className="mt-4 border-t border-white/5 pt-3"
|
||||
style={{ paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)' }}
|
||||
>
|
||||
<div className="grid h-14 grid-cols-4 gap-1 rounded-[1.2rem] bg-black/18 px-1 py-1">
|
||||
<div className="grid h-14 grid-cols-3 gap-1 rounded-[1.2rem] bg-black/18 px-1 py-1">
|
||||
<PlatformTabButton
|
||||
active={activeTab === 'home'}
|
||||
label="首页"
|
||||
@@ -1001,12 +956,6 @@ export function PlatformHomeView({
|
||||
iconSrc={tabIcons.create}
|
||||
onClick={() => onTabChange('create')}
|
||||
/>
|
||||
<PlatformTabButton
|
||||
active={activeTab === 'discover'}
|
||||
label="发现"
|
||||
iconSrc={tabIcons.discover}
|
||||
onClick={() => onTabChange('discover')}
|
||||
/>
|
||||
<PlatformTabButton
|
||||
active={activeTab === 'profile'}
|
||||
label="我的"
|
||||
|
||||
@@ -48,6 +48,7 @@ export function PlatformWorldDetailView({
|
||||
onStartGame,
|
||||
onContinueEdit,
|
||||
onPublish,
|
||||
onDelete,
|
||||
onUnpublish,
|
||||
}: {
|
||||
entry: CustomWorldLibraryEntry<CustomWorldProfile>;
|
||||
@@ -57,17 +58,21 @@ export function PlatformWorldDetailView({
|
||||
onStartGame: () => void;
|
||||
onContinueEdit?: (() => void) | null;
|
||||
onPublish?: (() => void) | null;
|
||||
onDelete?: (() => void) | null;
|
||||
onUnpublish?: (() => void) | null;
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const leadPortrait = resolvePlatformWorldLeadPortrait(entry);
|
||||
const previewCharacters = buildCustomWorldPlayableCharacters(entry.profile).slice(
|
||||
0,
|
||||
3,
|
||||
);
|
||||
const previewCharacters = buildCustomWorldPlayableCharacters(
|
||||
entry.profile,
|
||||
).slice(0, 3);
|
||||
const previewLandmarks = entry.profile.landmarks.slice(0, 3);
|
||||
const tags = [
|
||||
...new Set(buildPlatformWorldTags(entry).map((tag) => tag.trim()).filter(Boolean)),
|
||||
...new Set(
|
||||
buildPlatformWorldTags(entry)
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean),
|
||||
),
|
||||
].slice(0, 3);
|
||||
|
||||
return (
|
||||
@@ -89,7 +94,10 @@ export function PlatformWorldDetailView({
|
||||
<div className="space-y-4 pb-2">
|
||||
<div
|
||||
className="pixel-nine-slice relative overflow-hidden"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 18, paddingY: 16 })}
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 18,
|
||||
paddingY: 16,
|
||||
})}
|
||||
>
|
||||
{coverImage ? (
|
||||
<img
|
||||
@@ -150,7 +158,10 @@ export function PlatformWorldDetailView({
|
||||
<div className="grid gap-4 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<div
|
||||
className="pixel-nine-slice"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 16, paddingY: 14 })}
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 16,
|
||||
paddingY: 14,
|
||||
})}
|
||||
>
|
||||
<div className="text-[10px] tracking-[0.22em] text-zinc-500">
|
||||
世界信息
|
||||
@@ -160,13 +171,17 @@ export function PlatformWorldDetailView({
|
||||
<div className="text-[10px] tracking-[0.18em] text-zinc-500">
|
||||
可玩角色
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-bold">{entry.playableNpcCount}</div>
|
||||
<div className="mt-2 text-lg font-bold">
|
||||
{entry.playableNpcCount}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-3">
|
||||
<div className="text-[10px] tracking-[0.18em] text-zinc-500">
|
||||
地标
|
||||
</div>
|
||||
<div className="mt-2 text-lg font-bold">{entry.landmarkCount}</div>
|
||||
<div className="mt-2 text-lg font-bold">
|
||||
{entry.landmarkCount}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 bg-black/20 px-3 py-3">
|
||||
<div className="text-[10px] tracking-[0.18em] text-zinc-500">
|
||||
@@ -231,7 +246,10 @@ export function PlatformWorldDetailView({
|
||||
|
||||
<div
|
||||
className="pixel-nine-slice"
|
||||
style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 16, paddingY: 14 })}
|
||||
style={getNineSliceStyle(UI_CHROME.panel, {
|
||||
paddingX: 16,
|
||||
paddingY: 14,
|
||||
})}
|
||||
>
|
||||
<div className="text-[10px] tracking-[0.22em] text-zinc-500">
|
||||
操作
|
||||
@@ -265,6 +283,14 @@ export function PlatformWorldDetailView({
|
||||
disabled={isMutating}
|
||||
/>
|
||||
) : null}
|
||||
{onDelete ? (
|
||||
<ActionButton
|
||||
label="删除作品"
|
||||
onClick={onDelete}
|
||||
tone="danger"
|
||||
disabled={isMutating}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="mt-4 rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
executeCustomWorldAgentAction,
|
||||
getCustomWorldAgentOperation,
|
||||
getCustomWorldAgentSession,
|
||||
streamCustomWorldAgentMessage,
|
||||
} from '../../services/aiService';
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import {
|
||||
clearProfileBrowseHistory,
|
||||
deleteCustomWorldProfile,
|
||||
getProfileDashboard,
|
||||
listCustomWorldGallery,
|
||||
listCustomWorldLibrary,
|
||||
@@ -35,11 +37,12 @@ vi.mock('../../services/aiService', () => ({
|
||||
generateCustomWorldProfile: vi.fn(),
|
||||
getCustomWorldAgentOperation: vi.fn(),
|
||||
getCustomWorldAgentSession: vi.fn(),
|
||||
sendCustomWorldAgentMessage: vi.fn(),
|
||||
streamCustomWorldAgentMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/storageService', () => ({
|
||||
clearProfileBrowseHistory: vi.fn(),
|
||||
deleteCustomWorldProfile: vi.fn(),
|
||||
getCustomWorldGalleryDetail: vi.fn(),
|
||||
getProfileDashboard: vi.fn(),
|
||||
listCustomWorldGallery: vi.fn(),
|
||||
@@ -78,6 +81,53 @@ vi.mock('../custom-world-agent/CustomWorldAgentWorkspace', () => ({
|
||||
|
||||
const mockSession: CustomWorldAgentSessionSnapshot = {
|
||||
sessionId: 'custom-world-agent-session-1',
|
||||
currentTurn: 0,
|
||||
anchorContent: {
|
||||
worldPromise: {
|
||||
hook: '被海雾吞没的旧航路群岛。',
|
||||
differentiator: '灯塔与禁航令共同决定谁能穿过死潮。',
|
||||
desiredExperience: '压抑、潮湿、悬疑',
|
||||
},
|
||||
playerFantasy: {
|
||||
playerRole: '玩家是被迫返乡的守灯人继承者。',
|
||||
corePursuit: '查清沉船夜与假航灯的关系。',
|
||||
fearOfLoss: '失去家族最后一条可信航线。',
|
||||
},
|
||||
themeBoundary: {
|
||||
toneKeywords: ['压抑', '悬疑'],
|
||||
aestheticDirectives: ['潮湿群岛', '冷雾港口'],
|
||||
forbiddenDirectives: ['轻喜冒险'],
|
||||
},
|
||||
playerEntryPoint: {
|
||||
openingIdentity: '返乡守灯人继承者',
|
||||
openingProblem: '回港首夜撞见禁航区假航灯重亮',
|
||||
entryMotivation: '阻止更多船只误入死潮',
|
||||
},
|
||||
coreConflict: {
|
||||
surfaceConflicts: ['守灯会与航运公会争夺航路解释权'],
|
||||
hiddenCrisis: '有人在借假航灯持续清洗旧案证据',
|
||||
firstTouchedConflict: '玩家返乡当夜就被卷进封航冲突',
|
||||
},
|
||||
keyRelationships: [
|
||||
{
|
||||
pairs: '玩家 vs 沈砺',
|
||||
relationshipType: '旧友互疑',
|
||||
secretOrCost: '他知道沉船夜的另一半真相',
|
||||
},
|
||||
],
|
||||
hiddenLines: {
|
||||
hiddenTruths: ['沉船夜与假航灯骗局属于同一操盘链条'],
|
||||
misdirectionHints: ['表面像海雾自然失控'],
|
||||
revealPacing: '先见异常,再见旧案,再见操盘者',
|
||||
},
|
||||
iconicElements: {
|
||||
iconicMotifs: ['假航灯', '沉钟回响'],
|
||||
institutionsOrArtifacts: ['旧灯塔', '禁航碑'],
|
||||
hardRules: ['错误航灯会把船引进必死水域'],
|
||||
},
|
||||
},
|
||||
progressPercent: 0,
|
||||
lastAssistantReply: '先告诉我你想做一个怎样的 RPG 世界。',
|
||||
stage: 'clarifying',
|
||||
focusCardId: null,
|
||||
creatorIntent: {},
|
||||
@@ -180,6 +230,7 @@ beforeEach(() => {
|
||||
vi.mocked(listProfileBrowseHistory).mockResolvedValue([]);
|
||||
vi.mocked(upsertProfileBrowseHistory).mockResolvedValue([]);
|
||||
vi.mocked(clearProfileBrowseHistory).mockResolvedValue([]);
|
||||
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
|
||||
vi.mocked(upsertCustomWorldProfile).mockResolvedValue({
|
||||
entry: {
|
||||
ownerUserId: 'user-1',
|
||||
@@ -226,6 +277,7 @@ beforeEach(() => {
|
||||
error: null,
|
||||
});
|
||||
vi.mocked(getCustomWorldAgentSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(streamCustomWorldAgentMessage).mockResolvedValue(mockSession);
|
||||
});
|
||||
|
||||
test('create tab opens game type modal, keeps AIRP and visual novel locked, and enters agent workspace for RPG', async () => {
|
||||
@@ -284,6 +336,10 @@ test('starting draft generation leaves the agent workspace and shows the generat
|
||||
expect(await screen.findByText('世界草稿生成进度')).toBeTruthy();
|
||||
expect(screen.queryByText(/Agent工作区/u)).toBeNull();
|
||||
expect(screen.getAllByText('生成世界底稿').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('当前锚点信息')).toBeTruthy();
|
||||
expect(screen.getByText('世界承诺')).toBeTruthy();
|
||||
expect(screen.getByText(/灯塔与禁航令共同决定谁能穿过死潮/u)).toBeTruthy();
|
||||
expect(screen.queryByText('先告诉我你想做一个怎样的 RPG 世界。')).toBeNull();
|
||||
});
|
||||
|
||||
test('existing draft sessions enter the legacy result layout directly', async () => {
|
||||
@@ -448,6 +504,60 @@ test('profile tab loads server browse history and can clear it after confirmatio
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('你最近还没有浏览过作品详情,去首页或发现逛一逛吧。'),
|
||||
screen.getByText('你最近还没有浏览过作品详情,去首页逛一逛吧。'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('owned world detail can delete a work and return to the create tab list', async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
vi.mocked(listCustomWorldLibrary).mockResolvedValue([
|
||||
{
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-delete-1',
|
||||
profile: {
|
||||
id: 'world-delete-1',
|
||||
name: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summary: '用于测试删除流程的作品。',
|
||||
tone: '压抑、潮湿、悬疑',
|
||||
playerGoal: '查清旧案。',
|
||||
majorFactions: ['守灯会'],
|
||||
coreConflicts: ['雾潮正在逼近港口'],
|
||||
playableNpcs: [],
|
||||
storyNpcs: [],
|
||||
landmarks: [],
|
||||
} as never,
|
||||
visibility: 'draft',
|
||||
publishedAt: null,
|
||||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||||
authorDisplayName: '测试玩家',
|
||||
worldName: '潮雾列岛',
|
||||
subtitle: '旧灯塔与失控航路',
|
||||
summaryText: '用于测试删除流程的作品。',
|
||||
coverImageSrc: null,
|
||||
themeMode: 'tide',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
},
|
||||
]);
|
||||
vi.mocked(deleteCustomWorldProfile).mockResolvedValue([]);
|
||||
|
||||
render(<TestWrapper />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '创作' }));
|
||||
await user.click(await screen.findByText('潮雾列岛'));
|
||||
await user.click(await screen.findByRole('button', { name: '删除作品' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteCustomWorldProfile).toHaveBeenCalledWith('world-delete-1');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: '删除作品' })).toBeNull();
|
||||
});
|
||||
expect(
|
||||
screen.getByText('你还没有保存任何自定义世界,先创建一个草稿开始吧。'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
CustomWorldAgentMessage,
|
||||
CustomWorldAgentActionRequest,
|
||||
CustomWorldAgentOperationRecord,
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
@@ -27,10 +28,11 @@ import {
|
||||
executeCustomWorldAgentAction,
|
||||
getCustomWorldAgentOperation,
|
||||
getCustomWorldAgentSession,
|
||||
sendCustomWorldAgentMessage,
|
||||
streamCustomWorldAgentMessage,
|
||||
} from '../../services/aiService';
|
||||
import { buildCustomWorldProfileFromAgentDraft } from '../../services/customWorldAgentDraftResult';
|
||||
import {
|
||||
buildAgentDraftFoundationAnchorEntries,
|
||||
buildAgentDraftFoundationGenerationProgress,
|
||||
buildAgentDraftFoundationSettingText,
|
||||
isDraftFoundationOperation,
|
||||
@@ -55,6 +57,7 @@ import {
|
||||
} from '../../services/platformBrowseHistory';
|
||||
import {
|
||||
clearProfileBrowseHistory,
|
||||
deleteCustomWorldProfile,
|
||||
getCustomWorldGalleryDetail,
|
||||
getProfileDashboard,
|
||||
listCustomWorldGallery,
|
||||
@@ -66,10 +69,7 @@ import {
|
||||
upsertCustomWorldProfile,
|
||||
upsertProfileBrowseHistory,
|
||||
} from '../../services/storageService';
|
||||
import {
|
||||
type CustomWorldProfile,
|
||||
type GameState,
|
||||
} from '../../types';
|
||||
import { type CustomWorldProfile, type GameState } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PlatformCreationTypeModal } from './PlatformCreationTypeModal';
|
||||
import { type PlatformHomeTab, PlatformHomeView } from './PlatformHomeView';
|
||||
@@ -141,6 +141,16 @@ function createFailedAgentOperation(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function buildOptimisticAgentMessage(
|
||||
payload: Pick<CustomWorldAgentMessage, 'id' | 'role' | 'kind' | 'text'>,
|
||||
): CustomWorldAgentMessage {
|
||||
return {
|
||||
...payload,
|
||||
createdAt: new Date().toISOString(),
|
||||
relatedOperationId: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAgentSeedTextFromProfile(profile: CustomWorldProfile) {
|
||||
return (
|
||||
buildCustomWorldCreatorIntentGenerationText(profile.creatorIntent).trim() ||
|
||||
@@ -215,6 +225,8 @@ export function PreGameSelectionFlow({
|
||||
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);
|
||||
@@ -457,6 +469,8 @@ export function PreGameSelectionFlow({
|
||||
if (!activeAgentSessionId) {
|
||||
setAgentSession(null);
|
||||
setIsLoadingAgentSession(false);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -479,6 +493,8 @@ export function PreGameSelectionFlow({
|
||||
);
|
||||
setAgentSession(null);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
persistAgentUiState(null, null);
|
||||
setPlatformTab('create');
|
||||
setSelectionStage('platform');
|
||||
@@ -636,6 +652,10 @@ export function PreGameSelectionFlow({
|
||||
() => buildAgentDraftFoundationSettingText(agentSession),
|
||||
[agentSession],
|
||||
);
|
||||
const agentDraftAnchorPreviewEntries = useMemo(
|
||||
() => buildAgentDraftFoundationAnchorEntries(agentSession),
|
||||
[agentSession],
|
||||
);
|
||||
const agentDraftResultProfile = useMemo(
|
||||
() => buildCustomWorldProfileFromAgentDraft(agentSession),
|
||||
[agentSession],
|
||||
@@ -794,23 +814,63 @@ export function PreGameSelectionFlow({
|
||||
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 { operation } = await sendCustomWorldAgentMessage(
|
||||
const nextSession = await streamCustomWorldAgentMessage(
|
||||
activeAgentSessionId,
|
||||
payload,
|
||||
{
|
||||
onUpdate: (text) => {
|
||||
setStreamingAgentReplyText(text);
|
||||
},
|
||||
},
|
||||
);
|
||||
setAgentOperation(operation);
|
||||
persistAgentUiState(activeAgentSessionId, operation.operationId);
|
||||
setAgentSession(nextSession);
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
} catch (error) {
|
||||
const errorMessage = resolveErrorMessage(error, '发送共创消息失败。');
|
||||
setAgentOperation(
|
||||
createFailedAgentOperation({
|
||||
type: 'process_message',
|
||||
phaseLabel: '发送消息失败',
|
||||
error: errorMessage,
|
||||
}),
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -858,6 +918,8 @@ export function PreGameSelectionFlow({
|
||||
const leaveAgentWorkspace = () => {
|
||||
setPlatformTab('create');
|
||||
setAgentOperation(null);
|
||||
setStreamingAgentReplyText('');
|
||||
setIsStreamingAgentReply(false);
|
||||
setGeneratedCustomWorldProfile(null);
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldAutoSaveState('idle');
|
||||
@@ -1058,11 +1120,7 @@ export function PreGameSelectionFlow({
|
||||
customWorldAutoSaveTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [
|
||||
generatedCustomWorldProfile,
|
||||
saveGeneratedCustomWorld,
|
||||
selectionStage,
|
||||
]);
|
||||
}, [generatedCustomWorldProfile, saveGeneratedCustomWorld, selectionStage]);
|
||||
|
||||
const openSavedCustomWorldEditor = (
|
||||
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
||||
@@ -1070,7 +1128,8 @@ export function PreGameSelectionFlow({
|
||||
setSelectedDetailEntry(entry);
|
||||
const normalizedProfile = normalizeAgentBackedProfile(entry.profile);
|
||||
setGeneratedCustomWorldProfile(normalizedProfile);
|
||||
lastAutoSavedProfileSignatureRef.current = JSON.stringify(normalizedProfile);
|
||||
lastAutoSavedProfileSignatureRef.current =
|
||||
JSON.stringify(normalizedProfile);
|
||||
setCustomWorldAutoSaveState('saved');
|
||||
setCustomWorldAutoSaveError(null);
|
||||
setCustomWorldError(null);
|
||||
@@ -1129,6 +1188,36 @@ export function PreGameSelectionFlow({
|
||||
}
|
||||
};
|
||||
|
||||
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(
|
||||
@@ -1228,6 +1317,9 @@ export function PreGameSelectionFlow({
|
||||
? handleUnpublishSelectedWorld
|
||||
: null
|
||||
}
|
||||
onDelete={
|
||||
isSelectedWorldOwned ? handleDeleteSelectedWorld : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
@@ -1250,13 +1342,9 @@ export function PreGameSelectionFlow({
|
||||
<CustomWorldAgentWorkspace
|
||||
session={agentSession}
|
||||
activeOperation={agentOperation}
|
||||
streamingReplyText={streamingAgentReplyText}
|
||||
isStreamingReply={isStreamingAgentReply}
|
||||
onBack={leaveAgentWorkspace}
|
||||
onRefresh={() => {
|
||||
if (!activeAgentSessionId) {
|
||||
return;
|
||||
}
|
||||
void syncAgentSessionSnapshot(activeAgentSessionId);
|
||||
}}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitAgentMessage(payload);
|
||||
}}
|
||||
@@ -1290,6 +1378,7 @@ export function PreGameSelectionFlow({
|
||||
>
|
||||
<CustomWorldGenerationView
|
||||
settingText={activeGenerationSettingText}
|
||||
anchorEntries={agentDraftAnchorPreviewEntries}
|
||||
progress={activeGenerationProgress}
|
||||
isGenerating={isActiveGenerationRunning}
|
||||
error={activeGenerationError}
|
||||
@@ -1300,10 +1389,10 @@ export function PreGameSelectionFlow({
|
||||
backLabel="返回工作区"
|
||||
settingActionLabel="回到工作区"
|
||||
retryLabel="重新生成草稿"
|
||||
settingTitle="当前共创设定"
|
||||
settingTitle="当前锚点信息"
|
||||
settingDescription={
|
||||
isAgentDraftGenerationView
|
||||
? '这批锚点会被整理成第一版世界底稿与草稿卡。'
|
||||
? '将按当前八锚点结构编译第一版世界底稿与草稿卡。'
|
||||
: undefined
|
||||
}
|
||||
progressTitle={
|
||||
@@ -1385,7 +1474,6 @@ export function PreGameSelectionFlow({
|
||||
void openRpgAgentWorkspace();
|
||||
}}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user