fix: keep agent task input reachable at small viewports (HEL-732)

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-10-22 16:03:08 +08:00
co-authored by multica-agent
parent 31e5906462
commit 37cb6ec209
5 changed files with 65 additions and 12 deletions
@@ -257,10 +257,10 @@
}
.b4 .big-input select {
field-sizing: content;
max-width: var(--agent-input-select-max);
width: auto;
font-size: 12px;
font-weight: 600;
max-width: 90px;
border: 0;
background: none;
appearance: none;
@@ -405,6 +405,7 @@
gap: 18px;
}
.ahero {
flex-shrink: var(--agent-hero-shrink);
min-width: 0;
position: relative;
border-radius: var(--agent-radius-22);
+2
View File
@@ -14,6 +14,8 @@
--agent-seg-bg: var(--well);
--agent-seg-selected-bg: var(--bg-3);
--agent-seg-shadow: var(--sh-1);
--agent-hero-shrink: 0;
--agent-input-select-max: none;
--agent-radius-22: 22px;
--agent-radius-16: 16px;
--agent-radius-14: 14px;
@@ -43,10 +43,14 @@ HEL-732 / B4:智能体任务、可编辑成果与显式存入项目草稿
图证保留原 HTML 原始状态图;另交仅替换真实数据内容的参考图与实际页面并排图,覆盖四主题与三档尺寸、首页/任务两页。参考数据归一化没有改样本 CSS、布局、字号或尺寸;演示任务、模型计数和占位图未写入产品。尺寸/样式实测表和操作记录、完整 Playwright trace 随任务附件交付。最终视觉一致性仍须总工独立重拍审核,整套样本尚未完成。
## 交接阻塞:真实内容与样本的可用性冲突
## 2026-10-22 已批准的小窗口修正
本批不能标记已完成:原 HTML 的 `.agent-home` 是纵向 flex,`.ahero` 允许收缩又 `overflow:hidden`。在 1366×768、1280×720 中,真实模型/技能名称、成果标题参与排版后,参考样本和照抄的实际页面都会把 hero 压矮,裁掉任务输入/开始任务按钮;因为裁切发生在 hero 内,单纯滚动外层不能找回这些控件。逐项数值一致不能替代这个实际可用性问题。
主卡批准记录 `01a16270-5747-7dc5-8ff0-981f813364d5` 解除顶部裁切阻塞。统一 token `--agent-hero-shrink: 0` 由原 `.ahero` 规则消费;继续使用 `.agent-home` 原有滚动,不改字号、网格、圆角或其他页面。
任务卡要求“样本与真实业务/技术冲突先列具体冲突交总管处理,不自行改样”。因此保留源码行为作为问题证据,交总管决定是否授权仅在此卡片设置 `flex-shrink:0`,让既有 `.agent-home {overflow:auto}` 承接纵向内容。建议不改网格列宽、字号、圆角、配色或其他页面。附件提供原样并排图和仅在隔离浏览器临时施加上述一条规则的建议图,建议图不代表已进入代码或通过验收。
真实选择器的额外 `max-width:90px` 把 `reference-test` 截短,改变 flex spacer 是否换行,导致 1280 下**首页开始任务** x=390、原样本 x=760。它不是成果栏按钮偏移。改为统一 token `--agent-input-select-max:none`,保留 field-sizing 与原 spacer,让真实文字按样本自然宽度参与排版;当前 1280 开始按钮 x=760、1366 x=398、1440 x=920,均与内容对齐的参考一致。成果回流的 `.art-f .primary` 在三尺寸分别 x=1047.171875/1133.171875/1207.171875,原位置未修改;实际项目与阶段 select 保留,并单独测量、聚焦,未伪造样本中不存在的 select。
通用选择器等新增真业务控件也使个别小尺寸的 flex 换行位置有差异,保留在数值表,不过滤视觉失败。完整功能与构建已交,PR 为待处理施工稿;收到总管对冲突的明确裁定后,继续修正、重拍并补测。本批未交 in_review,更不代表整套样本还原完毕。
`e2e/hel732-visual.cjs` 保留未改 CSS 的原样本图与数值,在独立浏览器参考页仅施加已批准的 flex-shrink:0,另存批准版截图;参考页只按真实测试状态替换内容/选中状态/真实缩略图,原 HTML 文件哈希始终为 `0473d80d22b90194375f3566c1b8ca30dbd7433581b18f60956eabca252242a4`。三者清楚标注,未经修改的原演示状态也保留。24 组首页/任务对照、156 组实际选择器测量通过;这只覆盖指定选择器,不冒充全像素相同或独立审核。
新增 `e2e/hel732-layout.cjs` 通过真实应用与隔离假模型,在四主题×三窗口中验证:输入和焦点、正常点击开始任务并产生持久化成果、滚动到下面的最近成果/技能、无横向溢出、真实项目/阶段选择器和保存/回流按钮完整可见且可聚焦。截图测量脚本等待原文视图的异步渲染后再读取内容,避免旧 DOM 值造成误报。B4 功能链路与 B3 相关布局/确认/恢复回归重新通过,前端干净安装、lint/check(0错0警告)、test、build 通过。
本轮没有修改后端或迁移逻辑;既有 118 项后端测试、迁移和旧代码兼容证据继续保留,重新核对备份/导出/恢复文件 SHA256 与 integrity_check,未将其宣称为本轮独立复审。原迁移回退限制仍有效。四主题/全页面最终目检和独立审核交总管安排;本批完成施工待审核,整套样本未宣称完成,无部署、合并、线上数据/配置/密钥或真实生成操作。
+42
View File
@@ -0,0 +1,42 @@
// Real controls, normal pointer input and isolated application/model chain at every viewport/theme.
const {chromium}=require(process.env.PLAYWRIGHT_MODULE_PATH||'playwright');
const fs=require('node:fs'),assert=require('node:assert/strict');
const out='e2e/shots/hel732',WEB='http://127.0.0.1:5173',API='http://127.0.0.1:8247';
(async()=>{
const browser=await chromium.launch({args:['--no-sandbox','--disable-dev-shm-usage']});
const rows=[],errors=[],checks=[];
const board=JSON.parse(fs.readFileSync(out+'/fixture-state.json')).board;
const measure=async(page,selector)=>page.locator(selector).first().evaluate(e=>{const r=e.getBoundingClientRect();return {selector:e.getAttribute('aria-label')||e.className,x:r.x,y:r.y,width:r.width,height:r.height,right:r.right,bottom:r.bottom,visible:r.x>=0&&r.y>=0&&r.right<=innerWidth&&r.bottom<=innerHeight,hit:e.contains(document.elementFromPoint(r.x+r.width/2,r.y+r.height/2))}});
try{
for(const [width,height]of [[1440,900],[1366,768],[1280,720]])for(const theme of ['night','day','grade','bluehour']){
const context=await browser.newContext({viewport:{width,height}}),p=await context.newPage();
p.on('pageerror',e=>errors.push(e.message));
await p.goto(WEB+'/agents');await p.locator('.roster .acard').first().waitFor();
await p.evaluate(theme=>{document.documentElement.dataset.theme=theme;localStorage.setItem('aigc-theme',theme)},theme);
const home={hero:await measure(p,'.ahero'),input:await measure(p,'[aria-label="任务想法"]'),start:await measure(p,'.big-input .primary')};
assert(home.input.visible&&home.input.hit&&home.start.visible&&home.start.hit,JSON.stringify(home));
assert(home.start.bottom<=home.hero.bottom);
assert(await p.locator('.agent-home').evaluate(e=>e.scrollWidth<=e.clientWidth));
await p.getByLabel('任务想法').fill(`小窗口真实启动 ${theme} ${width},请调用工具读取工作区并写一段海岸故事。`);
await p.getByLabel('任务想法').click();assert(await p.getByLabel('任务想法').evaluate(e=>e===document.activeElement));
await p.screenshot({path:`${out}/layout-input-${theme}-${width}.png`,animations:'disabled'});
await p.locator('.mini-row').scrollIntoViewIfNeeded();
await p.waitForFunction(()=>{const r=document.querySelector('.mini-row').getBoundingClientRect();return r.bottom<=innerHeight+1&&r.top>=0});
await p.screenshot({path:`${out}/layout-scrolled-${theme}-${width}.png`,animations:'disabled'});
await p.getByRole('button',{name:'开始任务',exact:true}).click();
await p.waitForURL(/session=\d+/);const session=Number(new URL(p.url()).searchParams.get('session'));
await p.waitForFunction(async({API,session})=>(await(await fetch(API+'/api/results/deliverables?session_id='+session)).json()).length>0,{API,session});
checks.push(`输入、焦点、自然滚动和正常点击启动真实会话 ${theme} ${width}`);
await p.goto(WEB+'/agents?session='+board);await p.locator('.shot-copy textarea').first().waitFor();
const controls={};for(const selector of ['.art-f','[aria-label="成果项目"]','[aria-label="成果阶段"]','.art-f .secondary','.art-f .primary']){
controls[selector]=await measure(p,selector);assert(controls[selector].visible&&controls[selector].hit,JSON.stringify(controls[selector]));
}
for(const label of ['成果项目','成果阶段']){await p.getByLabel(label).focus();assert(await p.getByLabel(label).evaluate(e=>e===document.activeElement));}
for(const selector of ['.art-f .secondary','.art-f .primary']){await p.locator(selector).focus();assert(await p.locator(selector).evaluate(e=>e===document.activeElement));}
assert(await p.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));
await p.screenshot({path:`${out}/layout-footer-${theme}-${width}.png`,animations:'disabled'});
rows.push({theme,width,height,home,controls});await context.close();
}
assert.deepEqual(errors,[]);fs.writeFileSync(out+'/layout-checks.json',JSON.stringify({rows,checks,errors},null,2));console.log('PASS 12 theme/viewport input/start/scroll/real selector/focus/overflow checks');
}finally{await browser.close()}
})().catch(e=>{console.error(e);process.exit(1)});
+11 -7
View File
@@ -11,13 +11,13 @@ const style=loc=>loc.evaluate(e=>{const s=getComputedStyle(e),r=e.getBoundingCli
const sessions=(await Promise.all([api('/api/agent/sessions'),...registry.map(a=>api('/api/agent/sessions?agent_key='+a.key))])).flat().sort((a,b)=>b.updated_at.localeCompare(a.updated_at));
const b=await chromium.launch({args:['--no-sandbox','--disable-dev-shm-usage']});
const server=http.createServer((req,res)=>{res.setHeader('Content-Type','text/html;charset=utf-8');res.end(fs.readFileSync('../evidence/reference/夜片场-多主题交互样本.html'))});await new Promise(r=>server.listen(0,'127.0.0.1',r));
const rows=[],operations=[];
const rows=[],originalRows=[],operations=[];
try{
for(const [width,height] of [[1440,900],[1366,768],[1280,720]])for(const theme of ['night','day','grade','bluehour']){
const c=await b.newContext({viewport:{width,height}}),a=await c.newPage(),s=await c.newPage();
await a.goto(WEB+'/agents');await a.locator('.roster .acard').first().waitFor();await a.evaluate(t=>{document.documentElement.dataset.theme=t;localStorage.setItem('aigc-theme',t);document.documentElement.dataset.reduce='0'},theme);
await s.goto('http://127.0.0.1:'+server.address().port);await s.evaluate(({theme,registry,skills,results,sessions,board})=>{
go('agents');document.documentElement.dataset.theme=theme;
go('agents');document.documentElement.dataset.theme=theme;document.querySelectorAll('#p-agents .seg button').forEach((e,i)=>e.classList.toggle('on',i===1));
const clear=e=>{while(e.firstChild)e.removeChild(e.firstChild)};
for(const id of ['taskList','taskList2']){const list=document.getElementById(id);clear(list);for(const task of sessions){const item=document.createElement('div');item.className='titem'+(id==='taskList2'&&task.id===board?' on':'');const title=document.createElement('b');title.className='ellipsis';title.textContent=task.title;const meta=document.createElement('div');meta.className='meta';const dot=document.createElement('span');dot.className='dot';dot.style.background='var(--sc-agent)';const grow=document.createElement('span');grow.className='grow';meta.append(dot,({returned:'已带回项目',awaiting_confirm:'待你确认',ready:'未开始'}[task.task_status]||task.task_status||'未开始'),grow,task.project?.title||'沙盒');item.append(title,meta);list.append(item);}}
const chips=document.querySelector('#p-agents .big-input .row').querySelectorAll('.chip');chips[0].querySelector('b').textContent='沙盒';chips[1].querySelector('b').textContent='reference-test';chips[2].querySelector('b').textContent='不指定';
@@ -28,23 +28,27 @@ const style=loc=>loc.evaluate(e=>{const s=getComputedStyle(e),r=e.getBoundingCli
const outs=document.querySelector('#agentOuts');clear(outs);for(const r of results.slice(0,4)){const card=document.createElement('div');card.className='outcard';const thumb=document.createElement('div');thumb.className='t';const data=document.createElement('div');data.className='d';const title=document.createElement('b');title.textContent=r.title;const sub=document.createElement('span');sub.className='dim';sub.textContent=({script:'剧本创作',storyboard:'分镜设计'}[r.stage_key]||r.stage_key)+' · 草稿 v'+r.revision;data.append(title,sub);card.append(thumb,data);outs.append(card);}
const input=document.querySelector('#p-agents .big-input textarea');input.placeholder='描述你的创作想法、目标与要求…';document.querySelector('#p-agents .big-input .primary').disabled=true;
},{theme,registry,skills,results,sessions,board:state.board});
await a.screenshot({path:`${out}/home-${theme}-${width}-actual.png`,animations:'disabled'});await s.screenshot({path:`${out}/home-${theme}-${width}-reference-content.png`,animations:'disabled'});
await s.screenshot({path:`${out}/home-${theme}-${width}-original-content.png`,animations:'disabled'});
for(const selector of ['.ahero','.big-input','.big-input .primary'])originalRows.push({theme,width,height,selector,style:await style(s.locator(selector).first())});
await s.locator('.ahero').evaluate(e=>e.style.flexShrink='0');
await a.screenshot({path:`${out}/home-${theme}-${width}-actual.png`,animations:'disabled'});await s.screenshot({path:`${out}/home-${theme}-${width}-approved-content.png`,animations:'disabled'});
for(const [name,actual,ref] of [['hero','.ahero','.ahero'],['title','.ahero h2','.ahero h2'],['assistant','.roster .acard','.roster .acard'],['input','.big-input','.big-input'],['tasklist','.tasklist','.tasklist'],['primary','.big-input .primary','.big-input .primary']])rows.push({theme,width,height,page:'home',name,actual:await style(a.locator(actual).first()),sample:await style(s.locator(ref).first())});
await a.goto(WEB+'/agents?session='+state.board);await a.locator('.shot-copy textarea').first().waitFor();await s.evaluate(({shots,session})=>{
await a.goto(WEB+'/agents?session='+state.board);await a.locator('.shot-copy textarea').first().waitFor();const thumbnails=await a.locator('.shot .th').evaluateAll(es=>es.map(e=>e.querySelector('img')?.src||''));await s.evaluate(({shots,session,thumbnails})=>{
go('agent-task');document.querySelector('#p-agent-task .crumb b').textContent=session.title;document.querySelector('#p-agent-task .ph>.tag').textContent='已带回项目';document.querySelector('#p-agent-task .art-h .row>.dim').textContent=shots.length+' 镜 · '+shots.reduce((sum,s)=>sum+(Number(s.duration_sec)||0),0)+'s';
const list=document.querySelector('#shotList');const originals=Array.from(list.children);list.replaceChildren();for(const [i,shot]of shots.entries()){const el=originals[i].cloneNode(true);el.querySelector('.th').style.backgroundImage='none';el.querySelector('[contenteditable]').textContent=shot.description;el.querySelector('.tag.mono').textContent=shot.duration_sec+'s';list.append(el);}
},{shots,session});
const list=document.querySelector('#shotList');const originals=Array.from(list.children);list.replaceChildren();for(const [i,shot]of shots.entries()){const el=originals[i].cloneNode(true);el.querySelector('.th').style.backgroundImage=thumbnails[i]?`url("${thumbnails[i]}")`:'none';el.querySelector('[contenteditable]').textContent=shot.description;el.querySelector('.tag.mono').textContent=shot.duration_sec+'s';list.append(el);}
},{shots,session,thumbnails});
await a.screenshot({path:`${out}/task-${theme}-${width}-actual.png`,animations:'disabled'});await s.screenshot({path:`${out}/task-${theme}-${width}-reference-content.png`,animations:'disabled'});
for(const [name,actual,ref] of [['columns','.atask','.atask'],['artifact','.artifact','.artifact'],['header','.art-h','.art-h'],['footer','.art-f','.art-f'],['shot','.shot','.shot'],['save','.art-f .secondary','.art-f .btn:not(.primary)'],['primary','.art-f .primary','.art-f .primary']])rows.push({theme,width,height,page:'task',name,actual:await style(a.locator(actual).first()),sample:await style(s.locator(ref).first())});
if(width===1440&&theme==='night'){
await a.getByLabel('镜头 b4-shot-1 描述').fill('主题切换保持未保存编辑');await a.evaluate(async()=>{const {setTheme}=await import('/src/lib/theme.ts');setTheme('day')});await a.waitForFunction(()=>document.documentElement.dataset.theme==='day');assert.equal(await a.getByLabel('镜头 b4-shot-1 描述').inputValue(),'主题切换保持未保存编辑');operations.push('切换主题保持未保存成果文本');await a.screenshot({path:out+'/operation-theme-edit-preserved.png'});
await a.getByRole('button',{name:'原文',exact:true}).click();assert((await a.getByLabel('成果草稿').inputValue()).includes('主题切换保持未保存编辑'));operations.push('逐镜编辑与原文视图同步');await a.screenshot({path:out+'/operation-raw-edit.png'});
await a.getByRole('button',{name:'原文',exact:true}).click();await a.waitForFunction(()=>document.querySelector('[aria-label=成果草稿]')?.value.includes('主题切换保持未保存编辑'));assert((await a.getByLabel('成果草稿').inputValue()).includes('主题切换保持未保存编辑'));operations.push('逐镜编辑与原文视图同步');await a.screenshot({path:out+'/operation-raw-edit.png'});
await a.reload();await a.locator('.shot-copy textarea').first().waitFor();await a.getByRole('button',{name:'对比上一版',exact:true}).click();await a.getByRole('dialog').waitFor();await a.screenshot({path:out+'/operation-version-comparison.png'});await a.keyboard.press('Escape');
const tool=a.locator('.tool-card .head').first();await tool.click();await a.locator('.tool-card .body').first().waitFor();operations.push('工具详情真实参数与返回值可展开');await a.screenshot({path:out+'/operation-tool-expanded.png'});
await a.evaluate(()=>document.documentElement.dataset.reduce='1');assert.equal(await a.locator('.b4 .aurora').evaluate(e=>getComputedStyle(e).display),'none');operations.push('减少动效隐藏本页装饰光晕与粒子');
}
await c.close();
}
fs.writeFileSync(out+'/original-style-values.json',JSON.stringify(originalRows,null,2));
fs.writeFileSync(out+'/style-values.json',JSON.stringify(rows,null,2));
const compareKeys=['x','y','width','height','padding','borderRadius','fontSize','fontWeight','letterSpacing','filter','backgroundColor','backgroundImage','boxShadow','gridTemplateColumns'];const differences=rows.flatMap(r=>compareKeys.filter(k=>r.actual[k]!==r.sample[k]).map(k=>({theme:r.theme,width:r.width,page:r.page,name:r.name,property:k,actual:r.actual[k],sample:r.sample[k]})));fs.writeFileSync(out+'/measured-differences.json',JSON.stringify(differences,null,2));assert.deepEqual(differences,[],'Measured sample differences must be corrected');fs.writeFileSync(out+'/operations.json',JSON.stringify(operations,null,2));console.log('PASS normalized visual pairs: 24; measured selector pairs:',rows.length,'operations:',operations.length);
}finally{await b.close();await new Promise(r=>server.close(r));}