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

- Allow named API keys to restrict routing to specific provider regions while keeping legacy family grants compatible, and show the same region-aware scope in the model catalog

### 中文

- 支持将命名 API Key 的路由限制到具体供应商区域,同时兼容旧的供应商族授权,并让模型目录使用相同的区域权限过滤

## 0.5.0 - 2026-09-15

### English
Expand Down
42 changes: 19 additions & 23 deletions frontend/src/components/AddAccountModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BrandMark } from '@/components/BrandMark'
import { ProviderMark } from '@/components/ProviderMark'
import { CompactSwitch } from '@/components/ui/CompactSwitch'
import { FilterToggle } from '@/components/ui/FilterToggle'
import { FormRow } from '@/components/ui/FormRow'
import { OptionTiles } from '@/components/ui/OptionTiles'
import { useI18n } from '@/hooks/useI18n'
import {
Expand Down Expand Up @@ -514,17 +515,16 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
{typesReady && hint ? <p className="min-h-5 text-xs leading-5 text-muted">{hint}</p> : null}
</section>

<section className="mt-5 space-y-2.5">
<div className="space-y-1.5">
<Label className="text-sm font-medium text-muted">{t('accountName')}</Label>
<section className="mt-5 space-y-3">
<FormRow label={t('accountName')}>
<Input
value={name}
onChange={(event) => setName(event.target.value)}
placeholder={t('wizardNamePh')}
aria-label={t('accountName')}
disabled={settingsLocked}
/>
</div>
</FormRow>
<button
type="button"
onClick={() => setAdvancedOpen((open) => !open)}
Expand All @@ -535,8 +535,8 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
{t('wizardAdvanced')}
</button>
{advancedOpen ? (
<div className="space-y-5 rounded-lg border border-separator px-3.5 py-3">
<div className="grid gap-5 sm:grid-cols-2">
<div className="space-y-3 rounded-lg border border-separator px-3.5 py-3.5">
<FormRow label={t('maxInflight')} hint={t('maxInflightHint')}>
<NumberField
value={maxInFlight}
onChange={(value) => setMaxInFlight(value ?? 4)}
Expand All @@ -545,14 +545,14 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
isDisabled={settingsLocked}
isRequired
>
<Label className="text-sm font-medium text-muted">{t('maxInflight')}</Label>
<NumberField.Group>
<NumberField.DecrementButton />
<NumberField.Input />
<NumberField.Input aria-label={t('maxInflight')} />
<NumberField.IncrementButton />
</NumberField.Group>
<Description className="text-xs leading-5 text-muted">{t('maxInflightHint')}</Description>
</NumberField>
</FormRow>
<FormRow label={t('priority')} hint={t('priorityHint')}>
<NumberField
value={priority}
onChange={(value) => setPriority(value ?? 50)}
Expand All @@ -561,26 +561,22 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
isDisabled={settingsLocked}
isRequired
>
<Label className="text-sm font-medium text-muted">{t('priority')}</Label>
<NumberField.Group>
<NumberField.DecrementButton />
<NumberField.Input />
<NumberField.Input aria-label={t('priority')} />
<NumberField.IncrementButton />
</NumberField.Group>
<Description className="text-xs leading-5 text-muted">{t('priorityHint')}</Description>
</NumberField>
</div>
<label className="block space-y-1.5">
<span className="text-xs font-medium text-muted">{t('proxyUrl')}</span>
</FormRow>
<FormRow label={t('proxyUrl')} hint={t('proxyUrlHint')}>
<Input
value={proxyUrl}
onChange={(event) => setProxyUrl(event.target.value)}
placeholder={t('proxyUrlPlaceholder')}
aria-label={t('proxyUrl')}
disabled={settingsLocked}
/>
<p className="text-[11px] leading-4 text-muted">{t('proxyUrlHint')}</p>
</label>
</FormRow>
{showDropSystem ? (
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
Expand All @@ -596,7 +592,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
</div>
) : null}
{showAutoCheckin ? (
<div className="space-y-5">
<div className="space-y-3 border-t border-separator pt-3">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-muted">{t('autoCheckin')}</div>
Expand All @@ -610,17 +606,15 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
/>
</div>
{autoCheckin ? (
<div className="space-y-1.5">
<Label className="text-sm font-medium text-muted">{t('autoCheckinTime')}</Label>
<FormRow label={t('autoCheckinTime')} hint={t('autoCheckinTimeHint')}>
<Input
type="time"
value={autoCheckinTime}
onChange={(event) => setAutoCheckinTime(event.target.value || defaultCheckinTime)}
aria-label={t('autoCheckinTime')}
disabled={settingsLocked}
/>
<Description className="text-xs leading-5 text-muted">{t('autoCheckinTimeHint')}</Description>
</div>
</FormRow>
) : null}
</div>
) : null}
Expand Down Expand Up @@ -706,7 +700,9 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
</>
) : tab === 'pat' ? (
<>
<Input type="password" value={pat} onChange={(event) => setPat(event.target.value)} placeholder={t('wizardPatPh')} aria-label={t('wizardPatPh')} disabled={busy} />
<FormRow label={t('tabPat')}>
<Input type="password" value={pat} onChange={(event) => setPat(event.target.value)} placeholder={t('wizardPatPh')} aria-label={t('wizardPatPh')} disabled={busy} />
</FormRow>
{message ? (
<p className="flex items-center gap-2 rounded-lg border border-separator bg-surface-secondary px-3 py-2 text-xs">{isDone ? <CheckCircle size={14} className="shrink-0 text-success" /> : null}<span className={isDone ? 'font-medium text-foreground' : 'text-muted'}>{message}</span></p>
) : null}
Expand Down
36 changes: 15 additions & 21 deletions frontend/src/components/account/EditAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState } from 'react'
import { Alert, Button, Chip, Description, Form, Input, Label, Modal, NumberField } from '@heroui/react'
import { Alert, Button, Chip, Form, Input, Modal, NumberField } from '@heroui/react'
import { X } from '@phosphor-icons/react'
import { ProviderMark } from '@/components/ProviderMark'
import { FormRow } from '@/components/ui/FormRow'
import type { AccountRow } from '@/lib/account'
import { accountProviderLabel } from '@/lib/provider'

Expand Down Expand Up @@ -90,9 +91,8 @@ export function EditAccountModal({ account, busy, t, onClose, onSave }: Props) {
</Alert.Content>
</Alert>
) : null}
<Form className="space-y-5" onSubmit={(event) => void submit(event)}>
<div className="space-y-1.5">
<Label className="text-sm font-medium text-muted">{t('accountName')}</Label>
<Form className="space-y-4" onSubmit={(event) => void submit(event)}>
<FormRow label={t('accountName')}>
<Input
value={name}
onChange={(event) => setName(event.target.value)}
Expand All @@ -101,8 +101,8 @@ export function EditAccountModal({ account, busy, t, onClose, onSave }: Props) {
disabled={busy}
autoFocus
/>
</div>
<div className="grid gap-5 sm:grid-cols-2">
</FormRow>
<FormRow label={t('maxInflight')} hint={t('maxInflightHint')}>
<NumberField
value={maxInFlight}
onChange={(value) => setMaxInFlight(value ?? 4)}
Expand All @@ -111,14 +111,14 @@ export function EditAccountModal({ account, busy, t, onClose, onSave }: Props) {
isDisabled={busy}
isRequired
>
<Label className="text-sm font-medium text-muted">{t('maxInflight')}</Label>
<NumberField.Group>
<NumberField.DecrementButton />
<NumberField.Input />
<NumberField.Input aria-label={t('maxInflight')} />
<NumberField.IncrementButton />
</NumberField.Group>
<Description className="text-xs leading-5 text-muted">{t('maxInflightHint')}</Description>
</NumberField>
</FormRow>
<FormRow label={t('priority')} hint={t('priorityHint')}>
<NumberField
value={priority}
onChange={(value) => setPriority(value ?? 50)}
Expand All @@ -127,38 +127,32 @@ export function EditAccountModal({ account, busy, t, onClose, onSave }: Props) {
isDisabled={busy}
isRequired
>
<Label className="text-sm font-medium text-muted">{t('priority')}</Label>
<NumberField.Group>
<NumberField.DecrementButton />
<NumberField.Input />
<NumberField.Input aria-label={t('priority')} />
<NumberField.IncrementButton />
</NumberField.Group>
<Description className="text-xs leading-5 text-muted">{t('priorityHint')}</Description>
</NumberField>
</div>
<div className="space-y-1.5">
<Label className="text-sm font-medium text-muted">{t('proxyUrl')}</Label>
</FormRow>
<FormRow label={t('proxyUrl')} hint={t('proxyUrlHint')}>
<Input
value={proxyUrl}
onChange={(event) => setProxyUrl(event.target.value)}
placeholder={t('proxyUrlPlaceholder')}
aria-label={t('proxyUrl')}
disabled={busy}
/>
<Description className="text-xs leading-5 text-muted">{t('proxyUrlHint')}</Description>
</div>
</FormRow>
{showCheckinTime ? (
<div className="space-y-1.5">
<Label className="text-sm font-medium text-muted">{t('autoCheckinTime')}</Label>
<FormRow label={t('autoCheckinTime')} hint={t('autoCheckinTimeHint')}>
<Input
type="time"
value={autoCheckinTime}
onChange={(event) => setAutoCheckinTime(event.target.value || '09:00')}
aria-label={t('autoCheckinTime')}
disabled={busy}
/>
<Description className="text-xs leading-5 text-muted">{t('autoCheckinTimeHint')}</Description>
</div>
</FormRow>
) : null}
</Form>
</Modal.Body>
Expand Down
31 changes: 31 additions & 0 deletions frontend/src/components/ui/FormRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ReactNode } from 'react'
import { Label } from '@heroui/react'

type Props = {
label: string
/** Optional helper text rendered under the control, aligned with the control column. */
hint?: ReactNode
/** Label column width. Defaults to 7rem, wide enough for CJK labels + trailing colon. */
labelWidthClass?: string
htmlFor?: string
children: ReactNode
}

const defaultLabelWidthClass = 'w-28'

/**
* One label + one control per row: label fixed on the left, control fills
* the rest. Keeps every form in the console (account create/edit, system
* settings, key create) on the same alignment grid.
*/
export function FormRow({ label, hint, labelWidthClass = defaultLabelWidthClass, htmlFor, children }: Props) {
return (
<div>
<div className="flex items-center gap-3">
<Label htmlFor={htmlFor} className={`${labelWidthClass} shrink-0 text-sm font-medium text-foreground`}>{label}</Label>
<div className="min-w-0 flex-1">{children}</div>
</div>
{hint ? <p className="mt-1.5 pl-[7.75rem] text-xs leading-5 text-muted">{hint}</p> : null}
</div>
)
}
10 changes: 8 additions & 2 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,11 @@ export const messages: Record<Lang, Dict> = {
keysNamePh: 'CI bot',
keysNameRequired: 'Name is required.',
keysProviders: 'Providers',
keysProvidersHint: 'Leave all unchecked to allow every provider.',
keysProvidersHint: 'Restrict this key to specific provider regions. Leave all unchecked to allow every provider.',
keysAllProviders: 'All providers',
keysAllRegions: 'All regions',
keysProvidersNotLoaded: 'Provider list is still loading. Saving is disabled so the key permissions stay unchanged.',
keysProvidersLoadFailed: 'Failed to load provider list: {message}',
keysLastUsed: 'Last used {time}',
keysNeverUsed: 'Not used yet',
keysSecretTitle: 'Store this secret now',
Expand Down Expand Up @@ -1220,8 +1223,11 @@ export const messages: Record<Lang, Dict> = {
keysNamePh: 'CI 机器人',
keysNameRequired: '请填写名称。',
keysProviders: '供应商',
keysProvidersHint: '全部不勾选表示允许所有供应商。',
keysProvidersHint: '可将 Key 限制到特定供应商区域(如国内版 / 国际版)。全部不勾选表示允许所有供应商。',
keysAllProviders: '全部供应商',
keysAllRegions: '全区域',
keysProvidersNotLoaded: '供应商列表仍在加载中。已禁用保存,以保证 Key 权限不会被意外更改。',
keysProvidersLoadFailed: '供应商列表加载失败:{message}',
keysLastUsed: '最近使用 {time}',
keysNeverUsed: '尚未使用',
keysSecretTitle: '请立刻保存这把密钥',
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,32 @@
--font-mono: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace;
}

/* Softer field corners + visible borders: HeroUI inputs / textareas /
number-field groups / select triggers all read these vars. The stock
theme ships --field-border-width:0px and --field-border:transparent,
relying on a barely-visible shadow instead — fields look flat on both
light and dark surfaces, so draw a real border. */
:root,
.light,
.default,
[data-theme="light"],
[data-theme="default"] {
--field-radius: 16px;
--field-border-width: 1px;
--field-border: oklch(80% 0.008 286.32);
--field-border-hover: oklch(70% 0.012 285.9);
--field-border-focus: oklch(62% 0.04 285.9);
}

.dark,
[data-theme="dark"] {
--field-radius: 16px;
--field-border-width: 1px;
--field-border: oklch(38% 0.008 286.033);
--field-border-hover: oklch(47% 0.012 285.9);
--field-border-focus: oklch(58% 0.04 285.9);
}

/* Chrome accent is Charcoal Ink. Signal Cyan stays on the C tip / scan only.
Dark inverts the fill so an ink button does not vanish on Off-Black Night. */
:root,
Expand Down
Loading
Loading