|
|
@@ -17,6 +17,13 @@ import css from './ContextMeter.module.css'
|
|
|
const RADIUS = 5.5
|
|
|
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
|
|
|
|
|
|
+/**
|
|
|
+ * Marker the localized occupancy sentence is split on, so the panel headline
|
|
|
+ * keeps the reading in its own tone while each locale still owns the word
|
|
|
+ * order (`45% of context used` / `上下文已用 45%`).
|
|
|
+ */
|
|
|
+const READING_SLOT = '\u0000'
|
|
|
+
|
|
|
/** Panel legend rows, in bar-segment order; each color class carries the shared swatch/segment tint. */
|
|
|
const ROWS = [
|
|
|
{ key: 'systemTokens', label: 'context.system', color: css.colorSystem },
|
|
|
@@ -57,23 +64,30 @@ export function ContextMeter({ useProjection, t }: ContextMeterProps) {
|
|
|
const context = contextOccupancy(pressure)
|
|
|
if (context === null) return null
|
|
|
const percent = context.percent
|
|
|
+ const reading = `${percent}%`
|
|
|
+ const [headBefore = '', headAfter = ''] = t('context.aria', { percent: READING_SLOT })
|
|
|
+ .split(READING_SLOT)
|
|
|
+ .map(part => part.trim())
|
|
|
|
|
|
// The bar's overall length stays the provider-exact percent; the heuristic
|
|
|
- // breakdown only proportions its colored segments.
|
|
|
+ // breakdown only proportions its colored parts. A zero-width part is dropped
|
|
|
+ // instead of rendered: `.segment`'s min-width keeps a hairline part visible,
|
|
|
+ // which at 0% occupancy would draw a filled bar over an empty context.
|
|
|
const breakdownTotal = breakdown === undefined
|
|
|
? 0
|
|
|
: breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens
|
|
|
- const segments = breakdown === undefined || breakdownTotal === 0
|
|
|
- ? null
|
|
|
- : ROWS.map(row => ({ key: row.key, color: row.color, share: breakdown[row.key] / breakdownTotal }))
|
|
|
+ const parts = breakdown === undefined || breakdownTotal === 0
|
|
|
+ ? [{ key: 'total', color: undefined, width: percent }]
|
|
|
+ : ROWS.map(row => ({ key: row.key, color: row.color, width: percent * breakdown[row.key] / breakdownTotal }))
|
|
|
+ const segments = parts.filter(part => part.width > 0)
|
|
|
|
|
|
return (
|
|
|
<span ref={rootRef} className={css.root}>
|
|
|
- <Tooltip label={t('context.aria', { percent })} side="top" delayMs={200} disabled={open}>
|
|
|
+ <Tooltip label={t('context.aria', { percent: reading })} side="top" delayMs={200} disabled={open}>
|
|
|
<button
|
|
|
type="button"
|
|
|
className={css.trigger}
|
|
|
- aria-label={t('context.aria', { percent })}
|
|
|
+ aria-label={t('context.aria', { percent: reading })}
|
|
|
aria-haspopup="dialog"
|
|
|
aria-expanded={open}
|
|
|
onClick={() => { setOpen(!open) }}
|
|
|
@@ -94,22 +108,23 @@ export function ContextMeter({ useProjection, t }: ContextMeterProps) {
|
|
|
{open && (
|
|
|
<div className={css.panel} role="dialog" aria-label={t('context.used')}>
|
|
|
<div className={css.header}>
|
|
|
- <span className={css.percent}>{`${percent}%`}</span>
|
|
|
- <span className={css.headline}>{t('context.used')}</span>
|
|
|
+ {/* Empty sides collapse through `.headline:empty` so the locale that
|
|
|
+ needs no leading (or trailing) text spends no header gap. */}
|
|
|
+ <span className={css.headline}>{headBefore}</span>
|
|
|
+ <span className={css.percent}>{reading}</span>
|
|
|
+ <span className={css.headline}>{headAfter}</span>
|
|
|
<span className={css.figures}>
|
|
|
{`~${formatTokens(context.pressureTokens)} / ${formatTokens(context.contextWindow)}`}
|
|
|
</span>
|
|
|
</div>
|
|
|
<div className={css.bar}>
|
|
|
- {segments === null
|
|
|
- ? <div className={css.segment} style={{ width: `${percent}%` }} />
|
|
|
- : segments.map(segment => (
|
|
|
- <div
|
|
|
- key={segment.key}
|
|
|
- className={`${css.segment} ${segment.color}`}
|
|
|
- style={{ width: `${percent * segment.share}%` }}
|
|
|
- />
|
|
|
- ))}
|
|
|
+ {segments.map(segment => (
|
|
|
+ <div
|
|
|
+ key={segment.key}
|
|
|
+ className={segment.color === undefined ? css.segment : `${css.segment} ${segment.color}`}
|
|
|
+ style={{ width: `${segment.width}%` }}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
</div>
|
|
|
{breakdown !== undefined && (
|
|
|
<dl className={css.rows}>
|