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.
198 lines
6.4 KiB
TypeScript
Vendored
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>
|
|
)
|
|
}
|