Files
new-api/web/default/src/features/system-settings/content/chat-settings-section.tsx
T
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files.

The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
2026-05-09 11:35:07 +08:00

198 lines
6.4 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 { useEffect, useRef, useState } 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 { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea'
import { SettingsSection } from '../components/settings-section'
import { useUpdateOption } from '../hooks/use-update-option'
import { ChatSettingsVisualEditor } from './chat-settings-visual-editor'
import { formatJsonForEditor, normalizeJsonString } from './utils'
const createChatSchema = (t: (key: string) => string) =>
z.object({
Chats: z.string().superRefine((value, ctx) => {
try {
const parsed = JSON.parse(value || '[]')
if (!Array.isArray(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('Expected a JSON array.'),
})
return
}
for (const item of parsed) {
if (
item === null ||
typeof item !== 'object' ||
Array.isArray(item)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t(
'Each item must be an object with a single key-value pair.'
),
})
return
}
const entries = Object.entries(item)
if (entries.length !== 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('Each item must have exactly one key-value pair.'),
})
return
}
}
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('Invalid JSON string.'),
})
}
}),
})
type ChatSettingsFormValues = z.infer<ReturnType<typeof createChatSchema>>
type ChatSettingsSectionProps = {
defaultValue: string
}
export function ChatSettingsSection({
defaultValue,
}: ChatSettingsSectionProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
const [editMode, setEditMode] = useState<'visual' | 'json'>('visual')
const chatSchema = createChatSchema(t)
const formatted = formatJsonForEditor(defaultValue, '[]')
const form = useForm<ChatSettingsFormValues>({
resolver: zodResolver(chatSchema),
mode: 'onChange', // Enable real-time validation
defaultValues: {
Chats: formatted,
},
})
const initialNormalizedRef = useRef(normalizeJsonString(defaultValue, '[]'))
useEffect(() => {
form.reset({ Chats: formatJsonForEditor(defaultValue, '[]') })
initialNormalizedRef.current = normalizeJsonString(defaultValue, '[]')
}, [defaultValue, form])
const onSubmit = async (values: ChatSettingsFormValues) => {
const normalized = normalizeJsonString(values.Chats, '[]')
if (normalized === initialNormalizedRef.current) {
return
}
await updateOption.mutateAsync({
key: 'Chats',
value: normalized,
})
}
return (
<SettingsSection
title={t('Chat Presets')}
description={t('Configure predefined chat links surfaced to end users.')}
>
<Form {...form}>
{/* eslint-disable-next-line react-hooks/refs */}
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
<Tabs
value={editMode}
onValueChange={(value) => setEditMode(value as 'visual' | 'json')}
>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger value='visual'>{t('Visual')}</TabsTrigger>
<TabsTrigger value='json'>{t('JSON')}</TabsTrigger>
</TabsList>
<TabsContent value='visual' className='mt-6'>
<FormField
control={form.control}
name='Chats'
render={({ field }) => (
<FormItem>
<FormControl>
<ChatSettingsVisualEditor
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
<TabsContent value='json' className='mt-6'>
<FormField
control={form.control}
name='Chats'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Chat configuration JSON')}</FormLabel>
<FormControl>
<Textarea
rows={12}
placeholder={t(
'[{"ChatGPT":"https://chat.openai.com"},{"Lobe Chat":"https://chat-preview.lobehub.com/?settings={...}"}]'
)}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Array of chat client presets. Each item is an object with one key-value pair: client name and its URL.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</TabsContent>
</Tabs>
<Button type='submit' disabled={updateOption.isPending}>
{updateOption.isPending ? t('Saving...') : t('Save chat settings')}
</Button>
</form>
</Form>
</SettingsSection>
)
}