### 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
| @@ -0,0 +1,44 @@ | |||
| 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> | |||
| )} | |||
| /> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -1,44 +1,45 @@ | |||
| 'use client'; | |||
| 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 { 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 ( | |||
| <RadioGroupPrimitive.Root | |||
| className={cn('grid gap-2', className)} | |||
| data-slot="radio-group" | |||
| className={cn('grid gap-3', className)} | |||
| {...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 ( | |||
| <RadioGroupPrimitive.Item | |||
| ref={ref} | |||
| data-slot="radio-group-item" | |||
| 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, | |||
| )} | |||
| {...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.Item> | |||
| ); | |||
| }); | |||
| RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; | |||
| } | |||
| export { RadioGroup, RadioGroupItem }; | |||
| @@ -48,6 +48,7 @@ export const enum AgentApiAction { | |||
| FetchVersion = 'fetchVersion', | |||
| FetchAgentAvatar = 'fetchAgentAvatar', | |||
| FetchExternalAgentInputs = 'fetchExternalAgentInputs', | |||
| SetAgentSetting = 'setAgentSetting', | |||
| } | |||
| export const EmptyDsl = { | |||
| @@ -613,3 +614,30 @@ export const useFetchExternalAgentInputs = () => { | |||
| 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 }; | |||
| }; | |||
| @@ -32,7 +32,7 @@ export declare interface IFlow { | |||
| canvas_type: null; | |||
| create_date: string; | |||
| create_time: number; | |||
| description: null; | |||
| description: string; | |||
| dsl: DSL; | |||
| id: string; | |||
| title: string; | |||
| @@ -27,6 +27,7 @@ import { | |||
| LaptopMinimalCheck, | |||
| Logs, | |||
| ScreenShare, | |||
| Settings, | |||
| Upload, | |||
| } from 'lucide-react'; | |||
| import { ComponentPropsWithoutRef, useCallback } from 'react'; | |||
| @@ -43,6 +44,7 @@ import { | |||
| useWatchAgentChange, | |||
| } from './hooks/use-save-graph'; | |||
| import { useShowEmbedModal } from './hooks/use-show-dialog'; | |||
| import { SettingDialog } from './setting-dialog'; | |||
| import { UploadAgentDialog } from './upload-agent-dialog'; | |||
| import { useAgentHistoryManager } from './use-agent-history-manager'; | |||
| import { VersionDialog } from './version-dialog'; | |||
| @@ -92,6 +94,12 @@ export default function Agent() { | |||
| showModal: showVersionDialog, | |||
| } = useSetModalState(); | |||
| const { | |||
| visible: settingDialogVisible, | |||
| hideModal: hideSettingDialog, | |||
| showModal: showSettingDialog, | |||
| } = useSetModalState(); | |||
| const { showEmbedModal, hideEmbedModal, embedVisible, beta } = | |||
| useShowEmbedModal(); | |||
| const { navigateToAgentLogs } = useNavigatePage(); | |||
| @@ -149,11 +157,6 @@ export default function Agent() { | |||
| </Button> | |||
| </DropdownMenuTrigger> | |||
| <DropdownMenuContent> | |||
| {/* <AgentDropdownMenuItem onClick={openDocument}> | |||
| <Key /> | |||
| API | |||
| </AgentDropdownMenuItem> */} | |||
| {/* <DropdownMenuSeparator /> */} | |||
| <AgentDropdownMenuItem onClick={handleImportJson}> | |||
| <Download /> | |||
| {t('flow.import')} | |||
| @@ -163,6 +166,11 @@ export default function Agent() { | |||
| <Upload /> | |||
| {t('flow.export')} | |||
| </AgentDropdownMenuItem> | |||
| <DropdownMenuSeparator /> | |||
| <AgentDropdownMenuItem onClick={showSettingDialog}> | |||
| <Settings /> | |||
| {t('flow.setting')} | |||
| </AgentDropdownMenuItem> | |||
| {location.hostname !== 'demo.ragflow.io' && ( | |||
| <> | |||
| <DropdownMenuSeparator /> | |||
| @@ -201,6 +209,9 @@ export default function Agent() { | |||
| {versionDialogVisible && ( | |||
| <VersionDialog hideModal={hideVersionDialog}></VersionDialog> | |||
| )} | |||
| {settingDialogVisible && ( | |||
| <SettingDialog hideModal={hideSettingDialog}></SettingDialog> | |||
| )} | |||
| </section> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,53 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,158 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| import { MoreButton } from '@/components/more-button'; | |||
| import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | |||
| import { SharedBadge } from '@/components/shared-badge'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| import { IFlow } from '@/interfaces/database/flow'; | |||
| @@ -24,6 +25,7 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) { | |||
| avatar={data.avatar} | |||
| name={data.title || 'CN'} | |||
| ></RAGFlowAvatar> | |||
| <SharedBadge>{data.nickname}</SharedBadge> | |||
| </div> | |||
| <AgentDropdown | |||
| showAgentRenameModal={showAgentRenameModal} | |||
| @@ -59,6 +59,7 @@ module.exports = { | |||
| 'bg-base': 'var(--bg-base)', | |||
| 'bg-card': 'var(--bg-card)', | |||
| 'text-primary': 'var(--text-primary)', | |||
| 'text-secondary': 'var(--text-secondary)', | |||
| 'text-disabled': 'var(--text-disabled)', | |||
| 'text-input-tip': 'var(--text-input-tip)', | |||
| 'border-default': 'var(--border-default)', | |||