✨ refactor: system settings UI for consistent, compact layouts
Redesign the system settings interface to align with the rest of the console experience by using fixed header actions, removing redundant subtitles, respecting global content width, and standardizing responsive form layouts. Introduce reusable settings layout primitives for forms, switch rows, grouped controls, nested control sections, title status indicators, and page action portals. Replace duplicated card-style switch markup with explicit compact components, improve nested switch readability, and reduce visual noise across authentication, billing, content, integrations, maintenance, models, and request-limit settings. Also complete missing i18n translations, remove obsolete subtitle translation keys, refine i18n sync reporting, fix sidebar truncation for long labels, and verify the frontend with type checking and lint diagnostics.
This commit is contained in:
@@ -22,7 +22,6 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -35,6 +34,13 @@ import {
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
SettingsForm,
|
||||
SettingsSwitchContent,
|
||||
SettingsControlGroup,
|
||||
SettingsSwitchItem,
|
||||
} from '../components/settings-form-layout'
|
||||
import { SettingsPageFormActions } from '../components/settings-page-context'
|
||||
import { SettingsSection } from '../components/settings-section'
|
||||
import { useUpdateOption } from '../hooks/use-update-option'
|
||||
import {
|
||||
@@ -173,15 +179,14 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('Claude')}
|
||||
description={t(
|
||||
'Override Anthropic headers, defaults, and thinking adapter behavior'
|
||||
)}
|
||||
>
|
||||
<SettingsSection title={t('Claude')}>
|
||||
<Form {...form}>
|
||||
{/* eslint-disable-next-line react-hooks/refs */}
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
|
||||
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SettingsPageFormActions
|
||||
onSave={form.handleSubmit(onSubmit)}
|
||||
isSaving={updateOption.isPending}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='claude.model_headers_settings'
|
||||
@@ -219,29 +224,27 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='space-y-4 rounded-lg border p-4'>
|
||||
<SettingsControlGroup>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='claude.thinking_adapter_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Thinking Adapter')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Thinking Adapter')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Translate `-thinking` suffixes into Anthropic native thinking models while keeping pricing predictable.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -267,12 +270,8 @@ export function ClaudeSettingsCard({ defaultValues }: ClaudeSettingsCardProps) {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type='submit' disabled={updateOption.isPending}>
|
||||
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsControlGroup>
|
||||
</SettingsForm>
|
||||
</Form>
|
||||
</SettingsSection>
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@ import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -35,6 +34,13 @@ import {
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
SettingsForm,
|
||||
SettingsSwitchContent,
|
||||
SettingsControlGroup,
|
||||
SettingsSwitchItem,
|
||||
} from '../components/settings-form-layout'
|
||||
import { SettingsPageFormActions } from '../components/settings-page-context'
|
||||
import { SettingsSection } from '../components/settings-section'
|
||||
import { useUpdateOption } from '../hooks/use-update-option'
|
||||
import {
|
||||
@@ -226,14 +232,13 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
|
||||
)
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('Gemini')}
|
||||
description={t(
|
||||
'Configure Gemini safety behavior, version overrides, and thinking adapter'
|
||||
)}
|
||||
>
|
||||
<SettingsSection title={t('Gemini')}>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
|
||||
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SettingsPageFormActions
|
||||
onSave={form.handleSubmit(onSubmit)}
|
||||
isSaving={updateOption.isPending}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='gemini.safety_settings'
|
||||
@@ -295,16 +300,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='space-y-4 rounded-lg border p-4'>
|
||||
<SettingsControlGroup>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='gemini.thinking_adapter_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Thinking Adapter')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Thinking Adapter')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t('Supports `-thinking`, `-thinking-')}
|
||||
{'{{budget}}'}
|
||||
@@ -312,14 +315,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
|
||||
'`, and `-nothinking` suffixes while routing to the correct Gemini variant.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -353,15 +356,15 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</SettingsControlGroup>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='gemini.function_call_thought_signature_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>
|
||||
{t('Enable FunctionCall thoughtSignature Fill')}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -369,14 +372,14 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
|
||||
'Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -384,31 +387,25 @@ export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
|
||||
control={form.control}
|
||||
name='gemini.remove_function_response_id_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Remove functionResponse.id field')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Remove functionResponse.id field')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Vertex AI does not support functionResponse.id. Enable this to remove the field automatically.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type='submit' disabled={updateOption.isPending}>
|
||||
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsForm>
|
||||
</Form>
|
||||
</SettingsSection>
|
||||
)
|
||||
|
||||
@@ -38,6 +38,12 @@ import { Separator } from '@/components/ui/separator'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
SettingsForm,
|
||||
SettingsSwitchContent,
|
||||
SettingsSwitchItem,
|
||||
} from '../components/settings-form-layout'
|
||||
import { SettingsPageFormActions } from '../components/settings-page-context'
|
||||
import { SettingsSection } from '../components/settings-section'
|
||||
import { useUpdateOption } from '../hooks/use-update-option'
|
||||
|
||||
@@ -186,36 +192,33 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('Global Model Configuration')}
|
||||
description={t(
|
||||
'Control passthrough behavior and connection keep-alive settings'
|
||||
)}
|
||||
>
|
||||
<SettingsSection title={t('Global Model Configuration')}>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
|
||||
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SettingsPageFormActions
|
||||
onSave={form.handleSubmit(onSubmit)}
|
||||
isSaving={updateOption.isPending}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='global.pass_through_request_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Enable Request Passthrough')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Enable Request Passthrough')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Forward requests directly to upstream providers without any post-processing.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -349,24 +352,22 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) {
|
||||
control={form.control}
|
||||
name='general_setting.ping_interval_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Keep-alive Ping')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Keep-alive Ping')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Periodically send ping frames to keep streaming connections active.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -402,11 +403,7 @@ export function GlobalSettingsCard({ defaultValues }: GlobalSettingsCardProps) {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type='submit' disabled={updateOption.isPending}>
|
||||
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsForm>
|
||||
</Form>
|
||||
</SettingsSection>
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ import * as z from 'zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -31,6 +30,12 @@ import {
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
SettingsForm,
|
||||
SettingsSwitchContent,
|
||||
SettingsSwitchItem,
|
||||
} from '../components/settings-form-layout'
|
||||
import { SettingsPageFormActions } from '../components/settings-page-context'
|
||||
import { SettingsSection } from '../components/settings-section'
|
||||
import { useResetForm } from '../hooks/use-reset-form'
|
||||
import { useUpdateOption } from '../hooks/use-update-option'
|
||||
@@ -79,24 +84,19 @@ export function GrokSettingsCard(props: Props) {
|
||||
const enabled = form.watch('grok.violation_deduction_enabled')
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
title={t('Grok Settings')}
|
||||
description={t('Configure xAI Grok model specific settings')}
|
||||
>
|
||||
<SettingsSection title={t('Grok Settings')}>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
|
||||
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<SettingsPageFormActions
|
||||
onSave={form.handleSubmit(onSubmit)}
|
||||
isSaving={updateOption.isPending}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='grok.violation_deduction_enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center gap-2'>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Enable violation deduction')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
@@ -111,8 +111,14 @@ export function GrokSettingsCard(props: Props) {
|
||||
{t('Official documentation')}
|
||||
</a>
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -139,11 +145,7 @@ export function GrokSettingsCard(props: Props) {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type='submit' disabled={updateOption.isPending}>
|
||||
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsForm>
|
||||
</Form>
|
||||
</SettingsSection>
|
||||
)
|
||||
|
||||
@@ -45,6 +45,12 @@ import {
|
||||
} from '@/components/ui/sheet'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
SettingsForm,
|
||||
SettingsSwitchContent,
|
||||
SettingsSwitchItem,
|
||||
} from '../components/settings-form-layout'
|
||||
import { SettingsPageActionsPortal } from '../components/settings-page-context'
|
||||
import { GroupRatioVisualEditor } from './group-ratio-visual-editor'
|
||||
import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor'
|
||||
|
||||
@@ -112,6 +118,16 @@ export const GroupRatioForm = memo(function GroupRatioForm({
|
||||
<GroupPricingGuide open={guideOpen} onOpenChange={setGuideOpen} />
|
||||
|
||||
<Form {...form}>
|
||||
<SettingsPageActionsPortal>
|
||||
<Button
|
||||
type='button'
|
||||
size='sm'
|
||||
onClick={form.handleSubmit(onSave)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? t('Saving...') : t('Save group ratios')}
|
||||
</Button>
|
||||
</SettingsPageActionsPortal>
|
||||
{editMode === 'visual' ? (
|
||||
<div className='space-y-6'>
|
||||
<GroupRatioVisualEditor
|
||||
@@ -136,33 +152,27 @@ export const GroupRatioForm = memo(function GroupRatioForm({
|
||||
control={form.control}
|
||||
name='DefaultUseAutoGroup'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Default to auto groups')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Default to auto groups')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'When enabled, newly created tokens start in the first auto group.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button onClick={form.handleSubmit(onSave)} disabled={isSaving}>
|
||||
{isSaving ? t('Saving...') : t('Save group ratios')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={form.handleSubmit(onSave)} className='space-y-6'>
|
||||
<SettingsForm onSubmit={form.handleSubmit(onSave)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='GroupRatio'
|
||||
@@ -284,31 +294,25 @@ export const GroupRatioForm = memo(function GroupRatioForm({
|
||||
control={form.control}
|
||||
name='DefaultUseAutoGroup'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Default to auto groups')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Default to auto groups')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'When enabled, newly created tokens start in the first auto group.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type='submit' disabled={isSaving}>
|
||||
{isSaving ? t('Saving...') : t('Save group ratios')}
|
||||
</Button>
|
||||
</form>
|
||||
</SettingsForm>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { ModelSettings } from '../types'
|
||||
import {
|
||||
MODELS_DEFAULT_SECTION,
|
||||
getModelsSectionContent,
|
||||
getModelsSectionMeta,
|
||||
} from './section-registry.tsx'
|
||||
|
||||
const defaultModelSettings: ModelSettings = {
|
||||
@@ -77,6 +78,7 @@ export function ModelSettings() {
|
||||
defaultSettings={defaultModelSettings}
|
||||
defaultSection={MODELS_DEFAULT_SECTION}
|
||||
getSectionContent={getModelsSectionContent}
|
||||
getSectionMeta={getModelsSectionMeta}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,11 +33,9 @@ import {
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
} from '@/components/ui/field'
|
||||
import {
|
||||
Form,
|
||||
@@ -62,9 +60,12 @@ import {
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
|
||||
import {
|
||||
SettingsControlGroup,
|
||||
SettingsSwitchField,
|
||||
} from '../components/settings-form-layout'
|
||||
import { formatPricingNumber } from './pricing-format'
|
||||
import { TieredPricingEditor } from './tiered-pricing-editor'
|
||||
|
||||
@@ -1005,36 +1006,29 @@ function PriceLane(props: {
|
||||
const effectiveDisabled = props.disabled || !props.enabled
|
||||
|
||||
return (
|
||||
<Field
|
||||
className={cn(
|
||||
'rounded-lg border p-3',
|
||||
effectiveDisabled && 'bg-muted/35'
|
||||
)}
|
||||
<SettingsControlGroup
|
||||
className={cn('space-y-3', effectiveDisabled && 'opacity-75')}
|
||||
data-disabled={effectiveDisabled || undefined}
|
||||
>
|
||||
<div className='flex items-start justify-between gap-3'>
|
||||
<FieldContent>
|
||||
<FieldTitle>{props.title}</FieldTitle>
|
||||
<FieldDescription>{props.description}</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
checked={props.enabled}
|
||||
disabled={props.disabled}
|
||||
onCheckedChange={props.onEnabledChange}
|
||||
aria-label={props.title}
|
||||
/>
|
||||
</div>
|
||||
<SettingsSwitchField
|
||||
checked={props.enabled}
|
||||
disabled={props.disabled}
|
||||
onCheckedChange={props.onEnabledChange}
|
||||
label={props.title}
|
||||
description={props.description}
|
||||
aria-label={props.title}
|
||||
/>
|
||||
<PriceInput
|
||||
value={props.value}
|
||||
placeholder={props.placeholder}
|
||||
disabled={effectiveDisabled}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<FieldDescription>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{props.enabled
|
||||
? t('USD price per 1M tokens.')
|
||||
: t('Disabled lanes are omitted on save.')}
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</p>
|
||||
</SettingsControlGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ import {
|
||||
} from '@/components/ui/form'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
SettingsForm,
|
||||
SettingsSwitchContent,
|
||||
SettingsSwitchItem,
|
||||
} from '../components/settings-form-layout'
|
||||
import { SettingsPageActionsPortal } from '../components/settings-page-context'
|
||||
import { ModelRatioVisualEditor } from './model-ratio-visual-editor'
|
||||
|
||||
type ModelFormValues = {
|
||||
@@ -99,6 +105,25 @@ export const ModelRatioForm = memo(function ModelRatioForm({
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<SettingsPageActionsPortal>
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={onReset}
|
||||
disabled={isResetting}
|
||||
>
|
||||
{t('Reset prices')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
size='sm'
|
||||
onClick={form.handleSubmit(onSave)}
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? t('Saving...') : t('Save model prices')}
|
||||
</Button>
|
||||
</SettingsPageActionsPortal>
|
||||
{editMode === 'visual' ? (
|
||||
<div className='space-y-6'>
|
||||
<ModelRatioVisualEditor
|
||||
@@ -127,43 +152,27 @@ export const ModelRatioForm = memo(function ModelRatioForm({
|
||||
control={form.control}
|
||||
name='ExposeRatioEnabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Expose ratio API')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Expose ratio API')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Allow clients to query configured ratios via `/api/ratio`.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
<Button onClick={form.handleSubmit(onSave)} disabled={isSaving}>
|
||||
{isSaving ? t('Saving...') : t('Save model prices')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
onClick={onReset}
|
||||
disabled={isResetting}
|
||||
>
|
||||
{t('Reset prices')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={form.handleSubmit(onSave)} className='space-y-6'>
|
||||
<SettingsForm onSubmit={form.handleSubmit(onSave)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ModelPrice'
|
||||
@@ -318,41 +327,25 @@ export const ModelRatioForm = memo(function ModelRatioForm({
|
||||
control={form.control}
|
||||
name='ExposeRatioEnabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex flex-row items-center justify-between rounded-lg border p-4'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel className='text-base'>
|
||||
{t('Expose ratio API')}
|
||||
</FormLabel>
|
||||
<SettingsSwitchItem>
|
||||
<SettingsSwitchContent>
|
||||
<FormLabel>{t('Expose ratio API')}</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Allow clients to query configured ratios via `/api/ratio`.'
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</SettingsSwitchContent>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</SettingsSwitchItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
<Button type='submit' disabled={isSaving}>
|
||||
{isSaving ? t('Saving...') : t('Save model prices')}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
onClick={onReset}
|
||||
disabled={isResetting}
|
||||
>
|
||||
{t('Reset prices')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsForm>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
@@ -204,7 +204,6 @@ type RatioSettingsCardProps = {
|
||||
groupDefaults: GroupFormValues
|
||||
toolPricesDefault: string
|
||||
titleKey?: string
|
||||
descriptionKey?: string
|
||||
visibleTabs?: RatioTabId[]
|
||||
}
|
||||
|
||||
@@ -213,7 +212,6 @@ export function RatioSettingsCard({
|
||||
groupDefaults,
|
||||
toolPricesDefault,
|
||||
titleKey = 'Pricing Ratios',
|
||||
descriptionKey = 'Configure model, caching, and group ratios used for billing',
|
||||
visibleTabs = ['models', 'groups', 'tool-prices', 'upstream-sync'],
|
||||
}: RatioSettingsCardProps) {
|
||||
const { t } = useTranslation()
|
||||
@@ -497,7 +495,7 @@ export function RatioSettingsCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsSection title={t(titleKey)} description={t(descriptionKey)}>
|
||||
<SettingsSection title={t(titleKey)}>
|
||||
{visibleTabs.length === 1 ? (
|
||||
renderTabContent(defaultTab)
|
||||
) : (
|
||||
|
||||
@@ -39,7 +39,6 @@ const MODELS_SECTIONS = [
|
||||
{
|
||||
id: 'global',
|
||||
titleKey: 'Global Model Configuration',
|
||||
descriptionKey: 'Configure global model settings',
|
||||
build: (settings: ModelSettings) => (
|
||||
<GlobalSettingsCard
|
||||
defaultValues={{
|
||||
@@ -68,7 +67,6 @@ const MODELS_SECTIONS = [
|
||||
{
|
||||
id: 'gemini',
|
||||
titleKey: 'Gemini',
|
||||
descriptionKey: 'Configure Gemini model settings',
|
||||
build: (settings: ModelSettings) => (
|
||||
<GeminiSettingsCard
|
||||
defaultValues={{
|
||||
@@ -93,7 +91,6 @@ const MODELS_SECTIONS = [
|
||||
{
|
||||
id: 'claude',
|
||||
titleKey: 'Claude',
|
||||
descriptionKey: 'Configure Claude model settings',
|
||||
build: (settings: ModelSettings) => (
|
||||
<ClaudeSettingsCard
|
||||
defaultValues={{
|
||||
@@ -112,7 +109,6 @@ const MODELS_SECTIONS = [
|
||||
{
|
||||
id: 'grok',
|
||||
titleKey: 'Grok',
|
||||
descriptionKey: 'Configure xAI Grok model settings',
|
||||
build: (settings: ModelSettings) => (
|
||||
<GrokSettingsCard
|
||||
defaultValues={{
|
||||
@@ -127,7 +123,6 @@ const MODELS_SECTIONS = [
|
||||
{
|
||||
id: 'channel-affinity',
|
||||
titleKey: 'Channel Affinity',
|
||||
descriptionKey: 'Configure channel affinity (sticky routing) rules',
|
||||
build: (settings: ModelSettings) => (
|
||||
<ChannelAffinitySection
|
||||
defaultValues={{
|
||||
@@ -148,7 +143,6 @@ const MODELS_SECTIONS = [
|
||||
{
|
||||
id: 'model-deployment',
|
||||
titleKey: 'Model Deployment',
|
||||
descriptionKey: 'Configure model deployment provider settings',
|
||||
build: (settings: ModelSettings) => (
|
||||
<IoNetDeploymentSettingsSection
|
||||
defaultValues={{
|
||||
@@ -173,3 +167,4 @@ export const MODELS_SECTION_IDS = modelsRegistry.sectionIds
|
||||
export const MODELS_DEFAULT_SECTION = modelsRegistry.defaultSection
|
||||
export const getModelsSectionNavItems = modelsRegistry.getSectionNavItems
|
||||
export const getModelsSectionContent = modelsRegistry.getSectionContent
|
||||
export const getModelsSectionMeta = modelsRegistry.getSectionMeta
|
||||
|
||||
Reference in New Issue
Block a user