Files
hanzo.ai/components/benchmarks/controls.tsx
zeekay 1c0dd3195b benchmarks: the words go through @hanzo/i18n
The index, its boards and the shelf's pages wrap their prose: server
pages with <T>, the boards and the library with t(), and copy held in
data with phrase(). Figures read from the runs are values, and the
fields lib/kai, lib/tools, lib/coding and lib/tally hand a board are
drawn through t(), so they translate once those files emit keys. The
shared parts (Chapter, Fold, Stat, Tile, Panel, Mark) translate a
string where they draw it and take a node from a server page.

figures.spec lets the index import @hanzo/i18n: words in, words out,
never a figure.
2026-10-22 18:13:04 -07:00

182 lines
6.5 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState, type ReactNode } from 'react'
import { ArrowDown, ArrowUp, Download, Equal, Link2, Minus } from '@hanzogui/lucide-icons-2'
import { Text, XStack } from '@hanzo/ui'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
import { BEHIND, OURS, THEM } from './parts'
/**
* The controls the /benchmarks board and library share: the inks, state kept in
* the URL, segmented choices, the action buttons, the toolbar, and export.
*
* Every control writes its state to the URL (replaceState, no reload), so a view
* can be shared exactly; a page opened with that query starts in that view. Each
* component owns its own keys, so two never write the same one.
*/
export { BEHIND, OURS, THEM }
export const INK = 'var(--text-primary)'
export const SOFT = 'var(--text-secondary)'
export const DIM = 'var(--muted-foreground)'
export const LINE = 'var(--border)'
export const TRACK = 'var(--edge)'
export const MONO = '$mono'
export const FIGURES = { fontVariantNumeric: 'tabular-nums' } as const
/* ---------------- state in the URL ---------------- */
export function useParam<T extends string>(key: string, fallback: T, allowed?: readonly T[]): [T, (v: T) => void] {
const [value, setValue] = useState<T>(fallback)
useEffect(() => {
const q = new URLSearchParams(window.location.search).get(key)
if (q != null && (!allowed || (allowed as readonly string[]).includes(q))) setValue(q as T)
// Read once, on mount: the URL is the starting view, the controls own it after.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const set = useCallback(
(v: T) => {
setValue(v)
const u = new URL(window.location.href)
if (v === fallback || v === '') u.searchParams.delete(key)
else u.searchParams.set(key, v)
window.history.replaceState(window.history.state, '', u.toString())
},
[key, fallback],
)
return [value, set]
}
/* ---------------- the controls ---------------- */
export function Seg<T extends string>({ label, value, options, onChange }: { label: string; value: T; options: { id: T; name: string }[]; onChange: (v: T) => void }) {
return (
<XStack role="group" aria-label={label} items="center" gap={6} flexWrap="wrap" maxW="100%">
<Text fontSize="$1" color={DIM} letterSpacing={0.6} textTransform="uppercase" mr={4}>
{label}
</Text>
{options.map((o) => {
const on = o.id === value
return (
<XStack
key={o.id}
render={<button type="button" aria-pressed={on} onClick={() => onChange(o.id)} />}
px={10}
minH={30}
items="center"
rounded={6}
borderWidth={1}
borderColor={on ? 'var(--text-secondary)' : LINE}
bg={on ? 'var(--edge)' : 'transparent'}
cursor="pointer"
focusVisibleStyle={{ outlineColor: INK, outlineWidth: 2, outlineStyle: 'solid' }}
>
<Text fontSize="$2" color={on ? INK : SOFT}>
{o.name}
</Text>
</XStack>
)
})}
</XStack>
)
}
/** An icon from the one set the board uses (gui's lucide), sized and inked like the text beside it. */
type Icon = typeof Download
export function Act({ onClick, icon: Glyph, children }: { onClick: () => void; icon?: Icon; children: ReactNode }) {
return (
<XStack
render={<button type="button" onClick={onClick} />}
px={10}
minH={30}
gap={6}
items="center"
rounded={6}
borderWidth={1}
borderColor={LINE}
bg="transparent"
cursor="pointer"
focusVisibleStyle={{ outlineColor: INK, outlineWidth: 2, outlineStyle: 'solid' }}
>
{Glyph ? <Glyph size={14} color={SOFT} shrink={0} aria-hidden /> : null}
<Text fontSize="$2" color={SOFT}>
{children}
</Text>
</XStack>
)
}
/** How a result stands, as a glyph and a word in the page's ink: no colour carries it. */
const STANDS = { win: [ArrowUp, phrase('Win')], level: [Equal, phrase('Level')], loss: [ArrowDown, phrase('Loss')], unscored: [Minus, phrase('Unscored')] } as const
export function Mark({ s, word }: { s: keyof typeof STANDS; word?: string }) {
const { t } = useI18n()
const [Glyph, name] = STANDS[s]
// A caller's word is authored English too (a server page cannot translate it), so it is drawn through t.
const said = t(word ?? name)
return (
<XStack items="center" gap={4} aria-label={said}>
<Glyph size={14} color={s === 'unscored' ? THEM : INK} shrink={0} aria-hidden />
<Text fontSize="$2" fontWeight={s === 'loss' ? '600' : '500'} color={s === 'unscored' ? THEM : INK}>
{said}
</Text>
</XStack>
)
}
export function Bar({ children }: { children: ReactNode }) {
return (
<XStack flexWrap="wrap" items="center" gap={16} rowGap={10} py={12} borderTopWidth={1} borderBottomWidth={1} borderColor={LINE} my={16}>
{children}
</XStack>
)
}
/* ---------------- export ---------------- */
const csvCell = (v: unknown) => {
const s = v == null ? '' : String(v)
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
}
function save(name: string, type: string, body: string) {
const url = URL.createObjectURL(new Blob([body], { type }))
const a = document.createElement('a')
a.href = url
a.download = name
document.body.appendChild(a)
a.click()
a.remove()
URL.revokeObjectURL(url)
}
export function exportRows(name: string, rows: Record<string, unknown>[], as: 'csv' | 'json') {
if (as === 'json') return save(`${name}.json`, 'application/json', JSON.stringify(rows, null, 2))
const cols = Object.keys(rows[0] ?? {})
save(`${name}.csv`, 'text/csv', [cols.join(','), ...rows.map((r) => cols.map((c) => csvCell(r[c])).join(','))].join('\n'))
}
export function Exports({ name, rows }: { name: string; rows: () => Record<string, unknown>[] }) {
const { t } = useI18n()
const [copied, setCopied] = useState(false)
return (
<XStack items="center" gap={6} flexWrap="wrap" ml="auto">
<Act icon={Download} onClick={() => exportRows(name, rows(), 'csv')}>
CSV
</Act>
<Act icon={Download} onClick={() => exportRows(name, rows(), 'json')}>
JSON
</Act>
<Act
icon={Link2}
onClick={() => {
void navigator.clipboard?.writeText(window.location.href).then(() => {
setCopied(true)
setTimeout(() => setCopied(false), 1600)
})
}}
>
{copied ? t('Link copied') : t('Copy link')}
</Act>
</XStack>
)
}