fix: 思考展开恢复完整展示,去掉时长与限高滚动

This commit is contained in:
Rick Ye
2026-09-08 00:34:52 +08:00
parent f3dcd3f597
commit c584508b94
@@ -1,10 +1,10 @@
import { useEffect, useRef, useState } from 'react'
import { useRef, useState } from 'react'
import type { ReactNode } from 'react'
/** 可展开的思考轨迹(ZCode 式实时预览)。
* 流式中:展开区为限高滚动预览——新内容在尾部并自动吸底,用户上滚即取消
* 吸底(滚回底部恢复),表头显示「正在思考 · 持续了 N 秒」;收起时表头
* 改为「正在思考 · 最新一行」持续刷新。结束后收起为「思考过程 · 持续了 N 秒」,
* 流式收起时表头显示「正在思考 · 最新一行」持续刷新,不展示全部;
* 展开时完整展示全部思考(不截断),新内容在尾部、由会话级自动滚动跟随,
* 用户上滚即取消跟随(外层滚动已实现)。结束后收起为「思考过程」,
* 点击表头可手动展开/收起。rows 由真实思考文本逐行喂入。 */
export function Thinking({
rows,
@@ -20,44 +20,17 @@ export function Thinking({
const [manualExpanded, setManualExpanded] = useState<boolean | null>(null)
const expanded = manualExpanded ?? streaming
// 已思考时长:流式期间每秒递增;分段间歇(streaming 抖动)暂停但不清零
const [secs, setSecs] = useState(0)
useEffect(() => {
if (!streaming) return
const t = setInterval(() => setSecs((s) => s + 1), 1000)
return () => clearInterval(t)
}, [streaming])
// 滚动吸底:新内容到达且当前贴着底部时跟随;用户上滚即取消,滚回底部恢复。
// 收起时重置为吸底,重新展开直接落在最新内容。
const scrollRef = useRef<HTMLDivElement>(null)
const pinnedRef = useRef(true)
const handleScroll = (): void => {
const el = scrollRef.current
if (!el) return
pinnedRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24
}
useEffect(() => {
if (!expanded) {
pinnedRef.current = true
return
}
const el = scrollRef.current
if (el && pinnedRef.current) el.scrollTop = el.scrollHeight
}, [rows, expanded])
// 流式轨迹的行必须「随到随显」:fade-up 的 both+delay 会让新行在延迟期间
// 透明但占高,流式期间底部因此持续留出一大片空白。交错入场只保留给
// 非流式一次性渲染的静态轨迹(挂载时决定,之后不变更,避免整段重播动画)。
const streamedMountRef = useRef(streaming)
const statusWord = streaming ? activeLabel : expanded ? '思考' : doneLabel
const latest = rows.length > 0 ? rows[rows.length - 1].primary : ''
const suffix = expanded ? (secs > 0 ? `持续了 ${secs} 秒` : '') : streaming ? latest : ''
const suffix = streaming && !expanded ? latest : ''
return (
<div className="flex w-full max-w-95 flex-col">
{/* 表头:状态词 + 后缀(时长 / 最新一行)+ 旋转箭头 */}
{/* 表头:状态词 +(流式收起时)最新一行 + 旋转箭头 */}
<button
type="button"
aria-expanded={expanded}
@@ -77,22 +50,18 @@ export function Thinking({
animation: 'shimmer-text 1.4s linear infinite'
}}
>
{statusWord}
{activeLabel}
</span>
) : (
<span
className="whitespace-nowrap text-[13px] font-medium text-ink-2"
style={{ animation: 'fade-in 350ms ease-out both' }}
>
{statusWord}
{doneLabel}
</span>
)}
{suffix && (
<span
className={`min-w-0 text-[12.5px] text-ink-3 ${expanded ? 'whitespace-nowrap tabular-nums' : 'truncate'}`}
>
· {suffix}
</span>
<span className="min-w-0 truncate text-[12.5px] text-ink-3">· {suffix}</span>
)}
</span>
<svg
@@ -111,7 +80,7 @@ export function Thinking({
</svg>
</button>
{/* 展开轨迹:限高滚动预览(最新在尾部),左侧 hairline 随内容延伸 */}
{/* 展开轨迹:完整展示全部思考,左侧 hairline 随内容延伸 */}
<div
className="grid transition-[grid-template-rows,opacity]"
style={{
@@ -121,9 +90,9 @@ export function Thinking({
}}
>
<div className="min-h-0 overflow-hidden">
<div ref={scrollRef} onScroll={handleScroll} className="mt-1 ml-[5px] max-h-[220px] overflow-y-auto">
<div className="relative flex flex-col gap-1 py-1 pl-4">
<span aria-hidden className="absolute bottom-0 left-[3px] top-0 w-px bg-line" />
<div className="relative mt-1 ml-[5px] pl-4">
<span aria-hidden className="absolute bottom-0 left-[3px] top-0 w-px bg-line" />
<div className="flex flex-col gap-1 py-1">
{rows.map((row, i) => (
<div
key={i}