fix(web): localize the context panel headline order and drop empty bar parts

The panel header concatenated a `45%` span with a `context.used` fragment, so
Chinese rendered "45% 上下文已用" against the ring's own "上下文已用 45%". The
header now renders the one localized `context.aria` sentence split around its
`{percent}` slot: each locale owns the reading's position while the reading
keeps its primary tone, and the side a locale leaves empty collapses through
`.headline:empty` instead of spending a header gap.

The bar mapped every composition row to a segment unconditionally, and
`.segment`'s 2px min-width kept each one visible, so a 0% occupancy panel
painted an ~8px filled bar over an empty context. Parts are now computed with
their widths and zero-width parts are filtered out, which also collapses the
plain and segmented branches into one map.
This commit is contained in:
Yichen Jiang
2026-08-05 15:59:24 +08:00
parent d2aafc2a33
commit 46562b2c30
7 changed files with 79 additions and 27 deletions

View File

@@ -5,15 +5,16 @@
import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, fireEvent, render } from '@testing-library/react'
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
import { en as commonEn, zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/index.ts'
import { ContextMeter, type ContextMeterProps } from '../src/client/skeleton/ContextMeter.tsx'
import css from '../src/client/skeleton/ContextMeter.module.css'
import { zh } from '../src/client/locales.ts'
import { en, zh } from '../src/client/locales.ts'
afterEach(cleanup)
// Mirrors the real lookup chain (conversation namespace, then common).
const t = makeTranslate(zh, commonZh) as ContextMeterProps['t']
const tEn = makeTranslate(en, commonEn) as ContextMeterProps['t']
const BREAKDOWN = { systemTokens: 120, toolsTokens: 21_500, messageTokens: 477_000 }
@@ -25,8 +26,8 @@ function projections(values: Record<string, unknown>): ContextMeterProps['usePro
return (key: string) => values[key]
}
function meter(values: Record<string, unknown>) {
return render(<ContextMeter useProjection={projections(values)} t={t} />)
function meter(values: Record<string, unknown>, translate: ContextMeterProps['t'] = t) {
return render(<ContextMeter useProjection={projections(values)} t={translate} />)
}
describe('ContextMeter', () => {
@@ -58,6 +59,36 @@ describe('ContextMeter', () => {
expect(view.container.querySelector('[role="dialog"]')).toBeNull()
})
it('lets each locale own the headline word order around the reading', () => {
const values = {
contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 },
contextBreakdown: BREAKDOWN,
}
const zhView = meter(values)
fireEvent.click(zhView.getByRole('button', { name: '上下文已用 25%' }))
// The reading follows the label in Chinese and leads it in English; both
// headers read as one sentence rather than a concatenated fragment.
expect(zhView.container.querySelector('[role="dialog"]')!.textContent)
.toMatch(/^上下文已用25%/)
const enView = meter(values, tEn)
fireEvent.click(enView.getByRole('button', { name: '25% of context used' }))
expect(enView.container.querySelector('[role="dialog"]')!.textContent)
.toMatch(/^25%of context used/)
})
it('draws no bar segment at zero occupancy', () => {
const view = meter({
contextPressure: { pressureTokens: 0, contextWindow: 128_000 },
contextBreakdown: BREAKDOWN,
})
fireEvent.click(view.getByRole('button', { name: '上下文已用 0%' }))
const panel = view.container.querySelector('[role="dialog"]')!
// `.segment` carries a min-width, so a zero-width part would still paint a
// filled sliver over an empty context.
expect(panel.getElementsByClassName(segmentClass)).toHaveLength(0)
expect(panel.textContent).toContain('~0 / 128K')
})
it('omits the composition rows while the contextBreakdown projection is absent', () => {
const view = meter({ contextPressure: { pressureTokens: 32_000, contextWindow: 128_000 } })
fireEvent.click(view.getByRole('button', { name: '上下文已用 25%' }))