feat: unify phase one creation flow

This commit is contained in:
2026-05-30 05:05:02 +08:00
parent 3a87b2d966
commit 26975644b5
33 changed files with 2037 additions and 539 deletions

View File

@@ -0,0 +1,42 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import { UnifiedCreationPage } from './UnifiedCreationPage';
import { getUnifiedCreationSpec } from './unifiedCreationSpecs';
describe('UnifiedCreationPage', () => {
test('按后端字段 spec 暴露统一创作页字段契约', () => {
render(
<UnifiedCreationPage spec={getUnifiedCreationSpec('wooden-fish')}>
<div></div>
</UnifiedCreationPage>,
);
const root = screen
.getByText('敲木鱼工作台')
.closest('.unified-creation-page');
expect(root?.getAttribute('data-play-id')).toBe('wooden-fish');
expect(root?.getAttribute('data-field-kinds')).toBe(
'text,image,audio,text',
);
expect(root?.getAttribute('data-workspace-stage')).toBe(
'wooden-fish-workspace',
);
expect(root?.getAttribute('data-generation-stage')).toBe(
'wooden-fish-generating',
);
expect(root?.getAttribute('data-result-stage')).toBe('wooden-fish-result');
const fields = screen.getAllByTestId('unified-creation-field');
expect(fields.map((field) => field.getAttribute('data-field-id'))).toEqual([
'hitObjectPrompt',
'hitObjectReferenceImage',
'hitSoundAsset',
'floatingWords',
]);
expect(fields[2]?.getAttribute('data-field-kind')).toBe('audio');
expect(fields[3]?.getAttribute('data-required')).toBe('true');
});
});

View File

@@ -0,0 +1,44 @@
import type { ReactNode } from 'react';
import type { UnifiedCreationSpec } from './unifiedCreationSpecs';
type UnifiedCreationPageProps = {
spec: UnifiedCreationSpec;
children: ReactNode;
};
export function UnifiedCreationPage({
spec,
children,
}: UnifiedCreationPageProps) {
return (
<div
className="unified-creation-page flex h-full min-h-0 flex-col"
data-play-id={spec.playId}
data-field-kinds={spec.fields.map((field) => field.kind).join(',')}
data-workspace-stage={spec.workspaceStage}
data-generation-stage={spec.generationStage}
data-result-stage={spec.resultStage}
>
<div className="sr-only" data-testid="unified-creation-spec">
<h1>{spec.title}</h1>
<ul>
{spec.fields.map((field) => (
<li
key={field.id}
data-testid="unified-creation-field"
data-field-id={field.id}
data-field-kind={field.kind}
data-required={field.required ? 'true' : 'false'}
>
{field.label}
</li>
))}
</ul>
</div>
{children}
</div>
);
}
export default UnifiedCreationPage;

View File

@@ -0,0 +1,53 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import type { CustomWorldGenerationProgress } from '../../../packages/shared/src/contracts/runtime';
import { UnifiedGenerationPage } from './UnifiedGenerationPage';
function createProgress(): CustomWorldGenerationProgress {
return {
phaseId: 'puzzle-cover-image',
phaseLabel: '生成拼图首图',
phaseDetail: '正在生成图片。',
batchLabel: '生成拼图首图',
overallProgress: 36,
completedWeight: 36,
totalWeight: 100,
elapsedMs: 12_000,
estimatedRemainingMs: 30_000,
activeStepIndex: 0,
steps: [
{
id: 'puzzle-cover-image',
label: '生成拼图首图',
detail: '正在生成图片。',
completed: 0.36,
total: 1,
status: 'active',
},
],
};
}
describe('UnifiedGenerationPage', () => {
test('按玩法下发统一生成页文案并透传进度', () => {
render(
<UnifiedGenerationPage
playId="puzzle"
settingText="一只发光的纸船"
progress={createProgress()}
isGenerating
onBack={() => {}}
onEditSetting={() => {}}
onRetry={() => {}}
/>,
);
expect(document.body.textContent).toContain('拼图图片生成进度');
expect(screen.getByText('图片生成中')).toBeTruthy();
expect(screen.getAllByText('生成拼图首图').length).toBeGreaterThan(0);
expect(screen.getByText('当前拼图信息')).toBeTruthy();
});
});

View File

@@ -0,0 +1,91 @@
import type { CustomWorldGenerationProgress } from '../../../packages/shared/src/contracts/runtime';
import type { CustomWorldStructuredAnchorEntry } from '../../services/customWorldAgentGenerationProgress';
import { CustomWorldGenerationView } from '../CustomWorldGenerationView';
import type { UnifiedCreationPlayId } from './unifiedCreationSpecs';
type UnifiedGenerationPageProps = {
playId: UnifiedCreationPlayId;
settingText: string;
anchorEntries?: CustomWorldStructuredAnchorEntry[];
progress: CustomWorldGenerationProgress | null;
isGenerating: boolean;
error?: string | null;
onBack: () => void;
onEditSetting: () => void;
onRetry: () => void;
hideBatchModule?: boolean;
};
const UNIFIED_GENERATION_COPY = {
puzzle: {
retryLabel: '重新生成图片',
settingTitle: '当前拼图信息',
progressTitle: '拼图图片生成进度',
activeBadgeLabel: '图片生成中',
},
match3d: {
retryLabel: '重新生成草稿',
settingTitle: '当前抓大鹅信息',
progressTitle: '抓大鹅草稿生成进度',
activeBadgeLabel: '素材生成中',
},
'wooden-fish': {
retryLabel: '重新生成草稿',
settingTitle: '当前敲木鱼信息',
progressTitle: '敲木鱼草稿生成进度',
activeBadgeLabel: '素材生成中',
},
} as const satisfies Record<
UnifiedCreationPlayId,
{
retryLabel: string;
settingTitle: string;
progressTitle: string;
activeBadgeLabel: string;
}
>;
export function getUnifiedGenerationCopy(playId: UnifiedCreationPlayId) {
return UNIFIED_GENERATION_COPY[playId];
}
export function UnifiedGenerationPage({
playId,
settingText,
anchorEntries = [],
progress,
isGenerating,
error = null,
onBack,
onEditSetting,
onRetry,
hideBatchModule = false,
}: UnifiedGenerationPageProps) {
const copy = getUnifiedGenerationCopy(playId);
return (
<CustomWorldGenerationView
settingText={settingText}
anchorEntries={anchorEntries}
progress={progress}
isGenerating={isGenerating}
error={error}
onBack={onBack}
onEditSetting={onEditSetting}
onRetry={onRetry}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel={copy.retryLabel}
settingTitle={copy.settingTitle}
settingDescription={null}
progressTitle={copy.progressTitle}
activeBadgeLabel={copy.activeBadgeLabel}
pausedBadgeLabel="素材生成已暂停"
idleBadgeLabel="等待返回工作区"
hideBatchModule={hideBatchModule}
/>
);
}
export default UnifiedGenerationPage;

View File

@@ -0,0 +1,42 @@
import { describe, expect, test } from 'vitest';
import {
getUnifiedCreationSpec,
listUnifiedCreationSpecs,
} from './unifiedCreationSpecs';
describe('unified creation specs', () => {
test('一期只接拼图、抓大鹅和敲木鱼', () => {
expect(listUnifiedCreationSpecs().map((spec) => spec.playId).sort()).toEqual(
['match3d', 'puzzle', 'wooden-fish'],
);
});
test('字段模型只包含首期公共能力', () => {
const fieldKinds = new Set(
listUnifiedCreationSpecs().flatMap((spec) =>
spec.fields.map((field) => field.kind),
),
);
expect([...fieldKinds].sort()).toEqual(['audio', 'image', 'select', 'text']);
});
test('三条链路都映射到统一创作、生成、结果阶段', () => {
expect(getUnifiedCreationSpec('puzzle')).toMatchObject({
workspaceStage: 'puzzle-agent-workspace',
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
});
expect(getUnifiedCreationSpec('match3d')).toMatchObject({
workspaceStage: 'match3d-agent-workspace',
generationStage: 'match3d-generating',
resultStage: 'match3d-result',
});
expect(getUnifiedCreationSpec('wooden-fish')).toMatchObject({
workspaceStage: 'wooden-fish-workspace',
generationStage: 'wooden-fish-generating',
resultStage: 'wooden-fish-result',
});
});
});

View File

@@ -0,0 +1,107 @@
import type {
CreationEntryTypeConfig,
UnifiedCreationSpec,
} from '../../services/creationEntryConfigService';
export type UnifiedCreationPlayId = UnifiedCreationSpec['playId'];
export type { UnifiedCreationSpec };
const FALLBACK_UNIFIED_CREATION_SPECS: Record<
UnifiedCreationPlayId,
UnifiedCreationSpec
> = {
puzzle: {
playId: 'puzzle',
title: '想做个什么玩法?',
workspaceStage: 'puzzle-agent-workspace',
generationStage: 'puzzle-generating',
resultStage: 'puzzle-result',
fields: [
{
id: 'pictureDescription',
kind: 'text',
label: '画面描述',
required: true,
},
{
id: 'referenceImage',
kind: 'image',
label: '拼图画面',
required: false,
},
{
id: 'promptReferenceImages',
kind: 'image',
label: '参考图',
required: false,
},
],
},
match3d: {
playId: 'match3d',
title: '想做个什么玩法?',
workspaceStage: 'match3d-agent-workspace',
generationStage: 'match3d-generating',
resultStage: 'match3d-result',
fields: [
{
id: 'themeText',
kind: 'text',
label: '题材',
required: true,
},
{
id: 'difficulty',
kind: 'select',
label: '难度',
required: true,
},
],
},
'wooden-fish': {
playId: 'wooden-fish',
title: '想做个什么玩法?',
workspaceStage: 'wooden-fish-workspace',
generationStage: 'wooden-fish-generating',
resultStage: 'wooden-fish-result',
fields: [
{
id: 'hitObjectPrompt',
kind: 'text',
label: '敲什么',
required: false,
},
{
id: 'hitObjectReferenceImage',
kind: 'image',
label: '参考图',
required: false,
},
{
id: 'hitSoundAsset',
kind: 'audio',
label: '敲击音效',
required: false,
},
{
id: 'floatingWords',
kind: 'text',
label: '功德有什么',
required: true,
},
],
},
};
export function getUnifiedCreationSpec(
playId: UnifiedCreationPlayId,
configType?: CreationEntryTypeConfig | null,
) {
return (
configType?.unifiedCreationSpec ?? FALLBACK_UNIFIED_CREATION_SPECS[playId]
);
}
export function listUnifiedCreationSpecs() {
return Object.values(FALLBACK_UNIFIED_CREATION_SPECS);
}