From 7423a5806e69d0c95f8f2e4b4836bb91739a315a Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Mon, 10 Nov 2025 10:16:12 +0800 Subject: [PATCH] Feature: Added global variable functionality #10703 (#11117) ### What problem does this PR solve? Feature: Added global variable functionality ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/components/dynamic-form.tsx | 30 +++ web/src/interfaces/database/agent.ts | 8 + web/src/locales/en.ts | 5 + web/src/locales/zh.ts | 4 + .../agent/gobal-variable-sheet/contant.ts | 73 ++++++ .../agent/gobal-variable-sheet/index.tsx | 221 ++++++++++++++++++ web/src/pages/agent/hooks/use-build-dsl.ts | 13 +- web/src/pages/agent/hooks/use-save-graph.ts | 8 +- web/src/pages/agent/index.tsx | 20 ++ web/src/pages/agent/utils.ts | 24 ++ 10 files changed, 402 insertions(+), 4 deletions(-) create mode 100644 web/src/pages/agent/gobal-variable-sheet/contant.ts create mode 100644 web/src/pages/agent/gobal-variable-sheet/index.tsx diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index e770e0d44..e13c05fa1 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -70,6 +70,10 @@ interface DynamicFormProps { className?: string; children?: React.ReactNode; defaultValues?: DefaultValues; + onFieldUpdate?: ( + fieldName: string, + updatedField: Partial, + ) => void; } // Form ref interface @@ -77,6 +81,8 @@ export interface DynamicFormRef { submit: () => void; getValues: () => any; reset: (values?: any) => void; + watch: (field: string, callback: (value: any) => void) => () => void; + updateFieldType: (fieldName: string, newType: FormFieldType) => void; } // Generate Zod validation schema based on field configurations @@ -277,6 +283,7 @@ const DynamicForm = { className = '', children, defaultValues: formDefaultValues = {} as DefaultValues, + onFieldUpdate, }: DynamicFormProps, ref: React.Ref, ) => { @@ -311,6 +318,29 @@ const DynamicForm = { setError: form.setError, clearErrors: form.clearErrors, trigger: form.trigger, + watch: (field: string, callback: (value: any) => void) => { + const { unsubscribe } = form.watch((values: any) => { + if (values && values[field] !== undefined) { + callback(values[field]); + } + }); + return unsubscribe; + }, + + onFieldUpdate: ( + fieldName: string, + updatedField: Partial, + ) => { + setTimeout(() => { + if (onFieldUpdate) { + onFieldUpdate(fieldName, updatedField); + } else { + console.warn( + 'onFieldUpdate prop is not provided. Cannot update field type.', + ); + } + }, 0); + }, })); useEffect(() => { diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index a1dbcfa20..1733b4e74 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -45,6 +45,7 @@ export interface DSL { messages?: Message[]; reference?: IReference[]; globals: Record; + variables: Record; retrieval: IReference[]; } @@ -283,3 +284,10 @@ export interface IPipeLineListRequest { desc?: boolean; canvas_category?: AgentCategory; } + +export interface GobalVariableType { + name: string; + value: any; + description: string; + type: string; +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index f4449857c..49541b286 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -981,6 +981,11 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s pleaseUploadAtLeastOneFile: 'Please upload at least one file', }, flow: { + variableNameMessage: + 'Variable name can only contain letters and underscores', + variableDescription: 'Variable Description', + defaultValue: 'Default Value', + gobalVariable: 'Global Variable', recommended: 'Recommended', customerSupport: 'Customer Support', marketing: 'Marketing', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 1b3dabd00..1615544eb 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -930,6 +930,10 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 pleaseUploadAtLeastOneFile: '请上传至少一个文件', }, flow: { + variableNameMessage: '名称只能包含字母和下划线', + variableDescription: '变量的描述', + defaultValue: '默认值', + gobalVariable: '全局变量', recommended: '推荐', customerSupport: '客户支持', marketing: '营销', diff --git a/web/src/pages/agent/gobal-variable-sheet/contant.ts b/web/src/pages/agent/gobal-variable-sheet/contant.ts new file mode 100644 index 000000000..2f3bd395f --- /dev/null +++ b/web/src/pages/agent/gobal-variable-sheet/contant.ts @@ -0,0 +1,73 @@ +import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form'; +import { buildSelectOptions } from '@/utils/component-util'; +import { t } from 'i18next'; +// const TypesWithoutArray = Object.values(JsonSchemaDataType).filter( +// (item) => item !== JsonSchemaDataType.Array, +// ); +// const TypesWithArray = [ +// ...TypesWithoutArray, +// ...TypesWithoutArray.map((item) => `array<${item}>`), +// ]; + +export enum TypesWithArray { + String = 'string', + Number = 'number', + Boolean = 'boolean', + // Object = 'object', + // ArrayString = 'array', + // ArrayNumber = 'array', + // ArrayBoolean = 'array', + // ArrayObject = 'array', +} + +export const GobalFormFields = [ + { + label: t('flow.name'), + name: 'name', + placeholder: t('common.namePlaceholder'), + required: true, + validation: { + pattern: /^[a-zA-Z_]+$/, + message: t('flow.variableNameMessage'), + }, + type: FormFieldType.Text, + }, + { + label: t('flow.type'), + name: 'type', + placeholder: '', + required: true, + type: FormFieldType.Select, + options: buildSelectOptions(Object.values(TypesWithArray)), + }, + { + label: t('flow.defaultValue'), + name: 'value', + placeholder: '', + type: FormFieldType.Textarea, + }, + { + label: t('flow.description'), + name: 'description', + placeholder: t('flow.variableDescription'), + type: 'textarea', + }, +] as FormFieldConfig[]; + +export const GobalVariableFormDefaultValues = { + name: '', + type: TypesWithArray.String, + value: '', + description: '', +}; + +export const TypeMaps = { + [TypesWithArray.String]: FormFieldType.Textarea, + [TypesWithArray.Number]: FormFieldType.Number, + [TypesWithArray.Boolean]: FormFieldType.Checkbox, + // [TypesWithArray.Object]: FormFieldType.Textarea, + // [TypesWithArray.ArrayString]: FormFieldType.Textarea, + // [TypesWithArray.ArrayNumber]: FormFieldType.Textarea, + // [TypesWithArray.ArrayBoolean]: FormFieldType.Textarea, + // [TypesWithArray.ArrayObject]: FormFieldType.Textarea, +}; diff --git a/web/src/pages/agent/gobal-variable-sheet/index.tsx b/web/src/pages/agent/gobal-variable-sheet/index.tsx new file mode 100644 index 000000000..8d7ab3afe --- /dev/null +++ b/web/src/pages/agent/gobal-variable-sheet/index.tsx @@ -0,0 +1,221 @@ +import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog'; +import { + DynamicForm, + DynamicFormRef, + FormFieldConfig, +} from '@/components/dynamic-form'; +import { Button } from '@/components/ui/button'; +import { Modal } from '@/components/ui/modal/modal'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useFetchAgent } from '@/hooks/use-agent-request'; +import { GobalVariableType } from '@/interfaces/database/agent'; +import { cn } from '@/lib/utils'; +import { t } from 'i18next'; +import { Trash2 } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { FieldValues } from 'react-hook-form'; +import { useSaveGraph } from '../hooks/use-save-graph'; +import { + GobalFormFields, + GobalVariableFormDefaultValues, + TypeMaps, + TypesWithArray, +} from './contant'; + +export type IGobalParamModalProps = { + data: any; + hideModal: (open: boolean) => void; +}; +export const GobalParamSheet = (props: IGobalParamModalProps) => { + const { hideModal } = props; + const { data, refetch } = useFetchAgent(); + const [fields, setFields] = useState(GobalFormFields); + const { visible, showModal, hideModal: hideAddModal } = useSetModalState(); + const [defaultValues, setDefaultValues] = useState( + GobalVariableFormDefaultValues, + ); + const formRef = useRef(null); + + const handleFieldUpdate = ( + fieldName: string, + updatedField: Partial, + ) => { + setFields((prevFields) => + prevFields.map((field) => + field.name === fieldName ? { ...field, ...updatedField } : field, + ), + ); + }; + + useEffect(() => { + const typefileld = fields.find((item) => item.name === 'type'); + + if (typefileld) { + typefileld.onChange = (value) => { + // setWatchType(value); + handleFieldUpdate('value', { + type: TypeMaps[value as keyof typeof TypeMaps], + }); + const values = formRef.current?.getValues(); + setTimeout(() => { + switch (value) { + case TypesWithArray.Boolean: + setDefaultValues({ ...values, value: false }); + break; + case TypesWithArray.Number: + setDefaultValues({ ...values, value: 0 }); + break; + default: + setDefaultValues({ ...values, value: '' }); + } + }, 0); + }; + } + }, [fields]); + + const { saveGraph, loading } = useSaveGraph(); + + const handleSubmit = (value: FieldValues) => { + const param = { + ...(data.dsl?.variables || {}), + [value.name]: value, + } as Record; + saveGraph(undefined, { + gobalVariables: param, + }); + if (!loading) { + setTimeout(() => { + refetch(); + }, 500); + } + hideAddModal(); + }; + + const handleDeleteGobalVariable = (key: string) => { + const param = { + ...(data.dsl?.variables || {}), + } as Record; + delete param[key]; + saveGraph(undefined, { + gobalVariables: param, + }); + refetch(); + }; + + const handleEditGobalVariable = (item: FieldValues) => { + setDefaultValues(item); + showModal(); + }; + return ( + <> + + e.preventDefault()} + > + + + {t('flow.gobalVariable')} + + + +
+ +
+ +
+ {data?.dsl?.variables && + Object.keys(data.dsl.variables).map((key) => { + const item = data.dsl.variables[key]; + return ( +
{ + handleEditGobalVariable(item); + }} + > +
+
+ {item.name} + + {item.type} + +
+
+ {item.value} +
+
+
+ handleDeleteGobalVariable(key)} + > + + +
+
+ ); + })} +
+
+ + { + console.log(data); + }} + defaultValues={defaultValues} + onFieldUpdate={handleFieldUpdate} + > +
+ { + hideAddModal?.(); + }} + /> + { + handleSubmit(values); + // console.log(values); + // console.log(nodes, edges); + // handleOk(values); + }} + /> +
+
+
+
+ + ); +}; diff --git a/web/src/pages/agent/hooks/use-build-dsl.ts b/web/src/pages/agent/hooks/use-build-dsl.ts index f164c67b4..85c87daed 100644 --- a/web/src/pages/agent/hooks/use-build-dsl.ts +++ b/web/src/pages/agent/hooks/use-build-dsl.ts @@ -1,16 +1,20 @@ import { useFetchAgent } from '@/hooks/use-agent-request'; +import { GobalVariableType } from '@/interfaces/database/agent'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { useCallback } from 'react'; import { Operator } from '../constant'; import useGraphStore from '../store'; -import { buildDslComponentsByGraph } from '../utils'; +import { buildDslComponentsByGraph, buildDslGobalVariables } from '../utils'; export const useBuildDslData = () => { const { data } = useFetchAgent(); const { nodes, edges } = useGraphStore((state) => state); const buildDslData = useCallback( - (currentNodes?: RAGFlowNodeType[]) => { + ( + currentNodes?: RAGFlowNodeType[], + otherParam?: { gobalVariables: Record }, + ) => { const nodesToProcess = currentNodes ?? nodes; // Filter out placeholder nodes and related edges @@ -37,8 +41,13 @@ export const useBuildDslData = () => { data.dsl.components, ); + const gobalVariables = buildDslGobalVariables( + data.dsl, + otherParam?.gobalVariables, + ); return { ...data.dsl, + ...gobalVariables, graph: { nodes: filteredNodes, edges: filteredEdges }, components: dslComponents, }; diff --git a/web/src/pages/agent/hooks/use-save-graph.ts b/web/src/pages/agent/hooks/use-save-graph.ts index 8ce4dc15e..fc5bf0c0a 100644 --- a/web/src/pages/agent/hooks/use-save-graph.ts +++ b/web/src/pages/agent/hooks/use-save-graph.ts @@ -3,6 +3,7 @@ import { useResetAgent, useSetAgent, } from '@/hooks/use-agent-request'; +import { GobalVariableType } from '@/interfaces/database/agent'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { formatDate } from '@/utils/date'; import { useDebounceEffect } from 'ahooks'; @@ -18,11 +19,14 @@ export const useSaveGraph = (showMessage: boolean = true) => { const { buildDslData } = useBuildDslData(); const saveGraph = useCallback( - async (currentNodes?: RAGFlowNodeType[]) => { + async ( + currentNodes?: RAGFlowNodeType[], + otherParam?: { gobalVariables: Record }, + ) => { return setAgent({ id, title: data.title, - dsl: buildDslData(currentNodes), + dsl: buildDslData(currentNodes, otherParam), }); }, [setAgent, data, id, buildDslData], diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index e497f14e2..e8c1155ed 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -38,6 +38,7 @@ import { useParams } from 'umi'; import AgentCanvas from './canvas'; import { DropdownProvider } from './canvas/context'; import { Operator } from './constant'; +import { GobalParamSheet } from './gobal-variable-sheet'; import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow'; import { useHandleExportJsonFile } from './hooks/use-export-json'; import { useFetchDataOnMount } from './hooks/use-fetch-data'; @@ -123,6 +124,12 @@ export default function Agent() { hideModal: hidePipelineLogSheet, } = useSetModalState(); + const { + visible: gobalParamSheetVisible, + showModal: showGobalParamSheet, + hideModal: hideGobalParamSheet, + } = useSetModalState(); + const { isParsing, logs, @@ -206,6 +213,13 @@ export default function Agent() { > {t('flow.save')} + showGobalParamSheet()} + loading={loading} + > + {t('flow.gobalVariable')} +