### What problem does this PR solve? feat: Add component Invoke #2908 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.13.0
@@ -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", |
@@ -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", |
@@ -0,0 +1,15 @@ | |||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" | |||
y="0px" width="200" height="200" viewBox="0 0 1024 1024" enable-background="new 0 0 200 200" xml:space="preserve"> | |||
<image id="image0" width="1024" height="1024" x="0" y="0" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAIGNIUk0AAHomAACAhAAA+gAAAIDo | |||
AAB1MAAA6mAAADqYAAAXcJy6UTwAAABvUExUReb9E6S0FHJ8FbHDFCwuF1hfFomWFRgYGFFYFrLD | |||
FM7jE7zPFLXGFCcpF4uYFNvxE9PoE6KyFBwcF8LVE1deFkRJFuT7E83hEyMkF42bFMndEyUmF8nc | |||
E+L5E5uqFDI1F9nuE0VKFrHDE7zOE/////VUQJUAAAABYktHRCS0BvmZAAAAB3RJTUUH6AocBw8U | |||
/rEd/gAAAIVJREFUSMftkkkKgDAMRZ3qPFvnWe9/Rxtd2U0bEEHsW71CP4GfaJpC8Rt0g8MUBAyL | |||
3LBsUYDc3+T5gAO4oN6pooAfMMIoZpqABqlUV1leINulZYUL1E2LHNE2NS5QlRQ5ouszqX+DB4zM | |||
pvlUXxC4FreArlKLe+GWsOe92Rw7sl2F4sMcX94Fwx5NOjAAAAAldEVYdGRhdGU6Y3JlYXRlADIw | |||
MjQtMTAtMjhUMDc6MTU6MjArMDA6MDCKhNSdAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDI0LTEwLTI4 | |||
VDA3OjE1OjIwKzAwOjAw+9lsIQAAACh0RVh0ZGF0ZTp0aW1lc3RhbXAAMjAyNC0xMC0yOFQwNzox | |||
NToyMCswMDowMKzMTf4AAAAASUVORK5CYII=" /> | |||
</svg> |
@@ -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', |
@@ -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”', |
@@ -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', |
@@ -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 = [ |
@@ -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 = () => <div></div>; |
@@ -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<IInvokeVariable>['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 ( | |||
<Select | |||
style={{ width: '100%' }} | |||
allowClear | |||
options={options} | |||
value={text} | |||
disabled={trim(record.value) !== ''} | |||
onChange={handleComponentIdChange(record)} | |||
/> | |||
); | |||
}, | |||
}, | |||
{ | |||
title: t('value'), | |||
dataIndex: 'value', | |||
key: 'value', | |||
align: 'center', | |||
width: 140, | |||
render(text, record) { | |||
return ( | |||
<Input | |||
value={text} | |||
disabled={!!record.component_id} | |||
onChange={handleValueChange(record)} | |||
/> | |||
); | |||
}, | |||
}, | |||
{ | |||
title: t('operation'), | |||
dataIndex: 'operation', | |||
width: 20, | |||
key: 'operation', | |||
align: 'center', | |||
fixed: 'right', | |||
render(_, record) { | |||
return <DeleteOutlined onClick={handleRemove(record.id)} />; | |||
}, | |||
}, | |||
]; | |||
return ( | |||
<section> | |||
<Flex justify="end"> | |||
<Button size="small" onClick={handleAdd}> | |||
{t('add')} | |||
</Button> | |||
</Flex> | |||
<Table | |||
dataSource={dataSource} | |||
columns={columns} | |||
rowKey={'id'} | |||
className={styles.variableTable} | |||
components={components} | |||
rowClassName={() => styles.editableRow} | |||
scroll={{ x: true }} | |||
bordered | |||
/> | |||
</section> | |||
); | |||
}; | |||
export default DynamicVariables; |
@@ -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<HTMLInputElement> => | |||
(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, | |||
}; | |||
}; |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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 ( | |||
<Space> | |||
<InputNumber value={value} onChange={onChange} /> {t('common.s')} | |||
</Space> | |||
); | |||
}; | |||
const InvokeForm = ({ onValuesChange, form, node }: IOperatorForm) => { | |||
const { t } = useTranslation(); | |||
useSetLlmSetting(form); | |||
return ( | |||
<> | |||
<Form | |||
name="basic" | |||
autoComplete="off" | |||
form={form} | |||
onValuesChange={onValuesChange} | |||
layout={'vertical'} | |||
> | |||
<Form.Item name={'url'} label={t('flow.url')}> | |||
<Input /> | |||
</Form.Item> | |||
<Form.Item | |||
name={'method'} | |||
label={t('flow.method')} | |||
initialValue={Method.GET} | |||
> | |||
<Select options={MethodOptions} /> | |||
</Form.Item> | |||
<Form.Item name={'timeout'} label={t('flow.timeout')}> | |||
<TimeoutInput></TimeoutInput> | |||
</Form.Item> | |||
<Form.Item name={'headers'} label={t('flow.headers')}> | |||
<Editor height={200} defaultLanguage="json" theme="vs-dark" /> | |||
</Form.Item> | |||
<Form.Item name={'proxy'} label={t('flow.proxy')}> | |||
<Input /> | |||
</Form.Item> | |||
<DynamicVariables nodeId={node?.id}></DynamicVariables> | |||
</Form> | |||
</> | |||
); | |||
}; | |||
export default InvokeForm; |
@@ -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]); | |||
@@ -51,6 +51,10 @@ export interface IGenerateParameter { | |||
component_id?: string; | |||
} | |||
export interface IInvokeVariable extends IGenerateParameter { | |||
value?: string; | |||
} | |||
export type ICategorizeItemResult = Record< | |||
string, | |||
Omit<ICategorizeItem, 'name'> |