feat: unify creation entry templates
This commit is contained in:
@@ -26,6 +26,7 @@ type BigFishAgentWorkspaceProps = {
|
||||
onBack: () => void;
|
||||
onSubmitMessage: (payload: SendBigFishMessageRequest) => void;
|
||||
onExecuteAction: (payload: ExecuteBigFishActionRequest) => void;
|
||||
showBackButton?: boolean;
|
||||
};
|
||||
|
||||
const BIG_FISH_AGENT_THEME: CreationAgentTheme = {
|
||||
@@ -87,6 +88,7 @@ export function BigFishAgentWorkspace({
|
||||
onBack,
|
||||
onSubmitMessage,
|
||||
onExecuteAction,
|
||||
showBackButton = true,
|
||||
}: BigFishAgentWorkspaceProps) {
|
||||
return (
|
||||
<CreationAgentWorkspace
|
||||
@@ -100,6 +102,7 @@ export function BigFishAgentWorkspace({
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={createCreationAgentChatQuickActions()}
|
||||
showBackButton={showBackButton}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage(
|
||||
|
||||
@@ -359,7 +359,15 @@ test('creation agent workspace hides hero copy area when title and summary are a
|
||||
);
|
||||
|
||||
expect(screen.queryByText('统一共创')).toBeNull();
|
||||
expect(screen.getByText('创作进度')).toBeTruthy();
|
||||
expect(screen.getByText('创作进度').className).toContain(
|
||||
'creation-agent-hero__progress-label',
|
||||
);
|
||||
expect(screen.getByText('60%').className).toContain(
|
||||
'creation-agent-hero__progress-value',
|
||||
);
|
||||
expect(screen.getByText(/方向已经成形/u).className).toContain(
|
||||
'creation-agent-hero__progress-hint',
|
||||
);
|
||||
});
|
||||
|
||||
test('creation agent workspace stops auto-follow when user scrolls away from bottom', () => {
|
||||
|
||||
@@ -78,6 +78,7 @@ type CreationAgentWorkspaceProps = {
|
||||
referenceImagePreviewSrc?: string | null;
|
||||
referenceImageLabel?: string | null;
|
||||
referenceImageError?: string | null;
|
||||
showBackButton?: boolean;
|
||||
onBack: () => void;
|
||||
onSubmitText: (text: string, quickActionKey?: string) => void;
|
||||
onPrimaryAction: () => void;
|
||||
@@ -299,6 +300,7 @@ export function CreationAgentWorkspace({
|
||||
referenceImagePreviewSrc = null,
|
||||
referenceImageLabel = null,
|
||||
referenceImageError = null,
|
||||
showBackButton = true,
|
||||
onBack,
|
||||
onSubmitText,
|
||||
onPrimaryAction,
|
||||
@@ -465,18 +467,22 @@ export function CreationAgentWorkspace({
|
||||
return (
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col gap-3 overflow-hidden px-1 sm:px-0">
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-[1.8rem] px-4 py-4 text-white shadow-[0_20px_60px_rgba(15,23,42,0.18)] sm:px-5 ${theme.heroClass}`}
|
||||
className={`creation-agent-hero relative overflow-hidden rounded-[1.8rem] px-4 py-4 text-white shadow-[0_20px_60px_rgba(15,23,42,0.18)] sm:px-5 ${theme.heroClass}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
aria-label="返回"
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/16 bg-white/10 text-white/84 disabled:opacity-45"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
{showBackButton ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="返回"
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className="creation-agent-hero__icon-button inline-flex h-10 w-10 items-center justify-center rounded-full border border-white/20 bg-white/10 text-white/80 disabled:opacity-45"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</button>
|
||||
) : (
|
||||
<span aria-hidden="true" />
|
||||
)}
|
||||
{canShowPrimaryAction ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -498,7 +504,7 @@ export function CreationAgentWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
{session.assistantSummary ? (
|
||||
<div className="mt-2 max-w-2xl text-sm leading-6 text-white/76">
|
||||
<div className="creation-agent-hero__summary mt-2 max-w-2xl text-sm leading-6 text-white/75">
|
||||
{session.assistantSummary}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -507,15 +513,15 @@ export function CreationAgentWorkspace({
|
||||
|
||||
<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">
|
||||
<span className="creation-agent-hero__progress-label text-xs font-semibold tracking-[0.14em] text-white/75">
|
||||
创作进度
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-white/88">
|
||||
<span className="creation-agent-hero__progress-value text-sm font-semibold text-white/90">
|
||||
{progress}%
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-2 overflow-hidden rounded-full bg-white/12"
|
||||
className="creation-agent-hero__progress-track h-2 overflow-hidden rounded-full bg-white/20"
|
||||
role="progressbar"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
@@ -526,7 +532,7 @@ export function CreationAgentWorkspace({
|
||||
style={{ width: progressFillWidth }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-xs leading-5 text-white/64">
|
||||
<div className="creation-agent-hero__progress-hint mt-2 text-xs leading-5 text-white/70">
|
||||
{resolveCreationAgentProgressHint(progress, progressCopy)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -539,7 +545,7 @@ export function CreationAgentWorkspace({
|
||||
type="button"
|
||||
disabled={isBusy}
|
||||
onClick={() => onQuickAction?.(action)}
|
||||
className="rounded-full border border-white/14 bg-white/10 px-3 py-1.5 text-xs font-semibold text-white/78 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
className="creation-agent-hero__quick-action rounded-full border border-white/20 bg-white/10 px-3 py-1.5 text-xs font-semibold text-white/80 disabled:cursor-not-allowed disabled:opacity-45"
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
@@ -582,7 +588,7 @@ export function CreationAgentWorkspace({
|
||||
</div>
|
||||
|
||||
{referenceImagePreviewSrc ? (
|
||||
<div className="mx-4 mb-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/72 px-3 py-2">
|
||||
<div className="mx-4 mb-3 flex items-center gap-3 rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/70 px-3 py-2">
|
||||
<div className="h-12 w-12 shrink-0 overflow-hidden rounded-[0.9rem] bg-[var(--platform-track-fill)]">
|
||||
<img
|
||||
src={referenceImagePreviewSrc}
|
||||
|
||||
@@ -28,6 +28,7 @@ type CreativeAgentWorkspaceProps = {
|
||||
error: string | null;
|
||||
eventLog: CreativeAgentSseEvent[];
|
||||
onBack: () => void;
|
||||
showBackButton?: boolean;
|
||||
onSubmitMessage: (payload: {
|
||||
clientMessageId: string;
|
||||
content: CreativeAgentInputPart[];
|
||||
@@ -101,6 +102,7 @@ export function CreativeAgentWorkspace({
|
||||
error,
|
||||
eventLog,
|
||||
onBack,
|
||||
showBackButton = true,
|
||||
onSubmitMessage,
|
||||
onConfirmTemplate,
|
||||
onCancelTemplate,
|
||||
@@ -131,17 +133,21 @@ export function CreativeAgentWorkspace({
|
||||
return (
|
||||
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full flex-col xl:max-w-[min(100%,88rem)] xl:px-1">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className={`platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</span>
|
||||
</button>
|
||||
{showBackButton ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBack}
|
||||
disabled={isBusy}
|
||||
className={`platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
返回
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<span aria-hidden="true" />
|
||||
)}
|
||||
<div className="platform-pill platform-pill--cool px-3 text-[11px]">
|
||||
{CREATIVE_AGENT_STAGE_LABEL[stage]}
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,7 @@ type CustomWorldAgentWorkspaceProps = {
|
||||
onBack: () => void;
|
||||
onSubmitMessage: (payload: SendCustomWorldAgentMessageRequest) => void;
|
||||
onExecuteAction: (payload: CustomWorldAgentActionRequest) => void;
|
||||
showBackButton?: boolean;
|
||||
};
|
||||
|
||||
const CUSTOM_WORLD_AGENT_THEME: CreationAgentTheme = {
|
||||
@@ -164,6 +165,7 @@ export function CustomWorldAgentWorkspace({
|
||||
onBack,
|
||||
onSubmitMessage,
|
||||
onExecuteAction,
|
||||
showBackButton = true,
|
||||
}: CustomWorldAgentWorkspaceProps) {
|
||||
const isBusy =
|
||||
isCreationAgentOperationBusy(activeOperation) || isStreamingReply;
|
||||
@@ -194,6 +196,7 @@ export function CustomWorldAgentWorkspace({
|
||||
isStreamingReply={isStreamingReply}
|
||||
isBusy={isBusy}
|
||||
quickActions={createCreationAgentChatQuickActions()}
|
||||
showBackButton={showBackButton}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
submitMessage(text);
|
||||
|
||||
@@ -7,6 +7,11 @@ import { buildCreationWorkShelfItems } from './creationWorkShelf';
|
||||
import { CustomWorldCreationHub } from './CustomWorldCreationHub';
|
||||
|
||||
const noopCreateType = () => {};
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function buildUpdatedAtDaysAgo(daysAgo: number) {
|
||||
return new Date(Date.now() - daysAgo * DAY_MS).toISOString();
|
||||
}
|
||||
|
||||
const testEntryConfig = {
|
||||
startCard: {
|
||||
@@ -90,6 +95,20 @@ const testEntryConfig = {
|
||||
categorySortOrder: 20,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
id: 'jump-hop',
|
||||
title: '跳一跳',
|
||||
subtitle: '节奏跳跃挑战',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/jump-hop.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 45,
|
||||
categoryId: 'recommended',
|
||||
categoryLabel: '热门推荐',
|
||||
categorySortOrder: 20,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
{
|
||||
id: 'square-hole',
|
||||
title: '方洞',
|
||||
@@ -285,7 +304,7 @@ test('creation start card renders html banner in an empty-permission sandbox', (
|
||||
expect(html).toContain('<section><h1>自定义横幅</h1></section>');
|
||||
});
|
||||
|
||||
test('creation start card renders recent tab from real shelf summaries', () => {
|
||||
test('creation start card renders recent tab with the same template cards', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
items={[
|
||||
@@ -297,7 +316,7 @@ test('creation start card renders recent tab from real shelf summaries', () => {
|
||||
subtitle: '待完善草稿',
|
||||
summary: '这条内容来自作品架摘要。',
|
||||
coverImageSrc: null,
|
||||
updatedAt: new Date('2026-06-01T12:00:00.000Z').toISOString(),
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
publishedAt: null,
|
||||
stage: 'clarifying',
|
||||
stageLabel: '待完善草稿',
|
||||
@@ -317,7 +336,6 @@ test('creation start card renders recent tab from real shelf summaries', () => {
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
getWorkState={() => ({ isGenerating: true })}
|
||||
mode="start-only"
|
||||
/>,
|
||||
);
|
||||
@@ -325,12 +343,16 @@ test('creation start card renders recent tab from real shelf summaries', () => {
|
||||
expect(html).toContain('aria-label="创作入口页签"');
|
||||
expect(html).toContain('role="tab"');
|
||||
expect(html).toContain('aria-selected="true"');
|
||||
expect(html).toContain('creation-recent-work-grid');
|
||||
expect(html).toContain('aria-label="打开最近创作 1"');
|
||||
expect(html).toContain('creation-template-list__grid');
|
||||
expect(html).toContain('creation-template-card');
|
||||
expect(html).toContain('最近创作');
|
||||
expect(html).toContain('后端返回的最近草稿');
|
||||
expect(html).toContain('这条内容来自作品架摘要');
|
||||
expect(html).toContain('生成中');
|
||||
expect(html).toContain('仅显示最近7天内使用过的模板');
|
||||
expect(html).toContain('文字冒险');
|
||||
expect(html).toContain('经典 RPG 体验');
|
||||
expect(html).not.toContain('creation-recent-work-grid');
|
||||
expect(html).not.toContain('打开最近创作');
|
||||
expect(html).not.toContain('后端返回的最近草稿');
|
||||
expect(html).not.toContain('这条内容来自作品架摘要');
|
||||
});
|
||||
|
||||
test('creation start card prefers backend recent summaries over local pending placeholders', () => {
|
||||
@@ -344,7 +366,7 @@ test('creation start card prefers backend recent summaries over local pending pl
|
||||
subtitle: '真实作品架摘要',
|
||||
summary: '最近创作应该只读取后端摘要。',
|
||||
coverImageSrc: null,
|
||||
updatedAt: new Date('2026-06-03T12:00:00.000Z').toISOString(),
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
publishedAt: null,
|
||||
stage: 'failed',
|
||||
stageLabel: '生成失败',
|
||||
@@ -370,7 +392,7 @@ test('creation start card prefers backend recent summaries over local pending pl
|
||||
subtitle: '本地占位',
|
||||
summary: '这条占位不应该进入最近创作。',
|
||||
coverImageSrc: null,
|
||||
updatedAt: new Date('2026-06-04T12:00:00.000Z').toISOString(),
|
||||
updatedAt: buildUpdatedAtDaysAgo(0),
|
||||
publishedAt: null,
|
||||
stage: 'generating',
|
||||
stageLabel: '生成中',
|
||||
@@ -396,12 +418,56 @@ test('creation start card prefers backend recent summaries over local pending pl
|
||||
);
|
||||
|
||||
expect(html).toContain('最近创作');
|
||||
expect(html).toContain('后端最近草稿');
|
||||
expect(html).toContain('最近创作应该只读取后端摘要');
|
||||
expect(html).toContain('文字冒险');
|
||||
expect(html).toContain('经典 RPG 体验');
|
||||
expect(html).not.toContain('后端最近草稿');
|
||||
expect(html).not.toContain('最近创作应该只读取后端摘要');
|
||||
expect(html).not.toContain('本地生成中占位');
|
||||
});
|
||||
|
||||
test('creation start card marks backend jump-hop generating draft in recent tab', () => {
|
||||
test('creation start card excludes works older than the recent window', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
items={[
|
||||
{
|
||||
workId: 'draft:old-session',
|
||||
sourceType: 'agent_session',
|
||||
status: 'draft',
|
||||
title: '八天前的草稿',
|
||||
subtitle: '旧草稿',
|
||||
summary: '这条草稿已经超过最近创作期限。',
|
||||
coverImageSrc: null,
|
||||
updatedAt: buildUpdatedAtDaysAgo(8),
|
||||
publishedAt: null,
|
||||
stage: 'clarifying',
|
||||
stageLabel: '待完善草稿',
|
||||
playableNpcCount: 0,
|
||||
landmarkCount: 0,
|
||||
sessionId: 'old-session',
|
||||
profileId: null,
|
||||
canResume: true,
|
||||
canEnterWorld: false,
|
||||
},
|
||||
]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
mode="start-only"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).not.toContain('最近创作');
|
||||
expect(html).not.toContain('仅显示最近7天内使用过的模板');
|
||||
expect(html).not.toContain('八天前的草稿');
|
||||
expect(html).not.toContain('这条草稿已经超过最近创作期限');
|
||||
});
|
||||
|
||||
test('creation start card maps backend jump-hop draft to template card', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
items={[]}
|
||||
@@ -420,7 +486,7 @@ test('creation start card marks backend jump-hop generating draft in recent tab'
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: new Date('2026-06-03T13:00:00.000Z').toISOString(),
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'generating',
|
||||
@@ -440,9 +506,11 @@ test('creation start card marks backend jump-hop generating draft in recent tab'
|
||||
);
|
||||
|
||||
expect(html).toContain('最近创作');
|
||||
expect(html).toContain('跳一跳生成草稿');
|
||||
expect(html).toContain('后端仍在生成跳一跳玩法');
|
||||
expect(html).toContain('生成中');
|
||||
expect(html).toContain('跳一跳');
|
||||
expect(html).toContain('节奏跳跃挑战');
|
||||
expect(html).toContain('creation-template-card');
|
||||
expect(html).not.toContain('跳一跳生成草稿');
|
||||
expect(html).not.toContain('后端仍在生成跳一跳玩法');
|
||||
});
|
||||
|
||||
test('creation start card includes failed drafts in the recent tab', () => {
|
||||
@@ -457,7 +525,7 @@ test('creation start card includes failed drafts in the recent tab', () => {
|
||||
subtitle: '生成失败',
|
||||
summary: '失败草稿也来自真实作品架摘要。',
|
||||
coverImageSrc: null,
|
||||
updatedAt: new Date('2026-06-02T12:00:00.000Z').toISOString(),
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
publishedAt: null,
|
||||
stage: 'failed',
|
||||
stageLabel: '生成失败',
|
||||
@@ -482,13 +550,15 @@ test('creation start card includes failed drafts in the recent tab', () => {
|
||||
);
|
||||
|
||||
expect(html).toContain('最近创作');
|
||||
expect(html).toContain('creation-recent-work-grid');
|
||||
expect(html).toContain('失败但仍可恢复的草稿');
|
||||
expect(html).toContain('失败草稿也来自真实作品架摘要');
|
||||
expect(html).toContain('生成失败');
|
||||
expect(html).toContain('creation-template-list__grid');
|
||||
expect(html).toContain('文字冒险');
|
||||
expect(html).toContain('经典 RPG 体验');
|
||||
expect(html).not.toContain('creation-recent-work-grid');
|
||||
expect(html).not.toContain('失败但仍可恢复的草稿');
|
||||
expect(html).not.toContain('失败草稿也来自真实作品架摘要');
|
||||
});
|
||||
|
||||
test('creation start card maps failed mini-game drafts into recent status labels', () => {
|
||||
test('creation start card maps failed mini-game drafts into recent template cards', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CustomWorldCreationHub
|
||||
items={[]}
|
||||
@@ -514,7 +584,7 @@ test('creation start card maps failed mini-game drafts into recent status labels
|
||||
difficulty: 1,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: '2026-06-02T13:00:00.000Z',
|
||||
updatedAt: buildUpdatedAtDaysAgo(1),
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'failed',
|
||||
@@ -525,9 +595,11 @@ test('creation start card maps failed mini-game drafts into recent status labels
|
||||
);
|
||||
|
||||
expect(html).toContain('最近创作');
|
||||
expect(html).toContain('失败抓大鹅草稿');
|
||||
expect(html).toContain('失败的小玩法草稿也应该进入最近创作。');
|
||||
expect(html).toContain('生成失败');
|
||||
expect(html).toContain('抓大鹅');
|
||||
expect(html).toContain('3D 消除关卡');
|
||||
expect(html).toContain('creation-template-card');
|
||||
expect(html).not.toContain('失败抓大鹅草稿');
|
||||
expect(html).not.toContain('失败的小玩法草稿也应该进入最近创作。');
|
||||
});
|
||||
|
||||
test('creation start card keeps typography in compact UI scale', () => {
|
||||
|
||||
@@ -20,13 +20,13 @@ import type {
|
||||
import { isPlatformCreationTypeVisible } from '../platform-entry/platformEntryCreationTypes';
|
||||
import {
|
||||
buildCreationWorkShelfItems,
|
||||
getCreationWorkShelfItemTime,
|
||||
type CreationWorkShelfItem,
|
||||
type CreationWorkShelfMetricId,
|
||||
type CreationWorkShelfRuntimeState,
|
||||
} from './creationWorkShelf';
|
||||
import {
|
||||
CustomWorldCreationStartCard,
|
||||
type CreationEntryRecentWorkCard,
|
||||
} from './CustomWorldCreationStartCard';
|
||||
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
|
||||
import {
|
||||
@@ -37,6 +37,9 @@ import {
|
||||
const WORK_GRID_CLASS =
|
||||
'creation-work-list grid min-w-0 gap-3 sm:gap-3.5 xl:gap-4';
|
||||
const WORK_METRIC_CACHE_KEY = 'genarrative.creationHub.publishedMetrics.v1';
|
||||
const RECENT_CREATION_WINDOW_DAYS = 7;
|
||||
const RECENT_CREATION_WINDOW_MS =
|
||||
RECENT_CREATION_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
||||
|
||||
type WorkMetricSnapshot = Record<
|
||||
string,
|
||||
@@ -92,7 +95,7 @@ type CustomWorldCreationHubProps = {
|
||||
item: CreationWorkShelfItem,
|
||||
) => CreationWorkShelfRuntimeState | null;
|
||||
onOpenShelfItem?: (item: CreationWorkShelfItem) => void;
|
||||
// 中文注释:底部加号入口的最近创作可传入后端作品架摘要,避免混入本地 pending 占位。
|
||||
// 中文注释:底部加号入口可传入后端作品架摘要,用于推导最近使用过的模板。
|
||||
recentWorkItems?: CreationWorkShelfItem[];
|
||||
mode?: 'full' | 'start-only' | 'works-only';
|
||||
};
|
||||
@@ -160,35 +163,7 @@ function writeWorkMetricSnapshot(items: CreationWorkShelfItem[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化入口页最近创作状态,失败草稿和生成中草稿都保留真实后端摘要语义。 */
|
||||
function formatRecentWorkStatusLabel(item: CreationWorkShelfItem) {
|
||||
if (item.isGenerating) {
|
||||
return '生成中';
|
||||
}
|
||||
|
||||
if (item.status === 'published') {
|
||||
return '已发布';
|
||||
}
|
||||
|
||||
switch (item.source.kind) {
|
||||
case 'rpg':
|
||||
return item.source.item.stageLabel?.trim() || '草稿';
|
||||
case 'match3d':
|
||||
case 'jump-hop':
|
||||
case 'wooden-fish':
|
||||
return item.source.item.generationStatus === 'failed'
|
||||
? '生成失败'
|
||||
: '草稿';
|
||||
case 'bark-battle':
|
||||
return item.source.item.generationStatus === 'partial_failed'
|
||||
? '生成失败'
|
||||
: '草稿';
|
||||
default:
|
||||
return '草稿';
|
||||
}
|
||||
}
|
||||
|
||||
/** 渲染底部加号创作入口页与草稿作品架,入口页最近创作只来自后端作品摘要。 */
|
||||
/** 渲染底部加号创作入口页与草稿作品架,最近创作复用最近使用过的模板入口。 */
|
||||
export function CustomWorldCreationHub({
|
||||
items,
|
||||
loading,
|
||||
@@ -348,19 +323,21 @@ export function CustomWorldCreationHub({
|
||||
),
|
||||
[activeFilter, shelfItems],
|
||||
);
|
||||
// 中文注释:最近创作只来自作品架摘要;平台入口会传入不含本地 pending 占位的后端摘要。
|
||||
// 中文注释:最近创作只取 7 天内作品架摘要,再推导模板 ID 复用模板入口卡片。
|
||||
const recentCreationCutoffMs = Date.now() - RECENT_CREATION_WINDOW_MS;
|
||||
const recentWorkItems =
|
||||
mode === 'start-only'
|
||||
? (recentWorkSourceItems ?? shelfItems).slice(0, 4)
|
||||
? (recentWorkSourceItems ?? shelfItems)
|
||||
.filter(
|
||||
(item) =>
|
||||
getCreationWorkShelfItemTime(item.updatedAt) >=
|
||||
recentCreationCutoffMs,
|
||||
)
|
||||
.slice(0, 4)
|
||||
: [];
|
||||
const recentWorkCards: CreationEntryRecentWorkCard[] = recentWorkItems.map(
|
||||
(item) => ({
|
||||
id: `${item.kind}:${item.id}`,
|
||||
title: item.title,
|
||||
summary: item.summary,
|
||||
statusLabel: formatRecentWorkStatusLabel(item),
|
||||
}),
|
||||
);
|
||||
const recentCreationTypeIds = [
|
||||
...new Set(recentWorkItems.map((item) => item.kind)),
|
||||
];
|
||||
|
||||
function handleOpenShelfItem(item: CreationWorkShelfItem) {
|
||||
onOpenShelfItem?.(item);
|
||||
@@ -427,14 +404,9 @@ export function CustomWorldCreationHub({
|
||||
busy={createBusy}
|
||||
entryConfig={entryConfig}
|
||||
creationTypes={creationTypes}
|
||||
recentWorks={recentWorkCards}
|
||||
recentCreationTypeIds={recentCreationTypeIds}
|
||||
recentWindowDays={RECENT_CREATION_WINDOW_DAYS}
|
||||
onCreateType={onCreateType}
|
||||
onOpenRecentWork={(index) => {
|
||||
const item = recentWorkItems[index];
|
||||
if (item) {
|
||||
handleOpenShelfItem(item);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
type PlatformCreationTypeId,
|
||||
} from '../platform-entry/platformEntryCreationTypes';
|
||||
|
||||
/** 底部加号创作入口页的渲染参数,最近创作只接受作品架真实摘要。 */
|
||||
/** 底部加号创作入口页的渲染参数,最近创作用作品架摘要推导模板入口。 */
|
||||
type CustomWorldCreationStartCardProps = {
|
||||
busy?: boolean;
|
||||
entryConfig: CreationEntryConfig;
|
||||
creationTypes: readonly PlatformCreationTypeCard[];
|
||||
recentWorks?: readonly CreationEntryRecentWorkCard[];
|
||||
recentCreationTypeIds?: readonly PlatformCreationTypeId[];
|
||||
recentWindowDays?: number;
|
||||
onCreateType: (type: PlatformCreationTypeId) => void;
|
||||
onOpenRecentWork?: (index: number) => void;
|
||||
};
|
||||
|
||||
/** 创作入口公告卡兼容结构化和 HTML 两种后台配置。 */
|
||||
@@ -26,14 +26,6 @@ type CreationEventBannerCard = CreationEntryEventBannerConfig;
|
||||
const CREATION_ENTRY_BANNER_AUTOPLAY_MS = 4200;
|
||||
const CREATION_ENTRY_RECENT_TAB_ID = '__recent_creation__';
|
||||
|
||||
/** 底部加号创作入口页最近创作页签的展示数据,只来自后端作品架摘要。 */
|
||||
export type CreationEntryRecentWorkCard = {
|
||||
id: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
statusLabel: string;
|
||||
};
|
||||
|
||||
/** 判断模板 badge 是否需要展示,普通可创建态不额外占用卡片空间。 */
|
||||
function shouldShowCreationBadge(badge: string) {
|
||||
const normalizedBadge = badge.trim();
|
||||
@@ -84,29 +76,41 @@ export function CustomWorldCreationStartCard({
|
||||
busy = false,
|
||||
entryConfig,
|
||||
creationTypes,
|
||||
recentWorks = [],
|
||||
recentCreationTypeIds = [],
|
||||
recentWindowDays = 7,
|
||||
onCreateType,
|
||||
onOpenRecentWork,
|
||||
}: CustomWorldCreationStartCardProps) {
|
||||
const creationTypeGroups = useMemo(
|
||||
() => groupVisiblePlatformCreationTypes(creationTypes),
|
||||
[creationTypes],
|
||||
);
|
||||
const recentCreationTypes = useMemo(() => {
|
||||
const creationTypeById = new Map(
|
||||
creationTypes
|
||||
.filter((item) => !item.hidden)
|
||||
.map((item) => [item.id, item] as const),
|
||||
);
|
||||
return [...new Set(recentCreationTypeIds)]
|
||||
.map((id) => creationTypeById.get(id))
|
||||
.filter((item): item is PlatformCreationTypeCard => Boolean(item));
|
||||
}, [creationTypes, recentCreationTypeIds]);
|
||||
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
||||
const hasRecentWorks = recentWorks.length > 0;
|
||||
const hasRecentCreationTypes = recentCreationTypes.length > 0;
|
||||
const activeTabId =
|
||||
activeCategoryId ??
|
||||
(hasRecentWorks
|
||||
(hasRecentCreationTypes
|
||||
? CREATION_ENTRY_RECENT_TAB_ID
|
||||
: creationTypeGroups[0]?.id ?? null);
|
||||
const isRecentTabActive =
|
||||
hasRecentWorks && activeTabId === CREATION_ENTRY_RECENT_TAB_ID;
|
||||
hasRecentCreationTypes && activeTabId === CREATION_ENTRY_RECENT_TAB_ID;
|
||||
const activeGroup = isRecentTabActive
|
||||
? null
|
||||
: creationTypeGroups.find((group) => group.id === activeTabId) ??
|
||||
creationTypeGroups[0] ??
|
||||
null;
|
||||
const visibleCreationTypes = activeGroup?.items ?? [];
|
||||
const visibleCreationTypes = isRecentTabActive
|
||||
? recentCreationTypes
|
||||
: activeGroup?.items ?? [];
|
||||
const eventBanners = useMemo(
|
||||
() => resolveCreationEntryEventBanners(entryConfig),
|
||||
[entryConfig],
|
||||
@@ -119,14 +123,14 @@ export function CustomWorldCreationStartCard({
|
||||
}, [eventBanners.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRecentWorks) {
|
||||
if (hasRecentCreationTypes) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveCategoryId((currentId) =>
|
||||
currentId === CREATION_ENTRY_RECENT_TAB_ID ? null : currentId,
|
||||
);
|
||||
}, [hasRecentWorks]);
|
||||
}, [hasRecentCreationTypes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventBanners.length <= 1) {
|
||||
@@ -263,7 +267,7 @@ export function CustomWorldCreationStartCard({
|
||||
role="tablist"
|
||||
aria-label="创作入口页签"
|
||||
>
|
||||
{hasRecentWorks ? (
|
||||
{hasRecentCreationTypes ? (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -306,77 +310,59 @@ export function CustomWorldCreationStartCard({
|
||||
</div>
|
||||
|
||||
{isRecentTabActive ? (
|
||||
<div className="creation-recent-work-grid mt-2 grid grid-cols-2 gap-2 sm:mt-3 sm:gap-3">
|
||||
{recentWorks.map((item, index) => (
|
||||
<div className="creation-template-list__recent-window mt-2 text-[11px] font-bold leading-4 text-[#8b6654] sm:text-xs">
|
||||
仅显示最近{recentWindowDays}天内使用过的模板
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="creation-template-list__grid mt-2 grid grid-cols-2 gap-2 sm:mt-3 sm:gap-3">
|
||||
{visibleCreationTypes.map((item) => {
|
||||
const disabled = item.locked || busy;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
aria-label={`打开最近创作 ${index + 1}`}
|
||||
className="creation-recent-work-card min-h-[7.5rem] rounded-[1rem] border border-[#eadbd3] bg-white p-3 text-left shadow-[0_10px_22px_rgba(174,111,73,0.1)]"
|
||||
onClick={() => onOpenRecentWork?.(index)}
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
onCreateType(item.id);
|
||||
}}
|
||||
className={`creation-template-card platform-interactive-card relative flex min-h-[12.5rem] flex-col overflow-hidden rounded-[1rem] border bg-white p-0 text-left transition sm:min-h-[15rem] sm:rounded-[1.2rem] ${
|
||||
item.locked
|
||||
? 'cursor-not-allowed border-[#eadbd3] text-[#725b4d] opacity-72'
|
||||
: 'border-[#eadbd3] text-[#2f211b] hover:border-[#dc9a72] hover:shadow-[0_16px_34px_rgba(174,111,73,0.14)]'
|
||||
} ${busy && !item.locked ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<div className="line-clamp-1 text-sm font-black text-[#2f211b]">
|
||||
{item.title}
|
||||
<div className="creation-template-card__media relative aspect-[1.32/1] w-full overflow-hidden bg-[#f7ebe3]">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{shouldShowCreationBadge(item.badge) ? (
|
||||
<span className="absolute left-2 top-2 max-w-[calc(100%-1rem)] rounded-full bg-[#b66a3e] px-2 py-0.5 text-xs font-black text-white shadow-sm sm:left-3 sm:top-3 sm:px-2.5 sm:py-1">
|
||||
{item.badge}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="creation-template-card__cost-badge absolute bottom-2 right-2 inline-flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-full bg-[#fff7ec]/92 px-2 py-1 text-[11px] font-black leading-4 text-[#b65f2c] shadow-[0_8px_18px_rgba(119,72,44,0.16)]">
|
||||
<Coins className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">10-20泥点数</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-3 text-xs font-semibold leading-4 text-[#6f5a4c]">
|
||||
{item.summary}
|
||||
</div>
|
||||
<div className="mt-2 text-[11px] font-bold text-[#b65f2c]">
|
||||
{item.statusLabel}
|
||||
|
||||
<div className="creation-template-card__body flex min-h-[4.6rem] flex-1 flex-col bg-white px-2.5 pb-2.5 pt-2.5 text-[#2f211b] sm:min-h-[5.4rem] sm:px-3.5 sm:pb-3.5">
|
||||
<div className="creation-template-card__title line-clamp-1 text-sm font-black leading-5 text-[#2f211b]">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="creation-template-card__subtitle mt-1 line-clamp-2 text-xs font-semibold leading-4 text-[#6f5a4c] sm:leading-5">
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="creation-template-list__grid mt-2 grid grid-cols-2 gap-2 sm:mt-3 sm:gap-3">
|
||||
{visibleCreationTypes.map((item) => {
|
||||
const disabled = item.locked || busy;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
onCreateType(item.id);
|
||||
}}
|
||||
className={`creation-template-card platform-interactive-card relative flex min-h-[12.5rem] flex-col overflow-hidden rounded-[1rem] border bg-white p-0 text-left transition sm:min-h-[15rem] sm:rounded-[1.2rem] ${
|
||||
item.locked
|
||||
? 'cursor-not-allowed border-[#eadbd3] text-[#725b4d] opacity-72'
|
||||
: 'border-[#eadbd3] text-[#2f211b] hover:border-[#dc9a72] hover:shadow-[0_16px_34px_rgba(174,111,73,0.14)]'
|
||||
} ${busy && !item.locked ? 'opacity-70' : ''}`}
|
||||
>
|
||||
<div className="creation-template-card__media relative aspect-[1.32/1] w-full overflow-hidden bg-[#f7ebe3]">
|
||||
<img
|
||||
src={item.imageSrc}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{shouldShowCreationBadge(item.badge) ? (
|
||||
<span className="absolute left-2 top-2 max-w-[calc(100%-1rem)] rounded-full bg-[#b66a3e] px-2 py-0.5 text-xs font-black text-white shadow-sm sm:left-3 sm:top-3 sm:px-2.5 sm:py-1">
|
||||
{item.badge}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="creation-template-card__cost-badge absolute bottom-2 right-2 inline-flex max-w-[calc(100%-1rem)] items-center gap-1 rounded-full bg-[#fff7ec]/92 px-2 py-1 text-[11px] font-black leading-4 text-[#b65f2c] shadow-[0_8px_18px_rgba(119,72,44,0.16)]">
|
||||
<Coins className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">10-20泥点数</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="creation-template-card__body flex min-h-[4.6rem] flex-1 flex-col bg-white px-2.5 pb-2.5 pt-2.5 text-[#2f211b] sm:min-h-[5.4rem] sm:px-3.5 sm:pb-3.5">
|
||||
<div className="creation-template-card__title line-clamp-1 text-sm font-black leading-5 text-[#2f211b]">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="creation-template-card__subtitle mt-1 line-clamp-2 text-xs font-semibold leading-4 text-[#6f5a4c] sm:leading-5">
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1113,7 +1113,7 @@ function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) {
|
||||
case 'match3d':
|
||||
return item.source.item.generationStatus === 'generating';
|
||||
case 'jump-hop':
|
||||
// 中文注释:跳一跳后端生成中草稿也要同步到作品架与最近创作状态。
|
||||
// 中文注释:跳一跳后端生成中草稿也要同步到作品架,并参与最近模板推导。
|
||||
return item.source.item.generationStatus === 'generating';
|
||||
case 'puzzle':
|
||||
return isPersistedPuzzleDraftGenerating(item.source.item);
|
||||
|
||||
@@ -222,7 +222,11 @@ import {
|
||||
type MiniGameDraftGenerationState,
|
||||
} from '../../services/miniGameDraftGenerationProgress';
|
||||
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
|
||||
import { getUnifiedCreationSpec } from '../unified-creation/unifiedCreationSpecs';
|
||||
import { UnifiedCreationPage } from '../unified-creation/UnifiedCreationPage';
|
||||
import {
|
||||
getUnifiedCreationSpec,
|
||||
type UnifiedCreationPlayId,
|
||||
} from '../unified-creation/unifiedCreationSpecs';
|
||||
import {
|
||||
buildBabyObjectMatchPublicWorkCode,
|
||||
buildBarkBattlePublicWorkCode,
|
||||
@@ -3825,6 +3829,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
const entries = creationEntryConfig?.creationTypes ?? [];
|
||||
return new Map(entries.map((entry) => [entry.id, entry]));
|
||||
}, [creationEntryConfig]);
|
||||
const getUnifiedSpec = useCallback(
|
||||
(playId: UnifiedCreationPlayId) =>
|
||||
getUnifiedCreationSpec(playId, unifiedCreationConfigById.get(playId)),
|
||||
[unifiedCreationConfigById],
|
||||
);
|
||||
const isBigFishCreationVisible = isPlatformCreationTypeVisible(
|
||||
creationEntryTypes,
|
||||
'big-fish',
|
||||
@@ -16614,30 +16623,42 @@ export function PlatformEntryFlowShellImpl({
|
||||
<LazyPanelFallback label="正在加载 Agent 共创工作区..." />
|
||||
}
|
||||
>
|
||||
{sessionController.agentSession ? (
|
||||
<CustomWorldAgentWorkspace
|
||||
session={sessionController.agentSession}
|
||||
activeOperation={sessionController.agentOperation}
|
||||
streamingReplyText={sessionController.streamingAgentReplyText}
|
||||
isStreamingReply={sessionController.isStreamingAgentReply}
|
||||
onBack={leaveAgentWorkspace}
|
||||
onSubmitMessage={(payload) => {
|
||||
void sessionController.submitAgentMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void sessionController.executeAgentAction(payload);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||||
{sessionController.isLoadingAgentSession
|
||||
? '正在准备 Agent 共创工作区...'
|
||||
: sessionController.agentWorkspaceRestoreError ||
|
||||
'正在恢复创作工作区...'}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('rpg')}
|
||||
onBack={leaveAgentWorkspace}
|
||||
isBackDisabled={
|
||||
sessionController.isStreamingAgentReply ||
|
||||
Boolean(sessionController.agentOperation)
|
||||
}
|
||||
>
|
||||
{sessionController.agentSession ? (
|
||||
<CustomWorldAgentWorkspace
|
||||
session={sessionController.agentSession}
|
||||
activeOperation={sessionController.agentOperation}
|
||||
streamingReplyText={
|
||||
sessionController.streamingAgentReplyText
|
||||
}
|
||||
isStreamingReply={sessionController.isStreamingAgentReply}
|
||||
onBack={leaveAgentWorkspace}
|
||||
showBackButton={false}
|
||||
onSubmitMessage={(payload) => {
|
||||
void sessionController.submitAgentMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void sessionController.executeAgentAction(payload);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-[var(--platform-text-base)]">
|
||||
{sessionController.isLoadingAgentSession
|
||||
? '正在准备 Agent 共创工作区...'
|
||||
: sessionController.agentWorkspaceRestoreError ||
|
||||
'正在恢复创作工作区...'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -16655,20 +16676,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
<LazyPanelFallback label="正在加载大鱼吃小鱼共创工作区..." />
|
||||
}
|
||||
>
|
||||
<BigFishAgentWorkspace
|
||||
session={bigFishSession}
|
||||
streamingReplyText={streamingBigFishReplyText}
|
||||
isStreamingReply={isStreamingBigFishReply}
|
||||
isBusy={isBigFishBusy || isStreamingBigFishReply}
|
||||
error={bigFishError}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('big-fish')}
|
||||
onBack={leaveBigFishFlow}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitBigFishMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void executeBigFishAction(payload);
|
||||
}}
|
||||
/>
|
||||
isBackDisabled={isBigFishBusy || isStreamingBigFishReply}
|
||||
>
|
||||
<BigFishAgentWorkspace
|
||||
session={bigFishSession}
|
||||
streamingReplyText={streamingBigFishReplyText}
|
||||
isStreamingReply={isStreamingBigFishReply}
|
||||
isBusy={isBigFishBusy || isStreamingBigFishReply}
|
||||
error={bigFishError}
|
||||
onBack={leaveBigFishFlow}
|
||||
showBackButton={false}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitBigFishMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void executeBigFishAction(payload);
|
||||
}}
|
||||
/>
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -16807,10 +16835,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<UnifiedCreationWorkspace
|
||||
playId="match3d"
|
||||
spec={getUnifiedCreationSpec(
|
||||
'match3d',
|
||||
unifiedCreationConfigById.get('match3d'),
|
||||
)}
|
||||
spec={getUnifiedSpec('match3d')}
|
||||
session={match3dSession}
|
||||
isBusy={isStreamingMatch3DReply}
|
||||
error={match3dError}
|
||||
@@ -17058,15 +17083,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载宝贝识物创作..." />}
|
||||
>
|
||||
<BabyObjectMatchWorkspace
|
||||
isBusy={isBabyObjectMatchBusy}
|
||||
error={babyObjectMatchError}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('baby-object-match')}
|
||||
onBack={leaveBabyObjectMatchFlow}
|
||||
initialPayload={babyObjectMatchFormPayload}
|
||||
onCreateDraft={(payload) => {
|
||||
void createBabyObjectMatchDraftFromForm(payload);
|
||||
}}
|
||||
/>
|
||||
isBackDisabled={isBabyObjectMatchBusy}
|
||||
>
|
||||
<BabyObjectMatchWorkspace
|
||||
isBusy={isBabyObjectMatchBusy}
|
||||
error={babyObjectMatchError}
|
||||
onBack={leaveBabyObjectMatchFlow}
|
||||
showBackButton={false}
|
||||
title={null}
|
||||
initialPayload={babyObjectMatchFormPayload}
|
||||
onCreateDraft={(payload) => {
|
||||
void createBabyObjectMatchDraftFromForm(payload);
|
||||
}}
|
||||
/>
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -17219,14 +17252,22 @@ export function PlatformEntryFlowShellImpl({
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载汪汪声浪创作..." />}
|
||||
>
|
||||
<BarkBattleConfigEditor
|
||||
isBusy={isBarkBattleBusy}
|
||||
error={barkBattleError}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('bark-battle')}
|
||||
onBack={leaveBarkBattleFlow}
|
||||
onPreview={(payload) => {
|
||||
void createBarkBattleGeneratingDraft(payload);
|
||||
}}
|
||||
/>
|
||||
isBackDisabled={isBarkBattleBusy}
|
||||
>
|
||||
<BarkBattleConfigEditor
|
||||
isBusy={isBarkBattleBusy}
|
||||
error={barkBattleError}
|
||||
onBack={leaveBarkBattleFlow}
|
||||
showBackButton={false}
|
||||
title={null}
|
||||
onPreview={(payload) => {
|
||||
void createBarkBattleGeneratingDraft(payload);
|
||||
}}
|
||||
/>
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -17244,20 +17285,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
<LazyPanelFallback label="正在加载方洞挑战共创工作区..." />
|
||||
}
|
||||
>
|
||||
<SquareHoleAgentWorkspace
|
||||
session={squareHoleSession}
|
||||
streamingReplyText={streamingSquareHoleReplyText}
|
||||
isStreamingReply={isStreamingSquareHoleReply}
|
||||
isBusy={isSquareHoleBusy || isStreamingSquareHoleReply}
|
||||
error={squareHoleError}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('square-hole')}
|
||||
onBack={leaveSquareHoleFlow}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitSquareHoleMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void executeSquareHoleAction(payload);
|
||||
}}
|
||||
/>
|
||||
isBackDisabled={isSquareHoleBusy || isStreamingSquareHoleReply}
|
||||
>
|
||||
<SquareHoleAgentWorkspace
|
||||
session={squareHoleSession}
|
||||
streamingReplyText={streamingSquareHoleReplyText}
|
||||
isStreamingReply={isStreamingSquareHoleReply}
|
||||
isBusy={isSquareHoleBusy || isStreamingSquareHoleReply}
|
||||
error={squareHoleError}
|
||||
onBack={leaveSquareHoleFlow}
|
||||
showBackButton={false}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitSquareHoleMessage(payload);
|
||||
}}
|
||||
onExecuteAction={(payload) => {
|
||||
void executeSquareHoleAction(payload);
|
||||
}}
|
||||
/>
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -17462,10 +17510,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<UnifiedCreationWorkspace
|
||||
playId="jump-hop"
|
||||
spec={getUnifiedCreationSpec(
|
||||
'jump-hop',
|
||||
unifiedCreationConfigById.get('jump-hop'),
|
||||
)}
|
||||
spec={getUnifiedSpec('jump-hop')}
|
||||
isBusy={isJumpHopBusy}
|
||||
error={jumpHopError}
|
||||
onBack={leaveJumpHopFlow}
|
||||
@@ -17605,10 +17650,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<UnifiedCreationWorkspace
|
||||
playId="wooden-fish"
|
||||
spec={getUnifiedCreationSpec(
|
||||
'wooden-fish',
|
||||
unifiedCreationConfigById.get('wooden-fish'),
|
||||
)}
|
||||
spec={getUnifiedSpec('wooden-fish')}
|
||||
isBusy={isWoodenFishBusy}
|
||||
error={woodenFishError}
|
||||
onBack={leaveWoodenFishFlow}
|
||||
@@ -17724,23 +17766,30 @@ export function PlatformEntryFlowShellImpl({
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载智能创作..." />}
|
||||
>
|
||||
<CreativeAgentWorkspace
|
||||
session={creativeAgentSession}
|
||||
isBusy={isCreativeAgentBusy || isPuzzleBusy}
|
||||
isStreaming={isCreativeAgentStreaming}
|
||||
error={creativeAgentError}
|
||||
eventLog={creativeAgentEvents}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('creative-agent')}
|
||||
onBack={leaveCreativeAgentWorkspace}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitCreativeAgentMessage(payload);
|
||||
}}
|
||||
onConfirmTemplate={(selection) => {
|
||||
void confirmCreativeTemplateSelection(selection);
|
||||
}}
|
||||
onOpenTarget={() => {
|
||||
void openCreativeAgentTarget();
|
||||
}}
|
||||
/>
|
||||
isBackDisabled={isCreativeAgentBusy || isPuzzleBusy}
|
||||
>
|
||||
<CreativeAgentWorkspace
|
||||
session={creativeAgentSession}
|
||||
isBusy={isCreativeAgentBusy || isPuzzleBusy}
|
||||
isStreaming={isCreativeAgentStreaming}
|
||||
error={creativeAgentError}
|
||||
eventLog={creativeAgentEvents}
|
||||
onBack={leaveCreativeAgentWorkspace}
|
||||
showBackButton={false}
|
||||
onSubmitMessage={(payload) => {
|
||||
void submitCreativeAgentMessage(payload);
|
||||
}}
|
||||
onConfirmTemplate={(selection) => {
|
||||
void confirmCreativeTemplateSelection(selection);
|
||||
}}
|
||||
onOpenTarget={() => {
|
||||
void openCreativeAgentTarget();
|
||||
}}
|
||||
/>
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -17758,10 +17807,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
>
|
||||
<UnifiedCreationWorkspace
|
||||
playId="puzzle"
|
||||
spec={getUnifiedCreationSpec(
|
||||
'puzzle',
|
||||
unifiedCreationConfigById.get('puzzle'),
|
||||
)}
|
||||
spec={getUnifiedSpec('puzzle')}
|
||||
session={puzzleSession}
|
||||
isBusy={isStreamingPuzzleReply}
|
||||
error={puzzleError}
|
||||
@@ -17895,16 +17941,24 @@ export function PlatformEntryFlowShellImpl({
|
||||
<Suspense
|
||||
fallback={<LazyPanelFallback label="正在加载视觉小说创作..." />}
|
||||
>
|
||||
<VisualNovelAgentWorkspace
|
||||
session={visualNovelSession}
|
||||
isBusy={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||||
error={visualNovelError}
|
||||
<UnifiedCreationPage
|
||||
spec={getUnifiedSpec('visual-novel')}
|
||||
onBack={leaveVisualNovelFlow}
|
||||
initialFormPayload={visualNovelFormDraftPayload}
|
||||
onCreateFromForm={(payload) => {
|
||||
void createVisualNovelDraftFromForm(payload);
|
||||
}}
|
||||
/>
|
||||
isBackDisabled={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||||
>
|
||||
<VisualNovelAgentWorkspace
|
||||
session={visualNovelSession}
|
||||
isBusy={isVisualNovelBusy || isVisualNovelStreamingReply}
|
||||
error={visualNovelError}
|
||||
onBack={leaveVisualNovelFlow}
|
||||
showBackButton={false}
|
||||
title={null}
|
||||
initialFormPayload={visualNovelFormDraftPayload}
|
||||
onCreateFromForm={(payload) => {
|
||||
void createVisualNovelDraftFromForm(payload);
|
||||
}}
|
||||
/>
|
||||
</UnifiedCreationPage>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -3752,14 +3752,14 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('create tab shows recent tab when backend returns failed drafts', async () => {
|
||||
test('create tab shows recent template cards when backend returns failed drafts', async () => {
|
||||
const user = userEvent.setup();
|
||||
mockExistingRpgDraftShelf({
|
||||
title: '入口可见的失败草稿',
|
||||
summary: '失败草稿也要进入创作入口最近创作。',
|
||||
stage: 'failed',
|
||||
stageLabel: '生成失败待处理',
|
||||
updatedAt: '2026-06-02T10:00:00.000Z',
|
||||
updatedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
@@ -3775,11 +3775,18 @@ test('create tab shows recent tab when backend returns failed drafts', async ()
|
||||
.getByRole('tab', { name: '最近创作' })
|
||||
.getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(await within(panel).findByText('入口可见的失败草稿')).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByText('失败草稿也要进入创作入口最近创作。'),
|
||||
await within(panel).findByRole('button', { name: /文字冒险/u }),
|
||||
).toBeTruthy();
|
||||
expect(within(panel).getByText('生成失败待处理')).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByText('仅显示最近7天内使用过的模板'),
|
||||
).toBeTruthy();
|
||||
expect(within(panel).getByText('经典 RPG 体验')).toBeTruthy();
|
||||
expect(within(panel).queryByText('入口可见的失败草稿')).toBeNull();
|
||||
expect(
|
||||
within(panel).queryByText('失败草稿也要进入创作入口最近创作。'),
|
||||
).toBeNull();
|
||||
expect(within(panel).queryByText('生成失败待处理')).toBeNull();
|
||||
});
|
||||
|
||||
test('create tab refreshes recent works after opening from an empty draft shelf', async () => {
|
||||
@@ -3789,7 +3796,7 @@ test('create tab refreshes recent works after opening from an empty draft shelf'
|
||||
summary: '创作入口需要在进入时重新读取真实作品架。',
|
||||
stage: 'error',
|
||||
stageLabel: '发生错误',
|
||||
updatedAt: '2026-06-02T10:30:00.000Z',
|
||||
updatedAt: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
||||
});
|
||||
vi.mocked(listRpgCreationWorks)
|
||||
.mockResolvedValueOnce([])
|
||||
@@ -3803,11 +3810,18 @@ test('create tab refreshes recent works after opening from an empty draft shelf'
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
const panel = getPlatformTabPanel('create');
|
||||
|
||||
expect(await within(panel).findByText('点击创作后出现的失败草稿')).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByText('创作入口需要在进入时重新读取真实作品架。'),
|
||||
await within(panel).findByRole('button', { name: /文字冒险/u }),
|
||||
).toBeTruthy();
|
||||
expect(within(panel).getByText('发生错误')).toBeTruthy();
|
||||
expect(
|
||||
within(panel).getByText('仅显示最近7天内使用过的模板'),
|
||||
).toBeTruthy();
|
||||
expect(within(panel).getByText('经典 RPG 体验')).toBeTruthy();
|
||||
expect(within(panel).queryByText('点击创作后出现的失败草稿')).toBeNull();
|
||||
expect(
|
||||
within(panel).queryByText('创作入口需要在进入时重新读取真实作品架。'),
|
||||
).toBeNull();
|
||||
expect(within(panel).queryByText('发生错误')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
vi.mocked(listRpgCreationWorks).mock.calls.length,
|
||||
|
||||
@@ -26,6 +26,7 @@ type SquareHoleAgentWorkspaceProps = {
|
||||
onBack: () => void;
|
||||
onSubmitMessage: (payload: SendSquareHoleMessageRequest) => void;
|
||||
onExecuteAction: (payload: ExecuteSquareHoleActionRequest) => void;
|
||||
showBackButton?: boolean;
|
||||
};
|
||||
|
||||
const SQUARE_HOLE_AGENT_THEME: CreationAgentTheme = {
|
||||
@@ -108,6 +109,7 @@ export function SquareHoleAgentWorkspace({
|
||||
onBack,
|
||||
onSubmitMessage,
|
||||
onExecuteAction,
|
||||
showBackButton = true,
|
||||
}: SquareHoleAgentWorkspaceProps) {
|
||||
return (
|
||||
<CreationAgentWorkspace
|
||||
@@ -121,6 +123,7 @@ export function SquareHoleAgentWorkspace({
|
||||
isBusy={isBusy}
|
||||
error={error}
|
||||
quickActions={SQUARE_HOLE_QUICK_ACTIONS}
|
||||
showBackButton={showBackButton}
|
||||
onBack={onBack}
|
||||
onSubmitText={(text) => {
|
||||
onSubmitMessage(buildSquareHoleChatPayload({ text }));
|
||||
|
||||
@@ -43,9 +43,7 @@ describe('UnifiedCreationPage', () => {
|
||||
]);
|
||||
expect(fields[2]?.getAttribute('data-field-kind')).toBe('audio');
|
||||
expect(fields[3]?.getAttribute('data-required')).toBe('true');
|
||||
expect(screen.getByTestId('unified-creation-play-badge').textContent).toBe(
|
||||
'wooden-fish',
|
||||
);
|
||||
expect(screen.queryByTestId('unified-creation-play-badge')).toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '返回' }));
|
||||
expect(onBack).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByLabelText('创作字段')).toBeNull();
|
||||
|
||||
@@ -26,7 +26,7 @@ export function UnifiedCreationPage({
|
||||
data-result-stage={spec.resultStage}
|
||||
>
|
||||
<header className="unified-creation-page__header shrink-0 pb-3">
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
{onBack ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -42,12 +42,6 @@ export function UnifiedCreationPage({
|
||||
) : (
|
||||
<span aria-hidden="true" className="min-h-8 w-0 shrink-0" />
|
||||
)}
|
||||
<span
|
||||
className="unified-creation-page__play-badge shrink-0 rounded-full border border-[var(--platform-subpanel-border)] bg-white/80 px-3 py-1 text-[11px] font-black text-[var(--platform-text-soft)]"
|
||||
data-testid="unified-creation-play-badge"
|
||||
>
|
||||
{spec.playId}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h1 className="m-0 min-w-0 truncate text-[1.35rem] font-black leading-tight tracking-normal text-[var(--platform-text-strong)] sm:text-[1.65rem]">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CustomWorldGenerationProgress } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { CustomWorldStructuredAnchorEntry } from '../../services/customWorldAgentGenerationProgress';
|
||||
import { CustomWorldGenerationView } from '../CustomWorldGenerationView';
|
||||
import type { UnifiedCreationPlayId } from './unifiedCreationSpecs';
|
||||
import { getUnifiedGenerationCopy } from './unifiedGenerationCopy';
|
||||
import type { UnifiedGenerationPlayId } from './unifiedGenerationCopy';
|
||||
|
||||
type UnifiedGenerationPageProps = {
|
||||
playId: UnifiedCreationPlayId;
|
||||
playId: UnifiedGenerationPlayId;
|
||||
settingText: string;
|
||||
anchorEntries?: CustomWorldStructuredAnchorEntry[];
|
||||
progress: CustomWorldGenerationProgress | null;
|
||||
|
||||
@@ -6,9 +6,21 @@ import {
|
||||
} from './unifiedCreationSpecs';
|
||||
|
||||
describe('unified creation specs', () => {
|
||||
test('统一壳当前覆盖拼图、抓大鹅、跳一跳和敲木鱼', () => {
|
||||
test('统一壳覆盖所有已有创作模板工作台', () => {
|
||||
expect(listUnifiedCreationSpecs().map((spec) => spec.playId).sort()).toEqual(
|
||||
['jump-hop', 'match3d', 'puzzle', 'wooden-fish'],
|
||||
[
|
||||
'baby-object-match',
|
||||
'bark-battle',
|
||||
'big-fish',
|
||||
'creative-agent',
|
||||
'jump-hop',
|
||||
'match3d',
|
||||
'puzzle',
|
||||
'rpg',
|
||||
'square-hole',
|
||||
'visual-novel',
|
||||
'wooden-fish',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -22,7 +34,12 @@ describe('unified creation specs', () => {
|
||||
expect([...fieldKinds].sort()).toEqual(['audio', 'image', 'select', 'text']);
|
||||
});
|
||||
|
||||
test('四条链路都映射到统一创作、生成、结果阶段', () => {
|
||||
test('主要链路都映射到统一创作、生成、结果阶段', () => {
|
||||
expect(getUnifiedCreationSpec('rpg')).toMatchObject({
|
||||
workspaceStage: 'agent-workspace',
|
||||
generationStage: 'custom-world-generating',
|
||||
resultStage: 'custom-world-result',
|
||||
});
|
||||
expect(getUnifiedCreationSpec('puzzle')).toMatchObject({
|
||||
workspaceStage: 'puzzle-agent-workspace',
|
||||
generationStage: 'puzzle-generating',
|
||||
@@ -43,5 +60,20 @@ describe('unified creation specs', () => {
|
||||
generationStage: 'wooden-fish-generating',
|
||||
resultStage: 'wooden-fish-result',
|
||||
});
|
||||
expect(getUnifiedCreationSpec('bark-battle')).toMatchObject({
|
||||
workspaceStage: 'bark-battle-workspace',
|
||||
generationStage: 'bark-battle-generating',
|
||||
resultStage: 'bark-battle-result',
|
||||
});
|
||||
expect(getUnifiedCreationSpec('visual-novel')).toMatchObject({
|
||||
workspaceStage: 'visual-novel-agent-workspace',
|
||||
generationStage: 'visual-novel-generating',
|
||||
resultStage: 'visual-novel-result',
|
||||
});
|
||||
expect(getUnifiedCreationSpec('baby-object-match')).toMatchObject({
|
||||
workspaceStage: 'baby-object-match-workspace',
|
||||
generationStage: 'baby-object-match-generating',
|
||||
resultStage: 'baby-object-match-result',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,13 +3,58 @@ import type {
|
||||
UnifiedCreationSpec,
|
||||
} from '../../services/creationEntryConfigService';
|
||||
|
||||
export type UnifiedCreationPlayId = UnifiedCreationSpec['playId'];
|
||||
export const UNIFIED_CREATION_PLAY_IDS = [
|
||||
'rpg',
|
||||
'big-fish',
|
||||
'puzzle',
|
||||
'match3d',
|
||||
'jump-hop',
|
||||
'wooden-fish',
|
||||
'square-hole',
|
||||
'bark-battle',
|
||||
'visual-novel',
|
||||
'baby-object-match',
|
||||
'creative-agent',
|
||||
] as const;
|
||||
|
||||
export type UnifiedCreationPlayId =
|
||||
(typeof UNIFIED_CREATION_PLAY_IDS)[number];
|
||||
export type { UnifiedCreationSpec };
|
||||
|
||||
const FALLBACK_UNIFIED_CREATION_SPECS: Record<
|
||||
UnifiedCreationPlayId,
|
||||
UnifiedCreationSpec
|
||||
> = {
|
||||
rpg: {
|
||||
playId: 'rpg',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'agent-workspace',
|
||||
generationStage: 'custom-world-generating',
|
||||
resultStage: 'custom-world-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'message',
|
||||
kind: 'text',
|
||||
label: '创作想法',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
'big-fish': {
|
||||
playId: 'big-fish',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'big-fish-agent-workspace',
|
||||
generationStage: 'big-fish-generating',
|
||||
resultStage: 'big-fish-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'message',
|
||||
kind: 'text',
|
||||
label: '玩法想法',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
puzzle: {
|
||||
playId: 'puzzle',
|
||||
title: '想做个什么玩法?',
|
||||
@@ -148,12 +193,135 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record<
|
||||
},
|
||||
],
|
||||
},
|
||||
'square-hole': {
|
||||
playId: 'square-hole',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'square-hole-agent-workspace',
|
||||
generationStage: 'square-hole-generating',
|
||||
resultStage: 'square-hole-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'message',
|
||||
kind: 'text',
|
||||
label: '玩法想法',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
'bark-battle': {
|
||||
playId: 'bark-battle',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'bark-battle-workspace',
|
||||
generationStage: 'bark-battle-generating',
|
||||
resultStage: 'bark-battle-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'title',
|
||||
kind: 'text',
|
||||
label: '作品标题',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'themeDescription',
|
||||
kind: 'text',
|
||||
label: '主题/场景描述',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'playerImageDescription',
|
||||
kind: 'text',
|
||||
label: '玩家形象描述',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'opponentImageDescription',
|
||||
kind: 'text',
|
||||
label: '对手形象描述',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'onomatopoeia',
|
||||
kind: 'text',
|
||||
label: '拟声词',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'difficultyPreset',
|
||||
kind: 'select',
|
||||
label: '难度',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
'visual-novel': {
|
||||
playId: 'visual-novel',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'visual-novel-agent-workspace',
|
||||
generationStage: 'visual-novel-generating',
|
||||
resultStage: 'visual-novel-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'ideaText',
|
||||
kind: 'text',
|
||||
label: '一句话创作',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'visualStyleId',
|
||||
kind: 'select',
|
||||
label: '视觉画风',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
'baby-object-match': {
|
||||
playId: 'baby-object-match',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'baby-object-match-workspace',
|
||||
generationStage: 'baby-object-match-generating',
|
||||
resultStage: 'baby-object-match-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'itemAName',
|
||||
kind: 'text',
|
||||
label: '物品 A',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'itemBName',
|
||||
kind: 'text',
|
||||
label: '物品 B',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
'creative-agent': {
|
||||
playId: 'creative-agent',
|
||||
title: '想做个什么玩法?',
|
||||
workspaceStage: 'creative-agent-workspace',
|
||||
generationStage: 'puzzle-generating',
|
||||
resultStage: 'puzzle-result',
|
||||
fields: [
|
||||
{
|
||||
id: 'message',
|
||||
kind: 'text',
|
||||
label: '创作想法',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'referenceImage',
|
||||
kind: 'image',
|
||||
label: '参考图',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function getUnifiedCreationSpec(
|
||||
playId: UnifiedCreationPlayId,
|
||||
configType?: CreationEntryTypeConfig | null,
|
||||
) {
|
||||
): UnifiedCreationSpec {
|
||||
return (
|
||||
configType?.unifiedCreationSpec ?? FALLBACK_UNIFIED_CREATION_SPECS[playId]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import type { UnifiedCreationPlayId } from './unifiedCreationSpecs';
|
||||
|
||||
export type UnifiedGenerationPlayId = Extract<
|
||||
UnifiedCreationPlayId,
|
||||
'puzzle' | 'match3d' | 'jump-hop' | 'wooden-fish'
|
||||
>;
|
||||
|
||||
const UNIFIED_GENERATION_COPY = {
|
||||
puzzle: {
|
||||
retryLabel: '重新生成图片',
|
||||
@@ -26,7 +31,7 @@ const UNIFIED_GENERATION_COPY = {
|
||||
activeBadgeLabel: '素材生成中',
|
||||
},
|
||||
} as const satisfies Record<
|
||||
UnifiedCreationPlayId,
|
||||
UnifiedGenerationPlayId,
|
||||
{
|
||||
retryLabel: string;
|
||||
settingTitle: string;
|
||||
@@ -35,6 +40,6 @@ const UNIFIED_GENERATION_COPY = {
|
||||
}
|
||||
>;
|
||||
|
||||
export function getUnifiedGenerationCopy(playId: UnifiedCreationPlayId) {
|
||||
export function getUnifiedGenerationCopy(playId: UnifiedGenerationPlayId) {
|
||||
return UNIFIED_GENERATION_COPY[playId];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user