perf(ui-chat): retain the stats resize observer
This commit is contained in:
1 parent
f808112ec8
commit
aad1ce0c68
2 files changed
+59
-27
No files matched your search
@@ -2,7 +2,7 @@
|
||||
// Mounted on 'conversation.composer.dock' so it sticks with the composer in the
|
||||
// active conversation scrollport (see ConversationRoot data-conversation-scroll).
|
||||
|
||||
import { Fragment, memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Fragment, memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { UseProjection } from '@deepseek-ai/dsh-api-session-controller/client'
|
||||
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -122,6 +122,44 @@ export interface StatsLineProps {
|
||||
t: ChatViewSlotProps['t']
|
||||
}
|
||||
|
||||
/** Render and measure one non-empty statistics line. */
|
||||
const StatsLineContent = memo(function StatsLineContent({
|
||||
groups,
|
||||
line,
|
||||
}: {
|
||||
readonly groups: readonly string[]
|
||||
readonly line: string
|
||||
}) {
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
const [truncated, setTruncated] = useState(false)
|
||||
const measure = useCallback(() => {
|
||||
const el = rootRef.current
|
||||
if (el === null) return
|
||||
const next = el.scrollWidth > el.clientWidth
|
||||
setTruncated(current => current === next ? current : next)
|
||||
}, [])
|
||||
useLayoutEffect(() => {
|
||||
const el = rootRef.current
|
||||
if (el === null || typeof ResizeObserver === 'undefined') return
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(el)
|
||||
return () => { observer.disconnect() }
|
||||
}, [measure])
|
||||
useLayoutEffect(measure, [line, measure])
|
||||
return (
|
||||
<Tooltip label={line} side="top" delayMs={500} disabled={!truncated}>
|
||||
<div ref={rootRef} className={css.root}>
|
||||
{groups.map((group, i) => (
|
||||
<Fragment key={group}>
|
||||
{i > 0 && <><span className={css.sep} aria-hidden>|</span>{' '}</>}
|
||||
<span>{group}</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
|
||||
export const StatsLine = memo(function StatsLine({ useChat, useProjection, t }: StatsLineProps) {
|
||||
const settledNodes = useChat(s => s.legacy.nodes)
|
||||
const usage = useProjection('tokenUsage')
|
||||
@@ -166,31 +204,6 @@ export const StatsLine = memo(function StatsLine({ useChat, useProjection, t }:
|
||||
}))
|
||||
}
|
||||
const line = groups.join(' | ')
|
||||
// The row elides with ellipsis when overlong; a delayed hover tooltip carries
|
||||
// the full line, enabled only while content is actually clipped.
|
||||
const rootRef = useRef<HTMLDivElement | null>(null)
|
||||
const [truncated, setTruncated] = useState(false)
|
||||
useLayoutEffect(() => {
|
||||
const el = rootRef.current
|
||||
if (el === null) return
|
||||
const measure = () => { setTruncated(el.scrollWidth > el.clientWidth) }
|
||||
measure()
|
||||
if (typeof ResizeObserver === 'undefined') return
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(el)
|
||||
return () => { observer.disconnect() }
|
||||
}, [line])
|
||||
if (groups.length === 0) return null
|
||||
return (
|
||||
<Tooltip label={line} side="top" delayMs={500} disabled={!truncated}>
|
||||
<div ref={rootRef} className={css.root}>
|
||||
{groups.map((group, i) => (
|
||||
<Fragment key={group}>
|
||||
{i > 0 && <><span className={css.sep} aria-hidden>|</span>{' '}</>}
|
||||
<span>{group}</span>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
return <StatsLineContent groups={groups} line={line} />
|
||||
})
|
||||
@@ -162,6 +162,25 @@ describe('StatsLine', () => {
|
||||
return { uncachedInputTokens, outputTokens: 1, cacheReadTokens, cacheWriteTokens: 0 }
|
||||
}
|
||||
|
||||
it('mounts one size observer only while a non-empty line exists', () => {
|
||||
let observers = 0
|
||||
class CountingResizeObserver {
|
||||
constructor(_callback: ResizeObserverCallback) { observers++ }
|
||||
observe(): void {}
|
||||
disconnect(): void {}
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', CountingResizeObserver)
|
||||
const { set, source } = makeSource()
|
||||
render(<StatsLine {...props(source, {})} />)
|
||||
expect(observers).toBe(0)
|
||||
|
||||
act(() => { set({ nodes: [assistant(1, 1)] }) })
|
||||
expect(observers).toBe(1)
|
||||
|
||||
act(() => { set({ nodes: [assistant(1, 1), assistant(2, 1)] }) })
|
||||
expect(observers).toBe(1)
|
||||
})
|
||||
|
||||
it('renders the grouped stats row and hides a brand-new empty session', () => {
|
||||
const { source } = makeSource({ nodes: [assistant(1, 1)] })
|
||||
const view = render(<StatsLine {...props(source)} />)
|
||||
|
||||
Reference in new issue
Block a user