diff --git a/web/package-lock.json b/web/package-lock.json index ea0b007b22..c809df444e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,6 +11,7 @@ "@ant-design/pro-layout": "^7.17.16", "@antv/g6": "^5.0.10", "@js-preview/excel": "^1.7.8", + "@monaco-editor/react": "^4.6.0", "@tanstack/react-query": "^5.40.0", "@tanstack/react-query-devtools": "^5.51.5", "ahooks": "^3.7.10", @@ -3843,6 +3844,30 @@ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmmirror.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -19956,6 +19981,12 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "peer": true + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/mri/-/mri-1.2.0.tgz", @@ -25827,6 +25858,11 @@ "resolved": "https://registry.npmmirror.com/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmmirror.com/static-extend/-/static-extend-0.1.2.tgz", diff --git a/web/package.json b/web/package.json index e4ecbb5018..c0493cd177 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "@ant-design/pro-layout": "^7.17.16", "@antv/g6": "^5.0.10", "@js-preview/excel": "^1.7.8", + "@monaco-editor/react": "^4.6.0", "@tanstack/react-query": "^5.40.0", "@tanstack/react-query-devtools": "^5.51.5", "ahooks": "^3.7.10", diff --git a/web/src/assets/svg/invoke-ai.svg b/web/src/assets/svg/invoke-ai.svg new file mode 100644 index 0000000000..783fad8132 --- /dev/null +++ b/web/src/assets/svg/invoke-ai.svg @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 402f9b2c85..6ed87c5b69 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -29,6 +29,7 @@ export default { move: 'Move', warn: 'Warn', action: 'Action', + s: 'S', }, login: { login: 'Sign in', @@ -1016,6 +1017,13 @@ The above is the content you need to summarize.`, note: 'Note', noteDescription: 'Note', notePlaceholder: 'Please enter a note', + invoke: 'Invoke', + invokeDescription: + 'This component can invoke remote end point call. Put the output of other components as parameters or set constant parameters to call remote functions.', + url: 'Url', + method: 'Method', + timeout: 'Timeout', + headers: 'Headers', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 9335c3619d..2d33907c8a 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -29,6 +29,7 @@ export default { move: '移動', warn: '提醒', action: '操作', + s: '秒', }, login: { login: '登入', @@ -965,6 +966,13 @@ export default { note: '註解', noteDescription: '註解', notePlaceholder: '請輸入註釋', + invoke: 'Invoke', + invokeDescription: + '此元件可以呼叫遠端端點呼叫。將其他元件的輸出作為參數或設定常數參數來呼叫遠端函數。', + url: '網址', + method: '方法', + timeout: '超時', + headers: '請求頭', }, footer: { profile: '“保留所有權利 @ react”', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index e0a32171cc..30f65f9151 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -29,6 +29,7 @@ export default { move: '移动', warn: '提醒', action: '操作', + s: '秒', }, login: { login: '登录', @@ -985,6 +986,13 @@ export default { note: '注释', noteDescription: '注释', notePlaceholder: '请输入注释', + invoke: 'Invoke', + invokeDescription: + '该组件可以调用远程端点调用。将其他组件的输出作为参数或设置常量参数来调用远程函数。', + url: 'Url', + method: '方法', + timeout: '超时', + headers: '请求头', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/pages/flow/constant.tsx b/web/src/pages/flow/constant.tsx index 2874b2135d..2131bfa3c0 100644 --- a/web/src/pages/flow/constant.tsx +++ b/web/src/pages/flow/constant.tsx @@ -12,6 +12,7 @@ import { ReactComponent as ExeSqlIcon } from '@/assets/svg/exesql.svg'; import { ReactComponent as GithubIcon } from '@/assets/svg/github.svg'; import { ReactComponent as GoogleScholarIcon } from '@/assets/svg/google-scholar.svg'; import { ReactComponent as GoogleIcon } from '@/assets/svg/google.svg'; +import { ReactComponent as InvokeIcon } from '@/assets/svg/invoke-ai.svg'; import { ReactComponent as Jin10Icon } from '@/assets/svg/jin10.svg'; import { ReactComponent as KeywordIcon } from '@/assets/svg/keyword.svg'; import { ReactComponent as NoteIcon } from '@/assets/svg/note.svg'; @@ -75,6 +76,7 @@ export enum Operator { TuShare = 'TuShare', Note = 'Note', Crawler = 'Crawler', + Invoke = 'Invoke', } export const CommonOperatorList = Object.values(Operator).filter( @@ -113,6 +115,7 @@ export const operatorIconMap = { [Operator.TuShare]: TuShareIcon, [Operator.Note]: NoteIcon, [Operator.Crawler]: CrawlerIcon, + [Operator.Invoke]: InvokeIcon, }; export const operatorMap: Record< @@ -239,6 +242,9 @@ export const operatorMap: Record< [Operator.Crawler]: { backgroundColor: '#dee0e2', }, + [Operator.Invoke]: { + backgroundColor: '#dee0e2', + }, }; export const componentMenuList = [ @@ -332,6 +338,9 @@ export const componentMenuList = [ { name: Operator.Crawler, }, + { + name: Operator.Invoke, + }, ]; export const initialRetrievalValues = { @@ -509,6 +518,18 @@ export const initialCrawlerValues = { extract_type: 'markdown', }; +export const initialInvokeValues = { + url: 'http://', + method: 'GET', + timeout: 60, + headers: `{ + "Accept": "*/*", + "Cache-Control": "no-cache", + "Connection": "keep-alive" +}`, + proxy: 'http://', +}; + export const CategorizeAnchorPointPositions = [ { top: 1, right: 34 }, { top: 8, right: 18 }, @@ -621,6 +642,7 @@ export const NodeMap = { [Operator.TuShare]: 'ragNode', [Operator.Note]: 'noteNode', [Operator.Crawler]: 'ragNode', + [Operator.Invoke]: 'ragNode', }; export const LanguageOptions = [ diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx index e85976bc9d..979e6e44a3 100644 --- a/web/src/pages/flow/flow-drawer/index.tsx +++ b/web/src/pages/flow/flow-drawer/index.tsx @@ -20,6 +20,7 @@ import GenerateForm from '../form/generate-form'; import GithubForm from '../form/github-form'; import GoogleForm from '../form/google-form'; import GoogleScholarForm from '../form/google-scholar-form'; +import InvokeForm from '../form/invoke-form'; import Jin10Form from '../form/jin10-form'; import KeywordExtractForm from '../form/keyword-extract-form'; import MessageForm from '../form/message-form'; @@ -74,6 +75,9 @@ const FormMap = { [Operator.Jin10]: Jin10Form, [Operator.TuShare]: TuShareForm, [Operator.Crawler]: CrawlerForm, + [Operator.Invoke]: InvokeForm, + [Operator.Concentrator]: <>, + [Operator.Note]: <>, }; const EmptyContent = () =>
; diff --git a/web/src/pages/flow/form/invoke-form/dynamic-variables.tsx b/web/src/pages/flow/form/invoke-form/dynamic-variables.tsx new file mode 100644 index 0000000000..edb6ed9b0a --- /dev/null +++ b/web/src/pages/flow/form/invoke-form/dynamic-variables.tsx @@ -0,0 +1,119 @@ +import { EditableCell, EditableRow } from '@/components/editable-cell'; +import { useTranslate } from '@/hooks/common-hooks'; +import { DeleteOutlined } from '@ant-design/icons'; +import { Button, Flex, Input, Select, Table, TableProps } from 'antd'; +import { useBuildComponentIdSelectOptions } from '../../hooks'; +import { IInvokeVariable } from '../../interface'; +import { useHandleOperateParameters } from './hooks'; + +import { trim } from 'lodash'; +import styles from './index.less'; + +interface IProps { + nodeId?: string; +} + +const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, +}; + +const DynamicVariables = ({ nodeId }: IProps) => { + const { t } = useTranslate('flow'); + + const options = useBuildComponentIdSelectOptions(nodeId); + const { + dataSource, + handleAdd, + handleRemove, + handleSave, + handleComponentIdChange, + handleValueChange, + } = useHandleOperateParameters(nodeId!); + + const columns: TableProps['columns'] = [ + { + title: t('key'), + dataIndex: 'key', + key: 'key', + // width: 40, + onCell: (record: IInvokeVariable) => ({ + record, + editable: true, + dataIndex: 'key', + title: 'key', + handleSave, + }), + }, + { + title: t('componentId'), + dataIndex: 'component_id', + key: 'component_id', + align: 'center', + width: 140, + render(text, record) { + return ( + + ); + }, + }, + { + title: t('operation'), + dataIndex: 'operation', + width: 20, + key: 'operation', + align: 'center', + fixed: 'right', + render(_, record) { + return ; + }, + }, + ]; + + return ( +
+ + + + styles.editableRow} + scroll={{ x: true }} + bordered + /> + + ); +}; + +export default DynamicVariables; diff --git a/web/src/pages/flow/form/invoke-form/hooks.ts b/web/src/pages/flow/form/invoke-form/hooks.ts new file mode 100644 index 0000000000..3dfd383a9a --- /dev/null +++ b/web/src/pages/flow/form/invoke-form/hooks.ts @@ -0,0 +1,87 @@ +import get from 'lodash/get'; +import { ChangeEventHandler, useCallback, useMemo } from 'react'; +import { v4 as uuid } from 'uuid'; +import { IGenerateParameter, IInvokeVariable } from '../../interface'; +import useGraphStore from '../../store'; + +export const useHandleOperateParameters = (nodeId: string) => { + const { getNode, updateNodeForm } = useGraphStore((state) => state); + const node = getNode(nodeId); + const dataSource: IGenerateParameter[] = useMemo( + () => get(node, 'data.form.variables', []) as IGenerateParameter[], + [node], + ); + + const changeValue = useCallback( + (row: IInvokeVariable, field: string, value: string) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.id === item.id); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + [field]: value, + }); + + updateNodeForm(nodeId, { variables: newData }); + }, + [dataSource, nodeId, updateNodeForm], + ); + + const handleComponentIdChange = useCallback( + (row: IInvokeVariable) => (value: string) => { + changeValue(row, 'component_id', value); + }, + [changeValue], + ); + + const handleValueChange = useCallback( + (row: IInvokeVariable): ChangeEventHandler => + (e) => { + changeValue(row, 'value', e.target.value); + }, + [changeValue], + ); + + const handleRemove = useCallback( + (id?: string) => () => { + const newData = dataSource.filter((item) => item.id !== id); + updateNodeForm(nodeId, { variables: newData }); + }, + [updateNodeForm, nodeId, dataSource], + ); + + const handleAdd = useCallback(() => { + updateNodeForm(nodeId, { + variables: [ + ...dataSource, + { + id: uuid(), + key: '', + component_id: undefined, + value: '', + }, + ], + }); + }, [dataSource, nodeId, updateNodeForm]); + + const handleSave = (row: IGenerateParameter) => { + const newData = [...dataSource]; + const index = newData.findIndex((item) => row.id === item.id); + const item = newData[index]; + newData.splice(index, 1, { + ...item, + ...row, + }); + + updateNodeForm(nodeId, { variables: newData }); + }; + + return { + handleAdd, + handleRemove, + handleComponentIdChange, + handleValueChange, + handleSave, + dataSource, + }; +}; diff --git a/web/src/pages/flow/form/invoke-form/index.less b/web/src/pages/flow/form/invoke-form/index.less new file mode 100644 index 0000000000..8de5a4f532 --- /dev/null +++ b/web/src/pages/flow/form/invoke-form/index.less @@ -0,0 +1,21 @@ +.variableTable { + margin-top: 14px; +} +.editableRow { + :global(.editable-cell) { + position: relative; + } + + :global(.editable-cell-value-wrap) { + padding: 5px 12px; + cursor: pointer; + height: 30px !important; + } + &:hover { + :global(.editable-cell-value-wrap) { + padding: 4px 11px; + border: 1px solid #d9d9d9; + border-radius: 2px; + } + } +} diff --git a/web/src/pages/flow/form/invoke-form/index.tsx b/web/src/pages/flow/form/invoke-form/index.tsx new file mode 100644 index 0000000000..cca7594164 --- /dev/null +++ b/web/src/pages/flow/form/invoke-form/index.tsx @@ -0,0 +1,72 @@ +import Editor from '@monaco-editor/react'; +import { Form, Input, InputNumber, Select, Space } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useSetLlmSetting } from '../../hooks'; +import { IOperatorForm } from '../../interface'; +import DynamicVariables from './dynamic-variables'; + +enum Method { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', +} + +const MethodOptions = [Method.GET, Method.POST, Method.PUT].map((x) => ({ + label: x, + value: x, +})); + +interface TimeoutInputProps { + value?: number; + onChange?: (value: number | null) => void; +} + +const TimeoutInput = ({ value, onChange }: TimeoutInputProps) => { + const { t } = useTranslation(); + return ( + + {t('common.s')} + + ); +}; + +const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => { + const { t } = useTranslation(); + + useSetLlmSetting(form); + + return ( + <> +
+ + + + + + + + + + ); +}; + +export default InvokeForm; diff --git a/web/src/pages/flow/hooks.ts b/web/src/pages/flow/hooks.ts index 3fffeca3cb..580f86f24d 100644 --- a/web/src/pages/flow/hooks.ts +++ b/web/src/pages/flow/hooks.ts @@ -49,6 +49,7 @@ import { initialGithubValues, initialGoogleScholarValues, initialGoogleValues, + initialInvokeValues, initialJin10Values, initialKeywordExtractValues, initialMessageValues, @@ -132,6 +133,7 @@ export const useInitializeOperatorParams = () => { [Operator.TuShare]: initialTuShareValues, [Operator.Note]: initialNoteValues, [Operator.Crawler]: initialCrawlerValues, + [Operator.Invoke]: initialInvokeValues, }; }, [llmId]); diff --git a/web/src/pages/flow/interface.ts b/web/src/pages/flow/interface.ts index da3479f9bc..dea2b405f1 100644 --- a/web/src/pages/flow/interface.ts +++ b/web/src/pages/flow/interface.ts @@ -51,6 +51,10 @@ export interface IGenerateParameter { component_id?: string; } +export interface IInvokeVariable extends IGenerateParameter { + value?: string; +} + export type ICategorizeItemResult = Record< string, Omit