- {session.title}
+ {hasHeroCopy ? (
+
+ {session.title ? (
+
+ {session.title}
+
+ ) : null}
+ {session.assistantSummary ? (
+
+ {session.assistantSummary}
+
+ ) : null}
- {session.assistantSummary ? (
-
- {session.assistantSummary}
-
- ) : null}
-
+
创作进度
diff --git a/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx b/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx
index 9c18c2d9..060a24d6 100644
--- a/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx
+++ b/src/components/custom-world-agent/CustomWorldAgentWorkspace.test.tsx
@@ -77,6 +77,10 @@ test('custom world agent workspace renders minimum loop chat layout', () => {
expect(html).toContain('输入消息');
expect(html).toContain('总结当前设定');
expect(html).toContain('补全剩余设定');
+ expect(html).not.toContain('世界共创');
+ expect(html).not.toContain(
+ '先说一个你最想让玩家记住的世界方向,我会帮你收束成可生成草稿的锚点。',
+ );
expect(html).not.toContain('Agent');
expect(html).not.toContain('刷新');
expect(html).not.toContain('当前轮次');
diff --git a/src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx b/src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx
index 0d13e685..90df71af 100644
--- a/src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx
+++ b/src/components/custom-world-agent/CustomWorldAgentWorkspace.tsx
@@ -83,10 +83,9 @@ function mapCustomWorldSession(
): CreationAgentSessionView {
return {
sessionId: session.sessionId,
- title: '世界共创',
- assistantSummary:
- session.lastAssistantReply ||
- '先说一个你最想让玩家记住的世界方向,我会帮你收束成可生成草稿的锚点。',
+ // 自定义世界 Agent 聊天页顶部保持纯操作区,不额外显示标题和引导副文案。
+ title: null,
+ assistantSummary: null,
currentTurn: session.currentTurn,
progressPercent: session.progressPercent,
anchors: [
diff --git a/src/components/rpg-creation-result/RpgCreationResultActionBar.tsx b/src/components/rpg-creation-result/RpgCreationResultActionBar.tsx
index 69213f8b..75f1db86 100644
--- a/src/components/rpg-creation-result/RpgCreationResultActionBar.tsx
+++ b/src/components/rpg-creation-result/RpgCreationResultActionBar.tsx
@@ -1,4 +1,6 @@
-import type { ReactNode } from 'react';
+import { X } from 'lucide-react';
+import { type ReactNode, useState } from 'react';
+import { createPortal } from 'react-dom';
import type { CustomWorldProfile } from '../../types';
@@ -29,6 +31,81 @@ function SmallButton({
);
}
+function PublishBlockersDialog({
+ blockers,
+ onClose,
+}: {
+ blockers: string[];
+ onClose: () => void;
+}) {
+ if (typeof document === 'undefined') {
+ return null;
+ }
+
+ return createPortal(
+ {
+ if (event.target === event.currentTarget) {
+ onClose();
+ }
+ }}
+ >
+
event.stopPropagation()}
+ >
+
+
+
+ 发布前还需要补齐这些内容
+
+
+ 当前还有 {blockers.length} 个阻断项,补齐后再发布并进入世界。
+
+
+
+
+
+
+ {blockers.map((blocker, index) => (
+
+
+ 阻断项 {index + 1}
+
+
{blocker}
+
+ ))}
+
+
+
+
+
+
+
,
+ document.body,
+ );
+}
+
interface RpgCreationResultActionBarProps {
editActionLabel: string;
enterWorldActionLabel: string;
@@ -40,6 +117,7 @@ interface RpgCreationResultActionBarProps {
profile: CustomWorldProfile;
regenerateActionLabel: string;
publishReady: boolean;
+ publishBlockers: string[];
}
export function RpgCreationResultActionBar({
@@ -53,7 +131,21 @@ export function RpgCreationResultActionBar({
profile,
regenerateActionLabel,
publishReady,
+ publishBlockers,
}: RpgCreationResultActionBarProps) {
+ const [showPublishBlockersDialog, setShowPublishBlockersDialog] =
+ useState(false);
+
+ // 结果页保持清爽,只有用户发起发布动作时才弹出阻断项提示。
+ const handleEnterWorld = () => {
+ if (!publishReady) {
+ setShowPublishBlockersDialog(true);
+ return;
+ }
+
+ onEnterWorld?.();
+ };
+
return (
{profile.generationStatus === 'key_only' ? (
@@ -82,14 +174,24 @@ export function RpgCreationResultActionBar({
{onEnterWorld ? (
) : null}
+ {showPublishBlockersDialog ? (
+ 0
+ ? publishBlockers
+ : ['当前草稿还没有通过发布门槛,请先补齐必要内容。']
+ }
+ onClose={() => setShowPublishBlockersDialog(false)}
+ />
+ ) : null}
);
}
diff --git a/src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx b/src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx
index 16269603..f26fb244 100644
--- a/src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx
+++ b/src/components/rpg-creation-result/RpgCreationResultViewImpl.tsx
@@ -68,7 +68,6 @@ export function RpgCreationResultView({
publishReady = true,
publishBlockers = [],
qualityFindings = [],
- previewSourceLabel = null,
}: RpgCreationResultViewProps) {
const [activeTab, setActiveTab] = useState
('world');
const assetDebugEnabled = useMemo(
@@ -171,25 +170,6 @@ export function RpgCreationResultView({
{error}
) : null}
- {!error && compactAgentResultMode && previewSourceLabel ? (
-
- 当前结果页数据源:{previewSourceLabel}
-
- ) : null}
- {!error && compactAgentResultMode && publishBlockers.length > 0 ? (
-
- {publishReady
- ? '当前世界已满足发布门槛。'
- : `当前还有 ${publishBlockers.length} 个发布阻断项,请先补齐后再进入世界。`}
-
- {publishBlockers.slice(0, 4).map((entry, index) => (
-
- {index + 1}. {entry}
-
- ))}
-
-
- ) : null}
{!error &&
compactAgentResultMode &&
publishBlockers.length <= 0 &&
@@ -216,6 +196,7 @@ export function RpgCreationResultView({
profile={profile}
regenerateActionLabel={regenerateActionLabel}
publishReady={publishReady}
+ publishBlockers={publishBlockers}
/>
{
+test('agent result view shows publish blocker dialog before publish action when preview gate is not ready', async () => {
const user = userEvent.setup();
vi.mocked(getRpgCreationOperation).mockResolvedValue({
@@ -1128,11 +1128,33 @@ test('agent result view shows publish blockers and disables publish-enter action
await openNewRpgCreation(user);
- expect(await screen.findByText(/当前还有 1 个发布阻断项/u)).toBeTruthy();
- const actionButton = screen.getByRole('button', {
+ const actionButton = await screen.findByRole('button', {
name: /发布并进入世界/u,
});
- expect((actionButton as HTMLButtonElement).disabled).toBe(true);
+ expect((actionButton as HTMLButtonElement).disabled).toBe(false);
+
+ const publishWorldCallCountBeforeClick = vi
+ .mocked(executeRpgCreationAction)
+ .mock.calls.filter(
+ ([sessionId, payload]) =>
+ sessionId === 'custom-world-agent-session-1' &&
+ payload?.action === 'publish_world',
+ ).length;
+
+ await user.click(actionButton);
+
+ expect(await screen.findByRole('dialog', { name: '发布前检查' })).toBeTruthy();
+ expect(screen.getByText(/当前还有 1 个阻断项/u)).toBeTruthy();
+ expect(screen.getByText(/仍有角色缺少正式主图或动作资产/u)).toBeTruthy();
+
+ const publishWorldCallCountAfterClick = vi
+ .mocked(executeRpgCreationAction)
+ .mock.calls.filter(
+ ([sessionId, payload]) =>
+ sessionId === 'custom-world-agent-session-1' &&
+ payload?.action === 'publish_world',
+ ).length;
+ expect(publishWorldCallCountAfterClick).toBe(publishWorldCallCountBeforeClick);
});
test('agent draft result publishes before entering world and uses published preview profile', async () => {