feat: unify creation entry templates

This commit is contained in:
2026-06-03 10:24:03 +08:00
parent b0865cfa19
commit 3f742fbaca
25 changed files with 820 additions and 346 deletions

View File

@@ -16,6 +16,14 @@
---
## 2026-06-03 最近创作只复用创作模板入口
- 背景:底部加号创作入口的“最近创作”最初由真实作品架摘要驱动,但页面曾按作品标题、摘要和生成状态渲染独立最近创作卡,和其它模板页签的卡片样式及点击语义不一致。
- 决策:“最近创作”仍只由真实后端作品架摘要决定是否展示,但只纳入 `updatedAt` 在最近 7 天内的摘要,且摘要只用于推导最近使用过的模板 ID实际列表必须从后端入口配置的 `creationTypes` 中筛出对应模板,复用其它页签的模板卡结构、文案和 `onCreateType` 点击行为,不展示具体作品名称、作品摘要或草稿 / 生成状态也不新增独立最近创作组件。最近创作页签激活时页面必须显示“仅显示最近7天内使用过的模板”。
- 影响范围:`src/components/custom-world-home/CustomWorldCreationStartCard.tsx``src/components/custom-world-home/CustomWorldCreationHub.tsx``src/components/platform-entry`、创作入口相关测试与玩法链路文档。
- 验证方式:`CustomWorldCreationHub` 测试应断言最近创作页签包含 `creation-template-card`、模板标题 / 副标题,并且不出现旧 `creation-recent-work-grid`、作品标题、作品摘要或“打开最近创作”按钮文案RPG 入口交互测试应断言最近创作默认页签展示“文字冒险”模板卡。
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-06-02 底部加号创作入口页 banner 与最近创作口径
- 背景:创作入口页 banner 曾固定为前端两张主题赛卡,且模板分类兜底会产生 `recent` / `最近创作` 页签,和后台配置及真实作品数据口径冲突。

View File

@@ -23,6 +23,14 @@
- 验证:触发任一平台级异步失败时,页面应出现包含“错误来源”和“错误内容”的弹窗;复制内容应包含来源和错误正文;旧页面内错误 banner 不再重复出现。
- 关联:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``src/components/platform-entry/PlatformErrorDialog.tsx``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 暗色创作进度卡不要被 platform-remap-surface 改成深色文字
- 现象:统一创作页里的暗色进度卡背景是深绿 / 深蓝,但“创作进度”、百分比和进度提示显示成深色,移动端几乎看不清。
- 原因:`platform-remap-surface` 在浅色主题下会把后代 `[class*='text-white']` 强制重映射成 `var(--platform-text-strong)`,并且使用 `!important`;暗色 hero 卡片如果只写通用 `text-white*`,刷新后仍会被全局 remap 覆盖成深色。早期还混用了 `text-white/72``text-white/88``border-white/14``bg-white/12` 等不稳透明度档位,进一步放大了问题。
- 处理:给暗色 hero 加组件专属 class例如 `creation-agent-hero__progress-label``creation-agent-hero__progress-value``creation-agent-hero__progress-hint`,并在 `src/index.css` 的 remap 规则之后用更具体选择器和 `!important` 固定白色透明度、边框和进度条底色。
- 验证:`CreationAgentWorkspace` 测试应断言进度标题、百分比和提示文本带专属 class`src/index.test.ts` 应断言这些 class 在 remap surface 内有白色覆盖规则;移动端截图中暗色卡片文字应保持可读。
- 关联:`src/components/creation-agent/CreationAgentWorkspace.tsx``src/components/creation-agent/CreationAgentWorkspace.test.tsx``src/index.css``src/index.test.ts``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## VectorEngine 图片生成 SendRequest 超时要按传输失败排查
- 现象:`external_api_call_failure` 里看到 `failureStage=request_send``timeout=true``statusCode=null``errorSource` 可能是 `client error (SendRequest)` 或更完整的 reqwest 底层错误链,前端只知道图片生成失败。

View File

@@ -1,16 +1,16 @@
# 平台入口与玩法链路
更新时间:`2026-05-15`
更新时间:`2026-06-03`
## 平台创作入口
创作入口配置事实源在 SpacetimeDB通过 `GET /api/creation-entry/config` 下发;后台通过 `/admin/api/creation-entry/config` 管理。前端只在展示层派生可见卡片和入口状态,`api-server` 路由熔断也使用同一份配置。不要恢复前端硬编码入口配置文件。
当前点击底部加号进入的创作入口页承载后台公告位、创作入口页签和两列模板卡;页签中只有真实后端作品架摘要存在时才展示“最近创作”,其余为玩法模板分类。点击模板卡后直接进入对应玩法已有的入口创作表单 stage不再经过空白占位页也不把旧表单嵌进创作入口页。移动端创作入口页顶栏在 `陶泥儿` 品牌同一行显示真实账户泥点数,数据来自 `profileDashboard.walletBalance`,不得再把公告内容或活动奖池当作账号余额展示。创作入口页公告位数据优先读取 `GET /api/creation-entry/config``eventBanners` 数组,多条配置时前端自动轮播,旧 `eventBanner` 仅作为单条兼容兜底。后台公告配置面向表单:每条公告包含标题和 HTML 内容,后台保存时序列化为后端 `eventBannersJson` 传输字段,由前端空权限沙箱 iframe 展示;旧结构化 banner 字段仅保留回显兼容,不再作为后台公告配置主格式;不得执行 JSX 或把后台代码直接注入 DOM。玩法列表不再套外部边框卡片移动端需要压缩横向边距和两列间距玩法卡统一按“上图、左上状态标签仅非开放态显示、封面右下 `10-20泥点数`、下方白底标题/描述”结构展示,卡片高度保持紧凑但标题、描述和预估消耗点数都必须可见。创作入口页根容器不再使用 `platform-page-stage` 这类全局内容卡片壳,但继续保留 `platform-remap-surface` 作为主题和输入框样式命中钩子。创作入口页字号需要对齐平台普通 UI 档位:顶栏泥点组件、公告正文、分类 Tab 和玩法卡标题 / 副标题 / 消耗说明优先使用 `11px``14px`,不使用 `text-lg``text-xl` 或更大的展示级字号。草稿 Tab 继续承接作品架;底部加号入口页的“最近创作”来自真实后端作品架摘要,生成中和生成失败的草稿摘要都应进入最近创作并显示对应状态。RPG、RPG 之外的各玩法入口分别落到既有的 `agent-workspace``big-fish-agent-workspace``match3d-agent-workspace``square-hole-agent-workspace``jump-hop-workspace``wooden-fish-workspace``puzzle-agent-workspace``bark-battle-workspace``visual-novel-agent-workspace``baby-object-match-workspace`,这些入口继续承接各玩法自己的表单、草稿恢复和后续编排,不作为创作入口页内容。
当前点击底部加号进入的创作入口页承载后台公告位、创作入口页签和两列模板卡;页签中只有真实后端作品架摘要存在时才展示“最近创作”,其余为玩法模板分类。点击模板卡后直接进入对应玩法已有的入口创作表单 stage不再经过空白占位页也不把旧表单嵌进创作入口页。移动端创作入口页顶栏在 `陶泥儿` 品牌同一行显示真实账户泥点数,数据来自 `profileDashboard.walletBalance`,不得再把公告内容或活动奖池当作账号余额展示。创作入口页公告位数据优先读取 `GET /api/creation-entry/config``eventBanners` 数组,多条配置时前端自动轮播,旧 `eventBanner` 仅作为单条兼容兜底。后台公告配置面向表单:每条公告包含标题和 HTML 内容,后台保存时序列化为后端 `eventBannersJson` 传输字段,由前端空权限沙箱 iframe 展示;旧结构化 banner 字段仅保留回显兼容,不再作为后台公告配置主格式;不得执行 JSX 或把后台代码直接注入 DOM。玩法列表不再套外部边框卡片移动端需要压缩横向边距和两列间距玩法卡统一按“上图、左上状态标签仅非开放态显示、封面右下 `10-20泥点数`、下方白底标题/描述”结构展示,卡片高度保持紧凑但标题、描述和预估消耗点数都必须可见。创作入口页根容器不再使用 `platform-page-stage` 这类全局内容卡片壳,但继续保留 `platform-remap-surface` 作为主题和输入框样式命中钩子。创作入口页字号需要对齐平台普通 UI 档位:顶栏泥点组件、公告正文、分类 Tab 和玩法卡标题 / 副标题 / 消耗说明优先使用 `11px``14px`,不使用 `text-lg``text-xl` 或更大的展示级字号。草稿 Tab 继续承接作品架;底部加号入口页的“最近创作”只用 7 天内的真实后端作品架摘要判断是否展示,并从摘要里推导最近使用过的模板 ID页面必须展示“仅显示最近7天内使用过的模板”提示列表内容必须复用其它页签里的模板卡样式、文案和点击行为不展示具体作品名称、摘要或生成状态也不新增独立最近创作卡组件。RPG、RPG 之外的各玩法入口分别落到既有的 `agent-workspace``big-fish-agent-workspace``match3d-agent-workspace``square-hole-agent-workspace``jump-hop-workspace``wooden-fish-workspace``puzzle-agent-workspace``bark-battle-workspace``visual-novel-agent-workspace``baby-object-match-workspace`,这些入口继续承接各玩法自己的表单、草稿恢复和后续编排,不作为创作入口页内容。
创作恢复参数只保留 `sessionId``profileId``draftId``workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。
一期创作流程统一化覆盖拼图、抓大鹅、跳一跳和敲木鱼。四者在前端统一经过 `UnifiedCreationWorkspace``UnifiedGenerationPage``UnifiedCreationWorkspace` 作为平台壳唯一依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace``Match3DCreationWorkspace``JumpHopCreationWorkspace``WoodenFishCreationWorkspace`创作页字段清单由后端在 `GET /api/creation-entry/config``creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec首期字段类型只保留 `text``select``image``audio``UnifiedCreationPage` 提供统一标题栏、统一返回入口、页面级纵向滚动、内容区和隐藏字段契约,不在 UI 中额外展示字段说明 chip竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交但返回按钮只保留在统一页头工作台内部不再重复渲染。拼图、抓大鹅、跳一跳和敲木鱼的工作台实现都已收口到统一目录,只保留各自输入逻辑、素材选择和提交校验,不再由平台壳直接依赖旧工作台文件。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。视觉小说、`airp``component`、汪汪声浪、方洞、大鱼和宝贝识物不进入一期接线范围,已有链路保持现状。
统一创作入口覆盖当前可进入创作链路的已有模板:`rpg``big-fish``puzzle``match3d``jump-hop``wooden-fish``square-hole``bark-battle``visual-novel``baby-object-match``creative-agent``airp` 仍是未开放占位,不作为当前统一创作链路目标。拼图、抓大鹅、跳一跳和敲木鱼在前端继续经过 `UnifiedCreationWorkspace``UnifiedGenerationPage``UnifiedCreationWorkspace` 作为平台壳依赖的统一创作编排层,再内部调用 `src/components/unified-creation/workspaces/` 下的 `PuzzleCreationWorkspace``Match3DCreationWorkspace``JumpHopCreationWorkspace``WoodenFishCreationWorkspace`。其它已有模板由平台壳用 `UnifiedCreationPage` 包住既有工作台,复用统一标题栏、返回入口、页面级纵向滚动和隐藏字段契约,同时保留各玩法自己的表单、草稿恢复和后续编排。创作页字段清单由后端在 `GET /api/creation-entry/config``creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec字段类型只保留 `text``select``image``audio``UnifiedCreationPage` 不在 UI 中额外展示字段说明 chip,也不在右上角显示内部 `playId`、模板 ID 或工作台阶段名;竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交,但返回按钮只保留在统一页头,工作台内部不再重复渲染。暗色创作进度卡片位于 `platform-remap-surface` 内时,必须用组件专属 class 覆盖浅色主题 remap确保白字、浅色边框和进度条底色不会被全局规则改成深色不要只依赖通用 `text-white*`。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。
创作表单提交前的泥点余额前置校验只允许用独立弹窗提示失败原因,不得把用户退回创作入口或玩法模板列表,也不得清空当前表单状态。当前适用拼图、抓大鹅和汪汪声浪等会在前端提交前校验泥点的生成入口;余额不足、余额读取失败都应停留在当前工作台,由用户关闭提示后继续编辑或自行补足泥点。
@@ -20,7 +20,7 @@
`PlatformEntryFlowShellImpl.tsx` 仍是平台入口编排壳,后续维护时应优先把独立 UI 片段、公开作品映射、草稿生成 notice 和运行态状态 helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或同目录紧邻 helper 文件。拆分只允许改变文件组织不改变入口配置事实源、默认导出、props、页面阶段、UI 文案或现有交互;其中拼图首访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`
`platformEntryCreationTypes.ts` 只做前端展示派生,分组时必须把后端 `creationTypes` 里的 `categoryId` / `categoryLabel` 当作可缺失字段处理,空值统一回退到 `recommended` / `热门推荐`,并把历史 `recent` / `最近创作` 归一到推荐分类。`最近创作` 不属于模板分类页签,只能由真实草稿 / 作品架后端数据决定是否展示。
`platformEntryCreationTypes.ts` 只做前端展示派生,分组时必须把后端 `creationTypes` 里的 `categoryId` / `categoryLabel` 当作可缺失字段处理,空值统一回退到 `recommended` / `热门推荐`,并把历史 `recent` / `最近创作` 归一到推荐分类。`最近创作` 不属于模板分类页签,只能由 7 天内的真实草稿 / 作品架后端数据决定是否展示;展示内容仍然从后端入口配置的模板卡中筛选,不读取或渲染作品标题、作品摘要、草稿阶段文案
移动端底部一级导航是全局平台样式,不按单一玩法分叉。当前视觉统一为米白浮动胶囊底座、浅棕分隔线、棕色线性图标、橘色选中态和底部短下划线;中间 `创作` 入口保持凸起圆形主按钮,但凸起位移只能作用在按钮内容层,不能移动承载分隔线的 Tab 按钮容器确保创作左右分隔线与其他分隔线垂直位置一致。Tab 名称和可见性仍由现有 `PlatformHomeTab` / 登录态规则决定,样式调整不得改写 Tab 文案或导航状态。

View File

@@ -102,6 +102,18 @@ pub const UNIFIED_CREATION_FIELD_KINDS: [&str; 4] = ["text", "select", "image",
pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option<UnifiedCreationSpecResponse> {
let (workspace_stage, generation_stage, result_stage, fields) = match play_id {
"rpg" => (
"agent-workspace",
"custom-world-generating",
"custom-world-result",
vec![unified_creation_field("message", "text", "创作想法", true)],
),
"big-fish" => (
"big-fish-agent-workspace",
"big-fish-generating",
"big-fish-result",
vec![unified_creation_field("message", "text", "玩法想法", true)],
),
"puzzle" => (
"puzzle-agent-workspace",
"puzzle-generating",
@@ -147,6 +159,62 @@ pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option<UnifiedCreati
unified_creation_field("floatingWords", "text", "功德有什么", true),
],
),
"square-hole" => (
"square-hole-agent-workspace",
"square-hole-generating",
"square-hole-result",
vec![unified_creation_field("message", "text", "玩法想法", true)],
),
"bark-battle" => (
"bark-battle-workspace",
"bark-battle-generating",
"bark-battle-result",
vec![
unified_creation_field("title", "text", "作品标题", true),
unified_creation_field("themeDescription", "text", "主题/场景描述", true),
unified_creation_field(
"playerImageDescription",
"text",
"玩家形象描述",
true,
),
unified_creation_field(
"opponentImageDescription",
"text",
"对手形象描述",
true,
),
unified_creation_field("onomatopoeia", "text", "拟声词", false),
unified_creation_field("difficultyPreset", "select", "难度", true),
],
),
"visual-novel" => (
"visual-novel-agent-workspace",
"visual-novel-generating",
"visual-novel-result",
vec![
unified_creation_field("ideaText", "text", "一句话创作", true),
unified_creation_field("visualStyleId", "select", "视觉画风", true),
],
),
"baby-object-match" => (
"baby-object-match-workspace",
"baby-object-match-generating",
"baby-object-match-result",
vec![
unified_creation_field("itemAName", "text", "物品 A", true),
unified_creation_field("itemBName", "text", "物品 B", true),
],
),
"creative-agent" => (
"creative-agent-workspace",
"puzzle-generating",
"puzzle-result",
vec![
unified_creation_field("message", "text", "创作想法", true),
unified_creation_field("referenceImage", "image", "参考图", false),
],
),
_ => return None,
};
@@ -268,7 +336,7 @@ mod tests {
use super::*;
#[test]
fn phase1_unified_creation_specs_cover_four_templates() {
fn phase1_unified_creation_specs_cover_existing_templates() {
let puzzle = build_phase1_unified_creation_spec("puzzle").expect("puzzle spec");
assert_eq!(puzzle.fields[0].id, "pictureDescription");
assert_eq!(puzzle.fields[1].kind, "image");
@@ -300,8 +368,25 @@ mod tests {
let wooden_fish =
build_phase1_unified_creation_spec("wooden-fish").expect("wooden-fish spec");
assert!(wooden_fish.fields.iter().any(|field| field.kind == "audio"));
assert!(build_phase1_unified_creation_spec("visual-novel").is_none());
assert!(build_phase1_unified_creation_spec("bark-battle").is_none());
let visual_novel =
build_phase1_unified_creation_spec("visual-novel").expect("visual-novel spec");
assert_eq!(visual_novel.workspace_stage, "visual-novel-agent-workspace");
let bark_battle =
build_phase1_unified_creation_spec("bark-battle").expect("bark-battle spec");
assert_eq!(bark_battle.generation_stage, "bark-battle-generating");
let baby_object_match = build_phase1_unified_creation_spec("baby-object-match")
.expect("baby-object-match spec");
assert_eq!(
baby_object_match
.fields
.iter()
.filter(|field| field.kind == "text")
.count(),
2
);
}
#[test]

View File

@@ -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(

View File

@@ -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', () => {

View File

@@ -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">
{showBackButton ? (
<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"
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}

View File

@@ -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,6 +133,7 @@ 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">
{showBackButton ? (
<button
type="button"
onClick={onBack}
@@ -142,6 +145,9 @@ export function CreativeAgentWorkspace({
</span>
</button>
) : (
<span aria-hidden="true" />
)}
<div className="platform-pill platform-pill--cool px-3 text-[11px]">
{CREATIVE_AGENT_STAGE_LABEL[stage]}
</div>

View File

@@ -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);

View File

@@ -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('&lt;section&gt;&lt;h1&gt;自定义横幅&lt;/h1&gt;&lt;/section&gt;');
});
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', () => {

View File

@@ -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}

View File

@@ -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,28 +310,11 @@ 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) => (
<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)}
>
<div className="line-clamp-1 text-sm font-black text-[#2f211b]">
{item.title}
<div className="creation-template-list__recent-window mt-2 text-[11px] font-bold leading-4 text-[#8b6654] sm:text-xs">
{recentWindowDays}使
</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>
</button>
))}
</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;
@@ -376,7 +363,6 @@ export function CustomWorldCreationStartCard({
);
})}
</div>
)}
</section>
</div>
);

View File

@@ -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);

View File

@@ -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',
@@ -16613,14 +16622,25 @@ export function PlatformEntryFlowShellImpl({
fallback={
<LazyPanelFallback label="正在加载 Agent 共创工作区..." />
}
>
<UnifiedCreationPage
spec={getUnifiedSpec('rpg')}
onBack={leaveAgentWorkspace}
isBackDisabled={
sessionController.isStreamingAgentReply ||
Boolean(sessionController.agentOperation)
}
>
{sessionController.agentSession ? (
<CustomWorldAgentWorkspace
session={sessionController.agentSession}
activeOperation={sessionController.agentOperation}
streamingReplyText={sessionController.streamingAgentReplyText}
streamingReplyText={
sessionController.streamingAgentReplyText
}
isStreamingReply={sessionController.isStreamingAgentReply}
onBack={leaveAgentWorkspace}
showBackButton={false}
onSubmitMessage={(payload) => {
void sessionController.submitAgentMessage(payload);
}}
@@ -16638,6 +16658,7 @@ export function PlatformEntryFlowShellImpl({
</div>
</div>
)}
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
@@ -16654,6 +16675,11 @@ export function PlatformEntryFlowShellImpl({
fallback={
<LazyPanelFallback label="正在加载大鱼吃小鱼共创工作区..." />
}
>
<UnifiedCreationPage
spec={getUnifiedSpec('big-fish')}
onBack={leaveBigFishFlow}
isBackDisabled={isBigFishBusy || isStreamingBigFishReply}
>
<BigFishAgentWorkspace
session={bigFishSession}
@@ -16662,6 +16688,7 @@ export function PlatformEntryFlowShellImpl({
isBusy={isBigFishBusy || isStreamingBigFishReply}
error={bigFishError}
onBack={leaveBigFishFlow}
showBackButton={false}
onSubmitMessage={(payload) => {
void submitBigFishMessage(payload);
}}
@@ -16669,6 +16696,7 @@ export function PlatformEntryFlowShellImpl({
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}
@@ -17057,16 +17082,24 @@ export function PlatformEntryFlowShellImpl({
>
<Suspense
fallback={<LazyPanelFallback label="正在加载宝贝识物创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('baby-object-match')}
onBack={leaveBabyObjectMatchFlow}
isBackDisabled={isBabyObjectMatchBusy}
>
<BabyObjectMatchWorkspace
isBusy={isBabyObjectMatchBusy}
error={babyObjectMatchError}
onBack={leaveBabyObjectMatchFlow}
showBackButton={false}
title={null}
initialPayload={babyObjectMatchFormPayload}
onCreateDraft={(payload) => {
void createBabyObjectMatchDraftFromForm(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
@@ -17218,15 +17251,23 @@ export function PlatformEntryFlowShellImpl({
>
<Suspense
fallback={<LazyPanelFallback label="正在加载汪汪声浪创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('bark-battle')}
onBack={leaveBarkBattleFlow}
isBackDisabled={isBarkBattleBusy}
>
<BarkBattleConfigEditor
isBusy={isBarkBattleBusy}
error={barkBattleError}
onBack={leaveBarkBattleFlow}
showBackButton={false}
title={null}
onPreview={(payload) => {
void createBarkBattleGeneratingDraft(payload);
}}
/>
</UnifiedCreationPage>
</Suspense>
</motion.div>
)}
@@ -17243,6 +17284,11 @@ export function PlatformEntryFlowShellImpl({
fallback={
<LazyPanelFallback label="正在加载方洞挑战共创工作区..." />
}
>
<UnifiedCreationPage
spec={getUnifiedSpec('square-hole')}
onBack={leaveSquareHoleFlow}
isBackDisabled={isSquareHoleBusy || isStreamingSquareHoleReply}
>
<SquareHoleAgentWorkspace
session={squareHoleSession}
@@ -17251,6 +17297,7 @@ export function PlatformEntryFlowShellImpl({
isBusy={isSquareHoleBusy || isStreamingSquareHoleReply}
error={squareHoleError}
onBack={leaveSquareHoleFlow}
showBackButton={false}
onSubmitMessage={(payload) => {
void submitSquareHoleMessage(payload);
}}
@@ -17258,6 +17305,7 @@ export function PlatformEntryFlowShellImpl({
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}
@@ -17723,6 +17765,11 @@ export function PlatformEntryFlowShellImpl({
>
<Suspense
fallback={<LazyPanelFallback label="正在加载智能创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('creative-agent')}
onBack={leaveCreativeAgentWorkspace}
isBackDisabled={isCreativeAgentBusy || isPuzzleBusy}
>
<CreativeAgentWorkspace
session={creativeAgentSession}
@@ -17731,6 +17778,7 @@ export function PlatformEntryFlowShellImpl({
error={creativeAgentError}
eventLog={creativeAgentEvents}
onBack={leaveCreativeAgentWorkspace}
showBackButton={false}
onSubmitMessage={(payload) => {
void submitCreativeAgentMessage(payload);
}}
@@ -17741,6 +17789,7 @@ export function PlatformEntryFlowShellImpl({
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}
@@ -17894,17 +17940,25 @@ export function PlatformEntryFlowShellImpl({
>
<Suspense
fallback={<LazyPanelFallback label="正在加载视觉小说创作..." />}
>
<UnifiedCreationPage
spec={getUnifiedSpec('visual-novel')}
onBack={leaveVisualNovelFlow}
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>
)}

View File

@@ -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,

View File

@@ -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 }));

View File

@@ -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();

View File

@@ -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]">

View File

@@ -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;

View File

@@ -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',
});
});
});

View File

@@ -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]
);

View File

@@ -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];
}

View File

@@ -7096,6 +7096,46 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
color: var(--platform-text-base) !important;
}
.platform-theme--light
.platform-remap-surface
.creation-agent-hero
:where(
.creation-agent-hero__icon-button,
.creation-agent-hero__quick-action
) {
border-color: rgba(255, 255, 255, 0.2) !important;
background: rgba(255, 255, 255, 0.1) !important;
color: rgba(255, 255, 255, 0.82) !important;
}
.platform-theme--light
.platform-remap-surface
.creation-agent-hero
:where(.creation-agent-hero__summary, .creation-agent-hero__progress-label) {
color: rgba(255, 255, 255, 0.76) !important;
}
.platform-theme--light
.platform-remap-surface
.creation-agent-hero
.creation-agent-hero__progress-value {
color: rgba(255, 255, 255, 0.92) !important;
}
.platform-theme--light
.platform-remap-surface
.creation-agent-hero
.creation-agent-hero__progress-hint {
color: rgba(255, 255, 255, 0.72) !important;
}
.platform-theme--light
.platform-remap-surface
.creation-agent-hero
.creation-agent-hero__progress-track {
background: rgba(255, 255, 255, 0.2) !important;
}
.platform-theme--light
.platform-remap-surface
:where(

View File

@@ -80,3 +80,24 @@ describe('index stylesheet draft mobile cards', () => {
expect(editableBlock).toContain('-webkit-touch-callout: default;');
});
});
describe('index stylesheet creation agent hero contrast', () => {
it('keeps dark creation progress hero text light inside remap surfaces', () => {
const css = readIndexCss();
expect(css).toContain('.platform-remap-surface');
expect(css).toContain('.creation-agent-hero');
const labelBlock = getCssBlock(
css,
':where(.creation-agent-hero__summary, .creation-agent-hero__progress-label)',
);
expect(labelBlock).toContain('rgba(255, 255, 255, 0.76) !important');
const valueBlock = getCssBlock(css, '.creation-agent-hero__progress-value');
expect(valueBlock).toContain('rgba(255, 255, 255, 0.92) !important');
const hintBlock = getCssBlock(css, '.creation-agent-hero__progress-hint');
expect(hintBlock).toContain('rgba(255, 255, 255, 0.72) !important');
});
});

View File

@@ -27,23 +27,11 @@ export type UnifiedCreationField = {
/** 统一创作工作台契约,把入口类型映射到工作台、生成页和结果页阶段。 */
export type UnifiedCreationSpec = {
playId: 'puzzle' | 'match3d' | 'jump-hop' | 'wooden-fish';
playId: string;
title: string;
workspaceStage:
| 'puzzle-agent-workspace'
| 'match3d-agent-workspace'
| 'jump-hop-workspace'
| 'wooden-fish-workspace';
generationStage:
| 'puzzle-generating'
| 'match3d-generating'
| 'jump-hop-generating'
| 'wooden-fish-generating';
resultStage:
| 'puzzle-result'
| 'match3d-result'
| 'jump-hop-result'
| 'wooden-fish-result';
workspaceStage: string;
generationStage: string;
resultStage: string;
fields: UnifiedCreationField[];
};