diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index bdc69789..2a616fff 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -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` / `最近创作` 页签,和后台配置及真实作品数据口径冲突。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index b80df113..8fb8aacb 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -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 底层错误链,前端只知道图片生成失败。 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 414a976c..b5d29797 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -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 文案或导航状态。 diff --git a/server-rs/crates/shared-contracts/src/creation_entry_config.rs b/server-rs/crates/shared-contracts/src/creation_entry_config.rs index 5f1cacad..03525884 100644 --- a/server-rs/crates/shared-contracts/src/creation_entry_config.rs +++ b/server-rs/crates/shared-contracts/src/creation_entry_config.rs @@ -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 { 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 ( + "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] diff --git a/src/components/big-fish-creation/BigFishAgentWorkspace.tsx b/src/components/big-fish-creation/BigFishAgentWorkspace.tsx index f6578817..c650a34f 100644 --- a/src/components/big-fish-creation/BigFishAgentWorkspace.tsx +++ b/src/components/big-fish-creation/BigFishAgentWorkspace.tsx @@ -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 ( { onSubmitMessage( diff --git a/src/components/creation-agent/CreationAgentWorkspace.test.tsx b/src/components/creation-agent/CreationAgentWorkspace.test.tsx index b5c9b4b2..2ceaa232 100644 --- a/src/components/creation-agent/CreationAgentWorkspace.test.tsx +++ b/src/components/creation-agent/CreationAgentWorkspace.test.tsx @@ -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', () => { diff --git a/src/components/creation-agent/CreationAgentWorkspace.tsx b/src/components/creation-agent/CreationAgentWorkspace.tsx index 1bfa19e1..50c93d3d 100644 --- a/src/components/creation-agent/CreationAgentWorkspace.tsx +++ b/src/components/creation-agent/CreationAgentWorkspace.tsx @@ -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 (
- + {showBackButton ? ( + + ) : ( +
{referenceImagePreviewSrc ? ( -
+
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 (
- + {showBackButton ? ( + + ) : ( +
- ) : ( -
- {visibleCreationTypes.map((item) => { - const disabled = item.locked || busy; - - return ( - - ); - })} -
- )} + ); + })} +
); diff --git a/src/components/custom-world-home/creationWorkShelf.ts b/src/components/custom-world-home/creationWorkShelf.ts index 600694a2..1b12420f 100644 --- a/src/components/custom-world-home/creationWorkShelf.ts +++ b/src/components/custom-world-home/creationWorkShelf.ts @@ -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); diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index ba98a989..b305027c 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -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({ } > - {sessionController.agentSession ? ( - { - void sessionController.submitAgentMessage(payload); - }} - onExecuteAction={(payload) => { - void sessionController.executeAgentAction(payload); - }} - /> - ) : ( -
-
- {sessionController.isLoadingAgentSession - ? '正在准备 Agent 共创工作区...' - : sessionController.agentWorkspaceRestoreError || - '正在恢复创作工作区...'} + + {sessionController.agentSession ? ( + { + void sessionController.submitAgentMessage(payload); + }} + onExecuteAction={(payload) => { + void sessionController.executeAgentAction(payload); + }} + /> + ) : ( +
+
+ {sessionController.isLoadingAgentSession + ? '正在准备 Agent 共创工作区...' + : sessionController.agentWorkspaceRestoreError || + '正在恢复创作工作区...'} +
-
- )} + )} + )} @@ -16655,20 +16676,27 @@ export function PlatformEntryFlowShellImpl({ } > - { - void submitBigFishMessage(payload); - }} - onExecuteAction={(payload) => { - void executeBigFishAction(payload); - }} - /> + isBackDisabled={isBigFishBusy || isStreamingBigFishReply} + > + { + void submitBigFishMessage(payload); + }} + onExecuteAction={(payload) => { + void executeBigFishAction(payload); + }} + /> + )} @@ -16807,10 +16835,7 @@ export function PlatformEntryFlowShellImpl({ > } > - { - void createBabyObjectMatchDraftFromForm(payload); - }} - /> + isBackDisabled={isBabyObjectMatchBusy} + > + { + void createBabyObjectMatchDraftFromForm(payload); + }} + /> + )} @@ -17219,14 +17252,22 @@ export function PlatformEntryFlowShellImpl({ } > - { - void createBarkBattleGeneratingDraft(payload); - }} - /> + isBackDisabled={isBarkBattleBusy} + > + { + void createBarkBattleGeneratingDraft(payload); + }} + /> + )} @@ -17244,20 +17285,27 @@ export function PlatformEntryFlowShellImpl({ } > - { - void submitSquareHoleMessage(payload); - }} - onExecuteAction={(payload) => { - void executeSquareHoleAction(payload); - }} - /> + isBackDisabled={isSquareHoleBusy || isStreamingSquareHoleReply} + > + { + void submitSquareHoleMessage(payload); + }} + onExecuteAction={(payload) => { + void executeSquareHoleAction(payload); + }} + /> + )} @@ -17462,10 +17510,7 @@ export function PlatformEntryFlowShellImpl({ > } > - { - void submitCreativeAgentMessage(payload); - }} - onConfirmTemplate={(selection) => { - void confirmCreativeTemplateSelection(selection); - }} - onOpenTarget={() => { - void openCreativeAgentTarget(); - }} - /> + isBackDisabled={isCreativeAgentBusy || isPuzzleBusy} + > + { + void submitCreativeAgentMessage(payload); + }} + onConfirmTemplate={(selection) => { + void confirmCreativeTemplateSelection(selection); + }} + onOpenTarget={() => { + void openCreativeAgentTarget(); + }} + /> + )} @@ -17758,10 +17807,7 @@ export function PlatformEntryFlowShellImpl({ > } > - { - void createVisualNovelDraftFromForm(payload); - }} - /> + isBackDisabled={isVisualNovelBusy || isVisualNovelStreamingReply} + > + { + void createVisualNovelDraftFromForm(payload); + }} + /> + )} diff --git a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx index 04a3e7fd..37bfb3ee 100644 --- a/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx +++ b/src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx @@ -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(); @@ -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, diff --git a/src/components/square-hole-creation/SquareHoleAgentWorkspace.tsx b/src/components/square-hole-creation/SquareHoleAgentWorkspace.tsx index b8bbccad..4f7f254d 100644 --- a/src/components/square-hole-creation/SquareHoleAgentWorkspace.tsx +++ b/src/components/square-hole-creation/SquareHoleAgentWorkspace.tsx @@ -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 ( { onSubmitMessage(buildSquareHoleChatPayload({ text })); diff --git a/src/components/unified-creation/UnifiedCreationPage.test.tsx b/src/components/unified-creation/UnifiedCreationPage.test.tsx index ec6c0c07..5f47c6fa 100644 --- a/src/components/unified-creation/UnifiedCreationPage.test.tsx +++ b/src/components/unified-creation/UnifiedCreationPage.test.tsx @@ -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(); diff --git a/src/components/unified-creation/UnifiedCreationPage.tsx b/src/components/unified-creation/UnifiedCreationPage.tsx index ddfc2580..1f027c06 100644 --- a/src/components/unified-creation/UnifiedCreationPage.tsx +++ b/src/components/unified-creation/UnifiedCreationPage.tsx @@ -26,7 +26,7 @@ export function UnifiedCreationPage({ data-result-stage={spec.resultStage} >
-
+
{onBack ? (

diff --git a/src/components/unified-creation/UnifiedGenerationPage.tsx b/src/components/unified-creation/UnifiedGenerationPage.tsx index b15c5d1e..95ea551f 100644 --- a/src/components/unified-creation/UnifiedGenerationPage.tsx +++ b/src/components/unified-creation/UnifiedGenerationPage.tsx @@ -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; diff --git a/src/components/unified-creation/unifiedCreationSpecs.test.ts b/src/components/unified-creation/unifiedCreationSpecs.test.ts index cebb5657..8e6daa98 100644 --- a/src/components/unified-creation/unifiedCreationSpecs.test.ts +++ b/src/components/unified-creation/unifiedCreationSpecs.test.ts @@ -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', + }); }); }); diff --git a/src/components/unified-creation/unifiedCreationSpecs.ts b/src/components/unified-creation/unifiedCreationSpecs.ts index 170ef6e2..2996ea9d 100644 --- a/src/components/unified-creation/unifiedCreationSpecs.ts +++ b/src/components/unified-creation/unifiedCreationSpecs.ts @@ -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] ); diff --git a/src/components/unified-creation/unifiedGenerationCopy.ts b/src/components/unified-creation/unifiedGenerationCopy.ts index 4b3bdd2d..ec02f145 100644 --- a/src/components/unified-creation/unifiedGenerationCopy.ts +++ b/src/components/unified-creation/unifiedGenerationCopy.ts @@ -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]; } diff --git a/src/index.css b/src/index.css index 8509abaa..4df24a40 100644 --- a/src/index.css +++ b/src/index.css @@ -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( diff --git a/src/index.test.ts b/src/index.test.ts index b98f01b5..6a54c2f9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -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'); + }); +}); diff --git a/src/services/creationEntryConfigService.ts b/src/services/creationEntryConfigService.ts index bf6e987f..7ca6adef 100644 --- a/src/services/creationEntryConfigService.ts +++ b/src/services/creationEntryConfigService.ts @@ -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[]; };