mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-07 01:56:53 +00:00
Refine the default system settings structure and model pricing editor so pricing configuration is easier to scan and edit.
360 lines
12 KiB
TypeScript
Vendored
360 lines
12 KiB
TypeScript
Vendored
import * as z from 'zod'
|
|
import type { Resolver } from 'react-hook-form'
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { RotateCcw } from 'lucide-react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from '@/components/ui/form'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import { FormDirtyIndicator } from '../components/form-dirty-indicator'
|
|
import { FormNavigationGuard } from '../components/form-navigation-guard'
|
|
import { SettingsSection } from '../components/settings-section'
|
|
import { useSettingsForm } from '../hooks/use-settings-form'
|
|
import { useUpdateOption } from '../hooks/use-update-option'
|
|
|
|
const _systemInfoSchema = z.object({
|
|
theme: z.object({
|
|
frontend: z.enum(['default', 'classic']),
|
|
}),
|
|
SystemName: z.string().min(1),
|
|
ServerAddress: z.string().optional(),
|
|
Logo: z.string().url().optional().or(z.literal('')),
|
|
Footer: z.string().optional(),
|
|
About: z.string().optional(),
|
|
HomePageContent: z.string().optional(),
|
|
legal: z.object({
|
|
user_agreement: z.string().optional(),
|
|
privacy_policy: z.string().optional(),
|
|
}),
|
|
})
|
|
|
|
type SystemInfoFormValues = z.infer<typeof _systemInfoSchema>
|
|
|
|
type SystemInfoSectionProps = {
|
|
defaultValues: SystemInfoFormValues
|
|
}
|
|
|
|
function normalizeValue(value: unknown): string {
|
|
if (value === undefined || value === null) return ''
|
|
return typeof value === 'string' ? value : String(value)
|
|
}
|
|
|
|
export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
|
|
const { t } = useTranslation()
|
|
const updateOption = useUpdateOption()
|
|
|
|
const normalizedDefaults: SystemInfoFormValues = {
|
|
theme: {
|
|
frontend:
|
|
defaultValues.theme?.frontend === 'classic' ? 'classic' : 'default',
|
|
},
|
|
SystemName: normalizeValue(defaultValues.SystemName),
|
|
ServerAddress: normalizeValue(defaultValues.ServerAddress),
|
|
Logo: normalizeValue(defaultValues.Logo),
|
|
Footer: normalizeValue(defaultValues.Footer),
|
|
About: normalizeValue(defaultValues.About),
|
|
HomePageContent: normalizeValue(defaultValues.HomePageContent),
|
|
legal: {
|
|
user_agreement: normalizeValue(defaultValues.legal?.user_agreement),
|
|
privacy_policy: normalizeValue(defaultValues.legal?.privacy_policy),
|
|
},
|
|
}
|
|
|
|
const systemInfoSchemaWithI18n = z.object({
|
|
theme: z.object({
|
|
frontend: z.enum(['default', 'classic']),
|
|
}),
|
|
SystemName: z.string().min(1, {
|
|
error: () => t('System name is required'),
|
|
}),
|
|
ServerAddress: z.string().optional(),
|
|
Logo: z.string().url().optional().or(z.literal('')),
|
|
Footer: z.string().optional(),
|
|
About: z.string().optional(),
|
|
HomePageContent: z.string().optional(),
|
|
legal: z.object({
|
|
user_agreement: z.string().optional(),
|
|
privacy_policy: z.string().optional(),
|
|
}),
|
|
})
|
|
|
|
const { form, handleSubmit, handleReset, isDirty, isSubmitting } =
|
|
useSettingsForm<SystemInfoFormValues>({
|
|
resolver: zodResolver(systemInfoSchemaWithI18n) as Resolver<
|
|
SystemInfoFormValues,
|
|
unknown,
|
|
SystemInfoFormValues
|
|
>,
|
|
defaultValues: normalizedDefaults,
|
|
onSubmit: async (_data, changedFields) => {
|
|
for (const [key, value] of Object.entries(changedFields)) {
|
|
let v = normalizeValue(value)
|
|
if (key === 'ServerAddress') {
|
|
v = v.replace(/\/+$/, '')
|
|
}
|
|
await updateOption.mutateAsync({
|
|
key,
|
|
value: v,
|
|
})
|
|
}
|
|
},
|
|
})
|
|
|
|
return (
|
|
<>
|
|
<FormNavigationGuard when={isDirty} />
|
|
|
|
<SettingsSection
|
|
title={t('System Information')}
|
|
description={t('Configure basic system information and branding')}
|
|
>
|
|
<Form {...form}>
|
|
<form onSubmit={handleSubmit} className='space-y-6'>
|
|
<FormDirtyIndicator isDirty={isDirty} />
|
|
<FormField
|
|
control={form.control}
|
|
name='theme.frontend'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Frontend Theme')}</FormLabel>
|
|
<Select onValueChange={field.onChange} value={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger className='w-full'>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value='default'>
|
|
{t('Default (New Frontend)')}
|
|
</SelectItem>
|
|
<SelectItem value='classic'>
|
|
{t('Classic (Legacy Frontend)')}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormDescription>
|
|
{t(
|
|
'Switch between the new frontend and the classic frontend. Changes take effect after page reload.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='SystemName'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('System Name')}</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder={t('New API')} {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t('The name displayed across the application')}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='ServerAddress'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Server Address')}</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder='https://yourdomain.com' {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'The public URL of your server, used for OAuth callbacks, webhooks, and other external integrations'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='Logo'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Logo URL')}</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder={t('https://example.com/logo.png')}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t('URL to your logo image (optional)')}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='Footer'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Footer')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder={t(
|
|
'© 2025 Your Company. All rights reserved.'
|
|
)}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t('Footer text displayed at the bottom of pages')}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='About'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('About')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder={t(
|
|
'Enter HTML code (e.g., <p>About us...</p>) or a URL (e.g., https://example.com) to embed as iframe'
|
|
)}
|
|
rows={4}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Supports HTML markup or iframe embedding. Enter HTML code directly, or provide a complete URL to automatically embed it as an iframe.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='HomePageContent'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Home Page Content')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder={t('Welcome to our New API...')}
|
|
rows={6}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Content displayed on the home page (supports Markdown)'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='legal.user_agreement'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('User Agreement')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder={t(
|
|
'Provide Markdown, HTML, or an external URL for the user agreement'
|
|
)}
|
|
rows={6}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name='legal.privacy_policy'
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>{t('Privacy Policy')}</FormLabel>
|
|
<FormControl>
|
|
<Textarea
|
|
placeholder={t(
|
|
'Provide Markdown, HTML, or an external URL for the privacy policy'
|
|
)}
|
|
rows={6}
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
{t(
|
|
'Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.'
|
|
)}
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className='flex gap-2'>
|
|
<Button
|
|
type='submit'
|
|
disabled={isSubmitting || updateOption.isPending}
|
|
>
|
|
{updateOption.isPending ? t('Saving...') : t('Save Changes')}
|
|
</Button>
|
|
<Button
|
|
type='button'
|
|
variant='outline'
|
|
onClick={handleReset}
|
|
disabled={!isDirty || updateOption.isPending || isSubmitting}
|
|
>
|
|
<RotateCcw className='mr-2 h-4 w-4' />
|
|
{t('Reset')}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</SettingsSection>
|
|
</>
|
|
)
|
|
}
|