Files
new-api/web/default/src/features/system-settings/models/gemini-settings-card.tsx
T
t0ng7u b08febaa3c 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.
2026-05-25 00:34:26 +08:00

413 lines
14 KiB
TypeScript
Vendored
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect, useMemo, useRef } from 'react'
import * as z from 'zod'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
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 {
formatJsonForTextarea,
normalizeJsonString,
validateJsonString,
} from './utils'
const schema = z.object({
gemini: z.object({
safety_settings: z.string().superRefine((value, ctx) => {
const result = validateJsonString(value)
if (!result.valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: result.message || 'Invalid JSON',
})
}
}),
version_settings: z.string().superRefine((value, ctx) => {
const result = validateJsonString(value)
if (!result.valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: result.message || 'Invalid JSON',
})
}
}),
supported_imagine_models: z.string().superRefine((value, ctx) => {
const result = validateJsonString(value, {
predicate: (parsed) =>
Array.isArray(parsed) &&
parsed.every((item) => typeof item === 'string'),
predicateMessage: 'Expected a JSON array of model identifiers',
})
if (!result.valid) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: result.message || 'Invalid JSON array',
})
}
}),
thinking_adapter_enabled: z.boolean(),
thinking_adapter_budget_tokens_percentage: z.coerce
.number()
.min(0.002, { message: 'Must be at least 0.002' })
.max(1, { message: 'Must be 1 or less' }),
function_call_thought_signature_enabled: z.boolean(),
remove_function_response_id_enabled: z.boolean(),
}),
})
type GeminiSettingsFormValues = z.output<typeof schema>
type GeminiSettingsFormInput = z.input<typeof schema>
type FlatGeminiSettings = {
'gemini.safety_settings': string
'gemini.version_settings': string
'gemini.supported_imagine_models': string
'gemini.thinking_adapter_enabled': boolean
'gemini.thinking_adapter_budget_tokens_percentage': number
'gemini.function_call_thought_signature_enabled': boolean
'gemini.remove_function_response_id_enabled': boolean
}
type GeminiSettingsCardProps = {
defaultValues: GeminiSettingsFormInput
}
export function GeminiSettingsCard({ defaultValues }: GeminiSettingsCardProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const normalizedDefaultsRef = useRef<FlatGeminiSettings>({
'gemini.safety_settings': normalizeJsonString(
defaultValues.gemini.safety_settings
),
'gemini.version_settings': normalizeJsonString(
defaultValues.gemini.version_settings
),
'gemini.supported_imagine_models': normalizeJsonString(
defaultValues.gemini.supported_imagine_models
),
'gemini.thinking_adapter_enabled':
defaultValues.gemini.thinking_adapter_enabled,
'gemini.thinking_adapter_budget_tokens_percentage': Number(
defaultValues.gemini.thinking_adapter_budget_tokens_percentage
),
'gemini.function_call_thought_signature_enabled':
defaultValues.gemini.function_call_thought_signature_enabled ?? true,
'gemini.remove_function_response_id_enabled':
defaultValues.gemini.remove_function_response_id_enabled ?? true,
})
const buildFormDefaults = (
values: GeminiSettingsFormInput
): GeminiSettingsFormInput => ({
gemini: {
safety_settings: formatJsonForTextarea(values.gemini.safety_settings),
version_settings: formatJsonForTextarea(values.gemini.version_settings),
supported_imagine_models: formatJsonForTextarea(
values.gemini.supported_imagine_models
),
thinking_adapter_enabled: values.gemini.thinking_adapter_enabled,
thinking_adapter_budget_tokens_percentage:
values.gemini.thinking_adapter_budget_tokens_percentage,
function_call_thought_signature_enabled:
values.gemini.function_call_thought_signature_enabled ?? true,
remove_function_response_id_enabled:
values.gemini.remove_function_response_id_enabled ?? true,
},
})
const form = useForm<
GeminiSettingsFormInput,
unknown,
GeminiSettingsFormValues
>({
resolver: zodResolver(schema),
defaultValues: buildFormDefaults(defaultValues),
})
useEffect(() => {
normalizedDefaultsRef.current = {
'gemini.safety_settings': normalizeJsonString(
defaultValues.gemini.safety_settings
),
'gemini.version_settings': normalizeJsonString(
defaultValues.gemini.version_settings
),
'gemini.supported_imagine_models': normalizeJsonString(
defaultValues.gemini.supported_imagine_models
),
'gemini.thinking_adapter_enabled':
defaultValues.gemini.thinking_adapter_enabled,
'gemini.thinking_adapter_budget_tokens_percentage': Number(
defaultValues.gemini.thinking_adapter_budget_tokens_percentage
),
'gemini.function_call_thought_signature_enabled':
defaultValues.gemini.function_call_thought_signature_enabled ?? true,
'gemini.remove_function_response_id_enabled':
defaultValues.gemini.remove_function_response_id_enabled ?? true,
}
form.reset(buildFormDefaults(defaultValues))
}, [defaultValues, form])
const isAdapterEnabled = form.watch('gemini.thinking_adapter_enabled')
const onSubmit = async (values: GeminiSettingsFormValues) => {
const normalized: FlatGeminiSettings = {
'gemini.safety_settings': normalizeJsonString(
values.gemini.safety_settings
),
'gemini.version_settings': normalizeJsonString(
values.gemini.version_settings
),
'gemini.supported_imagine_models': normalizeJsonString(
values.gemini.supported_imagine_models
),
'gemini.thinking_adapter_enabled': values.gemini.thinking_adapter_enabled,
'gemini.thinking_adapter_budget_tokens_percentage':
values.gemini.thinking_adapter_budget_tokens_percentage,
'gemini.function_call_thought_signature_enabled':
values.gemini.function_call_thought_signature_enabled,
'gemini.remove_function_response_id_enabled':
values.gemini.remove_function_response_id_enabled,
}
const updates = (
Object.keys(normalized) as Array<keyof FlatGeminiSettings>
).filter((key) => normalized[key] !== normalizedDefaultsRef.current[key])
if (updates.length === 0) {
toast.info(t('No changes to save'))
return
}
for (const key of updates) {
await updateOption.mutateAsync({
key,
value: normalized[key],
})
}
}
const imaginePlaceholder = useMemo(
() => JSON.stringify(['gemini-2.0-flash-exp-image-generation'], null, 2),
[]
)
return (
<SettingsSection title={t('Gemini')}>
<Form {...form}>
<SettingsForm onSubmit={form.handleSubmit(onSubmit)}>
<SettingsPageFormActions
onSave={form.handleSubmit(onSubmit)}
isSaving={updateOption.isPending}
/>
<FormField
control={form.control}
name='gemini.safety_settings'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Safety Settings')}</FormLabel>
<FormControl>
<Textarea rows={8} {...field} />
</FormControl>
<FormDescription>
{t(
'Provide per-category safety overrides as JSON. Use `default` for fallback values.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='gemini.version_settings'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Version Overrides')}</FormLabel>
<FormControl>
<Textarea rows={8} {...field} />
</FormControl>
<FormDescription>
{t(
'Map model identifiers to Gemini API versions. A `default` entry applies when no specific match is found.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='gemini.supported_imagine_models'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Supported Imagine Models')}</FormLabel>
<FormControl>
<Textarea
rows={6}
placeholder={imaginePlaceholder}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Accepts a JSON array of model identifiers that support the Imagine API.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<SettingsControlGroup>
<FormField
control={form.control}
name='gemini.thinking_adapter_enabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Thinking Adapter')}</FormLabel>
<FormDescription>
{t('Supports `-thinking`, `-thinking-')}
{'{{budget}}'}
{t(
'`, and `-nothinking` suffixes while routing to the correct Gemini variant.'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name='gemini.thinking_adapter_budget_tokens_percentage'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Budget Tokens Ratio')}</FormLabel>
<FormControl>
<Input
{...field}
value={String(field.value ?? '')}
onChange={(event) => field.onChange(event.target.value)}
/>
</FormControl>
<FormDescription>
{t(
'Budget tokens = max tokens × ratio. Accepts a decimal between 0.002 and 1. Recommended to keep aligned with upstream billing.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{!isAdapterEnabled && (
<p className='text-muted-foreground text-sm'>
{t(
'Gemini will continue to auto-detect thinking mode even with the adapter disabled. Enable this only when you need finer control over pricing and budgeting.'
)}
</p>
)}
</SettingsControlGroup>
<FormField
control={form.control}
name='gemini.function_call_thought_signature_enabled'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>
{t('Enable FunctionCall thoughtSignature Fill')}
</FormLabel>
<FormDescription>
{t(
'Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
<FormField
control={form.control}
name='gemini.remove_function_response_id_enabled'
render={({ field }) => (
<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>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
</SettingsForm>
</Form>
</SettingsSection>
)
}