refactor(ui-chat): drop the flat usage-variant debug switch and square narrow pills
The A/B test settled on the twin-pill layout, so the TEMPORARY ?usage-variant=flat trigger, its locale keys, and its tests leave with it. Below 480px the stat pills now take the sibling action-button geometry so their bare icons keep the row's rhythm instead of drifting on the wider label padding and the -6px pair rebate.
This commit is contained in:
parent
9effa0c6b3
commit
6f16d5868c
7 changed files with 66 additions and 259 deletions
|
|
@ -28,14 +28,6 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
|
|||
const runMs = turn.start === undefined || turn.end === undefined
|
||||
? undefined
|
||||
: Math.max(0, turn.end.time - turn.start.time)
|
||||
// TEMPORARY usage-variant debug switch for user testing: `?usage-variant=flat`
|
||||
// or `#usage-variant=flat` (the hash form survives the login token's 303
|
||||
// redirect to a clean `/`) hands the whole meta line to TurnUsagePanel as the
|
||||
// dialog trigger; the default keeps the icon pill beside plain meta text.
|
||||
// Delete after the test.
|
||||
const flatVariant = data.tokenUsage !== undefined
|
||||
&& [window.location.search, window.location.hash.replace(/^#/, '')]
|
||||
.some(query => new URLSearchParams(query).get('usage-variant') === 'flat')
|
||||
// Interruption-frozen partials carry no messageId, so they address no
|
||||
// durable message and contribute no per-message actions.
|
||||
const messageId = closing.finalNode.messageId
|
||||
|
|
@ -51,37 +43,25 @@ export const TurnTailNodeView = memo(function TurnTailNodeView({
|
|||
{tail}
|
||||
<MessageIconActions
|
||||
text={assistantText(closing.blocks)}
|
||||
{...flatVariant ? {} : { time: closing.time }}
|
||||
time={closing.time}
|
||||
clock="end"
|
||||
onBranch={() => { forkAt(closing.finalNode.seq) }}
|
||||
branchUnavailable={data.branchUnavailable || hasLaterChatNode}
|
||||
className={css.actions}
|
||||
extraActions={assistantActions}
|
||||
usageAction={flatVariant
|
||||
? data.tokenUsage !== undefined && (
|
||||
<TurnUsagePanel
|
||||
usage={data.tokenUsage}
|
||||
variant="flat"
|
||||
time={closing.time}
|
||||
runMs={runMs}
|
||||
tokensPerSecond={data.tokensPerSecond}
|
||||
ttftMs={data.ttftMs}
|
||||
t={t}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{data.tokenUsage !== undefined && <TurnUsagePanel usage={data.tokenUsage} t={t} />}
|
||||
{runMs !== undefined && (
|
||||
<TurnTimePanel
|
||||
runMs={runMs}
|
||||
tokensPerSecond={data.tokensPerSecond}
|
||||
ttftMs={data.ttftMs}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
usageAction={(
|
||||
<>
|
||||
{data.tokenUsage !== undefined && <TurnUsagePanel usage={data.tokenUsage} t={t} />}
|
||||
{runMs !== undefined && (
|
||||
<TurnTimePanel
|
||||
runMs={runMs}
|
||||
tokensPerSecond={data.tokensPerSecond}
|
||||
ttftMs={data.ttftMs}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,36 +43,6 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* TEMPORARY (usage-variant debug): the flat whole-line trigger reads like the
|
||||
plain meta text, brightening on hover to hint clickability. The margin
|
||||
widens the row's 8px flex gap off the branch icon beside it. */
|
||||
.flatTrigger {
|
||||
min-width: 0;
|
||||
margin-left: 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-size: var(--dsh-content-font-size-secondary, 13px);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: calc(24px + var(--dsh-content-font-delta, 0px));
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flatTrigger:hover,
|
||||
.flatTrigger[aria-expanded='true'] {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* TEMPORARY (usage-variant debug): flat-line separators breathe wider than the
|
||||
plain meta line's 5px runTimeDot rhythm. */
|
||||
.flatDot {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.trigger svg {
|
||||
width: calc(15px + var(--dsh-content-font-delta, 0px));
|
||||
height: calc(15px + var(--dsh-content-font-delta, 0px));
|
||||
|
|
@ -85,11 +55,24 @@
|
|||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
/* Narrow viewport: the pills collapse to bare icons; the dialogs keep the words. */
|
||||
/* Narrow viewport: the pills collapse to bare icons with the sibling
|
||||
`.action` geometry (28px circle, 6px padding, centered glyph); the
|
||||
label-padding rebate no longer applies, so the pair keeps the row's plain
|
||||
8px rhythm. The dialogs keep the words. */
|
||||
@media (max-width: 480px) {
|
||||
.trigger {
|
||||
justify-content: center;
|
||||
width: calc(28px + var(--dsh-content-font-delta, 0px));
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.trigger .label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root + .root {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Portal surface: fixed in the viewport, left/top supplied inline from the
|
||||
|
|
@ -128,11 +111,6 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Second-section heading (Turn time), spaced well off the usage rows above. */
|
||||
.timeTitle {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Rule under each section heading, above its rows. */
|
||||
.titleRule {
|
||||
margin-bottom: 10px;
|
||||
|
|
|
|||
|
|
@ -2,36 +2,20 @@
|
|||
// click-opens the per-Turn usage dialog, and a clock pill labelled with the
|
||||
// turn wall time click-opens the Turn-time dialog. Both sit right of the
|
||||
// branch action in the tail's IconActions row, ahead of the plain clock text.
|
||||
// TEMPORARY: the `flat` variant (usage-variant debug switch) renders the
|
||||
// whole meta line as one trigger whose dialog stacks both sections; one of
|
||||
// the two variants will be deleted after user testing.
|
||||
|
||||
import { Fragment, useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from 'react'
|
||||
import { useEffect, useRef, useState, type CSSProperties, type MutableRefObject } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import {
|
||||
IconClockOutline16, IconDatabaseOutline16, useAnchoredPosition,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { TurnTokenUsage } from '../contract/chat-nodes.ts'
|
||||
import type { ChatViewSlotProps } from '../contract/slots.ts'
|
||||
import {
|
||||
formatLatencySeconds, formatMessageClock, formatRunDuration, formatTokensPerSecond,
|
||||
} from './message-chrome.ts'
|
||||
import { formatLatencySeconds, formatRunDuration, formatTokensPerSecond } from './message-chrome.ts'
|
||||
import { formatCacheHitPercent, formatExactTokens, formatTokens } from './token-format.ts'
|
||||
import { useCalendarDay } from './use-calendar-day.ts'
|
||||
import css from './TurnUsagePanel.module.css'
|
||||
|
||||
export interface TurnUsagePanelProps {
|
||||
usage: TurnTokenUsage
|
||||
/** Trigger form: the default icon pill or the temporary flat whole-line text. */
|
||||
variant?: 'pill' | 'flat'
|
||||
/** Unix epoch ms for the flat trigger's clock segment. */
|
||||
time?: number | undefined
|
||||
/** Turn wall time in ms; the flat trigger and its dialog's Turn-time section show it. */
|
||||
runMs?: number | undefined
|
||||
/** Turn decode throughput; the flat trigger and its dialog's Turn-time section show it. */
|
||||
tokensPerSecond?: number | undefined
|
||||
/** Turn first-step TTFT in ms; the flat trigger and its dialog's Turn-time section show it. */
|
||||
ttftMs?: number | undefined
|
||||
/** The owning view's locale seat, passed down as a plain prop. */
|
||||
t: ChatViewSlotProps['t']
|
||||
}
|
||||
|
|
@ -115,85 +99,31 @@ function useStatDialog(): StatDialogSeat {
|
|||
return { open, setOpen, rootRef, panelRef, pos }
|
||||
}
|
||||
|
||||
/** Turn-time dialog rows: run time plus throughput and TTFT when known. */
|
||||
function TimeDetails({ runMs, tokensPerSecond, ttftMs, t }: {
|
||||
runMs: number | undefined
|
||||
tokensPerSecond: number | undefined
|
||||
ttftMs: number | undefined
|
||||
t: ChatViewSlotProps['t']
|
||||
}) {
|
||||
return (
|
||||
<dl className={css.details} data-turn-time-details>
|
||||
{runMs !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.duration')}</dt>
|
||||
<dd>{formatRunDuration(runMs, t)}</dd>
|
||||
</>
|
||||
)}
|
||||
{tokensPerSecond !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.speed')}</dt>
|
||||
<dd>{t('message.tokensPerSecond', { tps: formatTokensPerSecond(tokensPerSecond) })}</dd>
|
||||
</>
|
||||
)}
|
||||
{ttftMs !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.ttft')}</dt>
|
||||
<dd>{t('duration.seconds', { seconds: formatLatencySeconds(ttftMs) })}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn-usage IconActions pill (or the temporary flat meta-line trigger) with
|
||||
* a click-open Turn-usage details dialog.
|
||||
* @param props - Turn usage buckets, trigger form, and locale seat.
|
||||
* Turn-usage IconActions pill with a click-open Turn-usage details dialog.
|
||||
* @param props - Turn usage buckets and locale seat.
|
||||
* @returns The trigger and, while open, its portaled dialog anchored above the trigger.
|
||||
*/
|
||||
export function TurnUsagePanel({ usage, variant = 'pill', time, runMs, tokensPerSecond, ttftMs, t }: TurnUsagePanelProps) {
|
||||
export function TurnUsagePanel({ usage, t }: TurnUsagePanelProps) {
|
||||
const { open, setOpen, rootRef, panelRef, pos } = useStatDialog()
|
||||
const day = useCalendarDay()
|
||||
|
||||
const cacheHit = usage.cacheReadTokens === undefined
|
||||
? null
|
||||
: formatCacheHitPercent(usage.cacheReadTokens, usage.totalTokens - usage.outputTokens, 1)
|
||||
const total = formatCompactCount(usage.totalTokens, t)
|
||||
const routes = usage.routes?.map(route => `${route.provider}/${route.model}`).join(', ') ?? ''
|
||||
const flatSegments: string[] = []
|
||||
if (variant === 'flat') {
|
||||
if (time !== undefined) flatSegments.push(formatMessageClock(time, t, day))
|
||||
if (runMs !== undefined) flatSegments.push(t('message.ranFor', { duration: formatRunDuration(runMs, t) }))
|
||||
flatSegments.push(t('message.turnUsage.consumed', { total }))
|
||||
if (cacheHit !== null) flatSegments.push(t('message.turnUsage.cacheHitRate', { percent: cacheHit }))
|
||||
if (tokensPerSecond !== undefined) {
|
||||
flatSegments.push(t('message.turnUsage.speed', { tps: formatTokensPerSecond(tokensPerSecond) }))
|
||||
}
|
||||
if (ttftMs !== undefined) flatSegments.push(t('message.ttft', { seconds: formatLatencySeconds(ttftMs) }))
|
||||
}
|
||||
const hasTimeFacts = runMs !== undefined || tokensPerSecond !== undefined || ttftMs !== undefined
|
||||
|
||||
return (
|
||||
<span ref={rootRef} className={css.root}>
|
||||
<button
|
||||
type="button"
|
||||
className={variant === 'flat' ? css.flatTrigger : css.trigger}
|
||||
className={css.trigger}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
onClick={() => { setOpen(!open) }}
|
||||
>
|
||||
{variant === 'flat' ? flatSegments.map((segment, index) => (
|
||||
<Fragment key={index}>
|
||||
{index > 0 && <span className={css.flatDot} aria-hidden>·</span>}
|
||||
{segment}
|
||||
</Fragment>
|
||||
)) : (
|
||||
<>
|
||||
<IconDatabaseOutline16 />
|
||||
<span className={css.label}>{t('message.turnUsage.consumed', { total })}</span>
|
||||
</>
|
||||
)}
|
||||
<IconDatabaseOutline16 />
|
||||
<span className={css.label}>{t('message.turnUsage.consumed', { total })}</span>
|
||||
</button>
|
||||
{open && createPortal(
|
||||
<div
|
||||
|
|
@ -248,18 +178,6 @@ export function TurnUsagePanel({ usage, variant = 'pill', time, runMs, tokensPer
|
|||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
{variant === 'flat' && hasTimeFacts && (
|
||||
<>
|
||||
<div className={`${css.title} ${css.timeTitle}`}>
|
||||
<span className={css.titleLabel}>
|
||||
<IconClockOutline16 />
|
||||
{t('message.turnTime.title')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={css.titleRule} aria-hidden />
|
||||
<TimeDetails runMs={runMs} tokensPerSecond={tokensPerSecond} ttftMs={ttftMs} t={t} />
|
||||
</>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
|
|
@ -301,7 +219,22 @@ export function TurnTimePanel({ runMs, tokensPerSecond, ttftMs, t }: TurnTimePan
|
|||
</span>
|
||||
</div>
|
||||
<div className={css.titleRule} aria-hidden />
|
||||
<TimeDetails runMs={runMs} tokensPerSecond={tokensPerSecond} ttftMs={ttftMs} t={t} />
|
||||
<dl className={css.details} data-turn-time-details>
|
||||
<dt>{t('message.turnTime.duration')}</dt>
|
||||
<dd>{formatRunDuration(runMs, t)}</dd>
|
||||
{tokensPerSecond !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.speed')}</dt>
|
||||
<dd>{t('message.tokensPerSecond', { tps: formatTokensPerSecond(tokensPerSecond) })}</dd>
|
||||
</>
|
||||
)}
|
||||
{ttftMs !== undefined && (
|
||||
<>
|
||||
<dt>{t('message.turnTime.ttft')}</dt>
|
||||
<dd>{t('duration.seconds', { seconds: formatLatencySeconds(ttftMs) })}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -88,12 +88,9 @@ export const zh = {
|
|||
'message.maxTokens': '已达到输出 token 上限',
|
||||
'message.maxTokens.hint': '回答被截断,已有输出保留在对话中。发送“继续”可让模型接着输出。',
|
||||
'message.ranFor': '用时 {duration}',
|
||||
'message.ttft': '首 token {seconds}秒',
|
||||
'message.tokensPerSecond': '{tps} tok/s',
|
||||
'message.turnUsage.title': '本轮用量',
|
||||
'message.turnUsage.consumed': '用量 {total}',
|
||||
'message.turnUsage.cacheHitRate': '缓存命中 {percent}%',
|
||||
'message.turnUsage.speed': '速度 {tps} tok/s',
|
||||
'message.turnUsage.model': '提供方 / 模型',
|
||||
'message.turnUsage.cacheHit': '缓存命中',
|
||||
'message.turnUsage.input': '未缓存输入',
|
||||
|
|
@ -207,12 +204,9 @@ export const en = {
|
|||
'message.maxTokens': 'Output token limit reached',
|
||||
'message.maxTokens.hint': 'The reply was cut off; earlier output is preserved in the conversation. Send "continue" to let the model resume.',
|
||||
'message.ranFor': 'Ran for {duration}',
|
||||
'message.ttft': 'TTFT {seconds}s',
|
||||
'message.tokensPerSecond': '{tps} tok/s',
|
||||
'message.turnUsage.title': 'Turn usage',
|
||||
'message.turnUsage.consumed': 'Usage {total}',
|
||||
'message.turnUsage.cacheHitRate': 'Cache hit {percent}%',
|
||||
'message.turnUsage.speed': '{tps} tok/s',
|
||||
'message.turnUsage.model': 'Provider / model',
|
||||
'message.turnUsage.cacheHit': 'Cache hit',
|
||||
'message.turnUsage.input': 'Uncached input',
|
||||
|
|
|
|||
|
|
@ -117,6 +117,20 @@ describe('chat flow font-size axis', () => {
|
|||
]))
|
||||
})
|
||||
|
||||
it('narrow viewports collapse the stat pills to the action-button circle', () => {
|
||||
// Below 480px the label hides and the pill takes the sibling `.action`
|
||||
// geometry (28px width, 6px padding, centered glyph); the -6px
|
||||
// label-padding rebate between adjacent pills resets so the icon pair
|
||||
// keeps the row's plain 8px rhythm instead of overlapping.
|
||||
const css = read('TurnUsagePanel.module.css')
|
||||
const narrow = /@media \(max-width: 480px\) \{([\s\S]*?)\n\}/.exec(css)?.[1] ?? ''
|
||||
expect(narrow).toMatch(/\.trigger \{[^}]*justify-content: center/)
|
||||
expect(narrow).toMatch(/\.trigger \{[^}]*width: calc\(28px \+ var\(--dsh-content-font-delta, 0px\)\)/)
|
||||
expect(narrow).toMatch(/\.trigger \{[^}]*padding: 6px/)
|
||||
expect(narrow).toMatch(/\.trigger \.label \{[^}]*display: none/)
|
||||
expect(narrow).toMatch(/\.root \+ \.root \{[^}]*margin-left: 0/)
|
||||
})
|
||||
|
||||
it('non-latest turn tails hide the whole actions row until hover or focus', () => {
|
||||
// TurnTailNodeView tags its root data-actions-reveal='hover' for every
|
||||
// turn but the latest; the gate lives under @media (hover: hover) so
|
||||
|
|
|
|||
|
|
@ -1586,49 +1586,6 @@ describe('ChatView', () => {
|
|||
expect(timeDialog.textContent).toContain('首 token 平均用时(TTFT)1.2秒')
|
||||
})
|
||||
|
||||
it('TEMPORARY: ?usage-variant=flat renders the whole meta line as the dialog trigger', () => {
|
||||
window.history.replaceState(null, '', '?usage-variant=flat')
|
||||
try {
|
||||
const first: AssistantMessageNode = {
|
||||
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1, blocks: [{ kind: 'text', text: 'draft' }],
|
||||
timing: { stepStartTime: 1_000, firstTokenTime: 2_200, completedTime: 5_200 },
|
||||
usage: { outputTokens: 40 },
|
||||
}
|
||||
const second: AssistantMessageNode = {
|
||||
kind: 'assistant', seq: 16, time: 16_000, turn: 1, step: 2, blocks: [{ kind: 'text', text: 'final' }],
|
||||
timing: { stepStartTime: 10_000, firstTokenTime: 10_200, completedTime: 12_200 },
|
||||
usage: { outputTokens: 60 },
|
||||
}
|
||||
const h = makeHarness({
|
||||
nodes: [user(1, 'hi'), first, second],
|
||||
turnTimings: new Map([[1, { startTime: 1_000, endTime: 20_000 }]]),
|
||||
turnEnds: new Map([[1, 20]]),
|
||||
turnUsages: new Map([[1, {
|
||||
uncachedInputTokens: 5_060,
|
||||
cacheReadTokens: 4_940,
|
||||
outputTokens: 100,
|
||||
totalTokens: 10_100,
|
||||
}]]),
|
||||
})
|
||||
const view = render(<h.ChatView {...h.props} />)
|
||||
const trigger = view.getByRole('button', { name: /用量 10\.1K tok/ })
|
||||
// Separator dots live in spaced spans, so textContent carries bare `·`.
|
||||
expect(trigger.textContent)
|
||||
.toMatch(/^.+·用时 19秒·用量 10\.1K tok·缓存命中 49\.4%·速度 20 tok\/s·首 token 1\.2秒$/)
|
||||
// The flat trigger absorbs the meta line, so no plain copy remains.
|
||||
expect(view.getByText(/用时 19秒/)).toBe(trigger)
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.firstChild?.textContent).toBe('本轮用量10,100 tok')
|
||||
expect(dialog.textContent).toContain('本轮用时和速度')
|
||||
expect(dialog.textContent).toContain('本轮总用时19秒')
|
||||
expect(dialog.textContent).toContain('输出速度(TPS)20 tok/s')
|
||||
expect(dialog.textContent).toContain('首 token 平均用时(TTFT)1.2秒')
|
||||
} finally {
|
||||
window.history.replaceState(null, '', window.location.pathname)
|
||||
}
|
||||
})
|
||||
|
||||
it('withholds the usage-details trigger when turn usage is outside the window', () => {
|
||||
const settled: AssistantMessageNode = {
|
||||
kind: 'assistant', seq: 2, time: 2_000, turn: 1, step: 1, blocks: [{ kind: 'text', text: 'answer' }],
|
||||
|
|
|
|||
|
|
@ -95,55 +95,6 @@ describe('TurnUsagePanel', () => {
|
|||
fireEvent.pointerDown(document.body)
|
||||
expect(view.queryByRole('dialog')).toBeNull()
|
||||
})
|
||||
|
||||
it('TEMPORARY flat variant: the whole meta line triggers a dialog with usage and time sections', () => {
|
||||
const usage: TurnTokenUsage = {
|
||||
uncachedInputTokens: 5_060,
|
||||
cacheReadTokens: 4_940,
|
||||
outputTokens: 5_800,
|
||||
totalTokens: 15_800,
|
||||
}
|
||||
const view = render(
|
||||
<TurnUsagePanel
|
||||
usage={usage}
|
||||
variant="flat"
|
||||
time={Date.now()}
|
||||
runMs={19_000}
|
||||
tokensPerSecond={20}
|
||||
ttftMs={1_200}
|
||||
t={t}
|
||||
/>,
|
||||
)
|
||||
const trigger = view.getByRole('button')
|
||||
// Separator dots live in spaced spans, so textContent carries bare `·`.
|
||||
expect(trigger.textContent)
|
||||
.toMatch(/^.+·Ran for 19s·Usage 15\.8K tok·Cache hit 49\.4%·20 tok\/s·TTFT 1\.2s$/)
|
||||
expect(trigger.querySelector('svg')).toBeNull()
|
||||
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.firstChild?.textContent).toBe('Turn usage15,800 tok')
|
||||
expect(dialog.textContent).toContain('Turn time and speed')
|
||||
const timeDetails = dialog.querySelector('[data-turn-time-details]') as HTMLElement
|
||||
expect(timeDetails.textContent).toContain('Total run time19s')
|
||||
expect(timeDetails.textContent).toContain('Tokens per second (TPS)20 tok/s')
|
||||
expect(timeDetails.textContent).toContain('Average time to first token (TTFT)1.2s')
|
||||
})
|
||||
|
||||
it('TEMPORARY flat variant: absent time facts leave no Turn-time section', () => {
|
||||
const usage: TurnTokenUsage = {
|
||||
uncachedInputTokens: 120,
|
||||
outputTokens: 30,
|
||||
totalTokens: 150,
|
||||
}
|
||||
const view = render(<TurnUsagePanel usage={usage} variant="flat" t={t} />)
|
||||
const trigger = view.getByRole('button')
|
||||
expect(trigger.textContent).toBe('Usage 150 tok')
|
||||
fireEvent.click(trigger)
|
||||
const dialog = view.getByRole('dialog')
|
||||
expect(dialog.textContent).not.toContain('Turn time')
|
||||
expect(dialog.querySelector('[data-turn-time-details]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('TurnTimePanel', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue