diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 74d8e5e41..74202d063 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -11230,8 +11230,9 @@ button.design-workspace-tree__entry:hover, .project-chat-composer.is-direct-codex { display: grid; grid-template-columns: minmax(0, 1fr); - grid-template-rows: auto auto; - gap: 0; + /* 三层(输入内容区 / 工具栏 / 状态提示区)之间靠行间距分开,任何一层都不再用 + 绝对定位去压另一层;`gap` 同时给队列 chip 与输入盒之间留出同一档间距。 */ + gap: 8px; align-items: stretch; position: relative; right: 0; @@ -11301,11 +11302,11 @@ button.design-workspace-tree__entry:hover, .project-chat-composer.is-direct-codex .project-chat-composer-controls { display: flex; + flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: 8px; + gap: 6px 8px; min-width: 0; - padding-top: 2px; pointer-events: auto; } @@ -11323,16 +11324,31 @@ button.design-workspace-tree__entry:hover, .project-chat-composer-controls-right { display: flex; align-items: center; - gap: 8px; + gap: 6px 8px; min-width: 0; } +/* 左侧动作组(`@` 引用 / `+` 附件)允许换行:窄面板下它和右侧控件组各占一行。 */ +.game-workbench-layout--design .project-chat-composer-controls-left, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls-left { + flex-wrap: wrap; +} + +/* 右侧那一组是**不可再压的控件**(推理档 / 模型 / 语音 / 发送):它原来写死 + `flex: 0 0 auto`,宽度不够时既不收缩也不换行,整组向右溢出输入盒、和左侧的 + `@` `+` 叠在一起(窄面板下 `推理档` 直接压在 `添加文件` 上)。改成可收缩的一行: + 组内按 `flex-shrink` 让模型钮先让位(模型名自带省略号),一组控件永远保持单行、 + 整组在容器放不下时由外层换到第二行右对齐。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .project-chat-composer-controls-right { - flex: 0 0 auto; + flex: 1 1 auto; margin-left: auto; + justify-content: flex-end; } /* 左侧引用按钮保留输入盒的内边距,点击区域不向外偏移。 */ @@ -12598,16 +12614,50 @@ button.design-workspace-tree__entry:hover, `.resource-reference-input` / `.resource-reference-input-actions` 规则决定, 同特异性下只有靠后写的才能盖回去。 - 输入区是两列网格(文本列 + 操作列),右侧那枚「AI 润色」按钮常年占掉 36px, - 于是文字右边缘比盒内右边缘少 36px——左右留白与上下留白看着就不一致。 - 这里把输入区改成单块并把操作按钮浮到右下角,文字可用满整宽。 */ + 输入区自己就是「文字 + 区内操作」的两行网格: + 第一行是编辑器(横跨整宽,文字用满盒内宽度); + 第二行左边是润色状态(`.resource-reference-input-status`)、右边是 + 「AI 润色 / 恢复原文」操作排(`.resource-reference-input-actions`)。 + 操作排**回到网格行里**,不再绝对定位浮到文本区右下角:它原来 `right: 0; + bottom: 0` 地压在编辑器最后一行文字上(实测按钮 28×24px 与编辑器相交;文字长到 + 贴近右下角时被盖住),并且和下方工具栏里的发送钮同一条竖线,看起来像"叠了两个按钮"。 */ .game-workbench-chat .project-chat-surface.is-direct-codex .project-chat-composer.is-direct-codex .resource-reference-input { - display: block; - position: relative; - min-height: 96px; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + align-content: start; + gap: 6px 8px; + min-height: 0; + padding: 0; +} + +/* 编辑器横跨两列:文字不再被右侧 28px 的操作列长期挤窄。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .resource-reference-input + .resource-reference-input-editor { + grid-row: 1; + grid-column: 1 / -1; + min-height: 72px; + max-height: 160px; +} + +/* 润色状态与操作排同一行、分列:状态出现/消失都不改变输入盒高度,两者也不会互相压。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .resource-reference-input + .resource-reference-input-status { + grid-row: 2; + grid-column: 1; + align-self: center; + min-width: 0; + margin-top: 0; + overflow-wrap: anywhere; } .game-workbench-chat @@ -12615,12 +12665,13 @@ button.design-workspace-tree__entry:hover, .project-chat-composer.is-direct-codex .resource-reference-input .resource-reference-input-actions { - position: absolute; - right: 0; - bottom: 0; + grid-row: 2; + grid-column: 2; + position: static; display: flex; - align-items: flex-end; + align-items: center; justify-content: flex-end; + min-width: 0; } /* 工具调用折叠块块头改成两行:第一行图标 + 汇总(超长省略),第二行状态 + 用时,箭头右侧跨两行。 @@ -13046,3 +13097,154 @@ details.design-agent-reasoning[open] text-align: center; white-space: pre-wrap; } + +/* ============================================================ + 输入区工具栏与状态提示(2026-10 · issue #600) + ============================================================ + 三组规则,输入区三层布局的收尾: + ① 工具栏里的下拉控件(推理档 / 模型)要能收缩、模型名要能省略,宽度不够时靠换行 + 让位,而不是把整组推到输入盒外面去和左边的 `@` `+` 重叠; + ② 模型控件自己的状态提示(`ConversationModelSelect` 的 `[role='alert']` / + `[role='status']`)从"浮在输入区上方"改成"控件排内独占一行":它原来的 + `bottom: 36px` 正好把错误文案画在输入内容区第 3~4 行文字上(实测 70×36px + 与编辑器相交); + ③ 状态提示区(队列 chip、提示段落)与输入盒、工具栏共用同一档行间距。 + + 这一组必须放在文件最后:`.conversation-model-select` 的定位与宽度由前面几条同 + 选择器(或更高权重)的规则决定,同特异性下只有靠后写的才能盖回去。 */ + +/* ① 下拉控件:可收缩 + 组内换行;模型名超长走省略号,不再把触发钮撑到 180px。 */ +.game-workbench-layout--design + .project-chat-composer-controls + .conversation-model-select, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-select { + flex-wrap: wrap; + justify-content: flex-end; +} + +.game-workbench-layout--design + .project-chat-composer-controls + .conversation-model-trigger-label, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-trigger-label { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* 推理档的读写失败提示同样回到控件排内,整行右对齐。 */ +.game-workbench-layout--design + .project-chat-composer-controls + .project-chat-reasoning-effort, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .project-chat-reasoning-effort { + /* 档位文案很短(默认 / 高 / 中 / 低),不参与收缩——收缩会把它截成半个字。 + 要让位时由模型钮(名字走省略号)承担。 */ + flex: 0 0 auto; + flex-wrap: wrap; + justify-content: flex-end; +} + +.game-workbench-layout--design + .project-chat-composer-controls + .project-chat-reasoning-effort + .project-chat-composer-notice, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .project-chat-reasoning-effort + .project-chat-composer-notice { + flex: 1 1 100%; + margin: 0; + text-align: right; +} + +/* ② 模型控件的错误 / 状态提示:从绝对定位浮层改为占一行,跟随控件换行。 */ +.game-workbench-layout--design + .project-chat-composer-controls + .conversation-model-select + [role='alert'], +.game-workbench-layout--design + .project-chat-composer-controls + .conversation-model-select + [role='status'], +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-select + [role='alert'], +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-select + [role='status'] { + position: static; + order: 1; + flex: 1 1 100%; + min-width: 0; + max-width: none; + margin-top: 2px; + padding: 0; + background: transparent; + font-size: 12px; + line-height: 1.4; + text-align: right; + overflow-wrap: anywhere; +} + +/* 模型控件的宽度上限与触发钮同档(160px):模型名、错误提示都在这一档里用省略号 / + 折行收住,这样「@ + · 推理档 · 模型 · 语音 · 发送」在 400px 以上的面板里始终是一行, + 更窄的面板由外层换行让位(左侧 `@` `+` 与右侧控件组各占一行)。 */ +.game-workbench-layout--design + .project-chat-composer-controls + .conversation-model-select, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-select { + max-width: 160px; +} + +.game-workbench-layout--design + .project-chat-composer-controls + .conversation-model-trigger, +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-controls + .conversation-model-trigger { + max-width: 160px; + /* 下限只是「别缩到看不见」的兜底:最窄的 280px 面板下模型钮仍有 ~74px。 */ + min-width: 64px; +} + +/* ③ 状态提示区:队列 chip 与提示段落跟输入盒、工具栏共用同一档行间距。 */ +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + .project-chat-composer-queue { + margin: 0; +} + +.game-workbench-chat + .project-chat-surface.is-direct-codex + .project-chat-composer.is-direct-codex + > .project-chat-composer-notice { + margin: 0; + max-height: 72px; + overflow: auto; +} diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index b66ab4c34..aa9dc20c1 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -5880,14 +5880,14 @@ export function registerProjectWorkbenchFoundationTests() { expect(styles).toMatch( /\.game-workbench-chat \.project-runtime-summary\s*\{[^}]*position:\s*sticky[^}]*top:\s*-10px/s, ); - // 输入盒是两行网格(文本区 / 控制排)的文档流块,不再是贴在列表下边的 - // 绝对定位浮层,也不再与 82px 的发送钮列共用网格。 + // 输入盒是单列网格的文档流块(三层:输入内容区 / 工具栏 / 状态提示区),不再是贴在 + // 列表下边的绝对定位浮层,也不再与 82px 的发送钮列共用网格;层与层之间用行间距分开。 const composerRule = styles.match( /\.game-workbench-chat\s+\.project-chat-surface\.is-direct-codex\s+\.project-chat-composer\.is-direct-codex\s*\{([^}]*)\}/s, ); expect(composerRule?.[1]).not.toBeUndefined(); expect(composerRule?.[1]).toContain('position: relative;'); - expect(composerRule?.[1]).toContain('grid-template-rows: auto auto;'); + expect(composerRule?.[1]).toContain('gap: 8px;'); expect(composerRule?.[1]).toContain('z-index: 1;'); // 四边留白统一 16px、盒内内边距四边同为 12px(文字左内缩必须等于上内缩); // 不允许再出现 `8px 12px 10px` / `10px 12px` 这类「左右一个值、上下另一个值」的写法。 diff --git a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts index bcb9b3135..17e632124 100644 --- a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts @@ -118,6 +118,22 @@ function marginBox(declarations: Map) { return { top: value(0), right: value(1), bottom: value(2), left: value(3) }; } +/** + * 展开 `gap` 简写,只关心主/交叉两轴。 + * + * 一值写四个方向同一个数;两值先 `row-gap` 后 `column-gap`(与 CSS 的 `gap: ` + * 一致)。三层输入区里两条行间距分别写在输入盒与输入盒自己身上,读值时必须分得清是哪一条。 + */ +function gapBox(declarations: Map) { + const shorthand = declarations.get('gap') ?? '0'; + const parts = shorthand.split(' ').filter(Boolean); + const value = (index: number) => { + const part = parts[index] ?? parts[parts.length - 1] ?? parts[0]; + return lengthPx(part!, `gap 简写 ${shorthand}`); + }; + return { row: value(0), column: value(1) }; +} + /* ============================================================ 选择器身份 ============================================================ */ @@ -132,10 +148,12 @@ const COMPOSER_PLAIN = `${CHAT} .project-chat-composer`; const CONVERSATION_COMPOSER = `${CONVERSATION} .project-chat-composer.is-direct-codex`; const COMPOSER_INPUT = `${COMPOSER} .resource-reference-input`; const COMPOSER_INPUT_PLAIN = `${COMPOSER_PLAIN} .resource-reference-input`; -const COMPOSER_EDITOR = `${COMPOSER_PLAIN} .resource-reference-input-editor`; +const COMPOSER_EDITOR = `${COMPOSER_INPUT} .resource-reference-input-editor`; +const COMPOSER_EDITOR_PLAIN = `${COMPOSER_PLAIN} .resource-reference-input-editor`; const COMPOSER_CONTROLS = `${COMPOSER} .project-chat-composer-controls`; const COMPOSER_SUBMIT = `${COMPOSER_CONTROLS} .project-chat-submit-button`; -const INPUT_ACTIONS = `${COMPOSER} .resource-reference-input-actions`; +const INPUT_ACTIONS = `${COMPOSER_INPUT} .resource-reference-input-actions`; +const INPUT_ACTIONS_PLAIN = `${COMPOSER} .resource-reference-input-actions`; const TOPBAR = `${CHAT} .project-chat-topbar`; /** 列表实际会命中的全部选择器:direct-codex 直连与基础规则。 */ const LIST_SELECTORS = [MESSAGE_LIST, MESSAGE_LIST_PLAIN]; @@ -169,12 +187,10 @@ function composerMaxHeight() { ); const editor = desktopDeclarations( COMPOSER_EDITOR, + COMPOSER_EDITOR_PLAIN, '.resource-reference-input-editor', ); - const actions = desktopDeclarations( - INPUT_ACTIONS, - '.resource-reference-input-actions', - ); + const actions = desktopDeclarations(INPUT_ACTIONS, INPUT_ACTIONS_PLAIN); const polishButton = desktopDeclarations( `${INPUT_ACTIONS} .resource-reference-input-polish`, '.resource-reference-input-polish', @@ -184,32 +200,45 @@ function composerMaxHeight() { const editorMaxHeight = pixelValue(editor, 'max-height'); const actionsRowHeight = pixelValue(polishButton, 'height'); - const inputRowGap = pixelValue(input, 'gap'); + // 输入盒与输入盒外各有一条行间距:前者是「文字区 / 操作排」之间,后者是 + // 「输入内容区 / 工具栏」之间(三层布局,见 styles.css 文件末尾的输入区区块)。 + const inputRowGap = gapBox(input).row; + const composerRowGap = gapBox(composer).row; const inputPaddingBottom = paddingBox(input).bottom; const composerPaddingTop = paddingBox(composer).top; const composerPaddingBottom = paddingBox(composer).bottom; - const controlsRowHeight = - pixelValue(controls, 'padding-top') + pixelValue(submit, 'height'); + // 工具栏自己的高度就是那颗 28px 方钮:与输入区之间的间距归输入盒的 row-gap, + // 工具栏不再自带 padding-top(否则就是同一段间距写两遍)。 + expect( + controls.has('padding-top'), + '工具栏与输入区之间的间距由输入盒的 row-gap 提供,不再用 padding-top', + ).toBe(false); + const controlsRowHeight = pixelValue(submit, 'height'); expect( pixelValue(editor, 'min-height'), '编辑器最小高度是输入盒高度的下界,改了要同步改这里的算式', - ).toBe(96); + ).toBe(72); // 操作排的高度就是那三只 28px 方钮自己撑起来的:它自己不设 height / min-height, // 一旦设了,网格行高会被顶起来、编辑器的 min-height 也跟着变,算式随之作废。 expect(actions.has('height')).toBe(false); expect(actions.has('min-height')).toBe(false); + // 操作排必须是网格里的第二行,不能再绝对定位浮到文字上(issue #600 的回归点)。 + expect(declaration(actions, 'position')).toBe('static'); + expect(declaration(actions, 'grid-row')).toBe('2'); + expect(declaration(actions, 'grid-column')).toBe('2'); return { actionsRowHeight, total: + composerPaddingTop + editorMaxHeight + inputRowGap + actionsRowHeight + inputPaddingBottom + - composerPaddingTop + - composerPaddingBottom + - controlsRowHeight, + composerRowGap + + controlsRowHeight + + composerPaddingBottom, }; } @@ -270,11 +299,12 @@ describe('陶泥儿对话区:Codex 三段式(顶栏 / 唯一滚动区 / 文 const list = desktopDeclarations(...LIST_SELECTORS); const { total, actionsRowHeight } = composerMaxHeight(); - // 输入盒最高高度由真实声明算出来:编辑器 140 + 行距 8 + 操作排 28 + 输入框下内边距 4 - // + 输入盒内边距 24(四边同为 12:文字左内缩必须等于上内缩)+ 操作条(2 + 28 方钮)= 234。 + // 输入盒最高高度由真实声明算出来(三层布局,自上而下): + // 盒内上内边距 12 + 编辑器 160 + 输入区行距 6 + 操作排 28 + 输入区下内边距 0 + // + 输入盒与工具栏的行距 8 + 工具栏 28 + 盒内下内边距 12 = 254。 expect(actionsRowHeight, '操作排按钮高度变了就要重算下面的算式').toBe(28); expect(pixelValue(desktopDeclarations(COMPOSER_SUBMIT), 'height')).toBe(28); - expect(total).toBe(234); + expect(total).toBe(254); const paddingBottom = paddingBox(list).bottom; // 新契约:留白归 0。旧版这里是一条 `inset + 输入区最高高度 + 间距` 的算式, diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index beca4a1a9..076863623 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -2,6 +2,14 @@ 这里只记录对当前开发仍有用的症状、根因、排查方法和风险边界。同一事实保留一个当前口径;退役对象的专属过程与单轮测试结果由 Git 历史追溯。遇到旧路径或版本时,以现行代码和专题文档为准。 +## 2026-10-03 AGC 输入区控件叠在一起:输入区内的操作排与状态提示不能用绝对定位 + +- **现象**:#600 截图里用户输入区域「元素堆叠」——空草稿时「AI 润色」按钮就压在输入内容区右下角(实测与编辑器相交 28×24px),模型控件的失败提示「本地模型服务不可用」直接画在第 3~4 行文字上(70×36px,长草稿时把整段文字盖住);面板越窄越糟:280px 面板下推理档整颗压在「添加文件」上(28×28px),「插入素材引用」与推理档也相交(20×28px)。 +- **原因**:两条「靠绝对定位把控件挪出流」的旧写法叠在一起:① `.resource-reference-input-actions` 被写成 `position: absolute; right: 0; bottom: 0`,浮到编辑器右下角(同一份样式早前其实是网格第二行,见文件里 11893 行那条注释,被后写的绝对定位规则盖掉);② `ConversationModelSelect` 的 `[role='alert'] / [role='status']` 是 `bottom: 36px` 的浮层,锚点就在输入盒正下方,提示必然落在文字上。另外 `.project-chat-composer-controls-right` 写死 `flex: 0 0 auto`,宽度不够时既不收缩也不换行,整组向右溢出输入盒(360px 面板下溢出 34px、280px 下 114px)。 +- **处理(现行口径)**:`.project-chat-composer.is-direct-codex` 是单列网格(`gap: 8px`),三层自上而下——输入内容区 / 工具栏 / 状态提示区;输入内容区自己是两行网格(编辑器横跨两列,第二行左放润色状态、右放「AI 润色 / 恢复原文」操作排),操作排 `position: static` 回到行里;模型提示改成控件排内独占一行(`position: static; flex: 1 1 100%`),不再浮在输入区上方;工具栏允许换行、右侧组改 `flex: 1 1 auto`,模型钮封顶 160px 且名字走省略号(收缩由它承担,推理档 `flex: 0 0 auto` 不参与收缩,避免把「默认」截成半个字)。 +- **判据/取证**:`apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts` 用层叠求值钉住三层几何与操作排 `grid-row: 2 / position: static`、输入盒最高高度算式(12+160+6+28+0+8+28+12 = 254);`tests/appSurface/project-development.suite.ts` 钉住 composer 的 `gap: 8px`。真实渲染对照见 #600 PR:同一夹具在 495/438/360/296/280px 面板 × 空 / 长多行 / 队列+提示 四态下,改前 2~4 处相交(含文字被提示盖住),改后 0 处相交、0 处溢出。 +- **关联**:`apps/ai-game-creator-shell/src/styles.css`(文件末尾「输入区工具栏与状态提示」区块)、`docs/【功能说明】AGC聊天AI润色与发送前提醒-2026-09-10.md`。 + ## 2026-10-03 AGC 栏目画布上传素材落「待归类」:kind 派生分类不等于入口栏目 - **现象**(Issue 359):在 AGC 资源栏目子画布(如「UI 交互」「角色与对象」)左下角工具栏点「上传」选图片 / 视频 / 代码类文件,提示条给出「已上传 1 个素材」,但当前栏目计数纹丝不动(仍「0 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。