AGC 用户输入区按三层重排布局,修掉控件叠压(#600)
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m49s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m26s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 5m12s
Project CI / Frontend tests (pull_request) Successful in 4m2s
Project CI / Backend tests (pull_request) Failing after 5m43s
Project CI / Repository checks (pull_request) Failing after 4m13s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m17s
Project CI / Native shell tests (pull_request) Successful in 7m20s

- 输入盒 `.project-chat-composer.is-direct-codex` 改单列网格 + `gap: 8px`,把输入内容区 / 工具栏 / 状态提示区三层用行间距分开,不再靠绝对定位互相压
- 「AI 润色 / 恢复原文」操作排从 `position: absolute; right: 0; bottom: 0` 回到输入区第二行网格(编辑器横跨全宽),不再压在编辑器文字上
- 模型控件的错误 / 状态提示改成控件排内独占一行(`position: static`),不再浮在输入内容区上方盖住文字
- 工具栏右侧组由 `flex: 0 0 auto` 改为可收缩 + 外层可换行;模型钮封顶 160px、名字走省略号,推理档不参与收缩
- 状态提示区(待发队列 chip、提示段落)与输入盒、工具栏共用同一档行间距(去掉重复的 margin / padding-top)
- 同步两处 CSS 契约断言:composer 几何(`tests/appSurface/project-development.suite.ts`)与输入盒最高高度算式 + 操作排网格行(`tests/chatDialogFrameLayout.test.ts`)
- pitfalls 记录本次叠压的实测尺寸、成因与现行口径
This commit is contained in:
2026-10-03 19:45:45 +08:00
parent 1c4073b500
commit f5886560bd
4 changed files with 275 additions and 35 deletions
+218 -16
View File
@@ -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;
}
@@ -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` 这类「左右一个值、上下另一个值」的写法。
@@ -118,6 +118,22 @@ function marginBox(declarations: Map<string, string>) {
return { top: value(0), right: value(1), bottom: value(2), left: value(3) };
}
/**
* 展开 `gap` 简写,只关心主/交叉两轴。
*
* 一值写四个方向同一个数;两值先 `row-gap` 后 `column-gap`(与 CSS 的 `gap: <row> <column>`
* 一致)。三层输入区里两条行间距分别写在输入盒与输入盒自己身上,读值时必须分得清是哪一条。
*/
function gapBox(declarations: Map<string, string>) {
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 + 输入区最高高度 + 间距` 的算式,
@@ -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 项」),素材出现在「待归类」。用户看到的是"上传成功了,可它就消失在这个页面里"。