### 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'> | |||