|
|
@@ -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,
|
|
|
)}
|