### What problem does this PR solve? Feat: Render agent setting dialog #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.1
| import { | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { ReactNode, cloneElement, isValidElement } from 'react'; | |||||
| import { ControllerRenderProps, useFormContext } from 'react-hook-form'; | |||||
| type RAGFlowFormItemProps = { | |||||
| name: string; | |||||
| label: ReactNode; | |||||
| tooltip?: ReactNode; | |||||
| children: ReactNode | ((field: ControllerRenderProps) => ReactNode); | |||||
| }; | |||||
| export function RAGFlowFormItem({ | |||||
| name, | |||||
| label, | |||||
| tooltip, | |||||
| children, | |||||
| }: RAGFlowFormItemProps) { | |||||
| const form = useFormContext(); | |||||
| return ( | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={name} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={tooltip}>{label}</FormLabel> | |||||
| <FormControl> | |||||
| {typeof children === 'function' | |||||
| ? children(field) | |||||
| : isValidElement(children) | |||||
| ? cloneElement(children, { ...field }) | |||||
| : children} | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| } |
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||||
| import { PropsWithChildren } from 'react'; | |||||
| export function SharedBadge({ children }: PropsWithChildren) { | |||||
| const { data: userInfo } = useFetchUserInfo(); | |||||
| if (typeof children === 'string' && userInfo.nickname === children) { | |||||
| return null; | |||||
| } | |||||
| return ( | |||||
| <span className="bg-text-secondary rounded-sm px-1 text-bg-base text-xs"> | |||||
| {children} | |||||
| </span> | |||||
| ); | |||||
| } |
| 'use client'; | 'use client'; | ||||
| import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; | ||||
| import { Circle } from 'lucide-react'; | |||||
| import { CircleIcon } from 'lucide-react'; | |||||
| import * as React from 'react'; | import * as React from 'react'; | ||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||
| const RadioGroup = React.forwardRef< | |||||
| React.ElementRef<typeof RadioGroupPrimitive.Root>, | |||||
| React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root> | |||||
| >(({ className, ...props }, ref) => { | |||||
| function RadioGroup({ | |||||
| className, | |||||
| ...props | |||||
| }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) { | |||||
| return ( | return ( | ||||
| <RadioGroupPrimitive.Root | <RadioGroupPrimitive.Root | ||||
| className={cn('grid gap-2', className)} | |||||
| data-slot="radio-group" | |||||
| className={cn('grid gap-3', className)} | |||||
| {...props} | {...props} | ||||
| ref={ref} | |||||
| /> | /> | ||||
| ); | ); | ||||
| }); | |||||
| RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; | |||||
| } | |||||
| const RadioGroupItem = React.forwardRef< | |||||
| React.ElementRef<typeof RadioGroupPrimitive.Item>, | |||||
| React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> | |||||
| >(({ className, ...props }, ref) => { | |||||
| function RadioGroupItem({ | |||||
| className, | |||||
| ...props | |||||
| }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) { | |||||
| return ( | return ( | ||||
| <RadioGroupPrimitive.Item | <RadioGroupPrimitive.Item | ||||
| ref={ref} | |||||
| data-slot="radio-group-item" | |||||
| className={cn( | className={cn( | ||||
| 'aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', | |||||
| 'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', | |||||
| className, | className, | ||||
| )} | )} | ||||
| {...props} | {...props} | ||||
| > | > | ||||
| <RadioGroupPrimitive.Indicator className="flex items-center justify-center"> | |||||
| <Circle className="h-2.5 w-2.5 fill-current text-current" /> | |||||
| <RadioGroupPrimitive.Indicator | |||||
| data-slot="radio-group-indicator" | |||||
| className="relative flex items-center justify-center" | |||||
| > | |||||
| <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" /> | |||||
| </RadioGroupPrimitive.Indicator> | </RadioGroupPrimitive.Indicator> | ||||
| </RadioGroupPrimitive.Item> | </RadioGroupPrimitive.Item> | ||||
| ); | ); | ||||
| }); | |||||
| RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; | |||||
| } | |||||
| export { RadioGroup, RadioGroupItem }; | export { RadioGroup, RadioGroupItem }; |
| FetchVersion = 'fetchVersion', | FetchVersion = 'fetchVersion', | ||||
| FetchAgentAvatar = 'fetchAgentAvatar', | FetchAgentAvatar = 'fetchAgentAvatar', | ||||
| FetchExternalAgentInputs = 'fetchExternalAgentInputs', | FetchExternalAgentInputs = 'fetchExternalAgentInputs', | ||||
| SetAgentSetting = 'setAgentSetting', | |||||
| } | } | ||||
| export const EmptyDsl = { | export const EmptyDsl = { | ||||
| return { data, loading, refetch }; | return { data, loading, refetch }; | ||||
| }; | }; | ||||
| export const useSetAgentSetting = () => { | |||||
| const { id } = useParams(); | |||||
| const queryClient = useQueryClient(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: [AgentApiAction.SetAgentSetting], | |||||
| mutationFn: async (params: any) => { | |||||
| const ret = await agentService.settingCanvas({ id, ...params }); | |||||
| if (ret?.data?.code === 0) { | |||||
| message.success('success'); | |||||
| queryClient.invalidateQueries({ | |||||
| queryKey: [AgentApiAction.FetchAgentDetail], | |||||
| }); | |||||
| } else { | |||||
| message.error(ret?.data?.data); | |||||
| } | |||||
| return ret?.data?.code; | |||||
| }, | |||||
| }); | |||||
| return { data, loading, setAgentSetting: mutateAsync }; | |||||
| }; |
| canvas_type: null; | canvas_type: null; | ||||
| create_date: string; | create_date: string; | ||||
| create_time: number; | create_time: number; | ||||
| description: null; | |||||
| description: string; | |||||
| dsl: DSL; | dsl: DSL; | ||||
| id: string; | id: string; | ||||
| title: string; | title: string; |
| LaptopMinimalCheck, | LaptopMinimalCheck, | ||||
| Logs, | Logs, | ||||
| ScreenShare, | ScreenShare, | ||||
| Settings, | |||||
| Upload, | Upload, | ||||
| } from 'lucide-react'; | } from 'lucide-react'; | ||||
| import { ComponentPropsWithoutRef, useCallback } from 'react'; | import { ComponentPropsWithoutRef, useCallback } from 'react'; | ||||
| useWatchAgentChange, | useWatchAgentChange, | ||||
| } from './hooks/use-save-graph'; | } from './hooks/use-save-graph'; | ||||
| import { useShowEmbedModal } from './hooks/use-show-dialog'; | import { useShowEmbedModal } from './hooks/use-show-dialog'; | ||||
| import { SettingDialog } from './setting-dialog'; | |||||
| import { UploadAgentDialog } from './upload-agent-dialog'; | import { UploadAgentDialog } from './upload-agent-dialog'; | ||||
| import { useAgentHistoryManager } from './use-agent-history-manager'; | import { useAgentHistoryManager } from './use-agent-history-manager'; | ||||
| import { VersionDialog } from './version-dialog'; | import { VersionDialog } from './version-dialog'; | ||||
| showModal: showVersionDialog, | showModal: showVersionDialog, | ||||
| } = useSetModalState(); | } = useSetModalState(); | ||||
| const { | |||||
| visible: settingDialogVisible, | |||||
| hideModal: hideSettingDialog, | |||||
| showModal: showSettingDialog, | |||||
| } = useSetModalState(); | |||||
| const { showEmbedModal, hideEmbedModal, embedVisible, beta } = | const { showEmbedModal, hideEmbedModal, embedVisible, beta } = | ||||
| useShowEmbedModal(); | useShowEmbedModal(); | ||||
| const { navigateToAgentLogs } = useNavigatePage(); | const { navigateToAgentLogs } = useNavigatePage(); | ||||
| </Button> | </Button> | ||||
| </DropdownMenuTrigger> | </DropdownMenuTrigger> | ||||
| <DropdownMenuContent> | <DropdownMenuContent> | ||||
| {/* <AgentDropdownMenuItem onClick={openDocument}> | |||||
| <Key /> | |||||
| API | |||||
| </AgentDropdownMenuItem> */} | |||||
| {/* <DropdownMenuSeparator /> */} | |||||
| <AgentDropdownMenuItem onClick={handleImportJson}> | <AgentDropdownMenuItem onClick={handleImportJson}> | ||||
| <Download /> | <Download /> | ||||
| {t('flow.import')} | {t('flow.import')} | ||||
| <Upload /> | <Upload /> | ||||
| {t('flow.export')} | {t('flow.export')} | ||||
| </AgentDropdownMenuItem> | </AgentDropdownMenuItem> | ||||
| <DropdownMenuSeparator /> | |||||
| <AgentDropdownMenuItem onClick={showSettingDialog}> | |||||
| <Settings /> | |||||
| {t('flow.setting')} | |||||
| </AgentDropdownMenuItem> | |||||
| {location.hostname !== 'demo.ragflow.io' && ( | {location.hostname !== 'demo.ragflow.io' && ( | ||||
| <> | <> | ||||
| <DropdownMenuSeparator /> | <DropdownMenuSeparator /> | ||||
| {versionDialogVisible && ( | {versionDialogVisible && ( | ||||
| <VersionDialog hideModal={hideVersionDialog}></VersionDialog> | <VersionDialog hideModal={hideVersionDialog}></VersionDialog> | ||||
| )} | )} | ||||
| {settingDialogVisible && ( | |||||
| <SettingDialog hideModal={hideSettingDialog}></SettingDialog> | |||||
| )} | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| } | } |
| import { ButtonLoading } from '@/components/ui/button'; | |||||
| import { | |||||
| Dialog, | |||||
| DialogContent, | |||||
| DialogFooter, | |||||
| DialogHeader, | |||||
| DialogTitle, | |||||
| } from '@/components/ui/dialog'; | |||||
| import { useSetAgentSetting } from '@/hooks/use-agent-request'; | |||||
| import { IModalProps } from '@/interfaces/common'; | |||||
| import { transformFile2Base64 } from '@/utils/file-util'; | |||||
| import { useCallback } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { | |||||
| AgentSettingId, | |||||
| SettingForm, | |||||
| SettingFormSchemaType, | |||||
| } from './setting-form'; | |||||
| export function SettingDialog({ hideModal }: IModalProps<any>) { | |||||
| const { t } = useTranslation(); | |||||
| const { setAgentSetting } = useSetAgentSetting(); | |||||
| const submit = useCallback( | |||||
| async (values: SettingFormSchemaType) => { | |||||
| const avatar = values.avatar; | |||||
| const code = await setAgentSetting({ | |||||
| ...values, | |||||
| avatar: avatar.length > 0 ? await transformFile2Base64(avatar[0]) : '', | |||||
| }); | |||||
| if (code === 0) { | |||||
| hideModal?.(); | |||||
| } | |||||
| }, | |||||
| [hideModal, setAgentSetting], | |||||
| ); | |||||
| return ( | |||||
| <Dialog open onOpenChange={hideModal}> | |||||
| <DialogContent> | |||||
| <DialogHeader> | |||||
| <DialogTitle>Are you absolutely sure?</DialogTitle> | |||||
| </DialogHeader> | |||||
| <SettingForm submit={submit}></SettingForm> | |||||
| <DialogFooter> | |||||
| <ButtonLoading type="submit" form={AgentSettingId} loading={false}> | |||||
| {t('common.save')} | |||||
| </ButtonLoading> | |||||
| </DialogFooter> | |||||
| </DialogContent> | |||||
| </Dialog> | |||||
| ); | |||||
| } |
| import { z } from 'zod'; | |||||
| import { | |||||
| FileUpload, | |||||
| FileUploadDropzone, | |||||
| FileUploadItem, | |||||
| FileUploadItemDelete, | |||||
| FileUploadItemMetadata, | |||||
| FileUploadItemPreview, | |||||
| FileUploadList, | |||||
| FileUploadTrigger, | |||||
| } from '@/components/file-upload'; | |||||
| import { RAGFlowFormItem } from '@/components/ragflow-form'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { Form, FormControl, FormItem, FormLabel } from '@/components/ui/form'; | |||||
| import { Input } from '@/components/ui/input'; | |||||
| import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; | |||||
| import { Textarea } from '@/components/ui/textarea'; | |||||
| import { useTranslate } from '@/hooks/common-hooks'; | |||||
| import { useFetchAgent } from '@/hooks/use-agent-request'; | |||||
| import { transformBase64ToFile } from '@/utils/file-util'; | |||||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||||
| import { CloudUpload, X } from 'lucide-react'; | |||||
| import { useEffect } from 'react'; | |||||
| import { useForm } from 'react-hook-form'; | |||||
| const formSchema = z.object({ | |||||
| title: z.string().min(1, {}), | |||||
| avatar: z.array(z.custom<File>()), | |||||
| description: z.string(), | |||||
| permission: z.string(), | |||||
| }); | |||||
| export type SettingFormSchemaType = z.infer<typeof formSchema>; | |||||
| export const AgentSettingId = 'agentSettingId'; | |||||
| type SettingFormProps = { | |||||
| submit: (values: SettingFormSchemaType) => void; | |||||
| }; | |||||
| export function SettingForm({ submit }: SettingFormProps) { | |||||
| const { t } = useTranslate('flow.settings'); | |||||
| const { data } = useFetchAgent(); | |||||
| const form = useForm<SettingFormSchemaType>({ | |||||
| resolver: zodResolver(formSchema), | |||||
| defaultValues: { | |||||
| title: '', | |||||
| permission: 'me', | |||||
| }, | |||||
| }); | |||||
| useEffect(() => { | |||||
| form.reset({ | |||||
| title: data?.title, | |||||
| description: data?.description, | |||||
| avatar: data.avatar ? [transformBase64ToFile(data.avatar)] : [], | |||||
| permission: data?.permission, | |||||
| }); | |||||
| }, [data, form]); | |||||
| return ( | |||||
| <Form {...form}> | |||||
| <form | |||||
| onSubmit={form.handleSubmit(submit)} | |||||
| className="space-y-8" | |||||
| id={AgentSettingId} | |||||
| > | |||||
| <RAGFlowFormItem name="title" label={t('title')}> | |||||
| <Input /> | |||||
| </RAGFlowFormItem> | |||||
| <RAGFlowFormItem name="avatar" label={t('photo')}> | |||||
| {(field) => ( | |||||
| <FileUpload | |||||
| value={field.value} | |||||
| onValueChange={field.onChange} | |||||
| accept="image/*" | |||||
| maxFiles={1} | |||||
| onFileReject={(_, message) => { | |||||
| form.setError('avatar', { | |||||
| message, | |||||
| }); | |||||
| }} | |||||
| multiple | |||||
| > | |||||
| <FileUploadDropzone className="flex-row flex-wrap border-dotted text-center"> | |||||
| <CloudUpload className="size-4" /> | |||||
| Drag and drop or | |||||
| <FileUploadTrigger asChild> | |||||
| <Button variant="link" size="sm" className="p-0"> | |||||
| choose files | |||||
| </Button> | |||||
| </FileUploadTrigger> | |||||
| to upload | |||||
| </FileUploadDropzone> | |||||
| <FileUploadList> | |||||
| {field.value?.map((file: File, index: number) => ( | |||||
| <FileUploadItem key={index} value={file}> | |||||
| <FileUploadItemPreview /> | |||||
| <FileUploadItemMetadata /> | |||||
| <FileUploadItemDelete asChild> | |||||
| <Button variant="ghost" size="icon" className="size-7"> | |||||
| <X /> | |||||
| <span className="sr-only">Delete</span> | |||||
| </Button> | |||||
| </FileUploadItemDelete> | |||||
| </FileUploadItem> | |||||
| ))} | |||||
| </FileUploadList> | |||||
| </FileUpload> | |||||
| )} | |||||
| </RAGFlowFormItem> | |||||
| <RAGFlowFormItem name="description" label={t('description')}> | |||||
| <Textarea rows={4} /> | |||||
| </RAGFlowFormItem> | |||||
| <RAGFlowFormItem | |||||
| name="permission" | |||||
| label={t('permissions')} | |||||
| tooltip={t('permissionsTip')} | |||||
| > | |||||
| {(field) => ( | |||||
| <RadioGroup | |||||
| onValueChange={field.onChange} | |||||
| value={field.value} | |||||
| className="flex" | |||||
| > | |||||
| <FormItem className="flex items-center gap-3"> | |||||
| <FormControl> | |||||
| <RadioGroupItem value="me" id="me" /> | |||||
| </FormControl> | |||||
| <FormLabel | |||||
| className="font-normal !m-0 cursor-pointer" | |||||
| htmlFor="me" | |||||
| > | |||||
| {t('me')} | |||||
| </FormLabel> | |||||
| </FormItem> | |||||
| <FormItem className="flex items-center gap-3"> | |||||
| <FormControl> | |||||
| <RadioGroupItem value="team" id="team" /> | |||||
| </FormControl> | |||||
| <FormLabel | |||||
| className="font-normal !m-0 cursor-pointer" | |||||
| htmlFor="team" | |||||
| > | |||||
| {t('team')} | |||||
| </FormLabel> | |||||
| </FormItem> | |||||
| </RadioGroup> | |||||
| )} | |||||
| </RAGFlowFormItem> | |||||
| </form> | |||||
| </Form> | |||||
| ); | |||||
| } |
| import { MoreButton } from '@/components/more-button'; | import { MoreButton } from '@/components/more-button'; | ||||
| import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | ||||
| import { SharedBadge } from '@/components/shared-badge'; | |||||
| import { Card, CardContent } from '@/components/ui/card'; | import { Card, CardContent } from '@/components/ui/card'; | ||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | ||||
| import { IFlow } from '@/interfaces/database/flow'; | import { IFlow } from '@/interfaces/database/flow'; | ||||
| avatar={data.avatar} | avatar={data.avatar} | ||||
| name={data.title || 'CN'} | name={data.title || 'CN'} | ||||
| ></RAGFlowAvatar> | ></RAGFlowAvatar> | ||||
| <SharedBadge>{data.nickname}</SharedBadge> | |||||
| </div> | </div> | ||||
| <AgentDropdown | <AgentDropdown | ||||
| showAgentRenameModal={showAgentRenameModal} | showAgentRenameModal={showAgentRenameModal} |
| 'bg-base': 'var(--bg-base)', | 'bg-base': 'var(--bg-base)', | ||||
| 'bg-card': 'var(--bg-card)', | 'bg-card': 'var(--bg-card)', | ||||
| 'text-primary': 'var(--text-primary)', | 'text-primary': 'var(--text-primary)', | ||||
| 'text-secondary': 'var(--text-secondary)', | |||||
| 'text-disabled': 'var(--text-disabled)', | 'text-disabled': 'var(--text-disabled)', | ||||
| 'text-input-tip': 'var(--text-input-tip)', | 'text-input-tip': 'var(--text-input-tip)', | ||||
| 'border-default': 'var(--border-default)', | 'border-default': 'var(--border-default)', |