mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:31:58 +00:00
右栏重构:助手常驻,属性改为盖上去的一层
两个问题都源于同一件事——一栏 360px 被要求同时干两件活。 **没地方引导写问题。** Copilot 输入框是个 38px 高的框,挤在一条长长的流 底下,比它上面任何一块都不起眼;例句被截断成 16 个字的 chip,示范"该说到 什么颗粒度"这个作用就没了,只剩装饰。改成一个组件两种体量:没说过话时它 就是这一栏的主体(竖直居中、三行高、自动聚焦、完整句子的例句),并且告诉 用户它能够到什么(N 个数据源 / M 个工具)——不然"查销售库"这种话有没有意义 只能靠猜。有了对话之后它退到底部两行。不做成两个组件:那就是两个实例, 切换时会卸载其中一个,草稿、光标、输入法候选词全没。 **Copilot 和属性挤在一起。** 它们原是 [助手|属性] 两个 tab,而 tab 是互斥的 ——跑图跑到一半点开节点看配置,整条执行过程就从眼前消失,切回来滚动位置和 输入草稿也没了(切 tab 会卸载组件)。改成属性滑出来盖在助手上:助手一直在 下面活着,属性是临时造访者。头部给一个明确的"‹ 助手",Esc 关得掉;盖住 期间底部留一条执行进度,点一下回去——不能因为换了实现就把原来最难受的 那个场景原样留着。 运行控件搬去工具栏。它原先和 Copilot 输入框一起常驻助手栏,两个都是"主要 动作"互相压着;而它们是两种意图(跑这张图 / 改这张图)。运行是对整张图的 动作,和保存、发布同类。有输入字段就弹表单,没有就一键直接跑。 顺带修了两处状态说反的地方: - 停在人工介入时,承载的节点还在转蓝圈。转圈说的是"它在忙,你等着",而 实际正相反——它在等你。改成待办色 + 手势图标。 - "继续执行"的去重原先靠"界面上还有没有 waiting 步骤",上一条改动多了一处 产生 waiting 的地方就失效了。改用事件相邻性判断——那是这两条事件的固有 性质,不会因为别处改了展示而变。 动效:面板滑入、例句依次入场、正在出字的那一步有一道光扫过(比转圈多说 一件事:它在产出,不只是在等)、空态标记轻微呼吸。全部 240ms 以内,缓动 统一 cubic-bezier(.32,.72,0,1)。另外补上了此前完全没有的 prefers-reduced-motion——这套界面里动的东西不少(边在流动、节点在脉冲、 面板在滑),系统里关了还照播,对前庭敏感的人是实打实的难受。 验证:check-ui 重写了助手栏那一节,核心断言是"选中节点再回来,输入的字 还在"——那正是 tab 方案做不到的。新增一条用 emulateMedia 验证关掉动效后 动画确实停了。真跑了一次带人工审批的图,期间打开属性面板确认进度条还在。 四套检查 + 82 个后端测试全过,生产构建通过。
This commit is contained in:
@@ -92,6 +92,26 @@ console.log('\n=== 人工审批(含重放)===')
|
||||
!all.some((s) => s.status === 'done' && s.level === 'warn'))
|
||||
}
|
||||
|
||||
console.log('\n=== 停在审批时的状态 ===')
|
||||
{
|
||||
// 只喂到中断为止,模拟"运行正卡在人工介入上"这一刻
|
||||
const upto = fixtures.human.slice(0, fixtures.human.findIndex((e) => e.type === 'run.interrupted') + 1)
|
||||
const all = flatten(mod.decodeRun(upto))
|
||||
const host = all.find((s) => s.kind === 'node' && s.nodeId === 'h')
|
||||
|
||||
// 转圈说的是"它在忙,你等着",而实际情况正相反:它在等你。
|
||||
// 两种状态的行动含义完全相反,不能共用一个图标
|
||||
check('被中断的节点是"等你"而不是"在跑"', host?.status === 'waiting', host?.status)
|
||||
check('那一刻没有任何东西在转圈',
|
||||
!all.some((s) => s.kind === 'node' && s.status === 'running'),
|
||||
all.filter((s) => s.status === 'running').map((s) => s.title).join(','))
|
||||
|
||||
// 恢复之后它得回到"在跑",否则整条流永远显示成在等人
|
||||
const after = mod.decodeRun(fixtures.human)
|
||||
const doneHost = flatten(after).find((s) => s.kind === 'node' && s.nodeId === 'h')
|
||||
check('恢复后回到正常状态', doneHost?.status === 'done', doneHost?.status)
|
||||
}
|
||||
|
||||
console.log('\n=== 循环里的多轮审批 ===')
|
||||
{
|
||||
// 真实运行:驳回 → 改写 → 再驳回(带备注)→ 改写 → 放行。三轮,每轮都有
|
||||
|
||||
+49
-16
@@ -42,25 +42,38 @@ console.log('=== 画布助手栏 ===')
|
||||
const { page, errors } = await visit('/studio')
|
||||
check('没有运行时报错', errors.length === 0, errors.join(' | '))
|
||||
|
||||
const tabs = await page.locator('nav, [role=tablist], button').allInnerTexts()
|
||||
const flat = tabs.join(' ')
|
||||
check('右栏是「助手 / 属性」两个 tab', flat.includes('助手') && flat.includes('属性'))
|
||||
check('不再有「时间线 / 成果 / 事件」的第二层嵌套',
|
||||
!flat.includes('时间线') && !flat.includes('原始事件'))
|
||||
const body = await page.locator('body').innerText()
|
||||
check('不再有两层 tab 嵌套',
|
||||
!body.includes('时间线') && !body.includes('原始事件'))
|
||||
|
||||
// 改版最容易顺手删掉的两个东西
|
||||
check('发起运行的入口还在', await page.getByText('试运行').count() > 0)
|
||||
check('Copilot 输入框就在栏里(不再是弹窗)',
|
||||
await page.locator('textarea[placeholder*="告诉它"], textarea[placeholder*="描述你要"]').count() > 0)
|
||||
// 用户的原话:"没有一个很好的地方引导用户写问题"。空态下输入区就该是主体
|
||||
check('空态给出了明确的邀请', body.includes('想让它做什么'))
|
||||
check('说清楚它能够到什么', /\d+ 个工具/.test(body), body.match(/\d+ 个工具/)?.[0])
|
||||
const examples = await page.locator('aside button').filter({ hasText: /。|,/ }).count()
|
||||
check('例句是完整句子而不是截断的 chip', examples >= 2, `${examples} 条`)
|
||||
|
||||
// 切到属性再切回来,输入框里的草稿不该没
|
||||
const composer = page.locator('textarea[placeholder*="告诉它"], textarea[placeholder*="描述你要"]').first()
|
||||
const composer = page.locator('aside textarea').first()
|
||||
check('输入框自动聚焦,不用先点一下',
|
||||
await composer.evaluate((el) => el === document.activeElement))
|
||||
|
||||
// 改版最容易顺手删掉的东西:唯一的运行入口
|
||||
check('发起运行的入口还在(已搬到工具栏)',
|
||||
await page.getByRole('button', { name: /^运行/ }).count() > 0)
|
||||
|
||||
// 这是这次重构的核心承诺:属性是盖在助手上的一层,不是把它换掉。
|
||||
// 切 tab 会卸载组件,草稿、滚动位置、展开状态全丢——那正是要修的问题
|
||||
await composer.fill('测试草稿不要丢')
|
||||
await page.getByRole('button', { name: '属性' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
await page.getByRole('button', { name: '助手' }).click()
|
||||
await page.waitForTimeout(200)
|
||||
check('切走再切回来,输入的字还在', await composer.inputValue() === '测试草稿不要丢',
|
||||
await page.locator('.react-flow__node').first().click()
|
||||
await page.waitForTimeout(350)
|
||||
const sheet = await page.locator('body').innerText()
|
||||
check('选中节点滑出属性面板', sheet.includes('节点名称') || sheet.includes('ID:'))
|
||||
check('面板上有明确的返回,让人知道底下还有东西',
|
||||
await page.getByTitle('返回助手(Esc)').count() > 0)
|
||||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.waitForTimeout(350)
|
||||
check('Esc 关得掉', await page.getByTitle('返回助手(Esc)').count() === 0)
|
||||
check('回来之后输入的字还在', await composer.inputValue() === '测试草稿不要丢',
|
||||
await composer.inputValue())
|
||||
await composer.fill('')
|
||||
|
||||
@@ -71,6 +84,26 @@ console.log('=== 画布助手栏 ===')
|
||||
await page.close()
|
||||
}
|
||||
|
||||
console.log('\n=== 动效对前庭敏感者可关 ===')
|
||||
{
|
||||
// 这套界面里动的东西不少(边在流动、节点在脉冲、面板在滑)。
|
||||
// 系统里关了动效还照播,对前庭敏感的人是实打实的难受
|
||||
const page = await ctx.newPage()
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
await page.goto(`${WEB}/studio`, { waitUntil: 'networkidle' })
|
||||
await page.waitForTimeout(500)
|
||||
const durations = await page.evaluate(() => {
|
||||
const probe = document.createElement('div')
|
||||
probe.className = 'rise-in'
|
||||
document.body.appendChild(probe)
|
||||
const d = getComputedStyle(probe).animationDuration
|
||||
probe.remove()
|
||||
return d
|
||||
})
|
||||
check('关掉动效后动画确实停了', parseFloat(durations) < 0.01, durations)
|
||||
await page.close()
|
||||
}
|
||||
|
||||
console.log('\n=== 运行历史 ===')
|
||||
{
|
||||
const { page, errors } = await visit('/runs')
|
||||
|
||||
Reference in New Issue
Block a user