/* 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 . 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 type GeminiSettingsFormInput = z.input 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({ '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 ).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 (
( {t('Safety Settings')}