Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`.

### English

- Replace native Logs and Models filter dropdowns with compact HeroUI Select controls that match the 32px toolbar

### 中文

- Logs 和模型页的筛选下拉改用紧凑 HeroUI Select,高度对齐 32px 工具栏

## 0.2.20 - 2026-08-29

### English
Expand Down
61 changes: 61 additions & 0 deletions frontend/src/components/ui/FilterSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Label, ListBox, Select } from '@heroui/react'

const ALL_VALUE = '__all__'

export type FilterSelectOption = {
id: string
label: string
}

export function FilterSelect({
value,
onChange,
ariaLabel,
options,
className,
}: {
value: string
onChange: (value: string) => void
ariaLabel: string
options: FilterSelectOption[]
className?: string
}) {
const items = options.map((option) => ({
id: option.id || ALL_VALUE,
label: option.label,
}))
const selectedId = value || ALL_VALUE
const selected = items.find((item) => item.id === selectedId)

return (
<Select
className={['w-fit', className].filter(Boolean).join(' ')}
aria-label={ariaLabel}
value={selectedId}
onChange={(next) => {
if (typeof next !== 'string') return
onChange(next === ALL_VALUE ? '' : next)
}}
>
<Select.Trigger className="h-8 min-h-8 min-w-36 items-center">
<Select.Value className="min-w-0 truncate">
{({ defaultChildren, isPlaceholder }) => {
if (isPlaceholder || !selected) return defaultChildren
return <span className="truncate">{selected.label}</span>
}}
</Select.Value>
<Select.Indicator />
</Select.Trigger>
<Select.Popover className="max-h-72 min-w-36 rounded-lg">
<ListBox>
{items.map((option) => (
<ListBox.Item key={option.id} id={option.id} textValue={option.label} className="rounded-lg">
<Label className="block truncate">{option.label}</Label>
<ListBox.ItemIndicator />
</ListBox.Item>
))}
</ListBox>
</Select.Popover>
</Select>
)
}
87 changes: 40 additions & 47 deletions frontend/src/pages/LogsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
type RequestLog,
type RuntimeLogEntry,
} from '@/api/logs'
import { FilterSelect } from '@/components/ui/FilterSelect'
import { ListPager, type PageSize } from '@/components/ui/ListPager'
import { LogsRequestListSkeleton, LogsRuntimeListSkeleton } from '@/components/ui/PageSkeletons'
import { useI18n } from '@/hooks/useI18n'
Expand All @@ -47,8 +48,6 @@ type StreamFilter = 'all' | 'stream' | 'sync'
type TimeRange = 'all' | '1h' | '24h' | '7d' | 'custom'
type ErrorKindFilter = 'all' | 'quota' | 'rate_limit' | 'auth' | 'not_ready' | 'unavailable' | 'invalid_request' | 'model_not_available'

const FILTER_SELECT_CLASS = 'h-8 min-w-36 rounded-lg border border-[var(--app-line-strong)] bg-[var(--app-surface-solid)] px-2.5 text-xs text-[var(--app-ink)]'

type DateRangeValue = { start: DateValue; end: DateValue }

function statusColor(status?: string): 'success' | 'warning' | 'danger' | 'default' {
Expand Down Expand Up @@ -407,28 +406,24 @@ export function LogsPage() {
</div>

<div className="flex flex-wrap items-center gap-2">
<select
className={FILTER_SELECT_CLASS}
<FilterSelect
ariaLabel={t('logsColAccount')}
value={accountFilter}
onChange={(event) => setAccountFilter(event.target.value)}
aria-label={t('logsColAccount')}
>
<option value="">{t('logsFilterAccountAll')}</option>
{(accounts || []).map((account) => (
<option key={account.id} value={account.id}>{account.name || account.id}</option>
))}
</select>
<select
className={FILTER_SELECT_CLASS}
onChange={setAccountFilter}
options={[
{ id: '', label: t('logsFilterAccountAll') },
...(accounts || []).map((account) => ({ id: account.id, label: account.name || account.id })),
]}
/>
<FilterSelect
ariaLabel={t('logsColModel')}
value={modelFilter}
onChange={(event) => setModelFilter(event.target.value)}
aria-label={t('logsColModel')}
>
<option value="">{t('logsFilterModelAll')}</option>
{(models || []).map((model) => (
<option key={model.id} value={model.id}>{model.display_name || model.id}</option>
))}
</select>
onChange={setModelFilter}
options={[
{ id: '', label: t('logsFilterModelAll') },
...(models || []).map((model) => ({ id: model.id, label: model.display_name || model.id })),
]}
/>
<ButtonGroup className="toolbar-group">
{([
['all', t('logsFilterStreamAll')],
Expand All @@ -445,21 +440,21 @@ export function LogsPage() {
</Button>
))}
</ButtonGroup>
<select
className={FILTER_SELECT_CLASS}
value={errorKind}
onChange={(event) => setErrorKind(event.target.value as ErrorKindFilter)}
aria-label={t('errorKind')}
>
<option value="all">{t('logsFilterKindAll')}</option>
<option value="quota">{t('logsKindQuota')}</option>
<option value="rate_limit">{t('logsKindRateLimit')}</option>
<option value="auth">{t('logsKindAuth')}</option>
<option value="not_ready">{t('logsKindNotReady')}</option>
<option value="unavailable">{t('logsKindUnavailable')}</option>
<option value="invalid_request">{t('logsKindInvalidRequest')}</option>
<option value="model_not_available">{t('logsKindModelNotAvailable')}</option>
</select>
<FilterSelect
ariaLabel={t('errorKind')}
value={errorKind === 'all' ? '' : errorKind}
onChange={(next) => setErrorKind((next || 'all') as ErrorKindFilter)}
options={[
{ id: '', label: t('logsFilterKindAll') },
{ id: 'quota', label: t('logsKindQuota') },
{ id: 'rate_limit', label: t('logsKindRateLimit') },
{ id: 'auth', label: t('logsKindAuth') },
{ id: 'not_ready', label: t('logsKindNotReady') },
{ id: 'unavailable', label: t('logsKindUnavailable') },
{ id: 'invalid_request', label: t('logsKindInvalidRequest') },
{ id: 'model_not_available', label: t('logsKindModelNotAvailable') },
]}
/>
</div>

<div className="flex flex-wrap items-center gap-2">
Expand Down Expand Up @@ -680,17 +675,15 @@ export function LogsPage() {
placeholder={t('logsSearchRuntime')}
aria-label={t('logsSearchRuntime')}
/>
<select
className={FILTER_SELECT_CLASS}
<FilterSelect
ariaLabel={t('logsColAccount')}
value={runtimeAccount}
onChange={(event) => setRuntimeAccount(event.target.value)}
aria-label={t('logsColAccount')}
>
<option value="">{t('logsFilterAccountAll')}</option>
{(accounts || []).map((account) => (
<option key={account.id} value={account.id}>{account.name || account.id}</option>
))}
</select>
onChange={setRuntimeAccount}
options={[
{ id: '', label: t('logsFilterAccountAll') },
...(accounts || []).map((account) => ({ id: account.id, label: account.name || account.id })),
]}
/>
<ButtonGroup className="toolbar-group">
{([
['all', t('logsLevelAll')],
Expand Down
21 changes: 9 additions & 12 deletions frontend/src/pages/ProvidersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import type { Overview } from '@/api/types'
import { ProviderMark } from '@/components/ProviderMark'
import { ModelDetailsModal, formatTokens } from '@/components/ModelDetailsModal'
import { CompactSwitch } from '@/components/ui/CompactSwitch'
import { FilterSelect } from '@/components/ui/FilterSelect'
import { ListPager, type PageSize } from '@/components/ui/ListPager'
import { ProvidersPageSkeleton } from '@/components/ui/PageSkeletons'

type ModelInfo = NonNullable<Overview['models']>[number]

const FILTER_SELECT_CLASS = 'h-8 min-w-36 rounded-lg border border-[var(--app-line-strong)] bg-[var(--app-surface-solid)] px-2.5 text-xs text-[var(--app-ink)]'

function modelSettingsKey(model: ModelInfo) {
return model.settings_key || model.id
}
Expand Down Expand Up @@ -205,17 +204,15 @@ export function ProvidersPage() {
</div>
<div data-gsap-reveal className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Input className="sm:w-72" value={filter} onChange={(event) => setFilter(event.target.value)} placeholder={t('filterPh')} aria-label={t('filter')} />
<select
className={FILTER_SELECT_CLASS}
<FilterSelect
ariaLabel={t('providerCol')}
value={providerFilter}
onChange={(event) => setProviderFilter(event.target.value)}
aria-label={t('providerCol')}
>
<option value="">{t('providerFilterAll')}</option>
{providers.map((provider) => (
<option key={provider} value={provider}>{provider}</option>
))}
</select>
onChange={setProviderFilter}
options={[
{ id: '', label: t('providerFilterAll') },
...providers.map((provider) => ({ id: provider, label: provider })),
]}
/>
<Button size="sm" variant="secondary" isPending={busy} onPress={() => void onRefresh()}>
<ArrowClockwise size={14} />
{busy ? t('refreshing') : t('refresh')}
Expand Down
28 changes: 0 additions & 28 deletions internal/webui/static/assets/index-B-pUxSHO.js

This file was deleted.

28 changes: 28 additions & 0 deletions internal/webui/static/assets/index-CLmJt0AE.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions internal/webui/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-B-pUxSHO.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DaeNix8f.css">
<script type="module" crossorigin src="/assets/index-CLmJt0AE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DWhSA4d5.css">
</head>
<body>
<div id="root"></div>
Expand Down
Loading