import { useCallback, useEffect, useRef, useState } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { ConfirmDialog } from '@/components/confirm-dialog' import { resetModelRatios } from '../api' import { SettingsSection } from '../components/settings-section' import { useUpdateOption } from '../hooks/use-update-option' import { GroupRatioForm } from './group-ratio-form' import { ModelRatioForm } from './model-ratio-form' import { ToolPriceSettings } from './tool-price-settings' import { UpstreamRatioSync } from './upstream-ratio-sync' import { formatJsonForTextarea, normalizeJsonString, validateJsonString, } from './utils' const modelSchema = z.object({ ModelPrice: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), ModelRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), CacheRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), CreateCacheRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), CompletionRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), ImageRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), AudioRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), AudioCompletionRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), ExposeRatioEnabled: z.boolean(), BillingMode: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), BillingExpr: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), }) const groupSchema = z.object({ GroupRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), TopupGroupRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), UserUsableGroups: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), GroupGroupRatio: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), AutoGroups: 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 group identifiers', }) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON array', }) } }), DefaultUseAutoGroup: z.boolean(), GroupSpecialUsableGroup: z.string().superRefine((value, ctx) => { const result = validateJsonString(value) if (!result.valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: result.message || 'Invalid JSON', }) } }), }) type ModelFormValues = z.infer type GroupFormValues = z.infer type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync' type RatioSettingsCardProps = { modelDefaults: ModelFormValues groupDefaults: GroupFormValues toolPricesDefault: string titleKey?: string descriptionKey?: string visibleTabs?: RatioTabId[] } export function RatioSettingsCard({ modelDefaults, 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() const updateOption = useUpdateOption() const queryClient = useQueryClient() const [confirmOpen, setConfirmOpen] = useState(false) const resetMutation = useMutation({ mutationFn: resetModelRatios, onSuccess: (data) => { if (data.success) { toast.success(t('Model ratios reset successfully')) queryClient.invalidateQueries({ queryKey: ['system-options'] }) setConfirmOpen(false) } else { toast.error(data.message || t('Failed to reset model ratios')) } }, onError: (error: Error) => { toast.error(error.message || t('Failed to reset model ratios')) }, }) const modelNormalizedDefaults = useRef({ ModelPrice: normalizeJsonString(modelDefaults.ModelPrice), ModelRatio: normalizeJsonString(modelDefaults.ModelRatio), CacheRatio: normalizeJsonString(modelDefaults.CacheRatio), CreateCacheRatio: normalizeJsonString(modelDefaults.CreateCacheRatio), CompletionRatio: normalizeJsonString(modelDefaults.CompletionRatio), ImageRatio: normalizeJsonString(modelDefaults.ImageRatio), AudioRatio: normalizeJsonString(modelDefaults.AudioRatio), AudioCompletionRatio: normalizeJsonString( modelDefaults.AudioCompletionRatio ), ExposeRatioEnabled: modelDefaults.ExposeRatioEnabled, BillingMode: normalizeJsonString(modelDefaults.BillingMode), BillingExpr: normalizeJsonString(modelDefaults.BillingExpr), }) const groupNormalizedDefaults = useRef({ GroupRatio: normalizeJsonString(groupDefaults.GroupRatio), TopupGroupRatio: normalizeJsonString(groupDefaults.TopupGroupRatio), UserUsableGroups: normalizeJsonString(groupDefaults.UserUsableGroups), GroupGroupRatio: normalizeJsonString(groupDefaults.GroupGroupRatio), AutoGroups: normalizeJsonString(groupDefaults.AutoGroups), DefaultUseAutoGroup: groupDefaults.DefaultUseAutoGroup, GroupSpecialUsableGroup: normalizeJsonString( groupDefaults.GroupSpecialUsableGroup ), }) const modelForm = useForm({ resolver: zodResolver(modelSchema), mode: 'onChange', defaultValues: { ...modelDefaults, ModelPrice: formatJsonForTextarea(modelDefaults.ModelPrice), ModelRatio: formatJsonForTextarea(modelDefaults.ModelRatio), CacheRatio: formatJsonForTextarea(modelDefaults.CacheRatio), CreateCacheRatio: formatJsonForTextarea(modelDefaults.CreateCacheRatio), CompletionRatio: formatJsonForTextarea(modelDefaults.CompletionRatio), ImageRatio: formatJsonForTextarea(modelDefaults.ImageRatio), AudioRatio: formatJsonForTextarea(modelDefaults.AudioRatio), AudioCompletionRatio: formatJsonForTextarea( modelDefaults.AudioCompletionRatio ), BillingMode: formatJsonForTextarea(modelDefaults.BillingMode), BillingExpr: formatJsonForTextarea(modelDefaults.BillingExpr), }, }) const groupForm = useForm({ resolver: zodResolver(groupSchema), mode: 'onChange', defaultValues: { ...groupDefaults, GroupRatio: formatJsonForTextarea(groupDefaults.GroupRatio), TopupGroupRatio: formatJsonForTextarea(groupDefaults.TopupGroupRatio), UserUsableGroups: formatJsonForTextarea(groupDefaults.UserUsableGroups), GroupGroupRatio: formatJsonForTextarea(groupDefaults.GroupGroupRatio), AutoGroups: formatJsonForTextarea(groupDefaults.AutoGroups), GroupSpecialUsableGroup: formatJsonForTextarea( groupDefaults.GroupSpecialUsableGroup ), }, }) useEffect(() => { modelNormalizedDefaults.current = { ModelPrice: normalizeJsonString(modelDefaults.ModelPrice), ModelRatio: normalizeJsonString(modelDefaults.ModelRatio), CacheRatio: normalizeJsonString(modelDefaults.CacheRatio), CreateCacheRatio: normalizeJsonString(modelDefaults.CreateCacheRatio), CompletionRatio: normalizeJsonString(modelDefaults.CompletionRatio), ImageRatio: normalizeJsonString(modelDefaults.ImageRatio), AudioRatio: normalizeJsonString(modelDefaults.AudioRatio), AudioCompletionRatio: normalizeJsonString( modelDefaults.AudioCompletionRatio ), ExposeRatioEnabled: modelDefaults.ExposeRatioEnabled, BillingMode: normalizeJsonString(modelDefaults.BillingMode), BillingExpr: normalizeJsonString(modelDefaults.BillingExpr), } modelForm.reset({ ...modelDefaults, ModelPrice: formatJsonForTextarea(modelDefaults.ModelPrice), ModelRatio: formatJsonForTextarea(modelDefaults.ModelRatio), CacheRatio: formatJsonForTextarea(modelDefaults.CacheRatio), CreateCacheRatio: formatJsonForTextarea(modelDefaults.CreateCacheRatio), CompletionRatio: formatJsonForTextarea(modelDefaults.CompletionRatio), ImageRatio: formatJsonForTextarea(modelDefaults.ImageRatio), AudioRatio: formatJsonForTextarea(modelDefaults.AudioRatio), AudioCompletionRatio: formatJsonForTextarea( modelDefaults.AudioCompletionRatio ), BillingMode: formatJsonForTextarea(modelDefaults.BillingMode), BillingExpr: formatJsonForTextarea(modelDefaults.BillingExpr), }) }, [modelDefaults, modelForm]) useEffect(() => { groupNormalizedDefaults.current = { GroupRatio: normalizeJsonString(groupDefaults.GroupRatio), TopupGroupRatio: normalizeJsonString(groupDefaults.TopupGroupRatio), UserUsableGroups: normalizeJsonString(groupDefaults.UserUsableGroups), GroupGroupRatio: normalizeJsonString(groupDefaults.GroupGroupRatio), AutoGroups: normalizeJsonString(groupDefaults.AutoGroups), DefaultUseAutoGroup: groupDefaults.DefaultUseAutoGroup, GroupSpecialUsableGroup: normalizeJsonString( groupDefaults.GroupSpecialUsableGroup ), } groupForm.reset({ ...groupDefaults, GroupRatio: formatJsonForTextarea(groupDefaults.GroupRatio), TopupGroupRatio: formatJsonForTextarea(groupDefaults.TopupGroupRatio), UserUsableGroups: formatJsonForTextarea(groupDefaults.UserUsableGroups), GroupGroupRatio: formatJsonForTextarea(groupDefaults.GroupGroupRatio), AutoGroups: formatJsonForTextarea(groupDefaults.AutoGroups), GroupSpecialUsableGroup: formatJsonForTextarea( groupDefaults.GroupSpecialUsableGroup ), }) }, [groupDefaults, groupForm]) const saveModelRatios = useCallback( async (values: ModelFormValues) => { const normalized = { ModelPrice: normalizeJsonString(values.ModelPrice), ModelRatio: normalizeJsonString(values.ModelRatio), CacheRatio: normalizeJsonString(values.CacheRatio), CreateCacheRatio: normalizeJsonString(values.CreateCacheRatio), CompletionRatio: normalizeJsonString(values.CompletionRatio), ImageRatio: normalizeJsonString(values.ImageRatio), AudioRatio: normalizeJsonString(values.AudioRatio), AudioCompletionRatio: normalizeJsonString(values.AudioCompletionRatio), ExposeRatioEnabled: values.ExposeRatioEnabled, BillingMode: normalizeJsonString(values.BillingMode), BillingExpr: normalizeJsonString(values.BillingExpr), } const apiKeyMap: Record = { BillingMode: 'billing_setting.billing_mode', BillingExpr: 'billing_setting.billing_expr', } const updates = ( Object.keys(normalized) as Array ).filter( (key) => normalized[key] !== modelNormalizedDefaults.current[key] ) for (const key of updates) { const apiKey = apiKeyMap[key as string] || (key as string) await updateOption.mutateAsync({ key: apiKey, value: normalized[key] }) } }, [updateOption] ) const saveGroupRatios = useCallback( async (values: GroupFormValues) => { const normalized = { GroupRatio: normalizeJsonString(values.GroupRatio), TopupGroupRatio: normalizeJsonString(values.TopupGroupRatio), UserUsableGroups: normalizeJsonString(values.UserUsableGroups), GroupGroupRatio: normalizeJsonString(values.GroupGroupRatio), AutoGroups: normalizeJsonString(values.AutoGroups), DefaultUseAutoGroup: values.DefaultUseAutoGroup, GroupSpecialUsableGroup: normalizeJsonString( values.GroupSpecialUsableGroup ), } // Map form field names to API keys (most are 1:1, except GroupSpecialUsableGroup) const apiKeyMap: Record = { GroupSpecialUsableGroup: 'group_ratio_setting.group_special_usable_group', } const updates = ( Object.keys(normalized) as Array ).filter( (key) => normalized[key] !== groupNormalizedDefaults.current[key] ) for (const key of updates) { const apiKey = apiKeyMap[key] || key await updateOption.mutateAsync({ key: apiKey, value: normalized[key] }) } }, [updateOption] ) const handleResetRatios = useCallback(() => { setConfirmOpen(true) }, []) const { mutate: resetMutate } = resetMutation const handleConfirmReset = useCallback(() => { resetMutate() }, [resetMutate]) const tabLabels: Record = { models: 'Model ratios', groups: 'Group ratios', 'tool-prices': 'Tool prices', 'upstream-sync': 'Upstream price sync', } const tabsGridClass = { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', }[visibleTabs.length] ?? 'grid-cols-4' const defaultTab = visibleTabs[0] ?? 'models' const renderTabContent = (tab: RatioTabId) => { if (tab === 'models') { return ( ) } if (tab === 'groups') { return ( ) } if (tab === 'tool-prices') { return } return ( ) } return ( {visibleTabs.map((tab) => ( {t(tabLabels[tab])} ))} {visibleTabs.map((tab) => ( {renderTabContent(tab)} ))} ) }