1
This commit is contained in:
@@ -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(
|
||||
<RpgCreationResultView
|
||||
profile={baseProfile}
|
||||
@@ -474,15 +474,48 @@ test('agent result view shows publish blockers and disables publish-enter action
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<RpgCreationResultView
|
||||
profile={baseProfile}
|
||||
previewCharacters={[]}
|
||||
isGenerating={false}
|
||||
progress={0}
|
||||
progressLabel=""
|
||||
error={null}
|
||||
onBack={() => {}}
|
||||
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', () => {
|
||||
|
||||
@@ -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(
|
||||
<CreationAgentWorkspace
|
||||
session={{
|
||||
sessionId: 'creation-agent-session-1',
|
||||
title: null,
|
||||
assistantSummary: null,
|
||||
currentTurn: 2,
|
||||
progressPercent: 60,
|
||||
anchors: [],
|
||||
messages: [
|
||||
{
|
||||
id: 'message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '继续把设定收束到可生成状态。',
|
||||
},
|
||||
],
|
||||
}}
|
||||
theme={testTheme}
|
||||
loadingText="正在准备"
|
||||
composerPlaceholder="输入消息"
|
||||
primaryActionLabel="生成结果页"
|
||||
onBack={() => {}}
|
||||
onSubmitText={() => {}}
|
||||
onPrimaryAction={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('统一共创')).toBeNull();
|
||||
expect(screen.getByText('创作进度')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<div className="text-2xl font-black leading-tight sm:text-3xl">
|
||||
{session.title}
|
||||
{hasHeroCopy ? (
|
||||
<div className="mt-6">
|
||||
{session.title ? (
|
||||
<div className="text-2xl font-black leading-tight sm:text-3xl">
|
||||
{session.title}
|
||||
</div>
|
||||
) : null}
|
||||
{session.assistantSummary ? (
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-white/76">
|
||||
{session.assistantSummary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{session.assistantSummary ? (
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-white/76">
|
||||
{session.assistantSummary}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4">
|
||||
<div className={hasHeroCopy ? 'mt-4' : 'mt-6'}>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<span className="text-xs font-semibold tracking-[0.14em] text-white/72">
|
||||
创作进度
|
||||
|
||||
@@ -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('当前轮次');
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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(
|
||||
<div
|
||||
className="platform-overlay fixed inset-0 z-[140] flex items-end justify-center bg-slate-950/56 p-3 backdrop-blur-sm sm:items-center sm:p-4"
|
||||
onClick={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="发布前检查"
|
||||
className="platform-modal-shell platform-remap-surface flex max-h-[min(88vh,42rem)] w-full max-w-lg flex-col overflow-hidden rounded-t-[1.75rem] shadow-[0_24px_80px_rgba(0,0,0,0.55)] sm:rounded-[1.75rem]"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-white/10 px-5 py-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">
|
||||
发布前还需要补齐这些内容
|
||||
</div>
|
||||
<div className="mt-1 text-sm leading-6 text-[var(--platform-text-soft)]">
|
||||
当前还有 {blockers.length} 个阻断项,补齐后再发布并进入世界。
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="关闭"
|
||||
className="platform-icon-button"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
||||
<div className="space-y-2">
|
||||
{blockers.map((blocker, index) => (
|
||||
<div
|
||||
key={`publish-blocker-${index}-${blocker}`}
|
||||
className="rounded-[1.1rem] border border-amber-300/18 bg-amber-500/10 px-4 py-3 text-sm leading-6 text-[var(--platform-text-strong)]"
|
||||
>
|
||||
<div className="text-xs font-semibold tracking-[0.14em] text-amber-100/78">
|
||||
阻断项 {index + 1}
|
||||
</div>
|
||||
<div className="mt-1">{blocker}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-white/10 px-5 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="platform-button platform-button--primary"
|
||||
>
|
||||
我知道了
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
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 (
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
{profile.generationStatus === 'key_only' ? (
|
||||
@@ -82,14 +174,24 @@ export function RpgCreationResultActionBar({
|
||||
{onEnterWorld ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEnterWorld}
|
||||
disabled={isGenerating || !publishReady}
|
||||
onClick={handleEnterWorld}
|
||||
disabled={isGenerating}
|
||||
className={`platform-button platform-button--primary ${isGenerating ? 'opacity-55' : ''}`}
|
||||
>
|
||||
{enterWorldActionLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{showPublishBlockersDialog ? (
|
||||
<PublishBlockersDialog
|
||||
blockers={
|
||||
publishBlockers.length > 0
|
||||
? publishBlockers
|
||||
: ['当前草稿还没有通过发布门槛,请先补齐必要内容。']
|
||||
}
|
||||
onClose={() => setShowPublishBlockersDialog(false)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ export function RpgCreationResultView({
|
||||
publishReady = true,
|
||||
publishBlockers = [],
|
||||
qualityFindings = [],
|
||||
previewSourceLabel = null,
|
||||
}: RpgCreationResultViewProps) {
|
||||
const [activeTab, setActiveTab] = useState<ResultTab>('world');
|
||||
const assetDebugEnabled = useMemo(
|
||||
@@ -171,25 +170,6 @@ export function RpgCreationResultView({
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
{!error && compactAgentResultMode && previewSourceLabel ? (
|
||||
<div className="platform-banner platform-banner--info mt-3 rounded-2xl text-sm leading-6">
|
||||
当前结果页数据源:{previewSourceLabel}
|
||||
</div>
|
||||
) : null}
|
||||
{!error && compactAgentResultMode && publishBlockers.length > 0 ? (
|
||||
<div className="platform-banner platform-banner--warning mt-3 rounded-2xl text-sm leading-6">
|
||||
{publishReady
|
||||
? '当前世界已满足发布门槛。'
|
||||
: `当前还有 ${publishBlockers.length} 个发布阻断项,请先补齐后再进入世界。`}
|
||||
<div className="mt-2 space-y-1">
|
||||
{publishBlockers.slice(0, 4).map((entry, index) => (
|
||||
<div key={`publish-blocker-${index}-${entry}`}>
|
||||
{index + 1}. {entry}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{!error &&
|
||||
compactAgentResultMode &&
|
||||
publishBlockers.length <= 0 &&
|
||||
@@ -216,6 +196,7 @@ export function RpgCreationResultView({
|
||||
profile={profile}
|
||||
regenerateActionLabel={regenerateActionLabel}
|
||||
publishReady={publishReady}
|
||||
publishBlockers={publishBlockers}
|
||||
/>
|
||||
|
||||
<RpgCreationEntityEditorModal
|
||||
|
||||
@@ -1097,7 +1097,7 @@ test('existing draft sessions open result page refinement instead of agent dialo
|
||||
expect(screen.getByRole('button', { name: /AI生成/u })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('agent result view shows publish blockers and disables publish-enter action when preview gate is not ready', async () => {
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user