diff --git a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md index 4f6a457a..01f24ccc 100644 --- a/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md +++ b/docs/prd/AI_NATIVE_AGENT_FIRST_CUSTOM_WORLD_CREATOR_PRD_2026-04-12.md @@ -1606,6 +1606,8 @@ type ResolvedAgentIntent = 1. 不再承担整世界重生成主入口 2. 不再承担核心 Agent 对话主流程 3. 仅保留“查看 / 导出 / 发布确认 / 进入世界” +4. 结果页底部不常驻展示“数据源”提示 +5. 发布阻断项只在创作者点击发布动作时,通过独立确认面板提示,不在结果页吸底常驻展开 ## 9.12 角色资产工坊 diff --git a/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md b/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md index 131bac39..93d40675 100644 --- a/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md +++ b/docs/technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md @@ -1217,8 +1217,9 @@ Phase 4 本轮已完成以下主链接线: - published works 明确输出 `canEnterWorld=true` 4. 前端 Agent 结果页已开始消费服务端 Phase4 状态: - 结果页在 Agent 草稿未发布时把主 CTA 改成“发布并进入世界” - - 结果页会展示服务端 preview source、publish blockers、warning 数量 - - 有 blocker 时会禁用“发布并进入世界”按钮,不再让前端继续假装可以直接进入世界 + - 结果页会消费服务端 gate 语义,但不再把 preview source 做成底部常驻提示 + - publish blockers 改为点击“发布并进入世界”时,通过独立面板提示 + - warning 数量仍可作为非阻断摘要展示 5. `useRpgCreationEnterWorld.ts` 与 `RpgEntryFlowShellImpl.tsx` 已把 Agent 结果页进入世界主链改成: - 先 `sync_result_profile` - 再执行后端 `publish_world` diff --git a/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md b/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md index d78d90b1..b63c1404 100644 --- a/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md +++ b/docs/technical/UNIFIED_CREATION_AGENT_CHAT_FRAMEWORK_2026-04-22.md @@ -50,9 +50,10 @@ src/services/creation-agent/ 聊天页展示规则: 1. Agent 聊天页不展示锚点内容卡片,锚点只作为进度与后端生成依据。 -2. 生成草稿 / 生成结果页主按钮只在 `progressPercent` 归一化后达到 `100%` 时显示。 -3. 进度条下方承载“总结当前设定”“补全剩余设定”等进度操作按钮。 -4. “补全剩余设定”必须配置 `minTurn: 2`,对话不足两轮时不显示。 +2. 标题区文案支持按品类留空;当 `title` 与 `assistantSummary` 都为空时,顶部模块只保留返回、进度和操作按钮,不显示额外标题与副文案。 +3. 生成草稿 / 生成结果页主按钮只在 `progressPercent` 归一化后达到 `100%` 时显示。 +4. 进度条下方承载“总结当前设定”“补全剩余设定”等进度操作按钮。 +5. “补全剩余设定”必须配置 `minTurn: 2`,对话不足两轮时不显示。 组件内部只做表现,不读取任何 RPG、Big Fish、Puzzle 专属字段。 diff --git a/src/components/CustomWorldResultView.test.tsx b/src/components/CustomWorldResultView.test.tsx index 7db2c2c4..83b70a50 100644 --- a/src/components/CustomWorldResultView.test.tsx +++ b/src/components/CustomWorldResultView.test.tsx @@ -443,7 +443,7 @@ test('readOnly result view hides edit and create actions for agent preview mode' expect(screen.queryByRole('button', { name: /批量删除/u })).toBeNull(); }); -test('agent result view shows publish blockers and disables publish-enter action', () => { +test('agent result view keeps publish-enter action clickable and hides sticky publish hints', () => { render( , ); - expect(screen.getByText(/当前结果页数据源:服务端预览/u)).toBeTruthy(); - expect(screen.getByText(/当前还有 2 个发布阻断项/u)).toBeTruthy(); - expect( - screen.getByText(/仍有角色缺少正式主图或动作资产/u), - ).toBeTruthy(); const actionButton = screen.getByRole('button', { name: '发布并进入世界', }); - expect((actionButton as HTMLButtonElement).disabled).toBe(true); + expect((actionButton as HTMLButtonElement).disabled).toBe(false); + expect(screen.queryByText(/当前结果页数据源:服务端预览/u)).toBeNull(); + expect(screen.queryByText(/当前还有 2 个发布阻断项/u)).toBeNull(); +}); + +test('agent result view opens publish blocker dialog only when user clicks publish action', async () => { + const user = userEvent.setup(); + + render( + {}} + onProfileChange={() => {}} + compactAgentResultMode + publishReady={false} + publishBlockers={[ + '仍有角色缺少正式主图或动作资产,发布前需要先补齐。', + '营地还缺少正式场景图资产,发布前需要先确认营地图。', + ]} + previewSourceLabel="服务端预览" + enterWorldActionLabel="发布并进入世界" + onEnterWorld={() => {}} + />, + ); + + await user.click(screen.getByRole('button', { name: '发布并进入世界' })); + + expect( + screen.getByRole('dialog', { name: '发布前检查' }), + ).toBeTruthy(); + expect(screen.getByText(/当前还有 2 个阻断项/u)).toBeTruthy(); + expect( + screen.getByText(/仍有角色缺少正式主图或动作资产/u), + ).toBeTruthy(); }); test('agent result view keeps publish-enter action enabled when publish gate is clear', () => { diff --git a/src/components/creation-agent/CreationAgentWorkspace.test.tsx b/src/components/creation-agent/CreationAgentWorkspace.test.tsx index 234fa038..f140ca95 100644 --- a/src/components/creation-agent/CreationAgentWorkspace.test.tsx +++ b/src/components/creation-agent/CreationAgentWorkspace.test.tsx @@ -205,3 +205,40 @@ test('creation agent workspace shows primary and progress actions at completed p expect(screen.getByRole('button', { name: '总结当前设定' })).toBeTruthy(); expect(screen.getByRole('button', { name: '补全剩余设定' })).toBeTruthy(); }); + +test('creation agent workspace hides hero copy area when title and summary are absent', () => { + if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = () => {}; + } + + render( + {}} + onSubmitText={() => {}} + onPrimaryAction={() => {}} + />, + ); + + expect(screen.queryByText('统一共创')).toBeNull(); + expect(screen.getByText('创作进度')).toBeTruthy(); +}); diff --git a/src/components/creation-agent/CreationAgentWorkspace.tsx b/src/components/creation-agent/CreationAgentWorkspace.tsx index c66257a6..7e6849c2 100644 --- a/src/components/creation-agent/CreationAgentWorkspace.tsx +++ b/src/components/creation-agent/CreationAgentWorkspace.tsx @@ -34,7 +34,7 @@ export type CreationAgentOperationView = { export type CreationAgentSessionView = { sessionId: string; - title: string; + title?: string | null; assistantSummary?: string | null; currentTurn: number; progressPercent: number; @@ -267,6 +267,7 @@ export function CreationAgentWorkspace({ } const progress = normalizeCreationAgentProgress(session.progressPercent); + const hasHeroCopy = Boolean(session.title || session.assistantSummary); const canShowPrimaryAction = progress >= 100; const visibleQuickActions = quickActions.filter((action) => shouldShowQuickAction(action, session, progress), @@ -313,18 +314,22 @@ export function CreationAgentWorkspace({ ) : null} -
-
- {session.title} + {hasHeroCopy ? ( +
+ {session.title ? ( +
+ {session.title} +
+ ) : null} + {session.assistantSummary ? ( +
+ {session.assistantSummary} +
+ ) : null}
- {session.assistantSummary ? ( -
- {session.assistantSummary} -
- ) : null} -
+ ) : 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 () => {