diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/AGENTS.md b/apps/ai-game-creator-shell/src/features/ui-editor/AGENTS.md deleted file mode 100644 index f3a8eb82d..000000000 --- a/apps/ai-game-creator-shell/src/features/ui-editor/AGENTS.md +++ /dev/null @@ -1,36 +0,0 @@ -- 本文件采用 append-only:只在末尾追加新的共同原则,不改写或删除既有内容。 -- UI Editor 保持桌面端专用,当前不实现持久化。 -- 本阶段不新增 UI 测试,不修改或新增其它文档。 -- `demo/` 是设计师提供的旧 mock。只复用设计目的、页面布局和组件种类,不复用其数据结构或静态假数据实现。 -- 原始需求位于仓库根目录 `req_{n}.txt`;本文件记录已经确认的工程解释。两者冲突时先停止编码并向用户确认。 -- 正式状态是`State` -- 核心布局结果 `State.ui_trees`。 -- Rust 是 UI Editor 领域类型的唯一源。并通过 `ts-rs` 生成 - `src/features/ui-editor/types/` 下的 TypeScript 文件。 -- React 只保存选择项、缩放、面板开关等临时 UI 状态。组件树、节点内容、审阅状态和人工锁定状态必须体现在同一个内存 `State` 中。 - -- 纯结构容器可使用空 `components`;视觉节点使用 `Image`;文本和动态数值使用 `Text`。动态数值的语义可写入节点描述,本期不引入独立 Number 组件。 - -- AI 识别由 Tauri/Rust 中的专用命令实现,入口放在 - `src-tauri/src/ui_editor/commands/mod.rs`。前端不得直接调用模型。 -- 复用项目现有 LLM 配置和 provider 能力;前端不得传递或持有 API Key。 -- 最多四张 UI 参考图必须作为真实多模态像素输入发送。Rust 读取图片并使用 - `data:;base64,...` 形式交给现有 LLM provider,不得只把文件路径、文件名或尺寸写进文字提示词。 -- base64 图片正文不得写入日志、错误详情、State、测试快照或其它持久记录。 -- 模型输出先进入命令内部识别 DTO,经结构校验和标准化后再构造正式 `UITree`、`Node`、`Component` 和 `Transform`;模型输出 DTO 不是公开 State 契约。 -- 模型使用的临时父子引用不得直接成为正式实体身份;正式 `NodeId` 由 Rust 校验和分配,并保证 State 内唯一。 - -- `container`、`button`、`tab`、`modal`、`image`、`text` 等名称只用于帮助理解 UI,不是正式数据类型,也不规定节点与 `Component` 的映射。正式能力只由当前 Rust 类型表达。 -- 本期只保留当前正式渲染组件 `Component::Image` 和 `Component::Text`。按钮、页签、弹窗、头像、进度条等更多组件类型以后再设计,不在本阶段预建枚举或 variant。 -- 节点数量、待审阅数量及其它概览信息只通过递归查询和过滤 `State.ui_trees` 得出,不向 `State` 添加重复统计或阶段确认字段。 -- 当前范围不实现UI交互关系、圆形识别框、复制、粘贴或手工新增节点,也不为这些概念预建状态。 -- 各 AI 阶段使用独立的输入/输出契约;后续阶段显式消费前一阶段结果,不从隐含的临时上下文推断前置事实。 -- AI 阶段的结果通过命令返回并更新同一个内存 `State`;阶段内部 DTO 只用于当前调用,不成为额外持久状态或跨阶段隐式参数。 -- 阶段命令可以只返回该阶段的 DTO;调用方负责将 DTO 显式合并到同一个内存 `State`。仅用于调试的验证入口与正式工作流分离。 - -- 节点布局与组件是独立通道:`layout_status` / `components_status` 分别记录阶段状态,`allow_llm_edit_layout` / `allow_llm_edit_component` 分别授权 LLM 修改;人工编辑不隐式改变状态或授权。页面根节点可承载组件,但其 Transform 不可修改。 - -- UI Editor 的项目资源持久化复用 manifest `kind: "UI"` 和 `application/json`,正式文件为严格的 `game-creator-ui-design-state.v1` envelope,绑定 `projectId`、`assetId`、资源 revision 与 Rust `State`;旧 `{}` 文件、未知字段、身份错误、超限或无效引用均失败关闭。 -- load/save 只能经专用 Tauri command 按 `expectedProjectId + manifest assetId` 解析受控 `localPath`;保存使用每资源 revision CAS。内容相同不推进项目 revision;安装、回读成功后才推进项目 revision,推进失败返回 `reconciliation-required`。 -- 每次非空保存保留一个最近有效的 `.previous` 恢复副本。主文件损坏时只从该副本恢复,外部图片丢失或未登记不使 `State` 无效;预览缺失显示占位且不阻断编辑。 -- `imageOrder`、当前选择、面板开关、缩放和 preview URL 仍是临时 React 状态,加载后从 `State` 派生;加载失败时禁编辑与保存。持久化代码必须覆盖 round-trip、CAS、严格 schema、恢复和缺失外部素材的定向测试。 diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts b/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts index 2cfe307c6..8d2c66026 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/prerequisites.ts @@ -1,3 +1,4 @@ +import type { StageStatus } from './types/StageStatus'; import type { State } from './types/State'; import type { UIDesignImageId } from './types/UIDesignImageId'; @@ -117,16 +118,15 @@ export function validateLayoutGenerationPrerequisites( resourceId: component.Image.target_graphic, }); } - if ( - 'Text' in component && - typeof component.Text.font !== 'string' && - !(component.Text.font.Bound in state.font_assets) - ) { - issues.push({ - code: 'missing-font', - message: '文本组件引用的字体不存在', - resourceId: component.Text.font.Bound, - }); + if ('Text' in component) { + const font = component.Text.font; + if (typeof font !== 'string' && !(font.Bound in state.font_assets)) { + issues.push({ + code: 'missing-font', + message: '文本组件引用的字体不存在', + resourceId: font.Bound, + }); + } } } visit(node.children); @@ -141,3 +141,86 @@ export function validateLayoutReviewPrerequisites( ): UiEditorPrerequisiteIssue[] { return validateLayoutGenerationPrerequisites(state); } + +function statusIssue( + status: StageStatus, + stage: 'layout' | 'components', +): UiEditorPrerequisiteIssue | null { + if (status === 'Pending') { + return { + code: `${stage}-pending`, + message: + stage === 'layout' + ? '存在尚未识别的节点布局' + : '存在尚未绑定的节点组件', + }; + } + if (status === 'Blocked') { + return { code: `${stage}-blocked`, message: '存在被阻塞的节点结果' }; + } + if (typeof status === 'object' && 'NeedReview' in status) { + return { + code: `${stage}-needs-review`, + message: status.NeedReview || '存在需要人工审阅的节点结果', + }; + } + return null; +} + +function visitNodes( + nodes: State['ui_trees'][number]['root']['children'], + visit: (node: State['ui_trees'][number]['root']) => void, +) { + for (const node of nodes) { + visit(node); + visitNodes(node.children, visit); + } +} + +// 结果检查与进入步骤的前置条件保持分离;调用方只把结果作为非阻塞提示。 +export function validateReferenceAnalysisResult( + state: State, +): UiEditorPrerequisiteIssue[] { + const images = Object.values(state.ui_design_images); + if (images.length === 0) { + return [{ code: 'missing-analysis-input', message: '尚未导入参考图' }]; + } + if (images.every((image) => image.metadata.role === null)) { + return [ + { + code: 'missing-reference-analysis', + message: '参考图尚未设置界面用途', + }, + ]; + } + return []; +} + +export function validateStructureRecognitionResult( + state: State, +): UiEditorPrerequisiteIssue[] { + if (state.ui_trees.length === 0) { + return [{ code: 'missing-ui-tree', message: '尚未生成界面结构' }]; + } + const issues: UiEditorPrerequisiteIssue[] = []; + for (const tree of state.ui_trees) { + visitNodes([tree.root], (node) => { + const issue = statusIssue(node.metadata.layout_status, 'layout'); + if (issue) issues.push(issue); + }); + } + return issues; +} + +export function validateVisualBindingResult( + state: State, +): UiEditorPrerequisiteIssue[] { + const issues: UiEditorPrerequisiteIssue[] = []; + for (const tree of state.ui_trees) { + visitNodes([tree.root], (node) => { + const issue = statusIssue(node.metadata.components_status, 'components'); + if (issue) issues.push(issue); + }); + } + return issues; +} diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx index 23586ba1c..3d01a0c37 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx @@ -1,9 +1,7 @@ -import { Image as ImageIcon, Plus, ScanSearch } from 'lucide-react'; +import { Image as ImageIcon, Plus } from 'lucide-react'; import type { UiEditorPageController } from '../useUiEditorPage'; -import { BindingOverview } from './BindingOverview'; import { ImportOverview } from './ImportOverview'; -import { RecognitionOverview } from './RecognitionOverview'; import { UiTreePanel } from './UiTreePanel'; export function InputSidebar({ @@ -21,82 +19,13 @@ export function InputSidebar({ images, sprites, spriteReferenceCounts, - activeTool, treeForActiveImage, selectedNodeId, + focusRequest, } = controller; return ( ); } diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx index daf35b0c0..af74c3210 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx @@ -10,7 +10,6 @@ import type { UIDesignImageId } from '../../../../features/ui-editor/types/UIDes import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UIDesignImageRole'; import { UI_DESIGN_IMAGE_ROLES } from '../../model'; import type { UiEditorPageController } from '../../useUiEditorPage'; -import { PrerequisiteIssues } from '../PrerequisiteIssues'; import { ComponentPanel } from './Components/ComponentPanel'; import { SpriteBorderEditor } from './SpriteBorder/SpriteBorderEditor'; import TransformEditor from './Transform/TransformEditor'; @@ -143,12 +142,6 @@ export function InspectorSidebar({ ) : null} {inspectorContent} - - {controller.issues ? ( -
- -
- ) : null} ); } diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/ToolNavigation.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/ToolNavigation.tsx index a5cf26c99..e347aedbb 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/ToolNavigation.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/ToolNavigation.tsx @@ -1,35 +1,40 @@ -import { UI_EDITOR_TOOLS, type UiEditorToolId } from '../model'; +import { UI_EDITOR_STEPS, type UiEditorStepId } from '../model'; export function ToolNavigation({ - activeTool, + activeStep, + furthestStepIndex, + disabled, onChange, }: { - activeTool: UiEditorToolId; - onChange: (tool: UiEditorToolId) => void; + activeStep: UiEditorStepId; + furthestStepIndex: number; + disabled: boolean; + onChange: (step: UiEditorStepId) => void; }) { return (