From 58a64000ea2850fdab8611baee19bb55e5d02fbb Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 8 Aug 2025 11:00:55 +0800 Subject: [PATCH] Feat: Render agent setting dialog #3221 (#9312) ### What problem does this PR solve? Feat: Render agent setting dialog #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/ragflow-form.tsx | 44 +++++ web/src/components/shared-badge.tsx | 16 ++ web/src/components/ui/radio-group.tsx | 39 ++--- web/src/hooks/use-agent-request.ts | 28 ++++ web/src/interfaces/database/flow.ts | 2 +- web/src/pages/agent/index.tsx | 21 ++- web/src/pages/agent/setting-dialog/index.tsx | 53 ++++++ .../agent/setting-dialog/setting-form.tsx | 158 ++++++++++++++++++ web/src/pages/agents/agent-card.tsx | 2 + web/tailwind.config.js | 1 + 10 files changed, 339 insertions(+), 25 deletions(-) create mode 100644 web/src/components/ragflow-form.tsx create mode 100644 web/src/components/shared-badge.tsx create mode 100644 web/src/pages/agent/setting-dialog/index.tsx create mode 100644 web/src/pages/agent/setting-dialog/setting-form.tsx diff --git a/web/src/components/ragflow-form.tsx b/web/src/components/ragflow-form.tsx new file mode 100644 index 000000000..488222714 --- /dev/null +++ b/web/src/components/ragflow-form.tsx @@ -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 ( + ( + + {label} + + {typeof children === 'function' + ? children(field) + : isValidElement(children) + ? cloneElement(children, { ...field }) + : children} + + + + )} + /> + ); +} diff --git a/web/src/components/shared-badge.tsx b/web/src/components/shared-badge.tsx new file mode 100644 index 000000000..a08379257 --- /dev/null +++ b/web/src/components/shared-badge.tsx @@ -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 ( + + {children} + + ); +} diff --git a/web/src/components/ui/radio-group.tsx b/web/src/components/ui/radio-group.tsx index f7e403a0f..f1e246ba7 100644 --- a/web/src/components/ui/radio-group.tsx +++ b/web/src/components/ui/radio-group.tsx @@ -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, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { +function RadioGroup({ + className, + ...props +}: React.ComponentProps) { return ( ); -}); -RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; +} -const RadioGroupItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { +function RadioGroupItem({ + className, + ...props +}: React.ComponentProps) { return ( - - + + ); -}); -RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; +} export { RadioGroup, RadioGroupItem }; diff --git a/web/src/hooks/use-agent-request.ts b/web/src/hooks/use-agent-request.ts index 40d92ab13..fb4015ba7 100644 --- a/web/src/hooks/use-agent-request.ts +++ b/web/src/hooks/use-agent-request.ts @@ -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 }; +}; diff --git a/web/src/interfaces/database/flow.ts b/web/src/interfaces/database/flow.ts index 2d4aa4cbd..95968114e 100644 --- a/web/src/interfaces/database/flow.ts +++ b/web/src/interfaces/database/flow.ts @@ -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; diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index fc776914e..e8017430c 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -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() { - {/* - - API - */} - {/* */} {t('flow.import')} @@ -163,6 +166,11 @@ export default function Agent() { {t('flow.export')} + + + + {t('flow.setting')} + {location.hostname !== 'demo.ragflow.io' && ( <> @@ -201,6 +209,9 @@ export default function Agent() { {versionDialogVisible && ( )} + {settingDialogVisible && ( + + )} ); } diff --git a/web/src/pages/agent/setting-dialog/index.tsx b/web/src/pages/agent/setting-dialog/index.tsx new file mode 100644 index 000000000..6d0e1e976 --- /dev/null +++ b/web/src/pages/agent/setting-dialog/index.tsx @@ -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) { + 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 ( + + + + Are you absolutely sure? + + + + + {t('common.save')} + + + + + ); +} diff --git a/web/src/pages/agent/setting-dialog/setting-form.tsx b/web/src/pages/agent/setting-dialog/setting-form.tsx new file mode 100644 index 000000000..d4fe0c07b --- /dev/null +++ b/web/src/pages/agent/setting-dialog/setting-form.tsx @@ -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()), + description: z.string(), + permission: z.string(), +}); + +export type SettingFormSchemaType = z.infer; + +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({ + 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 ( +
+ + + + + + {(field) => ( + { + form.setError('avatar', { + message, + }); + }} + multiple + > + + + Drag and drop or + + + + to upload + + + {field.value?.map((file: File, index: number) => ( + + + + + + + + ))} + + + )} + + +