### What problem does this PR solve? Feat: Add ChatBasicSetting component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.17.0
| @@ -2,7 +2,12 @@ import { DocumentParserType } from '@/constants/knowledge'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | |||
| import { UserOutlined } from '@ant-design/icons'; | |||
| import { Avatar, Form, Select, Space } from 'antd'; | |||
| import { Avatar as AntAvatar, Form, Select, Space } from 'antd'; | |||
| import { Book } from 'lucide-react'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'; | |||
| import { FormControl, FormField, FormItem, FormLabel } from './ui/form'; | |||
| import { MultiSelect } from './ui/multi-select'; | |||
| const KnowledgeBaseItem = () => { | |||
| const { t } = useTranslate('chat'); | |||
| @@ -16,7 +21,7 @@ const KnowledgeBaseItem = () => { | |||
| const knowledgeOptions = filteredKnowledgeList.map((x) => ({ | |||
| label: ( | |||
| <Space> | |||
| <Avatar size={20} icon={<UserOutlined />} src={x.avatar} /> | |||
| <AntAvatar size={20} icon={<UserOutlined />} src={x.avatar} /> | |||
| {x.name} | |||
| </Space> | |||
| ), | |||
| @@ -46,3 +51,49 @@ const KnowledgeBaseItem = () => { | |||
| }; | |||
| export default KnowledgeBaseItem; | |||
| export function KnowledgeBaseFormField() { | |||
| const form = useFormContext(); | |||
| const { t } = useTranslate('chat'); | |||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | |||
| const filteredKnowledgeList = knowledgeList.filter( | |||
| (x) => x.parser_id !== DocumentParserType.Tag, | |||
| ); | |||
| const knowledgeOptions = filteredKnowledgeList.map((x) => ({ | |||
| label: x.name, | |||
| value: x.id, | |||
| icon: () => ( | |||
| <Avatar className="size-4 mr-2"> | |||
| <AvatarImage src={x.avatar} /> | |||
| <AvatarFallback> | |||
| <Book /> | |||
| </AvatarFallback> | |||
| </Avatar> | |||
| ), | |||
| })); | |||
| return ( | |||
| <FormField | |||
| control={form.control} | |||
| name="kb_ids" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('knowledgeBases')}</FormLabel> | |||
| <FormControl> | |||
| <MultiSelect | |||
| options={knowledgeOptions} | |||
| onValueChange={field.onChange} | |||
| placeholder={t('knowledgeBasesMessage')} | |||
| variant="inverted" | |||
| maxCount={100} | |||
| {...field} | |||
| /> | |||
| </FormControl> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -1,3 +0,0 @@ | |||
| export function AppSettings() { | |||
| return <section className="p-6 w-[400px] max-w-[20%]">app-settings</section>; | |||
| } | |||
| @@ -0,0 +1,163 @@ | |||
| 'use client'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { FileUploader } from '@/components/file-uploader'; | |||
| import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { Subhead } from './subhead'; | |||
| import { SwitchFormField } from './switch-fom-field'; | |||
| export default function ChatBasicSetting() { | |||
| const { t } = useTranslate('chat'); | |||
| const promptConfigSchema = z.object({ | |||
| quote: z.boolean(), | |||
| keyword: z.boolean(), | |||
| tts: z.boolean(), | |||
| empty_response: z.string().min(1, { | |||
| message: t('emptyResponse'), | |||
| }), | |||
| prologue: z.string().min(2, {}), | |||
| }); | |||
| const formSchema = z.object({ | |||
| name: z.string().min(1, { message: t('assistantNameMessage') }), | |||
| icon: z.array(z.instanceof(File)), | |||
| language: z.string().min(1, { | |||
| message: 'Username must be at least 2 characters.', | |||
| }), | |||
| description: z.string(), | |||
| kb_ids: z.array(z.string()).min(0, { | |||
| message: 'Username must be at least 1 characters.', | |||
| }), | |||
| prompt_config: promptConfigSchema, | |||
| }); | |||
| const form = useForm<z.infer<typeof formSchema>>({ | |||
| resolver: zodResolver(formSchema), | |||
| defaultValues: { | |||
| name: '', | |||
| language: 'English', | |||
| prompt_config: { | |||
| quote: true, | |||
| keyword: false, | |||
| tts: false, | |||
| }, | |||
| }, | |||
| }); | |||
| function onSubmit(values: z.infer<typeof formSchema>) { | |||
| console.log(values); | |||
| } | |||
| return ( | |||
| <section> | |||
| <Subhead>Basic settings</Subhead> | |||
| <Form {...form}> | |||
| <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> | |||
| <FormField | |||
| control={form.control} | |||
| name="icon" | |||
| render={({ field }) => ( | |||
| <div className="space-y-6"> | |||
| <FormItem className="w-full"> | |||
| <FormLabel>{t('assistantAvatar')}</FormLabel> | |||
| <FormControl> | |||
| <FileUploader | |||
| value={field.value} | |||
| onValueChange={field.onChange} | |||
| maxFileCount={1} | |||
| maxSize={4 * 1024 * 1024} | |||
| // progresses={progresses} | |||
| // pass the onUpload function here for direct upload | |||
| // onUpload={uploadFiles} | |||
| // disabled={isUploading} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| </div> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="name" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('assistantName')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name="description" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('description')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name={'prompt_config.empty_response'} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('emptyResponse')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name={'prompt_config.prologue'} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('setAnOpener')}</FormLabel> | |||
| <FormControl> | |||
| <Input {...field}></Input> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <SwitchFormField | |||
| name={'prompt_config.quote'} | |||
| label={t('quote')} | |||
| ></SwitchFormField> | |||
| <SwitchFormField | |||
| name={'prompt_config.keyword'} | |||
| label={t('keyword')} | |||
| ></SwitchFormField> | |||
| <SwitchFormField | |||
| name={'prompt_config.tts'} | |||
| label={t('tts')} | |||
| ></SwitchFormField> | |||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | |||
| </form> | |||
| </Form> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| import { Subhead } from './subhead'; | |||
| export function ChatModelSettings() { | |||
| return ( | |||
| <section> | |||
| <Subhead>Model Setting</Subhead> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| import { Subhead } from './subhead'; | |||
| export function ChatPromptEngine() { | |||
| return ( | |||
| <section> | |||
| <Subhead>Prompt Engine</Subhead> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| import ChatBasicSetting from './chat-basic-settings'; | |||
| import { ChatModelSettings } from './chat-model-settings'; | |||
| import { ChatPromptEngine } from './chat-prompt-engine'; | |||
| export function AppSettings() { | |||
| return ( | |||
| <section className="p-6 w-[500px] max-w-[25%]"> | |||
| <div className="text-2xl font-bold mb-4 text-colors-text-neutral-strong"> | |||
| App settings | |||
| </div> | |||
| <ChatBasicSetting></ChatBasicSetting> | |||
| <ChatPromptEngine></ChatPromptEngine> | |||
| <ChatModelSettings></ChatModelSettings> | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| import { PropsWithChildren } from 'react'; | |||
| export function Subhead({ children }: PropsWithChildren) { | |||
| return ( | |||
| <div className="text-xl font-bold mb-4 text-colors-text-neutral-strong"> | |||
| {children} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,38 @@ | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormLabel, | |||
| } from '@/components/ui/form'; | |||
| import { Switch } from '@/components/ui/switch'; | |||
| import { ReactNode } from 'react'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| interface SwitchFormItemProps { | |||
| name: string; | |||
| label: ReactNode; | |||
| } | |||
| export function SwitchFormField({ label, name }: SwitchFormItemProps) { | |||
| const form = useFormContext(); | |||
| return ( | |||
| <FormField | |||
| control={form.control} | |||
| name={name} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex justify-between"> | |||
| <FormLabel className="text-base">{label}</FormLabel> | |||
| <FormControl> | |||
| <Switch | |||
| checked={field.value} | |||
| onCheckedChange={field.onChange} | |||
| aria-readonly | |||
| className="!m-0" | |||
| /> | |||
| </FormControl> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| ); | |||
| } | |||