fix(ui-chat): keep the steering entrance while the tail is pinned
A pinned tail zeroed both running status moves. Following scrolls absorb output growth, so the status keeps its on-screen position for the first output, but admitted steering still moves the status past its pending echo below it: in a long transcript at the floor the status jumped the height of that echo with no motion. The input-fold switch keeps zeroing both moves; the pinned switch now repeats only the first-output slide's structure, so the entrance after an admitted steering row keeps its duration while the page scrolls by the added slot. Update the README and the CSS check that the pin applies to the slide structure alone.
This commit is contained in:
1 parent
3cea71bd94
commit
39323bbc5f
6 files changed
+37
-17
No files matched your search
@@ -187,13 +187,23 @@
|
||||
animation: running-follows-output 110ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* An input fold, or a pinned tail whose following scrolls hold the status
|
||||
still, runs both status moves at zero duration. The structure alone names
|
||||
the animation, so a switch only changes the duration of a move its
|
||||
structural change already created: turning a switch off never replays a
|
||||
finished move, and turning it on completes a running one at once. The
|
||||
structural rules above are wrapped in :where() so this rule outranks them. */
|
||||
.column:is([data-input-fold], [data-chat-tail-pinned]) .running {
|
||||
/* Switches only change the duration of a move its structural change already
|
||||
created, so turning one off never replays a finished move and turning it on
|
||||
completes a running one at once. The structural rules above are wrapped in
|
||||
:where() so these rules outrank them. An input fold runs both moves at zero
|
||||
duration. */
|
||||
.column[data-input-fold] .running {
|
||||
animation-duration: 0s;
|
||||
}
|
||||
|
||||
/* While the tail is pinned, following scrolls absorb output growth and the
|
||||
status keeps its on-screen position, so the first-output slide runs at zero
|
||||
duration. Admitted steering still passes its pending echo below the status,
|
||||
so the steering entrance keeps its duration. */
|
||||
.column[data-chat-tail-pinned] > :global([data-slot="conversation.chat.flow"]) > :nth-last-child(2 of :not([hidden]):not([data-pending-steering]):not(.flowItem:is(:empty, :has(> [data-slot="conversation.chat.node"]:empty)))):is(
|
||||
.flowItem:not([data-chat-flow-kind="user"]):not([data-chat-flow-kind="steering"]):not([data-chat-flow-kind="turn-trigger"]),
|
||||
[data-chat-group-key]
|
||||
) ~ .running {
|
||||
animation-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,9 +782,9 @@ export class ChatViewport {
|
||||
/**
|
||||
* Mark the column while the tail is pinned: the reader follows the floor with no unsampled reader
|
||||
* movement, the test fold batches use for a tail landing, and the content overflows, so following scrolls
|
||||
* absorb content growth and the status keeps its on-screen position. Status moves run at zero duration
|
||||
* there. A resize calls this after its own follow scroll, before paint, so a move that starts in that
|
||||
* frame finishes before it is drawn; scroll and scrollend call it after delivering the sample, and
|
||||
* absorb output growth and the status keeps its on-screen position. The first-output slide runs at zero
|
||||
* duration there. A resize calls this after its own follow scroll, before paint, so a slide that starts in
|
||||
* that frame finishes before it is drawn; scroll and scrollend call it after delivering the sample, and
|
||||
* follow-intent commits call it with the latest measurement. The attribute changes only with the pin.
|
||||
* @param overflowing - a fresh overflow measurement; omitted, the latest one is reused.
|
||||
*/
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 91b35cbebe8c2a1d
|
||||
zh: a4fee6d7102bc874
|
||||
/chat-conversation-node-rules/whole-turn-folding:
|
||||
en: 367940c7208b3486
|
||||
zh: 7de635c8cb8ab99a
|
||||
en: c609b981a27dcdff
|
||||
zh: 891f1c4d8f5f7efb
|
||||
/chat-conversation-node-rules/display-modes:
|
||||
en: ada5a60d2c46f6a8
|
||||
zh: f3eaeb5cb4d9b3de
|
||||
|
||||
@@ -102,7 +102,7 @@ Clicking Load older anchors the first visible content item below that button in
|
||||
|
||||
Paging adds older content above the retained anchor without jumping to the new top. A capped group absorbs the displacement within its scroll range; the outer transcript absorbs the remainder, including when the group first reaches its cap. If the available scroll range is insufficient, compensation stops at the actual limit without adding bottom space. Later content growth keeps the same anchor until a reading gesture or explicit navigation releases it. Typing or clicking within the composer and non-scrolling transcript keys retain the anchor. If the reader scrolls while a page is loading, scrolling takes priority and its settled reading position becomes the new paging anchor.
|
||||
|
||||
While the Session runs, Chat shows a Session-level indicator at the running Turn's tail: a whale tail beside shimmering elapsed time. It follows the transcript rows and the echoes of submissions that open a Turn; steering still pending when a submission opens the next Turn is claimed first, so its echoes precede that submission's echo. Echoes of steering that waits for the running Turn's next Step follow the indicator. While the running Step has no output, the indicator sits directly below the input. When the first output row appears, the indicator slides from that position into its slot below the row; the travel equals the row's gap plus one row height, which follows the font size setting, so it starts exactly where it stood. Later output moves it without animation. When the transcript overflows the viewport and follows its bottom, following scrolls keep the indicator in place, so neither the steering entrance nor this slide plays. Before the current Turn start time arrives, the indicator shows status text without a duration. The clock updates in whole seconds, starts at one second, and uses hours from 60 minutes. When the Session stops running, the indicator disappears; the closed Turn process control shows the fixed duration, cancellation, or failure status. Lifecycle changes have a polite announcement; clock ticks do not. The whale is hidden from assistive technology and remains static under reduced motion.
|
||||
While the Session runs, Chat shows a Session-level indicator at the running Turn's tail: a whale tail beside shimmering elapsed time. It follows the transcript rows and the echoes of submissions that open a Turn; steering still pending when a submission opens the next Turn is claimed first, so its echoes precede that submission's echo. Echoes of steering that waits for the running Turn's next Step follow the indicator. While the running Step has no output, the indicator sits directly below the input. When the first output row appears, the indicator slides from that position into its slot below the row; the travel equals the row's gap plus one row height, which follows the font size setting, so it starts exactly where it stood. Later output moves it without animation. When the transcript overflows the viewport and follows its bottom, following scrolls keep the indicator in place as output grows, so this slide does not play; admitted steering still moves the indicator past its pending echo, so the steering entrance plays while the page scrolls by the added slot. Before the current Turn start time arrives, the indicator shows status text without a duration. The clock updates in whole seconds, starts at one second, and uses hours from 60 minutes. When the Session stops running, the indicator disappears; the closed Turn process control shows the fixed duration, cancellation, or failure status. Lifecycle changes have a polite announcement; clock ticks do not. The whale is hidden from assistive technology and remains static under reduced motion.
|
||||
|
||||
The top duration/status control and trigger title use the group-title font size and follow the font-size setting. Completed duration digits use the code font with tabular numerals; running and completed minutes and seconds have no leading zero. The timed running label ends with a space and “···”; completion shows “Completed in”.
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ description: "Chat 过程分组与活动摘要的明细业务规则。"
|
||||
|
||||
分页把更早内容加到保留的锚点上方,不主动跳到新内容顶部。限高组先在自己的滚动范围内吸收位移,剩余部分由外层文本滚动补偿,包括组正文刚达到限高的情况。可滚动范围不足时,补偿停在实际边界,不增加底部空白。后续内容撑高沿用同一个锚点,直到阅读手势或显式导航释放它。在 composer 中打字或点击,以及 transcript 内的非滚动按键,保留锚点。分页请求期间若读者滚动,优先服从滚动,停止后以新的阅读位置作为分页锚点。
|
||||
|
||||
会话运行期间,Chat 在运行中 Turn 的末尾显示 Session 级运行指示:鲸鱼摆尾旁显示闪烁的已用时间。它位于 transcript 行和开启 Turn 的提交回显之后;某次提交开启下一个 Turn 时仍在等待的 steering 会先被领取,因此其回显排在该提交回显之前。等待运行中 Turn 下一个 Step 的 steering 回显位于运行指示之后。运行中的 Step 尚无输出时,运行指示紧贴在输入下方。第一条输出行出现时,运行指示从原位置滑入该行下方的位置;滑动距离等于该行间距加一行高度(随字号设置变化),因此起点正是它原来的位置。之后的输出不再带动画。transcript 超出视口并跟随底部时,跟随滚动会让运行指示保持原位,因此 steering 进场和这次下滑都不播放。当前 Turn 的开始时间到达前,运行指示只显示状态文字,不显示时长。时钟按整秒更新,最少显示一秒,满 60 分钟使用小时。会话停止运行后,该指示消失;已结束 Turn 的过程控件显示固定时长、取消或失败状态。生命周期变化有礼貌播报,时钟跳动不播报。鲸鱼对辅助技术隐藏,启用减少动态效果时保持静态。
|
||||
会话运行期间,Chat 在运行中 Turn 的末尾显示 Session 级运行指示:鲸鱼摆尾旁显示闪烁的已用时间。它位于 transcript 行和开启 Turn 的提交回显之后;某次提交开启下一个 Turn 时仍在等待的 steering 会先被领取,因此其回显排在该提交回显之前。等待运行中 Turn 下一个 Step 的 steering 回显位于运行指示之后。运行中的 Step 尚无输出时,运行指示紧贴在输入下方。第一条输出行出现时,运行指示从原位置滑入该行下方的位置;滑动距离等于该行间距加一行高度(随字号设置变化),因此起点正是它原来的位置。之后的输出不再带动画。transcript 超出视口并跟随底部时,输出增长由跟随滚动吸收,运行指示保持原位,因此这次下滑不播放;被领取的 steering 仍会让运行指示越过其等待中的回显,因此 steering 进场照常播放,同时页面随新增的槽位滚动。当前 Turn 的开始时间到达前,运行指示只显示状态文字,不显示时长。时钟按整秒更新,最少显示一秒,满 60 分钟使用小时。会话停止运行后,该指示消失;已结束 Turn 的过程控件显示固定时长、取消或失败状态。生命周期变化有礼貌播报,时钟跳动不播报。鲸鱼对辅助技术隐藏,启用减少动态效果时保持静态。
|
||||
|
||||
顶部用时/状态控件与唤醒标题沿用二级汇总标题字号,并跟随字号设置。已完成用时的数字使用代码等宽字体;运行和完成状态的分钟和秒数均不补前导零。含用时的运行文案末尾附加一个空格及“···”,完成后显示“已完成,用时”。
|
||||
|
||||
|
||||
@@ -48,15 +48,25 @@ describe('Running status move below the first output', () => {
|
||||
|
||||
it('lets input folds and the pinned tail change only the duration of a status move', () => {
|
||||
const view = read('ChatView.module.css')
|
||||
const normalize = (selector: string) => selector.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\s+/g, ' ').trim()
|
||||
// Structure alone names each move, so a switch turning off never adds an animation name and replays a
|
||||
// finished move. The structure sits in :where() so the switch rule outranks it.
|
||||
const moves = [...view.matchAll(/([^{}]+)\{\s*animation: running-(?:enters-slot|follows-output) /g)].map(match => match[1]!.trim())
|
||||
// finished move. The structure sits in :where() so the switch rules outrank it.
|
||||
const moves = [...view.matchAll(/([^{}]+)\{\s*animation: running-(?:enters-slot|follows-output) /g)].map(match => normalize(match[1]!))
|
||||
expect(moves).toHaveLength(2)
|
||||
for (const selector of moves) {
|
||||
expect(selector).toMatch(/^:where\(/)
|
||||
expect(selector).not.toMatch(/data-input-fold|data-chat-tail-pinned/)
|
||||
}
|
||||
expect(view).toMatch(/\.column:is\(\[data-input-fold\], \[data-chat-tail-pinned\]\) \.running \{\s*animation-duration: 0s;\s*\}/)
|
||||
// An input fold zeroes both moves; the pinned tail zeroes only the first-output slide, whose structure it
|
||||
// repeats, and leaves the entrance after admitted steering at its duration.
|
||||
const slide = moves.find(selector => selector.includes('[data-chat-group-key]'))!
|
||||
expect(slide.endsWith(') ~ .running')).toBe(true)
|
||||
const slideStructure = slide.slice(':where('.length, -') ~ .running'.length)
|
||||
const switches = [...view.matchAll(/([^{}]+)\{\s*animation-duration: 0s;\s*\}/g)].map(match => normalize(match[1]!))
|
||||
expect(switches).toEqual([
|
||||
'.column[data-input-fold] .running',
|
||||
`${slideStructure.replace(/^\.column/, '.column[data-chat-tail-pinned]')} ~ .running`,
|
||||
])
|
||||
expect(view.match(/data-chat-tail-pinned/g)).toHaveLength(1)
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user