Files
new-api/web/default/src/features/system-settings/general/quota-settings-section.tsx
T
A_Words 90fa6fe6b6 fix(wallet): honor configured quota units for reward transfers (#5808)
* fix(wallet): honor configured quota units for reward transfers

* fix(i18n): localize quota preview descriptions

* fix(settings): normalize invalid quota input
2026-07-07 21:40:58 +08:00

311 lines
10 KiB
TypeScript
Vendored

/*
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 { zodResolver } from '@hookform/resolvers/zod'
import type { ChangeEvent } from 'react'
import type { Resolver } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import * as z from 'zod'
import { Alert, AlertDescription } from '@/components/ui/alert'
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 { formatQuota } from '@/lib/format'
import { FormDirtyIndicator } from '../components/form-dirty-indicator'
import { FormNavigationGuard } from '../components/form-navigation-guard'
import {
SettingsForm,
SettingsSwitchContent,
SettingsSwitchItem,
SettingsFormGrid,
SettingsFormGridItem,
} from '../components/settings-form-layout'
import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section'
import { useSettingsForm } from '../hooks/use-settings-form'
import { useUpdateOption } from '../hooks/use-update-option'
const quotaSchema = z.object({
QuotaForNewUser: z.coerce.number().min(0),
PreConsumedQuota: z.coerce.number().min(0),
QuotaForInviter: z.coerce.number().min(0),
QuotaForInvitee: z.coerce.number().min(0),
TopUpLink: z.string(),
general_setting: z.object({
docs_link: z.string(),
}),
quota_setting: z.object({
enable_free_model_pre_consume: z.boolean(),
}),
})
type QuotaFormValues = z.infer<typeof quotaSchema>
type QuotaInputValue = number | ''
function formatQuotaInputValue(value: QuotaInputValue): string {
return formatQuota(value === '' ? 0 : value)
}
type QuotaSettingsSectionProps = {
defaultValues: QuotaFormValues
complianceConfirmed?: boolean
}
export function QuotaSettingsSection({
defaultValues,
complianceConfirmed = true,
}: QuotaSettingsSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const handleNumberChange =
(onChange: (value: QuotaInputValue) => void) =>
(event: ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.valueAsNumber
onChange(Number.isNaN(value) ? '' : value)
}
const { form, handleSubmit, isDirty, isSubmitting } =
useSettingsForm<QuotaFormValues>({
resolver: zodResolver(quotaSchema) as Resolver<
QuotaFormValues,
unknown,
QuotaFormValues
>,
defaultValues,
onSubmit: async (_data, changedFields) => {
for (const [key, value] of Object.entries(changedFields)) {
await updateOption.mutateAsync({
key,
value: value as string | number | boolean,
})
}
},
})
return (
<SettingsSection title={t('Quota Settings')}>
<FormNavigationGuard when={isDirty} />
{!complianceConfirmed ? (
<Alert variant='destructive'>
<AlertDescription>
{t(
'Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.'
)}
</AlertDescription>
</Alert>
) : null}
<Form {...form}>
<SettingsForm onSubmit={handleSubmit}>
<SettingsPageFormActions
onSave={handleSubmit}
isSaving={updateOption.isPending || isSubmitting}
/>
<FormDirtyIndicator isDirty={isDirty} />
<SettingsFormGrid>
<FormField
control={form.control}
name='QuotaForNewUser'
render={({ field }) => (
<FormItem>
<FormLabel>{t('New User Quota')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t(
'Initial quota given to new users ({{formattedQuota}})',
{
formattedQuota: formatQuotaInputValue(field.value),
}
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='PreConsumedQuota'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Pre-Consumed Quota')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t('Quota consumed before charging users')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='QuotaForInviter'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Inviter Reward')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t(
'Quota given to users who invite others ({{formattedQuota}})',
{
formattedQuota: formatQuotaInputValue(field.value),
}
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='QuotaForInvitee'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Invitee Reward')}</FormLabel>
<FormControl>
<Input
type='number'
value={field.value ?? ''}
onChange={handleNumberChange(field.onChange)}
name={field.name}
onBlur={field.onBlur}
ref={field.ref}
/>
</FormControl>
<FormDescription>
{t('Quota given to invited users ({{formattedQuota}})', {
formattedQuota: formatQuotaInputValue(field.value),
})}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<SettingsFormGridItem span='full'>
<FormField
control={form.control}
name='quota_setting.enable_free_model_pre_consume'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Pre-Consume for Free Models')}</FormLabel>
<FormDescription>
{t(
'When enabled, zero-cost models also pre-consume quota before final settlement.'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={updateOption.isPending}
/>
</FormControl>
</SettingsSwitchItem>
)}
/>
</SettingsFormGridItem>
<FormField
control={form.control}
name='TopUpLink'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Top-Up Link')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://example.com/topup')}
{...field}
/>
</FormControl>
<FormDescription>
{t('External link for users to purchase quota')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='general_setting.docs_link'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Documentation Link')}</FormLabel>
<FormControl>
<Input
placeholder={t('https://docs.example.com')}
{...field}
/>
</FormControl>
<FormDescription>
{t('Link to your documentation site')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</SettingsFormGrid>
</SettingsForm>
</Form>
</SettingsSection>
)
}