收口统一创作流程一期

This commit is contained in:
2026-05-31 14:46:32 +00:00
parent 724d8be405
commit 23dec91bd6
36 changed files with 919 additions and 469 deletions

View File

@@ -43,12 +43,29 @@
## 2026-05-29 一期统一创作页必须提供可见统一外壳
- 背景:`UnifiedCreationPage` 首版只暴露隐藏 spec 元数据并包裹旧玩法工作台,用户打开拼图创作页时仍只能看到旧工作台外观,无法验收“统一创作页”。
- 决策:一期统一创作页(拼图、抓大鹅、敲木鱼)必须由 `UnifiedCreationPage` 提供统一标题栏、内容区和隐藏字段契约;字段元信息只留给测试和代码,不再额外作为可见 chip 占用首屏。玩法工作台只承载具体输入控件、上传、历史素材、校验和提交,不再各自渲染巨大入口标题。拼图继续复用 `PuzzleAgentWorkspace` 的上传、裁剪、历史图、AI 重绘和提交逻辑,抓大鹅继续复用 `Match3DAgentWorkspace` 的题材与难度表单逻辑;二者在统一壳内启用 `unifiedChrome`,收起旧标题与外层壳。敲木鱼右侧音效和功德面板不得再套内部滚动容器,移动端应自然跟随页面滚动。
- 追加决策:`UnifiedCreationPage` 不创建自己的纵向滚动;拼图、抓大鹅和敲木鱼三个统一创作入口由平台 stage 承担整页滚动,竖屏移动端必须能从统一标题、表单控件一路滑到提交按钮,避免工作台内部或右侧面板形成套滚动。
- 影响范围:`src/components/unified-creation/UnifiedCreationPage.tsx``src/components/puzzle-agent/PuzzleAgentWorkspace.tsx``src/components/wooden-fish-creation/WoodenFishWorkspace.tsx``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、玩法链路文档。
- 决策:一期统一创作页(拼图、抓大鹅、敲木鱼)必须由 `UnifiedCreationPage` 提供统一标题栏、内容区、页面级纵向滚动和隐藏字段契约;字段元信息只留给测试和代码,不再额外作为可见 chip 占用首屏。玩法工作台只承载具体输入控件、上传、历史素材、校验和提交,不再各自渲染巨大入口标题。拼图、抓大鹅与敲木鱼的实现已经统一收口到 `src/components/unified-creation/workspaces/`,统一壳只依赖 `UnifiedCreationWorkspace`。敲木鱼右侧音效和功德面板不得再套内部滚动容器,移动端应自然跟随页面滚动。
- 追加决策:`UnifiedCreationPage` 自己负责页面级滚动;拼图、抓大鹅、跳一跳和敲木鱼四条统一创作入口必须在同一页面壳内从统一标题、表单控件一路滑到提交按钮,避免工作台内部或右侧面板形成套滚动。
- 影响范围:`src/components/unified-creation/UnifiedCreationPage.tsx``src/components/unified-creation/UnifiedCreationWorkspace.tsx``src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx``src/components/unified-creation/workspaces/Match3DCreationWorkspace.tsx``src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.tsx``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、玩法链路文档。
- 验证方式:`UnifiedCreationPage` 测试应断言隐藏契约仍在但 UI 不再出现字段 chip拼图和抓大鹅工作台测试应断言 `unifiedChrome=true` 时不再渲染旧巨大标题且仍保留表单输入;木鱼工作台测试或手测应确认敲击音效和功德词条不再停留在独立滚动窗内。
- 关联文档:`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-31 统一创作壳扩展到跳一跳并接管页面级滚动
- 背景:最初的统一创作页只收口拼图、抓大鹅和敲木鱼,跳一跳仍通过独立工作台壳与独立生成壳渲染,导致用户在 `/creation/jump-hop` 看到的可见外壳与其它统一入口不一致。
- 决策:`jump-hop` 也纳入统一创作壳与统一生成壳;`UnifiedCreationPage` 现在承担页面级滚动和统一标题栏,拼图、抓大鹅、跳一跳、敲木鱼四条入口都通过同一外壳承载各自工作台。`JumpHopCreationWorkspace``WoodenFishCreationWorkspace` 也补了 `unifiedChrome` / `showBackButton` 受控能力,避免双标题或双返回按钮。
- 追加决策:`UnifiedCreationPage` 的统一页头现在承载唯一返回入口,工作台内部的返回按钮全部关闭,避免同一页面出现双返回按钮;`UnifiedCreationWorkspace` 统一把 `onBack` 透传给页头。
- 追加决策:统一创作页内容区必须保持自然高度,页面级滚动只由 `UnifiedCreationPage` 外层承担,工作台内部只负责内容展开,不再额外包滚动壳。
- 影响范围:`src/components/unified-creation/UnifiedCreationPage.tsx``src/components/unified-creation/unifiedCreationSpecs.ts``src/components/unified-creation/unifiedGenerationCopy.ts``src/components/unified-creation/workspaces/JumpHopCreationWorkspace.tsx``src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.tsx``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``server-rs/crates/shared-contracts/src/creation_entry_config.rs`
- 验证方式:`npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx``npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx``npm run test -- src/routing/appPageRoutes.test.ts`
## 2026-05-31 统一创作编排层必须由 UnifiedCreationWorkspace 统一收口
- 背景:`PlatformEntryFlowShellImpl` 仍直接 lazy import 并渲染四个旧工作台分支,虽然统一创作页已存在,但入口壳层仍然依赖旧工作台分支。
- 决策:新增 `UnifiedCreationWorkspace` 作为平台壳唯一依赖的统一创作编排层,由它内部按 `playId` 选择四条入口的真实工作台;平台壳层只再挂这一层,不再直接依赖旧工作台组件。旧工作台已移入 `src/components/unified-creation/workspaces/`,不再作为平台入口编排事实源。
- 影响范围:`src/components/unified-creation/UnifiedCreationWorkspace.tsx``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`、统一创作页相关测试与后续入口接入。
- 验证方式:平台壳源码中不应再直接出现四个旧工作台的入口渲染分支;创作 Tab 与 `/creation/<play>` 仍可正常进入对应工作台。
- 关联文档:`docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## 2026-05-27 生成页总进度圆弧锁定固定 SVG 坐标系
- 背景:多轮圆环角度微调后,`GenerationProgressHero` 的 SVG 圆弧仍会出现底部开口偏斜的问题;后来窄屏验收又发现固定 `400px` 外层宽度会让等待页右侧被裁切。
@@ -591,8 +608,8 @@
- 背景:抓大鹅草稿素材生成已经收敛为多视角 2D 图片素材,但入口页和旧参考图仍沿用黏土、低多边形、塑料、木雕、体素、金属等偏 3D 素材语言,容易让后续生成链路和用户预期继续漂移。
- 决策:抓大鹅创作入口 `2D素材风格` 固定为 `扁平图标 / 赛璐璐卡通 / 像素复古 / 手绘水彩 / 贴纸描边 / 厚涂图标 / 自定义`;默认风格为 `flat-icon`。入口参考图统一由 `npm run assets:match3d-style-references -- --live` 调用 VectorEngine `gpt-image-2` 生成,输出到 `public/match3d-style-references/`。旧 3D 风格参考图不再保留为入口资产。
- 影响范围:`Match3DAgentWorkspace`、抓大鹅入口交互测试、Match3D PRD、素材生成流水线技术文档、F1 入口文档和 `public/match3d-style-references/` 静态资产。
- 验证方式:执行 `npm run test -- src\components\match3d-creation\Match3DAgentWorkspace.interaction.test.tsx``cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml``npm run typecheck``npm run check:encoding`,并人工抽查 `.tmp/match3d-style-preview.png`
- 影响范围:抓大鹅统一创作工作台、抓大鹅入口交互测试、Match3D PRD、素材生成流水线技术文档、F1 入口文档和 `public/match3d-style-references/` 静态资产。
- 验证方式:执行 `npm run test -- src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx``cargo test -p shared-contracts match3d --manifest-path server-rs\Cargo.toml``npm run typecheck``npm run check:encoding`,并人工抽查 `.tmp/match3d-style-preview.png`
- 关联文档:`docs/prd/AI_NATIVE_MATCH3D_CREATOR_AND_GAMEPLAY_SYSTEM_PRD_2026-04-30.md``docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md``docs/technical/MATCH3D_F1_CREATION_ENTRY_AND_AGENT_UI_2026-04-30.md`
## 2026-05-12 拼图与抓大鹅草稿背景音乐按纯音乐自动生成
@@ -1109,7 +1126,7 @@
- 背景:敲木鱼工作台只应保留生成所需输入,作品标题、简介和主题标签适合放在生成草稿后的补录阶段。
- 决策:敲木鱼的 `workTitle``workDescription``themeTags` 从工作台首屏移到结果页;结果页编辑后在试玩或发布前先调用 `update-work-meta` 写回当前作品信息。主题标签编辑样式对齐拼图结果页的胶囊标签编辑器。
- 影响范围:`WoodenFishWorkspace``WoodenFishResultView``PlatformEntryFlowShellImpl`、敲木鱼 PRD 和平台入口链路文档。
- 影响范围:敲木鱼统一创作工作台`WoodenFishResultView``PlatformEntryFlowShellImpl`、敲木鱼 PRD 和平台入口链路文档。
- 验证方式:工作台首屏不再出现标题 / 简介 / 标签输入;结果页修改后点试玩或发布会先写回当前作品信息。
- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`

View File

@@ -142,6 +142,22 @@
- 验证:浏览器里这三页的根区应仍保留 `platform-remap-surface`,但不再出现 `platform-page-stage`;草稿页顶部筛选样式应和发现页频道标签一致。
- 关联:`src/components/custom-world-home/CustomWorldCreationHub.tsx``src/components/custom-world-home/CustomWorldWorkTabs.tsx``src/components/rpg-entry/RpgEntryHomeView.tsx``src/index.css`
## 统一创作壳现在自己负责页面滚动和四条入口外壳
- 现象:统一创作页最初只包住拼图、抓大鹅和敲木鱼的工作台内容,跳一跳仍然保留独立工作台壳,页面级滚动职责也散落在平台入口 motion wrapper 里,导致移动端不同入口的可见外壳不一致。
- 原因:`UnifiedCreationPage` 只做了标题和隐藏契约,入口壳还在各自工作台里保留 `platform-remap-surface` / `overflow-y-auto``jump-hop` 也没进入统一 spec。
- 处理:把 `jump-hop` 纳入 `unifiedCreationSpec`,让 `UnifiedCreationPage` 自己承担页面级滚动与统一标题栏;`JumpHopCreationWorkspace``WoodenFishCreationWorkspace``unifiedChrome` / `showBackButton`,平台壳不再给这几条统一入口套额外滚动壳。
- 验证:`npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx` 通过后,`/creation/puzzle``/creation/match3d``/creation/jump-hop``/creation/wooden-fish` 都应由同一套统一创作页外壳承载。
- 关联:`src/components/unified-creation/UnifiedCreationPage.tsx``src/components/unified-creation/unifiedCreationSpecs.ts``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
## 统一创作编排层不要再让平台壳直挂旧工作台
- 现象:平台入口壳已经切到统一创作外壳,但源码里仍直接 lazy import 并渲染四个旧工作台分支,看起来还是四套入口编排。
- 原因:统一创作页只收口了可见外壳,入口层没有再抽一层统一创作编排组件,导致平台壳依旧要认识各玩法旧工作台。
- 处理:新增 `UnifiedCreationWorkspace`,由它内部按 `playId` 选择真实工作台;平台壳只依赖这一层,不再直接挂旧工作台分支。旧工作台已迁入 `src/components/unified-creation/workspaces/`,不再是入口编排事实源。
- 验证:`PlatformEntryFlowShellImpl.tsx` 中不应再出现四个旧工作台的入口渲染分支,创作 Tab 与 `/creation/<play>` 仍能正常进入对应工作台。
- 关联:`src/components/unified-creation/UnifiedCreationWorkspace.tsx``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`
## Jenkinsfile 开头不能带 UTF-8 BOM
@@ -255,7 +271,7 @@
- 原因:通用图像输入是受控输入面板,不是只服务单页的临时实现;图片、提示词、参考图数组、重绘开关等业务真相应由外层页面持有,组件最多持有参考图预览、删除确认这类短生命周期 UI 状态。
- 处理:抽 `CreativeImageInputPanel` 时,保留上传卡、参考图入口、缩略图、预览弹层、删除确认和提交按钮的统一壳,但把主图文件读取、裁剪、历史素材、计费确认和具体提交动作留给外层页面;后续页面接入时只传业务回调和文案。
- 验证:拼图入口测试仍可通过,且新组件可通过不同页面复用而不需要复制上传卡实现。
- 关联:`src/components/common/CreativeImageInputPanel.tsx``src/components/puzzle-agent/PuzzleAgentWorkspace.tsx`
- 关联:`src/components/common/CreativeImageInputPanel.tsx``src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx`
## RPG 发布不能只依赖 agent session seed_text
@@ -693,7 +709,7 @@
- 原因:首图生成只通过 `compile_puzzle_draft.referenceImageSrc` 临时传 Data URL不持久化到 SpacetimeDB结果页重新生成则要把当前上传图或关卡 `pictureReference` 作为 `generate_puzzle_images.referenceImageSrc` 继续传给后端。
- 处理:浏览器 Network 里确认 action payload 带 `referenceImageSrc`api-server 日志按同一 `session_id` 查看 `拼图参考图解析完成``拼图 VectorEngine 图片生成 HTTP 返回``拼图 VectorEngine 图片下载完成``拼图生成图片已写入 OSS 与资产索引`可定位慢在参考图读取、VectorEngine、下载或 OSS。
- 验证:前端测试覆盖上传图 + AI 重绘、结果页保存的 `pictureReference` 重新生成;后端单测覆盖 VectorEngine 请求体 `image` 字段。
- 关联:`src/components/puzzle-agent/PuzzleAgentWorkspace.tsx``src/components/puzzle-result/PuzzleResultView.tsx``server-rs/crates/api-server/src/puzzle.rs`
- 关联:`src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx``src/components/puzzle-result/PuzzleResultView.tsx``server-rs/crates/api-server/src/puzzle.rs`
## 拼图首图生成后要把入口参考图写回 `pictureReference`
@@ -1447,16 +1463,16 @@
- 现象:拼图创作页或结果页打开“选择历史图片”后,历史列表显示 `账号 user-1` 之类归属文案而不是图片名;`1713686400.000000Z` 这类时间显示为未知;选中后预览或生成参考图可能被怀疑不可用。
- 原因:`/api/assets/history?kind=puzzle_cover_image` 返回的 `ownerLabel` 是资产归属账号,不是图片标题;`createdAt` 可能是 SpacetimeDB / shared-kernel 秒级时间字符串,不能只用浏览器 `new Date(value)` 解析。历史图的 `imageSrc``/generated-*` 私有兼容路径,浏览器预览必须换签。
- 处理:前端标题和选中标签从 `imageSrc` 路径末尾推导,例如 `image.png`;时间解析兼容 ISO 与 `1713686400.000000Z`;创作页主图、历史列表图和结果页参考图继续用 `ResolvedAssetImage`,提交给后端时仍保留原始 `imageSrc`
- 验证:`npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`,并执行 `npm run check:encoding`
- 关联:`src/services/puzzle-works/puzzleHistoryAsset.ts``src/components/puzzle-agent/PuzzleHistoryAssetPickerDialog.tsx``docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md`
- 验证:`npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`,并执行 `npm run check:encoding`
- 关联:`src/services/puzzle-works/puzzleHistoryAsset.ts``src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.tsx``docs/technical/ASSET_HISTORY_PUZZLE_COVER_KIND_FIX_2026-04-27.md`
## 拼图历史图关闭 AI 重绘不要强制 Data URL
- 现象:拼图创作页从历史生成图片中选择主图,再关闭 AI 重绘生成草稿时,后端报“上传图必须是图片 Data URL”。
- 原因:历史图 `imageSrc``/generated-puzzle-assets/...` 私有兼容路径AI 重绘开启时后端参考图分支会解析该路径,但关闭 AI 重绘的“直用上传图”分支旧实现只调用 `parse_puzzle_image_data_url`
- 处理:关闭 AI 重绘时也复用拼图参考图解析入口,允许 Data URL 与 `/generated-*` 历史路径统一转成 `PuzzleDownloadedImage` 后持久化;前端不需要下载历史图再转 base64。
- 验证:`npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx``cargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.toml``npm run dev:api-server` 后检查 `/healthz`
- 关联:`server-rs/crates/api-server/src/puzzle/draft.rs``server-rs/crates/api-server/src/puzzle/vector_engine.rs``src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx`
- 验证:`npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx``cargo test -p api-server puzzle_uploaded_cover_can_reuse_resolved_history_image --manifest-path server-rs\Cargo.toml``npm run dev:api-server` 后检查 `/healthz`
- 关联:`server-rs/crates/api-server/src/puzzle/draft.rs``server-rs/crates/api-server/src/puzzle/vector_engine.rs``src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx`
## 拼图结果页局部生图不要污染草稿生成态
@@ -1474,7 +1490,7 @@
- 原因:上传图直用路径应把 Data URL 或 `/generated-*` 历史图解析后持久化为 `sourceType=uploaded` 的正式候选,再继续生成 9:16 关卡画面、UI spritesheet 和纯背景;如果只把 `aiRedraw=false` 当作“不参考图片生成”,就会误走首图生成。
- 处理:入口页用 payload 的 `aiRedraw` 写入生成页 metadata`puzzleAiRedraw=false` 时进度跳过 `生成拼图首图`;后端 `compile_puzzle_draft` 和结果页 `generate_puzzle_images` 都在 `aiRedraw=false && referenceImageSrc 非空` 时走上传图直用候选。结果页关卡详情必须复用 `CreativeImageInputPanel`,不要把正式图当成可重绘参考图;本次上传或历史选择的图才显示 AI 重绘开关并可删除。
- 验证:`npm run test -- src/services/miniGameDraftGenerationProgress.test.ts src/components/puzzle-result/PuzzleResultView.test.tsx``cargo test -p api-server puzzle_result_level_direct_upload_skips_cover_image_generation --manifest-path server-rs\Cargo.toml`
- 关联:`src/services/miniGameDraftGenerationProgress.ts``src/components/puzzle-agent/PuzzleAgentWorkspace.tsx``src/components/puzzle-result/PuzzleResultView.tsx``server-rs/crates/api-server/src/puzzle/draft.rs``server-rs/crates/api-server/src/puzzle/generation.rs`
- 关联:`src/services/miniGameDraftGenerationProgress.ts``src/components/unified-creation/workspaces/PuzzleCreationWorkspace.tsx``src/components/puzzle-result/PuzzleResultView.tsx``server-rs/crates/api-server/src/puzzle/draft.rs``server-rs/crates/api-server/src/puzzle/generation.rs`
## Jenkins 数据库导入导出脚本先补 Node 工具链 PATH
@@ -1550,8 +1566,8 @@
- 现象:竖屏打开拼图、抓大鹅或敲木鱼创作页时,浏览器页面本身无法滚动,生成按钮或右侧表单面板落到视口外;木鱼的敲击音效和功德词条看起来像被塞进单独滑动窗口。
- 原因:平台根壳固定一屏并隐藏溢出,`UnifiedCreationPage` 又使用 `h-full min-h-0 overflow-hidden` 和内容区 `overflow-y-auto`,导致滚动责任落到内部内容窗,而不是整个创作 stage。
- 处理:`UnifiedCreationPage` 只保留统一标题、隐藏字段契约内容包装,不再设置内部纵向滚动;拼图、抓大鹅和敲木鱼三个统一创作入口的 `motion.div` stage 负责 `overflow-y-auto overflow-x-hidden`。拼图和抓大鹅`unifiedChrome` 下收起旧 `h-full overflow-hidden` 外壳,让表单主体跟随 stage 滚动。
- 验证:用竖屏浏览器视口打开 `/creation/wooden-fish``/creation/puzzle``/creation/match3d`,页面级 stage 应可滚动到生成按钮;`.unified-creation-page__content` 应包含 `overflow-y-auto`,木鱼工作台内部也不应出现独立纵向滚动容器,拼图 / 抓大鹅可见标题不应重复。
- 处理:`UnifiedCreationPage` 统一负责标题、隐藏字段契约内容包装和页面级纵向滚动;拼图、抓大鹅、跳一跳和敲木鱼的外层 `motion.div` 不再额外包 `overflow-y-auto`。各工作台`unifiedChrome` 下收起旧 `h-full overflow-hidden` 外壳,让表单主体跟随统一页面滚动。
- 验证:用竖屏浏览器视口打开 `/creation/wooden-fish``/creation/puzzle``/creation/match3d``/creation/jump-hop`,统一创作页应可滚动到生成按钮;`.unified-creation-page` 应包含页面级 `overflow-y-auto`,木鱼工作台内部也不应出现独立纵向滚动容器,拼图 / 抓大鹅可见标题不应重复。
- 验证:`npm run test -- src/components/bark-battle-creation/BarkBattleConfigEditor.test.tsx``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab shows template tabs"`、移动端视口检查最后一个输入框与“生成草稿”按钮不重叠。
- 关联:`src/components/bark-battle-creation/BarkBattleConfigEditor.tsx``src/components/platform-entry/PlatformEntryFlowShellImpl.tsx``docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`

View File

@@ -9,7 +9,7 @@
| 总轮次 | 5 |
| 当前轮次 | Round 4已收口 |
| 当前阶段 | Phase 6 |
| 当前状态 | Phase 0~6 已收口;跨玩法回归、移动端竖屏 smoke、API smoke 和冻结材料已补齐 |
| 当前状态 | Phase 0~6 已收口;统一创作页已升级为 `UnifiedCreationWorkspace`,平台壳不再直接依赖旧工作台文件 |
| 当前并行波次 | 波次 D验收与冻结 |
| 当前重点 | 以跨玩法门禁作为后续新增玩法和回归的常规质量基线 |
@@ -28,7 +28,8 @@
| 阶段 | 状态 | 说明 |
| --- | --- | --- |
| Phase 0 总计划与门禁 | 已完成 | 本文档、`docs/planning/README.md``docs/README.md``.hermes/shared-memory/document-map.md` 已补齐入口;后续按 phase 扩展门禁。 |
| Phase 1 首批统一壳 | 已收口 | `puzzle``match3d``wooden-fish` 已接入 `UnifiedCreationPage` / `UnifiedGenerationPage`,竖屏滚动和字段契约已回归。 |
| Phase 1 首批统一壳 | 已收口 | `puzzle``match3d``jump-hop``wooden-fish` 已接入 `UnifiedCreationPage` / `UnifiedGenerationPage`,竖屏滚动和字段契约已回归。 |
| Phase 1 补充统一壳 | 已收口 | `jump-hop` 也已接入 `UnifiedCreationPage` / `UnifiedGenerationPage`,统一创作页现在接管拼图、抓大鹅、跳一跳和敲木鱼四条入口的可见外壳与滚动。 |
| Phase 2 契约与配置治理 | 已完成 | `creationTypes[].unifiedCreationSpec`、前端 fallback、后台配置校验和文档门禁已按现有测试与 schema 检查收口。 |
| Phase 3 剩余表单/图片工作台接入 | 已收口 | 跳一跳、宝贝识物、方洞结果页与首批普通工作台回归已通过;方洞、大鱼按当前形态纳入最小回归,后续若迁移工作台再单独立项。 |
| Phase 4 特殊工作台接入策略 | 已收口 | RPG、视觉小说、汪汪声浪的最小例外/闭环回归已通过,例外口径已落到平台总链路文档。 |
@@ -69,12 +70,12 @@
### Phase 1首批统一壳收口
目标:用低风险的条链路验证统一创作/生成壳。
目标:用低风险的条链路验证统一创作/生成壳。
- 范围:`puzzle``match3d``wooden-fish`
- 范围:`puzzle``match3d``jump-hop``wooden-fish`
- 创作页统一经过 `UnifiedCreationPage`,工作台保留各自真实输入能力。
- 生成页统一经过 `UnifiedGenerationPage``CustomWorldGenerationView`
- 竖屏滚动由外层 stage 承担,避免内层滚动窗。
- 竖屏滚动由统一创作页承担,避免内层滚动窗。
状态:已收口。
@@ -113,6 +114,7 @@
- 自动素材生成走统一生成页;没有自动生成的玩法需要明确跳过生成页的阶段策略。
- 结果页和 runtime 不因迁移创作页而改业务真相。
- `square-hole``big-fish` 先评估是否保留 Agent 形态还是迁到表单/图片工作台,再决定是否进入直接迁移实现。
- `jump-hop` 已纳入统一创作壳,后续若要调整字段或视觉,只能在统一壳与工作台之间协同改,不再恢复独立入口壳。
退出条件:
@@ -189,7 +191,7 @@
目标:形成后续新增玩法可复用的稳定门禁。
- 按玩法输出创作入口、生成页、结果页、试玩、发布、作品架、广场、runtime smoke 矩阵。
- 增补 `quality-gates/README.md` 与跨玩法回归 / 冒烟门禁,不再只覆盖首批条链路。
- 增补 `quality-gates/README.md` 与跨玩法回归 / 冒烟门禁,不再只覆盖首批条链路。
- 固化移动端竖屏优先验收,桌面端作为兼容验证。
- 补齐“新增玩法接入 PRD 检查块”和代码评审检查清单。
@@ -217,7 +219,7 @@ Phase 6 不再继续拆新波次,当前只把 Phase 2 到 Phase 5 的最小验
| 阶段 | 最小命令 | 说明 |
| --- | --- | --- |
| Phase 2 | `npm run check:encoding``npm run typecheck``npm run admin-web:typecheck``npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx` | 校验入口配置、统一字段 spec、统一创作页和统一生成页。 |
| Phase 3 | `npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/match3d-creation/Match3DAgentWorkspace.interaction.test.tsx src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx src/components/jump-hop-creation/JumpHopWorkspace.test.tsx src/components/jump-hop-result/JumpHopResultView.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/big-fish-creation/BigFishAgentWorkspace.interaction.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"` | 校验普通表单 / 图片 / 音频工作台仍按结构化 payload 提交,跳一跳结果页直达恢复不白屏,并把 BabyObjectMatch / BigFish 一并纳入最小回归。 |
| Phase 3 | `npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/jump-hop-result/JumpHopResultView.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/big-fish-creation/BigFishAgentWorkspace.interaction.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"` | 校验普通表单 / 图片 / 音频工作台仍按结构化 payload 提交,跳一跳结果页直达恢复不白屏,并把 BabyObjectMatch / BigFish 一并纳入最小回归。 |
| Phase 4 | `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "opening RPG agent workspace does not refetch session snapshot in a render loop|create tab resumes agent workspace when draft has no compiled result yet|create tab resumes agent workspace when session has no draft profile even if summary counts look compiled|opening a compiled draft with a missing agent session falls back to draft hub"``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "create tab opens bark battle entry form from the template card|bark battle draft result can test before publish and publish to work detail|direct bark battle runtime public code opens published runtime"``npm run check:visual-novel-vn11` | 校验特殊工作台例外、Bark Battle 公开闭环和视觉小说负向门禁。 |
| Phase 5 | `npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "agent draft result publishes to gallery from publish panel|creation hub published work enters existing detail view|creation hub published work experience button enters world directly|creation hub published work start uses loaded detail profile instead of library summary"``npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "published puzzle works appear on home and mobile game category channel|published big fish works stay hidden from platform home and mobile game category channel|home recommendation Match3D runtime keeps profile generated models when card summary is stale|home recommendation Match3D runtime passes top-level UI background assets|home recommendation Match3D runtime reloads detail when card only has UI assets"``npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformFeedbackView.test.tsx src/components/rpg-entry/rpgEntryWorldPresentation.test.ts``npm run test -- src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx` | 校验结果页、发布、作品架、公开详情、推荐 runtime 和公开 read model并补公开详情作者展示口径与作品架恢复矩阵。 |
@@ -232,7 +234,7 @@ Phase 6 不再继续拆新波次,当前只把 Phase 2 到 Phase 5 的最小验
| T2-1 | 是 | 已完成 | `unifiedCreationSpec` 契约审计 | 字段种类、必填、阶段映射、fallback 规则 | T0-1 | 契约 owner |
| T2-2 | 是 | 已完成 | 后台入口配置治理 | 后台配置校验与配置说明 | T2-1 | 后台 owner |
| T2-3 | 是 | 已完成 | 前端入口读取与 fallback 测试 | 入口配置单测、异常兜底测试 | T2-1 | 前端 owner |
| T3-1 | 是 | 自动回归通过 | 跳一跳统一接入方案与实现 | 创作页/生成页迁移、验收 | T2-1 | 玩法 owner A |
| T3-1 | 是 | 自动回归通过 | 跳一跳统一接入方案与实现 | 统一创作工作台、生成页迁移、验收 | T2-1 | 玩法 owner A |
| T3-2 | 是 | 自动回归通过 | 宝贝识物统一接入方案与实现 | 创作页/生成页迁移、验收 | T2-1 | 玩法 owner B |
| T3-3 | 是 | 最小回归通过 | 方洞工作台形态评估与迁移方案 | 保留 Agent 形态还是迁表单的决策、边界和风险清单 | T2-1 | 玩法 owner C |
| T3-4 | 是 | 最小回归通过 | 大鱼工作台形态评估与迁移方案 | 保留 Agent 形态还是迁表单的决策、边界和风险清单 | T2-1 | 玩法 owner D |
@@ -243,6 +245,7 @@ Phase 6 不再继续拆新波次,当前只把 Phase 2 到 Phase 5 的最小验
| T5-2 | 是 | 最小回归通过 | 作品架恢复矩阵 | 生成中、失败、待发布、已发布恢复验收 | T3/T4 方案稳定 | 作品架 owner |
| T5-3 | 是 | 最小回归通过 | 公开 read model 对齐 | 广场/详情/分享码缺口与执行清单 | T3/T4 方案稳定 | 后端 owner |
| T5-4 | 是 | 最小回归通过 | 统一错误与完成反馈回归 | `PlatformErrorDialog``PlatformTaskCompletionDialog` 覆盖 | T3/T4 方案稳定 | 平台壳 owner |
| T5-5 | 是 | 自动回归通过 | 统一创作壳滚动收口 | `UnifiedCreationPage` 统一接管四条入口滚动、跳一跳纳入统一壳 | T3/T4 方案稳定 | 平台壳 owner |
| T6-1 | 否 | 已完成 | 全链路质量门禁扩展 | `quality-gates/README.md`、跨玩法回归 / 冒烟门禁、Phase 2 到 Phase 5 最小验证集合 | T3/T4/T5 完成 | QA owner |
| T6-2 | 否 | 已完成 | 全量验收与冻结 | 状态表、未接入声明、最终测试记录 | T6-1 | Release owner |
@@ -262,7 +265,7 @@ Phase 6 不再继续拆新波次,当前只把 Phase 2 到 Phase 5 的最小验
| P2-A 契约包 | 可与 P2-B、P2-C 并行 | 已完成 | 固定 `unifiedCreationSpec` 字段类型、必填、阶段映射、fallback 规则 | 不接新玩法,不改 UI 设计方向 | 前后端契约、配置字段文档、契约测试 | `npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/platform-entry/platformEntryCreationTypes.test.ts`;涉及 schema 时追加 `npm run check:spacetime-schema` |
| P2-B 后台配置包 | 可与 P2-A、P2-C 并行 | 已完成 | 后台入口配置能编辑、校验、保存统一创作契约 | 不做玩法工作台迁移 | 后台表单、保存校验、异常提示、后台单测 | `npm run admin-web:typecheck``npm run test -- apps/admin-web/src/pages/AdminCreationEntrySwitchPage.test.tsx` |
| P2-C 前台读取包 | 可与 P2-A、P2-B 并行 | 已完成 | 前台从 `/api/creation-entry/config` 读取统一 spec旧后端只走兜底 | 不把 fallback 当事实源,不恢复硬编码入口 | 入口派生、fallback 单测、统一创作/生成页回归 | `npm run test -- src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx` |
| P3-A 跳一跳接入包 | 可与 P3-B、P3-C、P3-D 并行 | 自动回归通过 | `jump-hop` 接入统一创作壳、生成页或明确跳过策略 | 不改正式 runtime 规则真相,不重做作品架 | 入口阶段映射、工作台接入、生成/结果跳转、竖屏验收 | 跳一跳相关单测、统一创作页回归、移动端 `/creation/jump-hop` smoke |
| P3-A 跳一跳接入包 | 可与 P3-B、P3-C、P3-D 并行 | 自动回归通过 | `jump-hop` 接入统一创作壳、生成页或明确跳过策略 | 不改正式 runtime 规则真相,不重做作品架 | 入口阶段映射、统一工作台接入、生成/结果跳转、竖屏验收 | 跳一跳相关单测、统一创作页回归、移动端 `/creation/jump-hop` smoke |
| P3-B 宝贝识物接入包 | 可与 P3-A、P3-C、P3-D 并行 | 自动回归通过 | `baby-object-match` 接入统一创作壳、生成页或明确跳过策略 | 不复制上传/历史素材逻辑 | 工作台接入、资产槽位复用、结果跳转、竖屏验收 | 宝贝识物相关单测、统一创作页回归、移动端 `/creation/baby-object-match` smoke |
| P3-C 方洞评估包 | 可与 P3-A、P3-B、P3-D 并行 | 部分回归通过 | 判断 `square-hole` 保留 Agent 形态还是迁表单/图片工作台 | 不直接大改实现 | 例外或迁移方案、字段清单、风险、验收用例 | 文档评审通过;若改代码,补对应工作台测试 |
| P3-D 大鱼评估包 | 可与 P3-A、P3-B、P3-C 并行 | 部分回归通过 | 判断 `big-fish` 保留 Agent 形态还是迁表单/图片工作台 | 不直接大改实现 | 例外或迁移方案、字段清单、风险、验收用例 | 文档评审通过;若改代码,补对应工作台测试 |
@@ -307,6 +310,7 @@ Phase 6 不再继续拆新波次,当前只把 Phase 2 到 Phase 5 的最小验
状态:自动回归已通过,遗留形态评估按缺口任务继续跟踪。
- `T3-1` `jump-hop` 统一接入
- `T5-5` 统一创作壳滚动收口
- `T3-2` `baby-object-match` 统一接入
- `T3-3` `square-hole` 工作台形态评估
- `T3-4` `big-fish` 工作台形态评估

View File

@@ -80,7 +80,7 @@ jump-hop-gallery-detail
新增前端组件建议:
1. `src/components/jump-hop-creation/JumpHopWorkspace.tsx`
1. `src/components/unified-creation/workspaces/JumpHopCreationWorkspace.tsx`
2. `src/components/jump-hop-result/JumpHopResultView.tsx`
3. `src/components/jump-hop-runtime/JumpHopRuntimeShell.tsx`
4. `src/services/jump-hop/jumpHopClient.ts`

View File

@@ -10,7 +10,7 @@
创作恢复参数只保留 `sessionId``profileId``draftId``workId` 这四个私有 query。它们只允许在同一条创作链路的结果页、生成页、工作台之间保留切到首页、公开作品详情、runtime 或另一条玩法链路时必须清掉。生成页等待时间统一以生成状态里的 `startedAtMs` 为准;创建该状态时优先使用后端 session 下发的时间戳,作品摘要里的 `updatedAt` 仍只用于排序与摘要展示,不作为前端自行推导业务状态的真相。
一期创作流程统一化覆盖拼图、抓大鹅和敲木鱼。者在前端统一经过 `UnifiedCreationPage``UnifiedGenerationPage`:创作页字段清单由后端在 `GET /api/creation-entry/config``creationTypes[].unifiedCreationSpec` 下发,前端仅在该扩展位缺失时回退到本地默认 spec首期字段类型只保留 `text``select``image``audio``UnifiedCreationPage` 提供统一标题栏、内容区和隐藏字段契约,不在 UI 中额外展示字段说明 chip,也不创建自己的纵向滚动窗;三条统一创作入口由平台 stage 承担整页纵向滚动,竖屏移动端必须能从标题、表单一路滑到提交按钮。各玩法工作台负责渲染真实输入控件、上传、历史素材、校验和提交。拼图工作台仍复用既有 `PuzzleAgentWorkspace` 的上传、裁剪、历史图、AI 重绘和提交逻辑,抓大鹅继续复用 `Match3DAgentWorkspace` 的题材与难度表单逻辑,但二者在统一壳内都只展示表单主体,保证打开创作页时能看到统一创作页外观且不出现双标题或旧外层壳。敲木鱼的音效和功德词条面板不得放进独立内部滚动容器,移动端应跟随页面自然滚动展开。生成页统一展示阶段、当前步骤、总进度、错误和重试动作。视觉小说、`airp``component`、汪汪声浪、方洞、大鱼、跳一跳和宝贝识物不进入一期接线范围,已有链路保持现状。
一期创作流程统一化覆盖拼图、抓大鹅、跳一跳和敲木鱼。者在前端统一经过 `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`、汪汪声浪、方洞、大鱼和宝贝识物不进入一期接线范围,已有链路保持现状。
创作表单提交前的泥点余额前置校验只允许用独立弹窗提示失败原因,不得把用户退回创作入口或玩法模板列表,也不得清空当前表单状态。当前适用拼图、抓大鹅和汪汪声浪等会在前端提交前校验泥点的生成入口;余额不足、余额读取失败都应停留在当前工作台,由用户关闭提示后继续编辑或自行补足泥点。

View File

@@ -16,7 +16,8 @@ npm run check:encoding
npm run typecheck
npm run admin-web:typecheck
npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts src/components/unified-creation/unifiedCreationSpecs.test.ts src/components/unified-creation/UnifiedCreationPage.test.tsx src/components/unified-creation/UnifiedGenerationPage.test.tsx
npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/match3d-creation/Match3DAgentWorkspace.interaction.test.tsx src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx
npm run test -- src/components/unified-creation/UnifiedCreationWorkspace.test.tsx
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx
```
## 使用说明

View File

@@ -4,13 +4,14 @@
```bash
npm run test -- src/components/unified-creation/unifiedCreationSpecs.test.ts
npm run test -- src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx
npm run test -- src/components/unified-creation/UnifiedCreationWorkspace.test.tsx
npm run test -- src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx
```
## 体验检查
- 拼图、抓大鹅、敲木鱼均从 `/creation/<playId>` 进入创作工作台。
- 条链路都经过 `UnifiedCreationPage`,字段 spec 只包含 `text``select``image``audio` 四类。
- 拼图、抓大鹅、跳一跳、敲木鱼均从 `/creation/<playId>` 进入创作工作台。
- 条链路都经过 `UnifiedCreationPage`,字段 spec 只包含 `text``select``image``audio` 四类。
- 拼图参考图仍走 `CreativeImageInputPanel`,不新增专属上传入口。
- 敲木鱼音频槽位走 `CreativeAudioInputPanel`,上传、录音、重置和默认音效状态可用。
- 抓大鹅难度只显示轻松、标准、进阶、硬核四档,提交 payload 仍派生 `clearCount``difficulty`

View File

@@ -29,7 +29,7 @@ npm run test -- src/components/platform-entry/platformEntryCreationTypes.test.ts
## Phase 3普通工作台
```bash
npm run test -- src/components/puzzle-agent/PuzzleAgentWorkspace.interaction.test.tsx src/components/match3d-creation/Match3DAgentWorkspace.interaction.test.tsx src/components/wooden-fish-creation/WoodenFishWorkspace.test.tsx src/components/jump-hop-creation/JumpHopWorkspace.test.tsx src/components/jump-hop-result/JumpHopResultView.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/big-fish-creation/BigFishAgentWorkspace.interaction.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx src/components/square-hole-result/SquareHoleResultView.test.tsx
npm run test -- src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/WoodenFishCreationWorkspace.test.tsx src/components/unified-creation/workspaces/JumpHopCreationWorkspace.test.tsx src/components/jump-hop-result/JumpHopResultView.test.tsx src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx src/components/edutainment-creation/BabyObjectMatchWorkspace.test.tsx src/components/edutainment-result/BabyObjectMatchResultView.test.tsx src/components/edutainment-runtime/BabyObjectMatchRuntimeShell.test.tsx src/components/big-fish-creation/BigFishAgentWorkspace.interaction.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx src/components/big-fish-runtime/BigFishRuntimeShell.test.tsx src/components/square-hole-result/SquareHoleResultView.test.tsx
npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "direct jump hop result route"
```

View File

@@ -99,6 +99,21 @@ pub fn build_phase1_unified_creation_spec(play_id: &str) -> Option<UnifiedCreati
unified_creation_field("difficulty", "select", "难度", true),
],
),
"jump-hop" => (
"jump-hop-workspace",
"jump-hop-generating",
"jump-hop-result",
vec![
unified_creation_field("workTitle", "text", "作品标题", true),
unified_creation_field("workDescription", "text", "作品简介", true),
unified_creation_field("themeTags", "text", "主题标签", true),
unified_creation_field("difficulty", "select", "难度", true),
unified_creation_field("stylePreset", "select", "风格", true),
unified_creation_field("characterPrompt", "text", "角色提示词", true),
unified_creation_field("tilePrompt", "text", "地块提示词", true),
unified_creation_field("endMoodPrompt", "text", "终点氛围", false),
],
),
"wooden-fish" => (
"wooden-fish-workspace",
"wooden-fish-generating",
@@ -231,7 +246,7 @@ mod tests {
use super::*;
#[test]
fn phase1_unified_creation_specs_only_cover_three_templates() {
fn phase1_unified_creation_specs_cover_four_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");
@@ -246,6 +261,10 @@ mod tests {
1
);
let jump_hop = build_phase1_unified_creation_spec("jump-hop").expect("jump-hop spec");
assert!(jump_hop.fields.iter().any(|field| field.id == "stylePreset"));
assert!(jump_hop.fields.iter().any(|field| field.id == "endMoodPrompt"));
let wooden_fish =
build_phase1_unified_creation_spec("wooden-fish").expect("wooden-fish spec");
assert!(wooden_fish.fields.iter().any(|field| field.kind == "audio"));

View File

@@ -101,6 +101,97 @@ test('creative image input panel handles reference uploads and preview', () => {
expect(onSubmit).toHaveBeenCalledTimes(1);
});
test('creative image input panel can opt out of filling the parent height', () => {
const { container } = render(
<CreativeImageInputPanel
fillHeight={false}
uploadedImageSrc=""
uploadedImageAlt="拼图图片"
mainImageInputId="image-upload-input"
promptTextareaId="image-prompt-input"
prompt="旧街灯牌下的猫。"
promptLabel="画面描述"
aiRedraw
promptReferenceImages={[]}
imageModelPicker={null}
submitLabel="生成"
submitDisabled={false}
labels={{
imageField: '拼图画面',
uploadImage: '上传拼图图片',
replaceImage: '更换拼图图片',
emptyImageHint: '上传图片/填写画面描述',
removeImage: '移除拼图图片',
removeImageConfirmTitle: '移除拼图图片?',
removeImageConfirmBody: '移除后需要重新上传图片。',
promptReferenceUpload: '上传参考图',
promptReferencePreviewAlt: '参考图预览',
closePromptReferencePreview: '关闭参考图预览',
}}
onMainImageFileSelect={() => {}}
onMainImageRemove={() => {}}
onAiRedrawChange={() => {}}
onPromptChange={() => {}}
onSubmit={() => {}}
/>,
);
const panel = container.querySelector('.creative-image-input-panel');
const body = container.querySelector('.creative-image-input-panel__body');
const section = container.querySelector('.creative-image-input-panel__section');
expect(panel?.className).toContain('flex-none');
expect(panel?.className).not.toContain('flex-1');
expect(body?.className).toContain('flex-none');
expect(body?.className).not.toContain('overflow-y-auto');
expect(section?.className).toContain('flex-none');
expect(section?.className).not.toContain('overflow-hidden');
});
test('creative image input panel fills the parent height by default', () => {
const { container } = render(
<CreativeImageInputPanel
uploadedImageSrc=""
uploadedImageAlt="拼图图片"
mainImageInputId="image-upload-input"
promptTextareaId="image-prompt-input"
prompt="旧街灯牌下的猫。"
promptLabel="画面描述"
aiRedraw
promptReferenceImages={[]}
imageModelPicker={null}
submitLabel="生成"
submitDisabled={false}
labels={{
imageField: '拼图画面',
uploadImage: '上传拼图图片',
replaceImage: '更换拼图图片',
emptyImageHint: '上传图片/填写画面描述',
removeImage: '移除拼图图片',
removeImageConfirmTitle: '移除拼图图片?',
removeImageConfirmBody: '移除后需要重新上传图片。',
promptReferenceUpload: '上传参考图',
promptReferencePreviewAlt: '参考图预览',
closePromptReferencePreview: '关闭参考图预览',
}}
onMainImageFileSelect={() => {}}
onMainImageRemove={() => {}}
onAiRedrawChange={() => {}}
onPromptChange={() => {}}
onSubmit={() => {}}
/>,
);
const panel = container.querySelector('.creative-image-input-panel');
const body = container.querySelector('.creative-image-input-panel__body');
const section = container.querySelector('.creative-image-input-panel__section');
expect(panel?.className).toContain('flex-1');
expect(panel?.className).not.toContain('flex-none');
expect(body?.className).toContain('flex-1');
expect(body?.className).toContain('overflow-y-auto');
expect(section?.className).toContain('flex-1');
expect(section?.className).toContain('overflow-hidden');
});
test('creative image input panel confirms before removing uploaded image', () => {
const onMainImageRemove = vi.fn();

View File

@@ -33,6 +33,7 @@ export type CreativeImageInputPanelLabels = {
export type CreativeImageInputPanelProps = {
className?: string;
fillHeight?: boolean;
disabled?: boolean;
isSubmitting?: boolean;
mainImageMode?: 'edit' | 'preview';
@@ -77,6 +78,7 @@ const DEFAULT_PROMPT_REFERENCE_LIMIT = 5;
export function CreativeImageInputPanel({
className = '',
fillHeight = true,
disabled = false,
isSubmitting = false,
mainImageMode = 'edit',
@@ -143,29 +145,48 @@ export function CreativeImageInputPanel({
}
}, [previewReferenceImage, promptReferenceImages]);
const bodyClassName = fillHeight
? 'creative-image-input-panel__body puzzle-creation-form-body flex min-h-0 flex-1 flex-col overflow-hidden pr-0 lg:overflow-y-auto lg:pr-1'
: 'creative-image-input-panel__body puzzle-creation-form-body flex flex-none flex-col overflow-visible pr-0 lg:pr-1';
const sectionClassName = fillHeight
? 'creative-image-input-panel__section puzzle-creation-form-section flex min-h-0 flex-1 flex-col overflow-hidden lg:overflow-visible'
: 'creative-image-input-panel__section puzzle-creation-form-section flex flex-none flex-col overflow-visible';
const gridSizeClassName = fillHeight ? 'min-h-0 flex-1' : 'flex-none';
const imageFieldClassName = fillHeight
? 'creative-image-input-panel__image-field puzzle-image-field flex min-h-0 min-w-0 flex-1 flex-col'
: 'creative-image-input-panel__image-field puzzle-image-field flex min-w-0 flex-none flex-col';
const imageFrameClassName = fillHeight
? 'creative-image-input-panel__image-frame puzzle-image-card-frame flex min-h-0 flex-1 items-center justify-center'
: 'creative-image-input-panel__image-frame puzzle-image-card-frame flex flex-none items-center justify-center';
const imageCardClassName = fillHeight
? 'creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square h-full min-h-[14rem] max-h-full max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem] lg:h-auto lg:w-full'
: 'creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square w-full min-h-[14rem] max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem]';
return (
<div
className={`creative-image-input-panel flex min-h-0 flex-1 flex-col ${className}`}
className={`creative-image-input-panel flex min-h-0 flex-col ${
fillHeight ? 'flex-1' : 'flex-none'
} ${className}`}
>
<div className="creative-image-input-panel__body puzzle-creation-form-body flex min-h-0 flex-1 flex-col overflow-hidden pr-0 lg:overflow-y-auto lg:pr-1">
<section className="creative-image-input-panel__section puzzle-creation-form-section flex min-h-0 flex-1 flex-col overflow-hidden lg:overflow-visible">
<div className={bodyClassName}>
<section className={sectionClassName}>
<div
className={`creative-image-input-panel__grid puzzle-creation-form-grid min-h-0 flex-1 gap-2.5 sm:gap-4 ${
className={`creative-image-input-panel__grid puzzle-creation-form-grid ${gridSizeClassName} gap-2.5 sm:gap-4 ${
showPrompt
? 'flex flex-col lg:grid lg:grid-cols-[minmax(15rem,0.9fr)_minmax(0,1.15fr)]'
: 'flex flex-col lg:grid lg:grid-cols-1'
}`}
>
<div
className={`creative-image-input-panel__image-field puzzle-image-field flex min-h-0 min-w-0 flex-1 flex-col ${
className={`${imageFieldClassName} ${
disabled ? 'opacity-55' : ''
}`}
>
<div className="mb-2 shrink-0 text-sm font-black text-[var(--platform-text-strong)]">
{labels.imageField}
</div>
<div className="creative-image-input-panel__image-frame puzzle-image-card-frame flex min-h-0 flex-1 items-center justify-center">
<div className="creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square h-full min-h-[14rem] max-h-full max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem] lg:h-auto lg:w-full">
<div className={imageFrameClassName}>
<div className={imageCardClassName}>
{canEditMainImage ? (
<>
<input

View File

@@ -1,105 +0,0 @@
import { ArrowLeft, Edit3, Sparkles } from 'lucide-react';
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
type Match3DDraftReadyViewProps = {
session: Match3DAgentSessionSnapshot;
isBusy?: boolean;
error?: string | null;
onBack: () => void;
};
export function Match3DDraftReadyView({
session,
isBusy = false,
error = null,
onBack,
}: Match3DDraftReadyViewProps) {
const draft = session.draft;
const title = draft?.gameName || '抓大鹅草稿';
return (
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-4xl flex-col">
<div className="mb-4 flex items-center justify-between gap-3">
<button
type="button"
onClick={onBack}
disabled={isBusy}
className={`platform-button platform-button--ghost min-h-0 self-start px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
>
<span className="inline-flex items-center gap-1.5">
<ArrowLeft className="h-3.5 w-3.5" />
</span>
</button>
</div>
<section className="platform-subpanel min-h-0 flex-1 overflow-y-auto rounded-[1.5rem] p-4 sm:p-5">
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
<div className="grid aspect-square w-full max-w-[10rem] shrink-0 place-items-center rounded-[1.2rem] bg-[radial-gradient(circle_at_30%_25%,rgba(190,242,100,0.36),transparent_34%),linear-gradient(135deg,rgba(16,185,129,0.18),rgba(251,146,60,0.16))] text-emerald-800">
<Sparkles className="h-10 w-10" />
</div>
<div className="min-w-0 flex-1">
<div className="text-2xl font-black leading-tight text-[var(--platform-text-strong)] sm:text-3xl">
{title}
</div>
<div className="mt-2 text-sm leading-6 text-[var(--platform-text-base)]">
{draft?.summaryText ?? session.lastAssistantReply ?? ''}
</div>
{draft ? (
<div className="mt-5 grid gap-2 sm:grid-cols-3">
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
<div className="text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
</div>
<div className="mt-1 truncate text-sm font-semibold text-[var(--platform-text-strong)]">
{draft.themeText}
</div>
</div>
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
<div className="text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
</div>
<div className="mt-1 text-sm font-semibold text-[var(--platform-text-strong)]">
{draft.totalItemCount ?? draft.clearCount * 3}
</div>
</div>
<div className="rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/68 px-3 py-3">
<div className="text-[11px] font-bold tracking-[0.16em] text-[var(--platform-text-soft)]">
</div>
<div className="mt-1 text-sm font-semibold text-[var(--platform-text-strong)]">
{draft.difficulty}
</div>
</div>
</div>
) : null}
{error ? (
<div className="platform-banner platform-banner--danger mt-4 rounded-[1.25rem] text-sm leading-6">
{error}
</div>
) : null}
</div>
</div>
</section>
<div className="mt-4 flex justify-end pb-[max(0.25rem,env(safe-area-inset-bottom))]">
<button
type="button"
disabled
className="platform-button platform-button--primary cursor-not-allowed opacity-55"
>
<span className="inline-flex items-center gap-2">
<Edit3 className="h-4 w-4" />
</span>
</button>
</div>
</div>
);
}
export default Match3DDraftReadyView;

View File

@@ -2856,10 +2856,10 @@ const CustomWorldGenerationView = lazy(async () => {
};
});
const UnifiedCreationPage = lazy(async () => {
const module = await import('../unified-creation/UnifiedCreationPage');
const UnifiedCreationWorkspace = lazy(async () => {
const module = await import('../unified-creation/UnifiedCreationWorkspace');
return {
default: module.UnifiedCreationPage,
default: module.UnifiedCreationWorkspace,
};
});
@@ -2907,13 +2907,6 @@ const BigFishRuntimeShell = lazy(async () => {
};
});
const Match3DAgentWorkspace = lazy(async () => {
const module = await import('../match3d-creation/Match3DAgentWorkspace');
return {
default: module.Match3DAgentWorkspace,
};
});
const Match3DResultView = lazy(async () => {
const module = await import('../match3d-result/Match3DResultView');
return {
@@ -2951,13 +2944,6 @@ const SquareHoleRuntimeShell = lazy(async () => {
};
});
const JumpHopWorkspace = lazy(async () => {
const module = await import('../jump-hop-creation/JumpHopWorkspace');
return {
default: module.JumpHopWorkspace,
};
});
const JumpHopResultView = lazy(async () => {
const module = await import('../jump-hop-result/JumpHopResultView');
return {
@@ -2972,13 +2958,6 @@ const JumpHopRuntimeShell = lazy(async () => {
};
});
const WoodenFishWorkspace = lazy(async () => {
const module = await import('../wooden-fish-creation/WoodenFishWorkspace');
return {
default: module.WoodenFishWorkspace,
};
});
const WoodenFishResultView = lazy(async () => {
const module = await import('../wooden-fish-result/WoodenFishResultView');
return {
@@ -3032,13 +3011,6 @@ const CustomWorldCreationHub = lazy(async () => {
};
});
const PuzzleAgentWorkspace = lazy(async () => {
const module = await import('../puzzle-agent/PuzzleAgentWorkspace');
return {
default: module.PuzzleAgentWorkspace,
};
});
const CreativeAgentWorkspace = lazy(async () => {
const module = await import('../creative-agent/CreativeAgentWorkspace');
return {
@@ -15732,37 +15704,33 @@ export function PlatformEntryFlowShellImpl({
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col overflow-y-auto overflow-x-hidden"
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={
<LazyPanelFallback label="正在加载抓大鹅共创工作区..." />
}
>
<UnifiedCreationPage
<UnifiedCreationWorkspace
playId="match3d"
spec={getUnifiedCreationSpec(
'match3d',
unifiedCreationConfigById.get('match3d'),
)}
>
<Match3DAgentWorkspace
session={match3dSession}
isBusy={isStreamingMatch3DReply}
error={match3dError}
onBack={leaveMatch3DFlow}
onExecuteAction={(payload) => {
void executeMatch3DAction(payload);
}}
initialFormPayload={match3dFormDraftPayload}
title={null}
unifiedChrome
onCreateFromForm={(payload) => {
runProtectedAction(() => {
void createMatch3DDraftFromForm(payload);
});
}}
/>
</UnifiedCreationPage>
session={match3dSession}
isBusy={isStreamingMatch3DReply}
error={match3dError}
onBack={leaveMatch3DFlow}
onExecuteAction={(payload) => {
void executeMatch3DAction(payload);
}}
initialFormPayload={match3dFormDraftPayload}
onCreateFromForm={(payload) => {
runProtectedAction(() => {
void createMatch3DDraftFromForm(payload);
});
}}
/>
</Suspense>
</motion.div>
)}
@@ -16398,7 +16366,12 @@ export function PlatformEntryFlowShellImpl({
<Suspense
fallback={<LazyPanelFallback label="正在加载跳一跳创作..." />}
>
<JumpHopWorkspace
<UnifiedCreationWorkspace
playId="jump-hop"
spec={getUnifiedCreationSpec(
'jump-hop',
unifiedCreationConfigById.get('jump-hop'),
)}
isBusy={isJumpHopBusy}
error={jumpHopError}
onBack={leaveJumpHopFlow}
@@ -16421,7 +16394,8 @@ export function PlatformEntryFlowShellImpl({
<Suspense
fallback={<LazyPanelFallback label="正在加载跳一跳生成面板..." />}
>
<CustomWorldGenerationView
<UnifiedGenerationPage
playId="jump-hop"
settingText={
jumpHopSession?.draft?.workTitle?.trim() ||
jumpHopSession?.draft?.workDescription?.trim() ||
@@ -16441,16 +16415,6 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('jump-hop-workspace');
}}
onRetry={retryJumpHopDraftGeneration}
onInterrupt={undefined}
backLabel="返回创作中心"
settingActionLabel={null}
retryLabel="重新生成草稿"
settingTitle="当前跳一跳信息"
settingDescription={null}
progressTitle="跳一跳草稿生成进度"
activeBadgeLabel="素材生成中"
pausedBadgeLabel="素材生成已暂停"
idleBadgeLabel="等待返回工作区"
/>
</Suspense>
</motion.div>
@@ -16540,26 +16504,24 @@ export function PlatformEntryFlowShellImpl({
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col overflow-y-auto overflow-x-hidden"
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载敲木鱼创作..." />}
>
<UnifiedCreationPage
<UnifiedCreationWorkspace
playId="wooden-fish"
spec={getUnifiedCreationSpec(
'wooden-fish',
unifiedCreationConfigById.get('wooden-fish'),
)}
>
<WoodenFishWorkspace
isBusy={isWoodenFishBusy}
error={woodenFishError}
onBack={leaveWoodenFishFlow}
onSubmitted={(result, payload) => {
void compileWoodenFishSession(result, payload);
}}
/>
</UnifiedCreationPage>
isBusy={isWoodenFishBusy}
error={woodenFishError}
onBack={leaveWoodenFishFlow}
onSubmitted={(result, payload) => {
void compileWoodenFishSession(result, payload);
}}
/>
</Suspense>
</motion.div>
)}
@@ -16695,39 +16657,35 @@ export function PlatformEntryFlowShellImpl({
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col overflow-y-auto overflow-x-hidden"
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载拼图创作..." />}
>
<UnifiedCreationPage
<UnifiedCreationWorkspace
playId="puzzle"
spec={getUnifiedCreationSpec(
'puzzle',
unifiedCreationConfigById.get('puzzle'),
)}
>
<PuzzleAgentWorkspace
session={puzzleSession}
isBusy={isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
onSubmitMessage={(payload) => {
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
executePuzzleWorkspaceAction(payload);
}}
initialFormPayload={puzzleFormDraftPayload}
title={null}
unifiedChrome
onCreateFromForm={(payload) => {
void createPuzzleDraftFromForm(payload);
}}
onAutoSaveForm={(payload) => {
void savePuzzleFormDraft(payload);
}}
/>
</UnifiedCreationPage>
session={puzzleSession}
isBusy={isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
onSubmitMessage={(payload) => {
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
executePuzzleWorkspaceAction(payload);
}}
initialFormPayload={puzzleFormDraftPayload}
onCreateFromForm={(payload) => {
void createPuzzleDraftFromForm(payload);
}}
onAutoSaveForm={(payload) => {
void savePuzzleFormDraft(payload);
}}
/>
</Suspense>
</motion.div>
)}

View File

@@ -1,94 +0,0 @@
export type PuzzleCreationTemplate = {
id: string;
title: string;
imageSrc: string;
prompt: string;
};
// 中文注释:模板只服务入口快速填词,正式作品信息仍在结果页补全。
export const PUZZLE_CREATION_TEMPLATES: PuzzleCreationTemplate[] = [
{
id: 'couple-memory',
title: '情侣合照拼图',
imageSrc: '/puzzle-creation-templates/couple-memory.webp',
prompt:
'温暖自然光下的一对情侣纪念合照,城市咖啡馆窗边,桌面有花束和两杯热饮,人物神情自然,画面主体清晰,前中后景层次明确。',
},
{
id: 'family-keepsake',
title: '家庭纪念拼图',
imageSrc: '/puzzle-creation-templates/family-keepsake.webp',
prompt:
'三代家人在客厅沙发前的家庭纪念合照,柔和午后阳光,孩子抱着生日蛋糕,长辈微笑,画面温暖完整,细节丰富但不杂乱。',
},
{
id: 'friends-party',
title: '朋友聚会拼图',
imageSrc: '/puzzle-creation-templates/friends-party.webp',
prompt:
'朋友们在露台夜晚聚会,彩灯、桌上零食和举杯瞬间,人物分布有层次,中央焦点清楚,氛围轻松热闹。',
},
{
id: 'festival-card',
title: '节日贺卡拼图',
imageSrc: '/puzzle-creation-templates/festival-card.webp',
prompt:
'节日餐桌与礼物布置,暖色灯光、彩带、蜡烛和窗外烟花,画面像无字贺卡,主体集中,边角细节可辨。',
},
{
id: 'knowledge-summary',
title: '知识总结拼图',
imageSrc: '/puzzle-creation-templates/knowledge-summary.webp',
prompt:
'一张无文字的知识学习主题插画,书桌上有打开的笔记本、便签、咖啡、台灯和思维导图式图形元素,构图整洁,重点明确。',
},
{
id: 'product-detail',
title: '商品细节拼图',
imageSrc: '/puzzle-creation-templates/product-detail.webp',
prompt:
'精致商品静物展示,一只高质感香水瓶放在丝绸与花瓣之间,玻璃反光清晰,包装和材质细节丰富,背景干净。',
},
{
id: 'healing-landscape',
title: '治愈风景拼图',
imageSrc: '/puzzle-creation-templates/healing-landscape.webp',
prompt:
'治愈风景插画,清晨湖边、薄雾、远山、木栈道和一盏小灯,色彩柔和。',
},
{
id: 'cute-pet',
title: '宠物可爱拼图',
imageSrc: '/puzzle-creation-templates/cute-pet.webp',
prompt:
'一只可爱的橘猫趴在阳光窗台上,旁边有绿植、毛线球和小毯子,猫的表情清楚,画面温柔干净。',
},
{
id: 'hot-topic-poster',
title: '热点海报拼图',
imageSrc: '/puzzle-creation-templates/hot-topic-poster.webp',
prompt:
'电影感热点海报风插画,雨夜街头、霓虹反光、奔跑的人影和远处光束,强烈视觉焦点,画面无文字。',
},
{
id: 'event-invitation',
title: '活动邀请拼图',
imageSrc: '/puzzle-creation-templates/event-invitation.webp',
prompt:
'活动邀请主题插画,展厅入口、花艺装置、签到台和柔和灯带,人群剪影自然分布,画面高级干净,无文字。',
},
{
id: 'daily-challenge',
title: '每日挑战拼图',
imageSrc: '/puzzle-creation-templates/daily-challenge.webp',
prompt:
'每日挑战主题插画,清爽桌面上摆放相机、明信片、计时器和小奖章,色彩明亮,构图有趣,细节可拆解。',
},
{
id: 'children-learning',
title: '儿童认知拼图',
imageSrc: '/puzzle-creation-templates/children-learning.webp',
prompt:
'儿童认知学习插画,木质桌面上有积木、彩色形状、动物玩偶和小书本,色彩明快,元素边界清晰,无文字。',
},
];

View File

@@ -24,12 +24,12 @@ import { getPuzzleHistoryAssetReferenceLabel } from '../../services/puzzle-works
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
import { useAuthUi } from '../auth/AuthUiContext';
import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel';
import PuzzleHistoryAssetPickerDialog from '../puzzle-agent/PuzzleHistoryAssetPickerDialog';
import PuzzleHistoryAssetPickerDialog from '../unified-creation/shared/PuzzleHistoryAssetPickerDialog';
import {
PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
type PuzzleImageModelId,
} from '../puzzle-agent/puzzleImageModelOptions';
import { PuzzleImageModelPicker } from '../puzzle-agent/PuzzleImageModelPicker';
} from '../unified-creation/shared/puzzleImageModelOptions';
import { PuzzleImageModelPicker } from '../unified-creation/shared/PuzzleImageModelPicker';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
type PuzzleResultViewProps = {

View File

@@ -803,8 +803,8 @@ vi.mock('../../services/puzzle-agent', () => ({
streamPuzzleAgentMessage: vi.fn(),
}));
vi.mock('../puzzle-agent/PuzzleAgentWorkspace', () => ({
PuzzleAgentWorkspace: ({
vi.mock('../unified-creation/workspaces/PuzzleCreationWorkspace', () => ({
PuzzleCreationWorkspace: ({
session,
isBusy,
error,
@@ -1007,8 +1007,8 @@ vi.mock('../match3d-result/Match3DResultView', () => ({
),
}));
vi.mock('../match3d-creation/Match3DAgentWorkspace', () => ({
Match3DAgentWorkspace: ({
vi.mock('../unified-creation/workspaces/Match3DCreationWorkspace', () => ({
Match3DCreationWorkspace: ({
session,
isBusy,
error,

View File

@@ -1,15 +1,20 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { describe, expect, test } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import { UnifiedCreationPage } from './UnifiedCreationPage';
import { getUnifiedCreationSpec } from './unifiedCreationSpecs';
describe('UnifiedCreationPage', () => {
test('按后端字段 spec 暴露统一创作页字段契约', () => {
const onBack = vi.fn();
render(
<UnifiedCreationPage spec={getUnifiedCreationSpec('wooden-fish')}>
<UnifiedCreationPage
spec={getUnifiedCreationSpec('wooden-fish')}
onBack={onBack}
>
<div></div>
</UnifiedCreationPage>,
);
@@ -41,6 +46,8 @@ describe('UnifiedCreationPage', () => {
expect(screen.getByTestId('unified-creation-play-badge').textContent).toBe(
'wooden-fish',
);
fireEvent.click(screen.getByRole('button', { name: '返回' }));
expect(onBack).toHaveBeenCalledTimes(1);
expect(screen.queryByLabelText('创作字段')).toBeNull();
expect(screen.queryByTestId('unified-creation-visible-field')).toBeNull();
expect(
@@ -48,7 +55,19 @@ describe('UnifiedCreationPage', () => {
.getByText('敲木鱼工作台')
.closest('.unified-creation-page__content')
?.className,
).not.toContain('overflow-y-auto');
expect(root?.className).not.toContain('overflow-hidden');
).toContain('flex');
expect(
screen
.getByText('敲木鱼工作台')
.closest('.unified-creation-page__content')
?.className,
).toContain('min-h-max');
expect(
screen
.getByText('敲木鱼工作台')
.closest('.unified-creation-page__content')
?.className,
).not.toContain('min-h-0');
expect(root?.className).toContain('overflow-y-auto');
});
});

View File

@@ -1,3 +1,4 @@
import { ArrowLeft } from 'lucide-react';
import type { ReactNode } from 'react';
import type { UnifiedCreationSpec } from './unifiedCreationSpecs';
@@ -5,15 +6,19 @@ import type { UnifiedCreationSpec } from './unifiedCreationSpecs';
type UnifiedCreationPageProps = {
spec: UnifiedCreationSpec;
children: ReactNode;
onBack?: () => void;
isBackDisabled?: boolean;
};
export function UnifiedCreationPage({
spec,
children,
onBack,
isBackDisabled = false,
}: UnifiedCreationPageProps) {
return (
<div
className="unified-creation-page platform-remap-surface mx-auto flex w-full max-w-5xl flex-col px-3 pt-2 sm:px-4 sm:pt-3"
className="unified-creation-page platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col overflow-y-auto overflow-x-hidden px-3 pt-2 sm:px-4 sm:pt-3"
data-play-id={spec.playId}
data-field-kinds={spec.fields.map((field) => field.kind).join(',')}
data-workspace-stage={spec.workspaceStage}
@@ -21,10 +26,22 @@ export function UnifiedCreationPage({
data-result-stage={spec.resultStage}
>
<header className="unified-creation-page__header shrink-0 pb-3">
<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]">
{spec.title}
</h1>
<div className="mb-2 flex items-center justify-between gap-3">
{onBack ? (
<button
type="button"
onClick={onBack}
disabled={isBackDisabled}
className={`platform-button platform-button--ghost min-h-0 shrink-0 px-3 py-1.5 text-[11px] ${
isBackDisabled ? 'cursor-not-allowed opacity-45' : ''
}`}
>
<ArrowLeft className="h-3.5 w-3.5" />
</button>
) : (
<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"
@@ -32,6 +49,11 @@ export function UnifiedCreationPage({
{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]">
{spec.title}
</h1>
</div>
</header>
<div className="sr-only" data-testid="unified-creation-spec">
<h1>{spec.title}</h1>
@@ -49,7 +71,7 @@ export function UnifiedCreationPage({
))}
</ul>
</div>
<div className="unified-creation-page__content pb-3 sm:pb-4">
<div className="unified-creation-page__content flex min-h-max flex-col pb-3 sm:pb-4">
{children}
</div>
</div>

View File

@@ -0,0 +1,184 @@
/* @vitest-environment jsdom */
import { render, screen } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import { UnifiedCreationWorkspace } from './UnifiedCreationWorkspace';
import { getUnifiedCreationSpec } from './unifiedCreationSpecs';
vi.mock('./workspaces/PuzzleCreationWorkspace', () => ({
PuzzleCreationWorkspace: ({
unifiedChrome,
}: {
unifiedChrome?: boolean;
}) => (
<div
className="puzzle-agent-workspace"
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
</div>
),
}));
vi.mock('./workspaces/Match3DCreationWorkspace', () => ({
Match3DCreationWorkspace: ({
unifiedChrome,
}: {
unifiedChrome?: boolean;
}) => (
<div
className="match3d-agent-workspace"
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
</div>
),
}));
vi.mock('./workspaces/JumpHopCreationWorkspace', () => ({
JumpHopCreationWorkspace: ({
unifiedChrome,
}: {
unifiedChrome?: boolean;
}) => (
<div
className="jump-hop-workspace"
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
</div>
),
}));
vi.mock('./workspaces/WoodenFishCreationWorkspace', () => ({
WoodenFishCreationWorkspace: ({
unifiedChrome,
}: {
unifiedChrome?: boolean;
}) => (
<div
className="wooden-fish-workspace"
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
</div>
),
}));
describe('UnifiedCreationWorkspace', () => {
test('统一承载四条首批创作入口', () => {
const onBack = vi.fn();
const puzzleResult = render(
<UnifiedCreationWorkspace
playId="puzzle"
spec={getUnifiedCreationSpec('puzzle')}
session={null}
isBusy={false}
error={null}
onBack={onBack}
onSubmitMessage={() => {}}
onExecuteAction={() => {}}
onCreateFromForm={() => {}}
onAutoSaveForm={() => {}}
initialFormPayload={null}
/>,
);
const puzzleWorkspace = screen
.getByText('拼图工作台')
.closest('[data-unified-chrome]');
const puzzlePage = screen
.getByText('拼图工作台')
.closest('.unified-creation-page');
expect(puzzleWorkspace?.getAttribute('data-unified-chrome')).toBe('true');
expect(puzzlePage?.getAttribute('data-play-id')).toBe('puzzle');
expect(screen.getByRole('button', { name: '返回' })).toBeTruthy();
puzzleResult.unmount();
const match3dResult = render(
<UnifiedCreationWorkspace
playId="match3d"
spec={getUnifiedCreationSpec('match3d')}
session={null}
isBusy={false}
error={null}
onBack={onBack}
onExecuteAction={() => {}}
onSubmitMessage={() => {}}
onCreateFromForm={() => {}}
initialFormPayload={null}
/>,
);
const match3dWorkspace = screen
.getByText('抓大鹅工作台')
.closest('[data-unified-chrome]');
const match3dPage = screen
.getByText('抓大鹅工作台')
.closest('.unified-creation-page');
expect(match3dWorkspace?.getAttribute('data-unified-chrome')).toBe('true');
expect(match3dPage?.getAttribute('data-play-id')).toBe('match3d');
match3dResult.unmount();
const jumpHopResult = render(
<UnifiedCreationWorkspace
playId="jump-hop"
spec={getUnifiedCreationSpec('jump-hop')}
isBusy={false}
error={null}
onBack={onBack}
onSubmitted={() => {}}
/>,
);
const jumpHopWorkspace = screen
.getByText('跳一跳工作台')
.closest('[data-unified-chrome]');
const jumpHopPage = screen
.getByText('跳一跳工作台')
.closest('.unified-creation-page');
expect(jumpHopWorkspace?.getAttribute('data-unified-chrome')).toBe('true');
expect(jumpHopPage?.getAttribute('data-play-id')).toBe('jump-hop');
jumpHopResult.unmount();
const woodenFishResult = render(
<UnifiedCreationWorkspace
playId="wooden-fish"
spec={getUnifiedCreationSpec('wooden-fish')}
isBusy={false}
error={null}
onBack={onBack}
onSubmitted={() => {}}
/>,
);
const woodenFishWorkspace = screen
.getByText('敲木鱼工作台')
.closest('[data-unified-chrome]');
const woodenFishPage = screen
.getByText('敲木鱼工作台')
.closest('.unified-creation-page');
expect(woodenFishWorkspace?.getAttribute('data-unified-chrome')).toBe(
'true',
);
expect(woodenFishPage?.getAttribute('data-play-id')).toBe('wooden-fish');
woodenFishResult.unmount();
});
test('统一页头返回按钮会透传给当前玩法壳层', async () => {
const onBack = vi.fn();
render(
<UnifiedCreationWorkspace
playId="jump-hop"
spec={getUnifiedCreationSpec('jump-hop')}
isBusy={false}
error={null}
onBack={onBack}
onSubmitted={() => {}}
/>,
);
screen.getByRole('button', { name: '返回' }).click();
expect(onBack).toHaveBeenCalledTimes(1);
expect(screen.queryAllByRole('button', { name: '返回' })).toHaveLength(1);
});
});

View File

@@ -0,0 +1,125 @@
import type { ComponentProps } from 'react';
import { UnifiedCreationPage } from './UnifiedCreationPage';
import type { UnifiedCreationSpec } from './unifiedCreationSpecs';
import { Match3DCreationWorkspace } from './workspaces/Match3DCreationWorkspace';
import { PuzzleCreationWorkspace } from './workspaces/PuzzleCreationWorkspace';
import { JumpHopCreationWorkspace } from './workspaces/JumpHopCreationWorkspace';
import { WoodenFishCreationWorkspace } from './workspaces/WoodenFishCreationWorkspace';
type PuzzleCreationWorkspaceProps = ComponentProps<
typeof PuzzleCreationWorkspace
>;
type Match3DCreationWorkspaceProps = ComponentProps<
typeof Match3DCreationWorkspace
>;
type JumpHopCreationWorkspaceProps = ComponentProps<
typeof JumpHopCreationWorkspace
>;
type WoodenFishCreationWorkspaceProps = ComponentProps<
typeof WoodenFishCreationWorkspace
>;
type UnifiedCreationWorkspaceBaseProps = {
spec: UnifiedCreationSpec;
};
type PuzzleUnifiedCreationWorkspaceProps =
UnifiedCreationWorkspaceBaseProps & {
playId: 'puzzle';
} & PuzzleCreationWorkspaceProps;
type Match3DUnifiedCreationWorkspaceProps =
UnifiedCreationWorkspaceBaseProps & {
playId: 'match3d';
} & Match3DCreationWorkspaceProps;
type JumpHopUnifiedCreationWorkspaceProps =
UnifiedCreationWorkspaceBaseProps & {
playId: 'jump-hop';
} & JumpHopCreationWorkspaceProps;
type WoodenFishUnifiedCreationWorkspaceProps =
UnifiedCreationWorkspaceBaseProps & {
playId: 'wooden-fish';
} & WoodenFishCreationWorkspaceProps;
export type UnifiedCreationWorkspaceProps =
| PuzzleUnifiedCreationWorkspaceProps
| Match3DUnifiedCreationWorkspaceProps
| JumpHopUnifiedCreationWorkspaceProps
| WoodenFishUnifiedCreationWorkspaceProps;
export function UnifiedCreationWorkspace(props: UnifiedCreationWorkspaceProps) {
switch (props.playId) {
case 'puzzle':
return (
<UnifiedCreationPage spec={props.spec} onBack={props.onBack}>
<PuzzleCreationWorkspace
session={props.session}
isBusy={props.isBusy}
error={props.error}
onBack={props.onBack}
onSubmitMessage={props.onSubmitMessage}
onExecuteAction={props.onExecuteAction}
onCreateFromForm={props.onCreateFromForm}
onAutoSaveForm={props.onAutoSaveForm}
initialFormPayload={props.initialFormPayload}
showBackButton={false}
title={null}
unifiedChrome
/>
</UnifiedCreationPage>
);
case 'match3d':
return (
<UnifiedCreationPage spec={props.spec} onBack={props.onBack}>
<Match3DCreationWorkspace
session={props.session}
isBusy={props.isBusy}
error={props.error}
onBack={props.onBack}
onExecuteAction={props.onExecuteAction}
onCreateFromForm={props.onCreateFromForm}
onSubmitMessage={props.onSubmitMessage}
initialFormPayload={props.initialFormPayload}
showBackButton={false}
title={null}
unifiedChrome
/>
</UnifiedCreationPage>
);
case 'jump-hop':
return (
<UnifiedCreationPage spec={props.spec} onBack={props.onBack}>
<JumpHopCreationWorkspace
isBusy={props.isBusy}
error={props.error}
onBack={props.onBack}
onSubmitted={props.onSubmitted}
showBackButton={false}
unifiedChrome
/>
</UnifiedCreationPage>
);
case 'wooden-fish':
return (
<UnifiedCreationPage spec={props.spec} onBack={props.onBack}>
<WoodenFishCreationWorkspace
isBusy={props.isBusy}
error={props.error}
onBack={props.onBack}
onSubmitted={props.onSubmitted}
showBackButton={false}
unifiedChrome
/>
</UnifiedCreationPage>
);
default: {
const exhaustiveCheck: never = props;
return exhaustiveCheck;
}
}
}
export default UnifiedCreationWorkspace;

View File

@@ -50,4 +50,22 @@ describe('UnifiedGenerationPage', () => {
expect(screen.getAllByText('生成拼图首图').length).toBeGreaterThan(0);
expect(screen.getByText('当前拼图信息')).toBeTruthy();
});
test('jump-hop generation page uses unified copy', () => {
render(
<UnifiedGenerationPage
playId="jump-hop"
settingText="云端糖果塔"
progress={createProgress()}
isGenerating
onBack={() => {}}
onEditSetting={() => {}}
onRetry={() => {}}
/>,
);
expect(document.body.textContent).toContain('跳一跳草稿生成进度');
expect(screen.getByText('素材生成中')).toBeTruthy();
expect(screen.getByText('当前跳一跳信息')).toBeTruthy();
});
});

View File

@@ -5,13 +5,13 @@ import { createPortal } from 'react-dom';
import {
puzzleAssetClient,
type PuzzleHistoryAsset,
} from '../../services/puzzle-works/puzzleAssetClient';
} from '../../../services/puzzle-works/puzzleAssetClient';
import {
formatPuzzleHistoryAssetCreatedAt,
getPuzzleHistoryAssetDisplayName,
} from '../../services/puzzle-works/puzzleHistoryAsset';
import { useAuthUi } from '../auth/AuthUiContext';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
} from '../../../services/puzzle-works/puzzleHistoryAsset';
import { useAuthUi } from '../../auth/AuthUiContext';
import { ResolvedAssetImage } from '../../ResolvedAssetImage';
type PuzzleHistoryAssetPickerDialogProps = {
isBusy: boolean;

View File

@@ -6,9 +6,9 @@ import {
} from './unifiedCreationSpecs';
describe('unified creation specs', () => {
test('一期只接拼图、抓大鹅和敲木鱼', () => {
test('统一壳当前覆盖拼图、抓大鹅、跳一跳和敲木鱼', () => {
expect(listUnifiedCreationSpecs().map((spec) => spec.playId).sort()).toEqual(
['match3d', 'puzzle', 'wooden-fish'],
['jump-hop', 'match3d', 'puzzle', 'wooden-fish'],
);
});
@@ -22,7 +22,7 @@ describe('unified creation specs', () => {
expect([...fieldKinds].sort()).toEqual(['audio', 'image', 'select', 'text']);
});
test('条链路都映射到统一创作、生成、结果阶段', () => {
test('条链路都映射到统一创作、生成、结果阶段', () => {
expect(getUnifiedCreationSpec('puzzle')).toMatchObject({
workspaceStage: 'puzzle-agent-workspace',
generationStage: 'puzzle-generating',
@@ -33,6 +33,11 @@ describe('unified creation specs', () => {
generationStage: 'match3d-generating',
resultStage: 'match3d-result',
});
expect(getUnifiedCreationSpec('jump-hop')).toMatchObject({
workspaceStage: 'jump-hop-workspace',
generationStage: 'jump-hop-generating',
resultStage: 'jump-hop-result',
});
expect(getUnifiedCreationSpec('wooden-fish')).toMatchObject({
workspaceStage: 'wooden-fish-workspace',
generationStage: 'wooden-fish-generating',

View File

@@ -58,6 +58,63 @@ const FALLBACK_UNIFIED_CREATION_SPECS: Record<
},
],
},
'jump-hop': {
playId: 'jump-hop',
title: '想做个什么玩法?',
workspaceStage: 'jump-hop-workspace',
generationStage: 'jump-hop-generating',
resultStage: 'jump-hop-result',
fields: [
{
id: 'workTitle',
kind: 'text',
label: '作品标题',
required: true,
},
{
id: 'workDescription',
kind: 'text',
label: '作品简介',
required: true,
},
{
id: 'themeTags',
kind: 'text',
label: '主题标签',
required: true,
},
{
id: 'difficulty',
kind: 'select',
label: '难度',
required: true,
},
{
id: 'stylePreset',
kind: 'select',
label: '风格',
required: true,
},
{
id: 'characterPrompt',
kind: 'text',
label: '角色提示词',
required: true,
},
{
id: 'tilePrompt',
kind: 'text',
label: '地块提示词',
required: true,
},
{
id: 'endMoodPrompt',
kind: 'text',
label: '终点氛围',
required: false,
},
],
},
'wooden-fish': {
playId: 'wooden-fish',
title: '想做个什么玩法?',

View File

@@ -13,6 +13,12 @@ const UNIFIED_GENERATION_COPY = {
progressTitle: '抓大鹅草稿生成进度',
activeBadgeLabel: '素材生成中',
},
'jump-hop': {
retryLabel: '重新生成草稿',
settingTitle: '当前跳一跳信息',
progressTitle: '跳一跳草稿生成进度',
activeBadgeLabel: '素材生成中',
},
'wooden-fish': {
retryLabel: '重新生成草稿',
settingTitle: '当前敲木鱼信息',

View File

@@ -4,11 +4,11 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';
import type { JumpHopSessionResponse } from '../../../packages/shared/src/contracts/jumpHop';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
import { JumpHopWorkspace } from './JumpHopWorkspace';
import type { JumpHopSessionResponse } from '../../../../packages/shared/src/contracts/jumpHop';
import { jumpHopClient } from '../../../services/jump-hop/jumpHopClient';
import { JumpHopCreationWorkspace } from './JumpHopCreationWorkspace';
vi.mock('../../services/jump-hop/jumpHopClient', () => ({
vi.mock('../../../services/jump-hop/jumpHopClient', () => ({
jumpHopClient: {
createSession: vi.fn(),
},
@@ -40,7 +40,7 @@ test('jump hop workspace submits structured payload after required fields are fi
mockCreateSession.mockResolvedValue(sessionResponse);
render(
<JumpHopWorkspace onBack={() => {}} onSubmitted={onSubmitted} />,
<JumpHopCreationWorkspace onBack={() => {}} onSubmitted={onSubmitted} />,
);
const submitButton = screen.getByRole('button', { name: '生成' });
@@ -84,9 +84,26 @@ test('jump hop workspace calls back when return button is clicked', async () =>
const user = userEvent.setup();
const onBack = vi.fn();
render(<JumpHopWorkspace onBack={onBack} onSubmitted={() => {}} />);
render(<JumpHopCreationWorkspace onBack={onBack} onSubmitted={() => {}} />);
await user.click(screen.getByRole('button', { name: '返回' }));
expect(onBack).toHaveBeenCalledTimes(1);
});
test('jump hop workspace can defer visible chrome to the unified creation page', () => {
const { container } = render(
<JumpHopCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
showBackButton={false}
unifiedChrome
/>,
);
const workspace = container.querySelector('.jump-hop-workspace');
expect(workspace?.getAttribute('data-unified-chrome')).toBe('true');
expect(workspace?.className).toContain('max-w-none');
expect(workspace?.className).not.toContain('platform-remap-surface');
expect(screen.queryByRole('button', { name: '返回' })).toBeNull();
});

View File

@@ -6,10 +6,10 @@ import type {
JumpHopSessionResponse,
JumpHopStylePreset,
JumpHopWorkspaceCreateRequest,
} from '../../../packages/shared/src/contracts/jumpHop';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
} from '../../../../packages/shared/src/contracts/jumpHop';
import { jumpHopClient } from '../../../services/jump-hop/jumpHopClient';
type JumpHopWorkspaceProps = {
type JumpHopCreationWorkspaceProps = {
isBusy?: boolean;
error?: string | null;
onBack: () => void;
@@ -17,6 +17,8 @@ type JumpHopWorkspaceProps = {
result: JumpHopSessionResponse,
payload: JumpHopWorkspaceCreateRequest,
) => void;
showBackButton?: boolean;
unifiedChrome?: boolean;
};
type JumpHopWorkspaceFormState = {
@@ -41,12 +43,14 @@ const DEFAULT_FORM_STATE: JumpHopWorkspaceFormState = {
endMoodPrompt: '',
};
export function JumpHopWorkspace({
export function JumpHopCreationWorkspace({
isBusy = false,
error = null,
onBack,
onSubmitted,
}: JumpHopWorkspaceProps) {
showBackButton = true,
unifiedChrome = false,
}: JumpHopCreationWorkspaceProps) {
const [formState, setFormState] = useState(DEFAULT_FORM_STATE);
const [localError, setLocalError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -99,17 +103,26 @@ export function JumpHopWorkspace({
};
return (
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4">
<div className="mb-3 flex items-center justify-between gap-3">
<button
type="button"
onClick={onBack}
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
>
<ArrowLeft className="h-4 w-4" />
</button>
</div>
<div
className={
unifiedChrome
? 'jump-hop-workspace mx-auto flex min-h-0 w-full max-w-none flex-col overflow-visible'
: 'jump-hop-workspace platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4'
}
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
{showBackButton ? (
<div className="mb-3 flex items-center justify-between gap-3">
<button
type="button"
onClick={onBack}
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
>
<ArrowLeft className="h-4 w-4" />
</button>
</div>
) : null}
<div className="grid gap-3 sm:grid-cols-2">
<label className="block sm:col-span-2">
@@ -275,4 +288,4 @@ export function JumpHopWorkspace({
);
}
export default JumpHopWorkspace;
export default JumpHopCreationWorkspace;

View File

@@ -3,8 +3,8 @@
import { fireEvent, render, screen, within } from '@testing-library/react';
import { expect, test, vi } from 'vitest';
import type { Match3DAgentSessionSnapshot } from '../../../packages/shared/src/contracts/match3dAgent';
import { Match3DAgentWorkspace } from './Match3DAgentWorkspace';
import type { Match3DAgentSessionSnapshot } from '../../../../packages/shared/src/contracts/match3dAgent';
import { Match3DCreationWorkspace } from './Match3DCreationWorkspace';
const baseSession: Match3DAgentSessionSnapshot = {
sessionId: 'match3d-session-1',
@@ -70,7 +70,7 @@ test('match3d workspace submits derived entry form payload instead of agent chat
const onExecuteAction = vi.fn();
render(
<Match3DAgentWorkspace
<Match3DCreationWorkspace
session={null}
onBack={() => {}}
onExecuteAction={onExecuteAction}
@@ -114,7 +114,7 @@ test('match3d workspace submits derived entry form payload instead of agent chat
test('match3d workspace can defer visible chrome to the unified creation page', () => {
const { container } = render(
<Match3DAgentWorkspace
<Match3DCreationWorkspace
session={null}
onBack={() => {}}
onExecuteAction={() => {}}
@@ -138,7 +138,7 @@ test('match3d workspace omits legacy asset style fields from entry payload', ()
const onCreateFromForm = vi.fn();
render(
<Match3DAgentWorkspace
<Match3DCreationWorkspace
session={null}
onBack={() => {}}
onExecuteAction={() => {}}
@@ -162,7 +162,7 @@ test('match3d workspace keeps click sound generation disabled from entry form',
const onCreateFromForm = vi.fn();
render(
<Match3DAgentWorkspace
<Match3DCreationWorkspace
session={null}
onBack={() => {}}
onExecuteAction={() => {}}
@@ -188,7 +188,7 @@ test('match3d workspace falls back to compile action when restored from the lega
const onExecuteAction = vi.fn();
render(
<Match3DAgentWorkspace
<Match3DCreationWorkspace
session={baseSession}
onBack={() => {}}
onExecuteAction={onExecuteAction}

View File

@@ -6,9 +6,9 @@ import type {
ExecuteMatch3DActionRequest,
Match3DAgentSessionSnapshot,
SendMatch3DMessageRequest,
} from '../../../packages/shared/src/contracts/match3dAgent';
} from '../../../../packages/shared/src/contracts/match3dAgent';
type Match3DAgentWorkspaceProps = {
type Match3DCreationWorkspaceProps = {
session: Match3DAgentSessionSnapshot | null;
isBusy?: boolean;
error?: string | null;
@@ -104,10 +104,9 @@ function resolveInitialFormState(
}
/**
* Agent
* Match3DAgentWorkspace稿
*
*/
export function Match3DAgentWorkspace({
export function Match3DCreationWorkspace({
session,
isBusy = false,
error = null,
@@ -118,7 +117,7 @@ export function Match3DAgentWorkspace({
showBackButton = true,
title = '想做个什么玩法?',
unifiedChrome = false,
}: Match3DAgentWorkspaceProps) {
}: Match3DCreationWorkspaceProps) {
const [formState, setFormState] = useState<Match3DFormState>(() =>
resolveInitialFormState(session, initialFormPayload),
);
@@ -368,4 +367,4 @@ export function Match3DAgentWorkspace({
);
}
export default Match3DAgentWorkspace;
export default Match3DCreationWorkspace;

View File

@@ -10,11 +10,11 @@ import {
} from '@testing-library/react';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type { PuzzleAgentSessionSnapshot } from '../../../packages/shared/src/contracts/puzzleAgentSession';
import { puzzleAssetClient } from '../../services/puzzle-works/puzzleAssetClient';
import { PuzzleAgentWorkspace } from './PuzzleAgentWorkspace';
import type { PuzzleAgentSessionSnapshot } from '../../../../packages/shared/src/contracts/puzzleAgentSession';
import { puzzleAssetClient } from '../../../services/puzzle-works/puzzleAssetClient';
import { PuzzleCreationWorkspace } from './PuzzleCreationWorkspace';
vi.mock('../ResolvedAssetImage', () => ({
vi.mock('../../ResolvedAssetImage', () => ({
ResolvedAssetImage: ({
src,
alt,
@@ -26,7 +26,7 @@ vi.mock('../ResolvedAssetImage', () => ({
}) => (src ? <img src={src} alt={alt} className={className} /> : null),
}));
vi.mock('../../services/puzzle-works/puzzleAssetClient', () => ({
vi.mock('../../../services/puzzle-works/puzzleAssetClient', () => ({
puzzleAssetClient: {
listHistoryAssets: vi.fn(),
uploadReferenceImage: vi.fn(),
@@ -177,7 +177,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
const onCreateFromForm = vi.fn();
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -217,7 +217,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
test('puzzle workspace can defer visible chrome to the unified creation page', () => {
const { container } = render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -229,11 +229,14 @@ test('puzzle workspace can defer visible chrome to the unified creation page', (
);
const workspace = container.querySelector('.puzzle-agent-workspace');
const imagePanel = container.querySelector('.creative-image-input-panel');
expect(workspace?.getAttribute('data-unified-chrome')).toBe('true');
expect(workspace?.className).toContain('max-w-none');
expect(workspace?.className).not.toContain('h-full');
expect(workspace?.className).not.toContain('overflow-hidden');
expect(workspace?.className).not.toContain('platform-remap-surface');
expect(imagePanel?.className).toContain('flex-none');
expect(imagePanel?.className).not.toContain('flex-1');
expect(screen.queryByRole('heading', { name: '想做个什么玩法?' })).toBeNull();
expect(screen.getByLabelText('画面描述')).toBeTruthy();
});
@@ -241,7 +244,7 @@ test('puzzle workspace can defer visible chrome to the unified creation page', (
test('puzzle workspace keeps the reference image upload as a primary panel', () => {
const onCreateFromForm = vi.fn();
const { container } = render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -320,7 +323,7 @@ test('puzzle workspace selects a history image from the upload card', async () =
]);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -377,7 +380,7 @@ test('puzzle workspace selects a history image from the upload card', async () =
test('puzzle upload card stays light in light theme', () => {
const onCreateFromForm = vi.fn();
const { container } = render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -407,7 +410,7 @@ test('puzzle workspace falls back to compile action for restored sessions', () =
const onCreateFromForm = vi.fn();
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={baseSession}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -438,7 +441,7 @@ test('puzzle workspace switches image mode without exposing model names', () =>
const onCreateFromForm = vi.fn();
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -502,7 +505,7 @@ test('puzzle workspace restores form draft fields and autosaves edits', () => {
};
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={formDraftSession}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -541,7 +544,7 @@ test('puzzle workspace hides prompt and cost when AI redraw is off', async () =>
stubReferenceImageUpload(uploadedDataUrl);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -600,7 +603,7 @@ test('puzzle workspace submits history image when AI redraw is off', async () =>
]);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -645,7 +648,7 @@ test('puzzle workspace submits uploaded reference image as data URL when AI redr
stubReferenceImageUpload(uploadedDataUrl);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -728,7 +731,7 @@ test('puzzle workspace uploads prompt references as asset object ids', async ()
});
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -803,7 +806,7 @@ test('puzzle workspace uploads prompt reference images from the description box'
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -870,7 +873,7 @@ test('puzzle workspace shows AI redraw switch only after upload', async () => {
stubReferenceImageUpload(uploadedDataUrl);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -907,7 +910,7 @@ test('puzzle workspace confirms before removing uploaded image', async () => {
stubReferenceImageUpload(uploadedDataUrl);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}
@@ -949,7 +952,7 @@ test('puzzle workspace opens crop tool for non-square uploads', async () => {
const drawImage = stubCanvas(croppedDataUrl);
render(
<PuzzleAgentWorkspace
<PuzzleCreationWorkspace
session={null}
onBack={() => {}}
onSubmitMessage={() => {}}

View File

@@ -6,38 +6,38 @@ import {
useState,
} from 'react';
import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions';
import type { PuzzleAgentActionRequest } from '../../../../packages/shared/src/contracts/puzzleAgentActions';
import type {
CreatePuzzleAgentSessionRequest,
PuzzleAgentSessionSnapshot,
SendPuzzleAgentMessageRequest,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import { getPuzzleHistoryAssetReferenceLabel } from '../../services/puzzle-works/puzzleHistoryAsset';
} from '../../../../packages/shared/src/contracts/puzzleAgentSession';
import { getPuzzleHistoryAssetReferenceLabel } from '../../../services/puzzle-works/puzzleHistoryAsset';
import {
cropPuzzleReferenceImageDataUrl,
isPuzzleReferenceImageSquare,
readPuzzleReferenceImageAsDataUrl,
readPuzzleReferenceImageForUpload,
} from '../../services/puzzleReferenceImage';
} from '../../../services/puzzleReferenceImage';
import {
CreativeImageInputPanel,
type CreativeImageInputReferenceImage,
} from '../common/CreativeImageInputPanel';
} from '../../common/CreativeImageInputPanel';
import {
buildCenteredSquareImageCropRect,
clampSquareImageCropRect,
SquareImageCropModal,
type SquareImageCropRect,
} from '../common/SquareImageCropModal';
import PuzzleHistoryAssetPickerDialog from './PuzzleHistoryAssetPickerDialog';
} from '../../common/SquareImageCropModal';
import PuzzleHistoryAssetPickerDialog from '../shared/PuzzleHistoryAssetPickerDialog';
import {
normalizePuzzleImageModel,
PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
type PuzzleImageModelId,
} from './puzzleImageModelOptions';
import { PuzzleImageModelPicker } from './PuzzleImageModelPicker';
} from '../shared/puzzleImageModelOptions';
import { PuzzleImageModelPicker } from '../shared/PuzzleImageModelPicker';
type PuzzleAgentWorkspaceProps = {
type PuzzleCreationWorkspaceProps = {
session: PuzzleAgentSessionSnapshot | null;
isBusy?: boolean;
error?: string | null;
@@ -234,9 +234,9 @@ function addPuzzlePromptReferenceImage(
/**
* Agent
* PuzzleAgentWorkspace 稿
*
*/
export function PuzzleAgentWorkspace({
export function PuzzleCreationWorkspace({
session,
isBusy = false,
error = null,
@@ -248,7 +248,7 @@ export function PuzzleAgentWorkspace({
showBackButton = true,
title = '想做个什么玩法?',
unifiedChrome = false,
}: PuzzleAgentWorkspaceProps) {
}: PuzzleCreationWorkspaceProps) {
const [formState, setFormState] = useState<PuzzleFormState>(() =>
resolveInitialFormState(session, initialFormPayload),
);
@@ -633,6 +633,7 @@ export function PuzzleAgentWorkspace({
<CreativeImageInputPanel
className={unifiedChrome ? 'min-h-0 flex-none' : ''}
fillHeight={!unifiedChrome}
disabled={isBusy}
isSubmitting={isBusy}
uploadedImageSrc={formState.referenceImageSrc}
@@ -782,4 +783,4 @@ export function PuzzleAgentWorkspace({
);
}
export default PuzzleAgentWorkspace;
export default PuzzleCreationWorkspace;

View File

@@ -3,11 +3,11 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import { woodenFishClient } from '../../services/wooden-fish/woodenFishClient';
import { WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT } from '../../services/wooden-fish/woodenFishDefaults';
import { WoodenFishWorkspace } from './WoodenFishWorkspace';
import { woodenFishClient } from '../../../services/wooden-fish/woodenFishClient';
import { WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT } from '../../../services/wooden-fish/woodenFishDefaults';
import { WoodenFishCreationWorkspace } from './WoodenFishCreationWorkspace';
vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
vi.mock('../../../services/wooden-fish/woodenFishClient', () => ({
woodenFishClient: {
createSession: vi.fn(),
},
@@ -31,7 +31,7 @@ test('敲什么输入栏初始置空但提交时仍使用默认生成提示词',
const onSubmitted = vi.fn();
render(
<WoodenFishWorkspace
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={onSubmitted}
/>,
@@ -48,7 +48,7 @@ test('敲什么输入栏初始置空但提交时仍使用默认生成提示词',
test('功德有什么默认只显示基础词条,不显示运行态 +1 后缀', () => {
render(
<WoodenFishWorkspace
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
/>,
@@ -72,7 +72,7 @@ test('功德有什么默认只显示基础词条,不显示运行态 +1 后缀'
test('功德有什么支持通过加号新增词条并移除新增格子', () => {
render(
<WoodenFishWorkspace
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
/>,
@@ -92,7 +92,7 @@ test('功德有什么支持通过加号新增词条并移除新增格子', () =>
test('敲击音效临时关闭提示词生成入口,仅保留上传和录音', () => {
render(
<WoodenFishWorkspace
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
/>,
@@ -109,7 +109,7 @@ test('敲击音效临时关闭提示词生成入口,仅保留上传和录音',
test('敲击音效和功德词条不放进独立滚动窗', () => {
const { container } = render(
<WoodenFishWorkspace
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
/>,
@@ -130,7 +130,7 @@ test('敲击音效和功德词条不放进独立滚动窗', () => {
test('工作台只保留一个生成按钮', () => {
render(
<WoodenFishWorkspace
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
/>,
@@ -138,3 +138,35 @@ test('工作台只保留一个生成按钮', () => {
expect(screen.getAllByRole('button', { name: '生成' })).toHaveLength(1);
});
test('敲木鱼工作台可以交给统一创作页承载可见外壳', () => {
const { container } = render(
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
showBackButton={false}
unifiedChrome
/>,
);
const workspace = container.querySelector('.wooden-fish-workspace');
expect(workspace?.getAttribute('data-unified-chrome')).toBe('true');
expect(workspace?.className).toContain('max-w-none');
expect(workspace?.className).not.toContain('platform-remap-surface');
expect(screen.queryByRole('button', { name: '返回' })).toBeNull();
});
test('敲木鱼工作台在统一壳下不强行填满左侧图片面板高度', () => {
const { container } = render(
<WoodenFishCreationWorkspace
onBack={() => {}}
onSubmitted={() => {}}
showBackButton={false}
unifiedChrome
/>,
);
const imagePanel = container.querySelector('.creative-image-input-panel');
expect(imagePanel?.className).toContain('flex-none');
expect(imagePanel?.className).not.toContain('flex-1');
});

View File

@@ -11,17 +11,17 @@ import type {
WoodenFishAudioAsset,
WoodenFishSessionResponse,
WoodenFishWorkspaceCreateRequest,
} from '../../../packages/shared/src/contracts/woodenFish';
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
import { woodenFishClient } from '../../services/wooden-fish/woodenFishClient';
} from '../../../../packages/shared/src/contracts/woodenFish';
import { readPuzzleReferenceImageAsDataUrl } from '../../../services/puzzleReferenceImage';
import { woodenFishClient } from '../../../services/wooden-fish/woodenFishClient';
import {
WOODEN_FISH_DEFAULT_HIT_OBJECT_PROMPT,
WOODEN_FISH_DEFAULT_HIT_SOUND_ASSET,
} from '../../services/wooden-fish/woodenFishDefaults';
import { CreativeAudioInputPanel } from '../common/CreativeAudioInputPanel';
import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel';
} from '../../../services/wooden-fish/woodenFishDefaults';
import { CreativeAudioInputPanel } from '../../common/CreativeAudioInputPanel';
import { CreativeImageInputPanel } from '../../common/CreativeImageInputPanel';
type WoodenFishWorkspaceProps = {
type WoodenFishCreationWorkspaceProps = {
isBusy?: boolean;
error?: string | null;
onBack: () => void;
@@ -29,6 +29,8 @@ type WoodenFishWorkspaceProps = {
result: WoodenFishSessionResponse,
payload: WoodenFishWorkspaceCreateRequest,
) => void;
showBackButton?: boolean;
unifiedChrome?: boolean;
};
type WoodenFishWorkspaceFormState = {
@@ -66,12 +68,14 @@ function normalizeFloatingWords(words: string[]) {
return normalized.length > 0 ? normalized : [...DEFAULT_FLOATING_WORDS];
}
export function WoodenFishWorkspace({
export function WoodenFishCreationWorkspace({
isBusy = false,
error = null,
onBack,
onSubmitted,
}: WoodenFishWorkspaceProps) {
showBackButton = true,
unifiedChrome = false,
}: WoodenFishCreationWorkspaceProps) {
const [formState, setFormState] = useState(DEFAULT_FORM_STATE);
const [localError, setLocalError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -155,21 +159,31 @@ export function WoodenFishWorkspace({
};
return (
<div className="platform-remap-surface mx-auto flex w-full max-w-5xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4">
<div className="mb-3 flex items-center justify-between gap-3">
<button
type="button"
onClick={onBack}
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
>
<ArrowLeft className="h-4 w-4" />
</button>
</div>
<div
className={
unifiedChrome
? 'wooden-fish-workspace mx-auto flex min-h-0 w-full max-w-none flex-col overflow-visible'
: 'wooden-fish-workspace platform-remap-surface mx-auto flex w-full max-w-5xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4'
}
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
{showBackButton ? (
<div className="mb-3 flex items-center justify-between gap-3">
<button
type="button"
onClick={onBack}
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
>
<ArrowLeft className="h-4 w-4" />
</button>
</div>
) : null}
<div className="grid gap-3 lg:grid-cols-[minmax(0,1.12fr)_minmax(19rem,0.88fr)]">
<div className="flex min-h-[26rem] min-w-0 flex-col">
<CreativeImageInputPanel
fillHeight={!unifiedChrome}
disabled={isBusy || isSubmitting}
isSubmitting={isSubmitting}
uploadedImageSrc={formState.hitObjectReferenceImageSrc}
@@ -320,4 +334,4 @@ export function WoodenFishWorkspace({
);
}
export default WoodenFishWorkspace;
export default WoodenFishCreationWorkspace;

View File

@@ -24,17 +24,23 @@ export type UnifiedCreationField = {
};
export type UnifiedCreationSpec = {
playId: 'puzzle' | 'match3d' | 'wooden-fish';
playId: 'puzzle' | 'match3d' | 'jump-hop' | 'wooden-fish';
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' | 'wooden-fish-result';
resultStage:
| 'puzzle-result'
| 'match3d-result'
| 'jump-hop-result'
| 'wooden-fish-result';
fields: UnifiedCreationField[];
};