ソースを参照

feat: add custom edge (#1061)

### What problem does this PR solve?
feat: add custom edge
feat: add flow card
feat: add store for canvas
#918 

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
tags/v0.8.0
balibabu 1年前
コミット
39ac3b1e60
コミッターのメールアドレスに関連付けられたアカウントが存在しません
42個のファイルの変更1559行の追加387行の削除
  1. 87
    203
      web/package-lock.json
  2. 4
    1
      web/package.json
  3. 16
    11
      web/src/app.tsx
  4. 37
    0
      web/src/components/knowledge-base-item.tsx
  5. 6
    0
      web/src/components/llm-setting-items/index.less
  6. 259
    0
      web/src/components/llm-setting-items/index.tsx
  7. 23
    0
      web/src/components/top-n-item.tsx
  8. 70
    0
      web/src/hooks/flow-hooks.ts
  9. 8
    4
      web/src/hooks/userSettingHook.ts
  10. 4
    4
      web/src/interfaces/database/flow.ts
  11. 1
    0
      web/src/locales/en.ts
  12. 2
    24
      web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx
  13. 9
    15
      web/src/pages/chat/chat-configuration-modal/model-setting.tsx
  14. 2
    15
      web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx
  15. 5
    0
      web/src/pages/flow/answer-form/index.tsx
  16. 47
    0
      web/src/pages/flow/begin-form/index.tsx
  17. 1
    1
      web/src/pages/flow/canvas/context-menu/index.tsx
  18. 15
    0
      web/src/pages/flow/canvas/edge/index.less
  19. 72
    0
      web/src/pages/flow/canvas/edge/index.tsx
  20. 4
    0
      web/src/pages/flow/canvas/index.less
  21. 42
    41
      web/src/pages/flow/canvas/index.tsx
  22. 10
    1
      web/src/pages/flow/canvas/node/index.less
  23. 15
    3
      web/src/pages/flow/canvas/node/index.tsx
  24. 6
    0
      web/src/pages/flow/constant.ts
  25. 32
    4
      web/src/pages/flow/flow-drawer/index.tsx
  26. 83
    0
      web/src/pages/flow/generate-form/index.tsx
  27. 63
    50
      web/src/pages/flow/hooks.ts
  28. 4
    1
      web/src/pages/flow/index.tsx
  29. 58
    0
      web/src/pages/flow/interface.ts
  30. 78
    0
      web/src/pages/flow/list/flow-card/index.less
  31. 94
    0
      web/src/pages/flow/list/flow-card/index.tsx
  32. 48
    0
      web/src/pages/flow/list/hooks.ts
  33. 48
    0
      web/src/pages/flow/list/index.less
  34. 53
    0
      web/src/pages/flow/list/index.tsx
  35. 15
    8
      web/src/pages/flow/mock.tsx
  36. 43
    0
      web/src/pages/flow/retrieval-form/index.tsx
  37. 106
    0
      web/src/pages/flow/store.ts
  38. 33
    0
      web/src/pages/flow/utils.ts
  39. 4
    0
      web/src/routes.ts
  40. 43
    0
      web/src/services/flow-service.ts
  41. 8
    0
      web/src/utils/api.ts
  42. 1
    1
      web/src/utils/registerServer.ts

+ 87
- 203
web/package-lock.json ファイルの表示

@@ -10,6 +10,7 @@
"@ant-design/pro-components": "^2.6.46",
"@ant-design/pro-layout": "^7.17.16",
"@js-preview/excel": "^1.7.8",
"@tanstack/react-query": "^5.40.0",
"ahooks": "^3.7.10",
"antd": "^5.12.7",
"axios": "^1.6.3",
@@ -39,10 +40,12 @@
"umi": "^4.0.90",
"umi-request": "^1.4.0",
"unist-util-visit-parents": "^6.0.1",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"zustand": "^4.5.2"
},
"devDependencies": {
"@react-dev-inspector/umi4-plugin": "^2.0.1",
"@redux-devtools/extension": "^3.3.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/dagre": "^0.7.52",
@@ -3915,40 +3918,6 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/background/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true
},
"node_modules/@reactflow/background/node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/controls": {
"version": "11.2.12",
"resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.12.tgz",
@@ -3963,40 +3932,6 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/controls/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true
},
"node_modules/@reactflow/controls/node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/core": {
"version": "11.11.2",
"resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.2.tgz",
@@ -4017,40 +3952,6 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/core/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true
},
"node_modules/@reactflow/core/node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/minimap": {
"version": "11.7.12",
"resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.12.tgz",
@@ -4069,40 +3970,6 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/minimap/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true
},
"node_modules/@reactflow/minimap/node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-resizer": {
"version": "2.2.12",
"resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.12.tgz",
@@ -4119,40 +3986,6 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-resizer/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true
},
"node_modules/@reactflow/node-resizer/node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/@reactflow/node-toolbar": {
"version": "1.3.12",
"resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.12.tgz",
@@ -4167,38 +4000,17 @@
"react-dom": ">=17"
}
},
"node_modules/@reactflow/node-toolbar/node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true
},
"node_modules/@reactflow/node-toolbar/node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"node_modules/@redux-devtools/extension": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/@redux-devtools/extension/-/extension-3.3.0.tgz",
"integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==",
"dev": true,
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
"@babel/runtime": "^7.23.2",
"immutable": "^4.3.4"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
"redux": "^3.1.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@rgrove/parse-xml": {
@@ -4441,6 +4253,30 @@
"integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==",
"dev": true
},
"node_modules/@tanstack/react-query": {
"version": "5.40.0",
"resolved": "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.40.0.tgz",
"integrity": "sha512-iv/W0Axc4aXhFzkrByToE1JQqayxTPNotCoSCnarR/A1vDIHaoKpg7FTIfP3Ev2mbKn1yrxq0ZKYUdLEJxs6Tg==",
"dependencies": {
"@tanstack/query-core": "5.40.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-query/node_modules/@tanstack/query-core": {
"version": "5.40.0",
"resolved": "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.40.0.tgz",
"integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.1.0",
"resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.1.0.tgz",
@@ -6692,6 +6528,16 @@
"value-equal": "^1.0.1"
}
},
"node_modules/@umijs/plugins/node_modules/immer": {
"version": "8.0.4",
"resolved": "https://registry.npmmirror.com/immer/-/immer-8.0.4.tgz",
"integrity": "sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@umijs/plugins/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-0.0.1.tgz",
@@ -13621,9 +13467,20 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immer": {
"version": "8.0.4",
"resolved": "https://registry.npmmirror.com/immer/-/immer-8.0.4.tgz",
"integrity": "sha512-jMfL18P+/6P6epANRvRk6q8t+3gGhqsJ9EuJ25AXE+9bNTYtssvzeYbEd0mXRYWCmmXSIbnlpz6vd6iJlmGGGQ==",
"version": "10.1.1",
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"optional": true,
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.3.6",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-4.3.6.tgz",
"integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==",
"dev": true
},
"node_modules/import-fresh": {
@@ -26064,6 +25921,33 @@
"node": ">=10"
}
},
"node_modules/zustand": {
"version": "4.5.2",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
"integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0.6",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",

+ 4
- 1
web/package.json ファイルの表示

@@ -15,6 +15,7 @@
"@ant-design/pro-components": "^2.6.46",
"@ant-design/pro-layout": "^7.17.16",
"@js-preview/excel": "^1.7.8",
"@tanstack/react-query": "^5.40.0",
"ahooks": "^3.7.10",
"antd": "^5.12.7",
"axios": "^1.6.3",
@@ -44,10 +45,12 @@
"umi": "^4.0.90",
"umi-request": "^1.4.0",
"unist-util-visit-parents": "^6.0.1",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"zustand": "^4.5.2"
},
"devDependencies": {
"@react-dev-inspector/umi4-plugin": "^2.0.1",
"@redux-devtools/extension": "^3.3.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
"@types/dagre": "^0.7.52",

+ 16
- 11
web/src/app.tsx ファイルの表示

@@ -6,13 +6,14 @@ import zh_HK from 'antd/locale/zh_HK';
import React, { ReactNode, useEffect, useState } from 'react';
import storage from './utils/authorizationUtil';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import dayjs from 'dayjs';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import localeData from 'dayjs/plugin/localeData';
import weekday from 'dayjs/plugin/weekday';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import weekYear from 'dayjs/plugin/weekYear';
import weekday from 'dayjs/plugin/weekday';

dayjs.extend(customParseFormat);
dayjs.extend(advancedFormat);
@@ -27,6 +28,8 @@ const AntLanguageMap = {
'zh-TRADITIONAL': zh_HK,
};

const queryClient = new QueryClient();

type Locale = ConfigProviderProps['locale'];

const RootProvider = ({ children }: React.PropsWithChildren) => {
@@ -49,16 +52,18 @@ const RootProvider = ({ children }: React.PropsWithChildren) => {
}, []);

return (
<ConfigProvider
theme={{
token: {
fontFamily: 'Inter',
},
}}
locale={locale}
>
<App> {children}</App>
</ConfigProvider>
<QueryClientProvider client={queryClient}>
<ConfigProvider
theme={{
token: {
fontFamily: 'Inter',
},
}}
locale={locale}
>
<App> {children}</App>
</ConfigProvider>
</QueryClientProvider>
);
};


+ 37
- 0
web/src/components/knowledge-base-item.tsx ファイルの表示

@@ -0,0 +1,37 @@
import { useTranslate } from '@/hooks/commonHooks';
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { Form, Select } from 'antd';

const KnowledgeBaseItem = () => {
const { t } = useTranslate('chat');

const { list: knowledgeList } = useFetchKnowledgeList(true);

const knowledgeOptions = knowledgeList.map((x) => ({
label: x.name,
value: x.id,
}));

return (
<Form.Item
label={t('knowledgeBases')}
name="kb_ids"
tooltip={t('knowledgeBasesTip')}
rules={[
{
required: true,
message: t('knowledgeBasesMessage'),
type: 'array',
},
]}
>
<Select
mode="multiple"
options={knowledgeOptions}
placeholder={t('knowledgeBasesMessage')}
></Select>
</Form.Item>
);
};

export default KnowledgeBaseItem;

+ 6
- 0
web/src/components/llm-setting-items/index.less ファイルの表示

@@ -0,0 +1,6 @@
.sliderInputNumber {
width: 80px;
}
.variableSlider {
width: 100%;
}

+ 259
- 0
web/src/components/llm-setting-items/index.tsx ファイルの表示

@@ -0,0 +1,259 @@
import { LlmModelType, ModelVariableType } from '@/constants/knowledge';
import { Divider, Flex, Form, InputNumber, Select, Slider, Switch } from 'antd';
import camelCase from 'lodash/camelCase';

import { useTranslate } from '@/hooks/commonHooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/llmHooks';
import { useMemo } from 'react';
import styles from './index.less';

interface IProps {
prefix?: string;
handleParametersChange(value: ModelVariableType): void;
}

const LlmSettingItems = ({ prefix, handleParametersChange }: IProps) => {
const { t } = useTranslate('chat');
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: t(camelCase(x)),
value: x,
}));

const memorizedPrefix = useMemo(() => (prefix ? [prefix] : []), [prefix]);

const modelOptions = useSelectLlmOptionsByModelType();

return (
<>
<Form.Item
label={t('model')}
name="llm_id"
tooltip={t('modelTip')}
rules={[{ required: true, message: t('modelMessage') }]}
>
<Select options={modelOptions[LlmModelType.Chat]} showSearch />
</Form.Item>
<Divider></Divider>
<Form.Item
label={t('freedom')}
name="parameters"
tooltip={t('freedomTip')}
initialValue={ModelVariableType.Precise}
>
<Select<ModelVariableType>
options={parameterOptions}
onChange={handleParametersChange}
/>
</Form.Item>
<Form.Item label={t('temperature')} tooltip={t('temperatureTip')}>
<Flex gap={20} align="center">
<Form.Item
name={'temperatureEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Form.Item noStyle dependencies={['temperatureEnabled']}>
{({ getFieldValue }) => {
const disabled = !getFieldValue('temperatureEnabled');
return (
<>
<Flex flex={1}>
<Form.Item
name={[...memorizedPrefix, 'temperature']}
noStyle
>
<Slider
className={styles.variableSlider}
max={1}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</Flex>
<Form.Item name={[...memorizedPrefix, 'temperature']} noStyle>
<InputNumber
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label={t('topP')} tooltip={t('topPTip')}>
<Flex gap={20} align="center">
<Form.Item name={'topPEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Form.Item noStyle dependencies={['topPEnabled']}>
{({ getFieldValue }) => {
const disabled = !getFieldValue('topPEnabled');
return (
<>
<Flex flex={1}>
<Form.Item name={[...memorizedPrefix, 'top_p']} noStyle>
<Slider
className={styles.variableSlider}
max={1}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</Flex>
<Form.Item name={[...memorizedPrefix, 'top_p']} noStyle>
<InputNumber
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label={t('presencePenalty')} tooltip={t('presencePenaltyTip')}>
<Flex gap={20} align="center">
<Form.Item
name={'presencePenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Form.Item noStyle dependencies={['presencePenaltyEnabled']}>
{({ getFieldValue }) => {
const disabled = !getFieldValue('presencePenaltyEnabled');
return (
<>
<Flex flex={1}>
<Form.Item
name={[...memorizedPrefix, 'presence_penalty']}
noStyle
>
<Slider
className={styles.variableSlider}
max={1}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</Flex>
<Form.Item
name={[...memorizedPrefix, 'presence_penalty']}
noStyle
>
<InputNumber
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</Flex>
</Form.Item>
<Form.Item
label={t('frequencyPenalty')}
tooltip={t('frequencyPenaltyTip')}
>
<Flex gap={20} align="center">
<Form.Item
name={'frequencyPenaltyEnabled'}
valuePropName="checked"
noStyle
>
<Switch size="small" />
</Form.Item>
<Form.Item noStyle dependencies={['frequencyPenaltyEnabled']}>
{({ getFieldValue }) => {
const disabled = !getFieldValue('frequencyPenaltyEnabled');
return (
<>
<Flex flex={1}>
<Form.Item
name={[...memorizedPrefix, 'frequency_penalty']}
noStyle
>
<Slider
className={styles.variableSlider}
max={1}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</Flex>
<Form.Item
name={[...memorizedPrefix, 'frequency_penalty']}
noStyle
>
<InputNumber
className={styles.sliderInputNumber}
max={1}
min={0}
step={0.01}
disabled={disabled}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</Flex>
</Form.Item>
<Form.Item label={t('maxTokens')} tooltip={t('maxTokensTip')}>
<Flex gap={20} align="center">
<Form.Item name={'maxTokensEnabled'} valuePropName="checked" noStyle>
<Switch size="small" />
</Form.Item>
<Form.Item noStyle dependencies={['maxTokensEnabled']}>
{({ getFieldValue }) => {
const disabled = !getFieldValue('maxTokensEnabled');

return (
<>
<Flex flex={1}>
<Form.Item
name={[...memorizedPrefix, 'max_tokens']}
noStyle
>
<Slider
className={styles.variableSlider}
max={2048}
disabled={disabled}
/>
</Form.Item>
</Flex>
<Form.Item name={[...memorizedPrefix, 'max_tokens']} noStyle>
<InputNumber
disabled={disabled}
className={styles.sliderInputNumber}
max={2048}
min={0}
/>
</Form.Item>
</>
);
}}
</Form.Item>
</Flex>
</Form.Item>
</>
);
};

export default LlmSettingItems;

+ 23
- 0
web/src/components/top-n-item.tsx ファイルの表示

@@ -0,0 +1,23 @@
import { useTranslate } from '@/hooks/commonHooks';
import { Form, Slider } from 'antd';

type FieldType = {
top_n?: number;
};

const TopNItem = () => {
const { t } = useTranslate('chat');

return (
<Form.Item<FieldType>
label={t('topN')}
name={'top_n'}
initialValue={8}
tooltip={t('topNTip')}
>
<Slider max={30} />
</Form.Item>
);
};

export default TopNItem;

+ 70
- 0
web/src/hooks/flow-hooks.ts ファイルの表示

@@ -0,0 +1,70 @@
import flowService from '@/services/flow-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';

export const useFetchFlowTemplates = () => {
const { data } = useQuery({
queryKey: ['fetchFlowTemplates'],
initialData: [],
queryFn: async () => {
const { data } = await flowService.listTemplates();

return data;
},
});

return data;
};

export const useFetchFlowList = () => {
const { data, isFetching: loading } = useQuery({
queryKey: ['fetchFlowList'],
initialData: [],
queryFn: async () => {
const { data } = await flowService.listCanvas();

return data?.data ?? [];
},
});

return { data, loading };
};

export const useSetFlow = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['setFlow'],
mutationFn: async (params: any) => {
const { data } = await flowService.setCanvas(params);
if (data.retcode === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] });
}
return data?.retcode;
},
});

return { data, loading, setFlow: mutateAsync };
};

export const useDeleteFlow = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteFlow'],
mutationFn: async (canvasIds: string[]) => {
const { data } = await flowService.removeCanvas({ canvasIds });
if (data.retcode === 0) {
queryClient.invalidateQueries({ queryKey: ['fetchFlowList'] });
}
return data?.data ?? [];
},
});

return { data, loading, deleteFlow: mutateAsync };
};

+ 8
- 4
web/src/hooks/userSettingHook.ts ファイルの表示

@@ -99,10 +99,14 @@ export const useFetchSystemVersion = () => {
const [loading, setLoading] = useState(false);

const fetchSystemVersion = useCallback(async () => {
setLoading(true);
const { data } = await userService.getSystemVersion();
if (data.retcode === 0) {
setVersion(data.data);
try {
setLoading(true);
const { data } = await userService.getSystemVersion();
if (data.retcode === 0) {
setVersion(data.data);
setLoading(false);
}
} catch (error) {
setLoading(false);
}
}, []);

+ 4
- 4
web/src/interfaces/database/flow.ts ファイルの表示

@@ -1,4 +1,4 @@
export type DSLComponents = Record<string, Operator>;
export type DSLComponents = Record<string, IOperator>;

export interface DSL {
components: DSLComponents;
@@ -7,13 +7,13 @@ export interface DSL {
answer: any[];
}

export interface Operator {
obj: OperatorNode;
export interface IOperator {
obj: IOperatorNode;
downstream: string[];
upstream: string[];
}

export interface OperatorNode {
export interface IOperatorNode {
component_name: string;
params: Record<string, unknown>;
}

+ 1
- 0
web/src/locales/en.ts ファイルの表示

@@ -541,6 +541,7 @@ The above is the content you need to summarize.`,
preview: 'Preview',
fileError: 'File error',
},
flow: { cite: 'Cite', citeTip: 'citeTip' },
footer: {
profile: 'All rights reserved @ React',
},

+ 2
- 24
web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx ファイルの表示

@@ -1,18 +1,13 @@
import { useFetchKnowledgeList } from '@/hooks/knowledgeHook';
import { PlusOutlined } from '@ant-design/icons';
import { Form, Input, Select, Switch, Upload } from 'antd';
import classNames from 'classnames';
import { ISegmentedContentProps } from '../interface';

import KnowledgeBaseItem from '@/components/knowledge-base-item';
import { useTranslate } from '@/hooks/commonHooks';
import styles from './index.less';

const AssistantSetting = ({ show }: ISegmentedContentProps) => {
const { list: knowledgeList } = useFetchKnowledgeList(true);
const knowledgeOptions = knowledgeList.map((x) => ({
label: x.name,
value: x.id,
}));
const { t } = useTranslate('chat');

const normFile = (e: any) => {
@@ -95,24 +90,7 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => {
>
<Switch />
</Form.Item>
<Form.Item
label={t('knowledgeBases')}
name="kb_ids"
tooltip={t('knowledgeBasesTip')}
rules={[
{
required: true,
message: t('knowledgeBasesMessage'),
type: 'array',
},
]}
>
<Select
mode="multiple"
options={knowledgeOptions}
placeholder={t('knowledgeBasesMessage')}
></Select>
</Form.Item>
<KnowledgeBaseItem></KnowledgeBaseItem>
</section>
);
};

+ 9
- 15
web/src/pages/chat/chat-configuration-modal/model-setting.tsx ファイルの表示

@@ -1,16 +1,12 @@
import {
LlmModelType,
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { Divider, Flex, Form, InputNumber, Select, Slider, Switch } from 'antd';
import classNames from 'classnames';
import camelCase from 'lodash/camelCase';
import { useEffect } from 'react';
import { ISegmentedContentProps } from '../interface';

import { useTranslate } from '@/hooks/commonHooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/llmHooks';
import LlmSettingItems from '@/components/llm-setting-items';
import { Variable } from '@/interfaces/database/chat';
import { variableEnabledFieldMap } from '../constants';
import styles from './index.less';
@@ -24,14 +20,6 @@ const ModelSetting = ({
initialLlmSetting?: Variable;
visible?: boolean;
}) => {
const { t } = useTranslate('chat');
const parameterOptions = Object.values(ModelVariableType).map((x) => ({
label: t(camelCase(x)),
value: x,
}));

const modelOptions = useSelectLlmOptionsByModelType();

const handleParametersChange = (value: ModelVariableType) => {
const variable = settledModelVariableMap[value];
form.setFieldsValue({ llm_setting: variable });
@@ -62,7 +50,13 @@ const ModelSetting = ({
[styles.segmentedHidden]: !show,
})}
>
<Form.Item
{visible && (
<LlmSettingItems
prefix="llm_setting"
handleParametersChange={handleParametersChange}
></LlmSettingItems>
)}
{/* <Form.Item
label={t('model')}
name="llm_id"
tooltip={t('modelTip')}
@@ -279,7 +273,7 @@ const ModelSetting = ({
}}
</Form.Item>
</Flex>
</Form.Item>
</Form.Item> */}
</section>
);
};

+ 2
- 15
web/src/pages/chat/chat-configuration-modal/prompt-engine.tsx ファイルの表示

@@ -7,7 +7,6 @@ import {
Form,
Input,
Row,
Slider,
Switch,
Table,
TableProps,
@@ -30,16 +29,11 @@ import {
import { EditableCell, EditableRow } from './editable-cell';

import Rerank from '@/components/rerank';
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/commonHooks';
import { useSelectPromptConfigParameters } from '../hooks';
import styles from './index.less';

type FieldType = {
similarity_threshold?: number;
vector_similarity_weight?: number;
top_n?: number;
};

const PromptEngine = (
{ show }: ISegmentedContentProps,
ref: ForwardedRef<Array<IPromptConfigParameters>>,
@@ -165,14 +159,7 @@ const PromptEngine = (
</Form.Item>
<Divider></Divider>
<SimilaritySlider isTooltipShown></SimilaritySlider>
<Form.Item<FieldType>
label={t('topN')}
name={'top_n'}
initialValue={8}
tooltip={t('topNTip')}
>
<Slider max={30} />
</Form.Item>
<TopNItem></TopNItem>
<Rerank></Rerank>
<section className={classNames(styles.variableContainer)}>
<Row align={'middle'} justify="end">

+ 5
- 0
web/src/pages/flow/answer-form/index.tsx ファイルの表示

@@ -0,0 +1,5 @@
const AnswerForm = () => {
return <div>AnswerForm</div>;
};

export default AnswerForm;

+ 47
- 0
web/src/pages/flow/begin-form/index.tsx ファイルの表示

@@ -0,0 +1,47 @@
import { useTranslate } from '@/hooks/commonHooks';
import type { FormProps } from 'antd';
import { Form, Input } from 'antd';
import { IOperatorForm } from '../interface';

type FieldType = {
prologue?: string;
};

const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
console.log('Success:', values);
};

const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => {
console.log('Failed:', errorInfo);
};

const BeginForm = ({ onValuesChange }: IOperatorForm) => {
const { t } = useTranslate('chat');
const [form] = Form.useForm();

return (
<Form
name="basic"
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
onValuesChange={onValuesChange}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
name={'prologue'}
label={t('setAnOpener')}
tooltip={t('setAnOpenerTip')}
initialValue={t('setAnOpenerInitial')}
>
<Input.TextArea autoSize={{ minRows: 5 }} />
</Form.Item>
</Form>
);
};

export default BeginForm;

+ 1
- 1
web/src/pages/flow/canvas/context-menu/index.tsx ファイルの表示

@@ -86,7 +86,7 @@ export const useHandleNodeContextMenu = (sideWidth: number) => {

setMenu({
id: node.id,
top: event.clientY - 72,
top: event.clientY - 144,
left: event.clientX - sideWidth,
// top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
// left: event.clientX < pane.width - 200 ? event.clientX : 0,

+ 15
- 0
web/src/pages/flow/canvas/edge/index.less ファイルの表示

@@ -0,0 +1,15 @@
.edgeButton {
width: 14px;
height: 14px;
background: #eee;
border: 1px solid #fff;
padding: 0;
cursor: pointer;
border-radius: 50%;
font-size: 10px;
line-height: 1;
}

.edgeButton:hover {
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08);
}

+ 72
- 0
web/src/pages/flow/canvas/edge/index.tsx ファイルの表示

@@ -0,0 +1,72 @@
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from 'reactflow';
import useStore from '../../store';

import { useMemo } from 'react';
import styles from './index.less';

export function ButtonEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
selected,
}: EdgeProps) {
const deleteEdgeById = useStore((state) => state.deleteEdgeById);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});

const selectedStyle = useMemo(() => {
return selected ? { strokeWidth: 1, stroke: '#1677ff' } : {};
}, [selected]);

const onEdgeClick = () => {
deleteEdgeById(id);
};

return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, ...selectedStyle }}
/>
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
// everything inside EdgeLabelRenderer has no pointer events by default
// if you have an interactive element, set pointer-events: all
pointerEvents: 'all',
}}
className="nodrag nopan"
>
<button
className={styles.edgeButton}
type="button"
onClick={onEdgeClick}
>
×
</button>
</div>
</EdgeLabelRenderer>
</>
);
}

+ 4
- 0
web/src/pages/flow/canvas/index.less ファイルの表示

@@ -0,0 +1,4 @@
.canvasWrapper {
position: relative;
height: 100%;
}

+ 42
- 41
web/src/pages/flow/canvas/index.tsx ファイルの表示

@@ -1,76 +1,64 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
Background,
Controls,
Edge,
Node,
MarkerType,
NodeMouseHandler,
OnConnect,
OnEdgesChange,
OnNodesChange,
addEdge,
applyEdgeChanges,
applyNodeChanges,
} from 'reactflow';
import 'reactflow/dist/style.css';

import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
import { ButtonEdge } from './edge';

import FlowDrawer from '../flow-drawer';
import {
useHandleDrop,
useHandleKeyUp,
useHandleSelectionChange,
useSelectCanvasData,
useShowDrawer,
} from '../hooks';
import { dsl } from '../mock';
import { TextUpdaterNode } from './node';

import styles from './index.less';

const nodeTypes = { textUpdater: TextUpdaterNode };

const edgeTypes = {
buttonEdge: ButtonEdge,
};

interface IProps {
sideWidth: number;
}

function FlowCanvas({ sideWidth }: IProps) {
const [nodes, setNodes] = useState<Node[]>(dsl.graph.nodes);
const [edges, setEdges] = useState<Edge[]>(dsl.graph.edges);

const { selectedEdges, selectedNodes } = useHandleSelectionChange();
const {
nodes,
edges,
onConnect,
onEdgesChange,
onNodesChange,
onSelectionChange,
} = useSelectCanvasData();

const { ref, menu, onNodeContextMenu, onPaneClick } =
useHandleNodeContextMenu(sideWidth);
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();

const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
useShowDrawer();

const onConnect: OnConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
const onNodeClick: NodeMouseHandler = useCallback(
(e, node) => {
showDrawer(node);
},
[showDrawer],
);

const onNodeClick: NodeMouseHandler = useCallback(() => {
showDrawer();
}, [showDrawer]);

const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);

const { handleKeyUp } = useHandleKeyUp(selectedEdges, selectedNodes);
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();

useEffect(() => {
console.info('nodes:', nodes);
console.info('edges:', edges);
}, [nodes, edges]);
const { handleKeyUp } = useHandleKeyUp();

return (
<div style={{ height: '100%', width: '100%' }}>
<div className={styles.canvasWrapper}>
<ReactFlow
ref={ref}
nodes={nodes}
@@ -81,12 +69,21 @@ function FlowCanvas({ sideWidth }: IProps) {
fitView
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onPaneClick={onPaneClick}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
onInit={setReactFlowInstance}
onKeyUp={handleKeyUp}
onSelectionChange={onSelectionChange}
nodeOrigin={[0.5, 0]}
defaultEdgeOptions={{
type: 'buttonEdge',
markerEnd: {
type: MarkerType.ArrowClosed,
},
}}
>
<Background />
<Controls />
@@ -94,7 +91,11 @@ function FlowCanvas({ sideWidth }: IProps) {
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
)}
</ReactFlow>
<FlowDrawer visible={drawerVisible} hideModal={hideDrawer}></FlowDrawer>
<FlowDrawer
node={clickedNode}
visible={drawerVisible}
hideModal={hideDrawer}
></FlowDrawer>
</div>
);
}

+ 10
- 1
web/src/pages/flow/canvas/node/index.less ファイルの表示

@@ -1,6 +1,6 @@
.textUpdaterNode {
// height: 50px;
border: 1px solid black;
border: 1px solid gray;
padding: 5px;
border-radius: 5px;
background: white;
@@ -10,3 +10,12 @@
font-size: 12px;
}
}
.selectedNode {
border-color: #1677ff;
}

.handle {
display: inline-flex;
text-align: center;
// align-items: center;
}

+ 15
- 3
web/src/pages/flow/canvas/node/index.tsx ファイルの表示

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import { Handle, NodeProps, Position } from 'reactflow';

import styles from './index.less';
@@ -5,19 +6,30 @@ import styles from './index.less';
export function TextUpdaterNode({
data,
isConnectable = true,
selected,
}: NodeProps<{ label: string }>) {
return (
<div className={styles.textUpdaterNode}>
<div
className={classNames(styles.textUpdaterNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
type="target"
position={Position.Left}
isConnectable={isConnectable}
/>
className={styles.handle}
>
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
/>
className={styles.handle}
>
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle>
<div>{data.label}</div>
</div>
);

+ 6
- 0
web/src/pages/flow/constant.ts ファイルの表示

@@ -0,0 +1,6 @@
export enum Operator {
Begin = 'Begin',
Retrieval = 'Retrieval',
Generate = 'Generate',
Answer = 'Answer',
}

+ 32
- 4
web/src/pages/flow/flow-drawer/index.tsx ファイルの表示

@@ -1,18 +1,46 @@
import { IModalProps } from '@/interfaces/common';
import { Drawer } from 'antd';
import { Node } from 'reactflow';
import AnswerForm from '../answer-form';
import BeginForm from '../begin-form';
import { Operator } from '../constant';
import GenerateForm from '../generate-form';
import { useHandleFormValuesChange } from '../hooks';
import RetrievalForm from '../retrieval-form';

interface IProps {
node?: Node;
}

const FormMap = {
[Operator.Begin]: BeginForm,
[Operator.Retrieval]: RetrievalForm,
[Operator.Generate]: GenerateForm,
[Operator.Answer]: AnswerForm,
};

const FlowDrawer = ({
visible,
hideModal,
node,
}: IModalProps<any> & IProps) => {
const operatorName: Operator = node?.data.label;
const OperatorForm = FormMap[operatorName];
const { handleValuesChange } = useHandleFormValuesChange(node?.id);

const FlowDrawer = ({ visible, hideModal }: IModalProps<any>) => {
return (
<Drawer
title="Basic Drawer"
title={node?.data.label}
placement="right"
// closable={false}
onClose={hideModal}
open={visible}
getContainer={false}
mask={false}
width={470}
>
<p>Some contents...</p>
{visible && (
<OperatorForm onValuesChange={handleValuesChange}></OperatorForm>
)}
</Drawer>
);
};

+ 83
- 0
web/src/pages/flow/generate-form/index.tsx ファイルの表示

@@ -0,0 +1,83 @@
import LlmSettingItems from '@/components/llm-setting-items';
import {
ModelVariableType,
settledModelVariableMap,
} from '@/constants/knowledge';
import { useTranslate } from '@/hooks/commonHooks';
import { Variable } from '@/interfaces/database/chat';
import { variableEnabledFieldMap } from '@/pages/chat/constants';
import { Form, Input, Switch } from 'antd';
import { useCallback, useEffect } from 'react';
import { IOperatorForm } from '../interface';

const GenerateForm = ({ onValuesChange }: IOperatorForm) => {
const { t } = useTranslate('flow');
const [form] = Form.useForm();
const initialLlmSetting = undefined;

const handleParametersChange = useCallback(
(value: ModelVariableType) => {
const variable = settledModelVariableMap[value];
form.setFieldsValue(variable);
},
[form],
);

useEffect(() => {
const switchBoxValues = Object.keys(variableEnabledFieldMap).reduce<
Record<string, boolean>
>((pre, field) => {
pre[field] =
initialLlmSetting === undefined
? true
: !!initialLlmSetting[
variableEnabledFieldMap[
field as keyof typeof variableEnabledFieldMap
] as keyof Variable
];
return pre;
}, {});
const otherValues = settledModelVariableMap[ModelVariableType.Precise];
form.setFieldsValue({ ...switchBoxValues, ...otherValues });
}, [form, initialLlmSetting]);

return (
<Form
name="basic"
labelCol={{ span: 9 }}
wrapperCol={{ span: 15 }}
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
>
<LlmSettingItems
handleParametersChange={handleParametersChange}
></LlmSettingItems>
<Form.Item
name={['prompt']}
label={t('prompt', { keyPrefix: 'knowledgeConfiguration' })}
initialValue={t('promptText', { keyPrefix: 'knowledgeConfiguration' })}
tooltip={t('promptTip', { keyPrefix: 'knowledgeConfiguration' })}
rules={[
{
required: true,
message: t('promptMessage'),
},
]}
>
<Input.TextArea rows={8} />
</Form.Item>
<Form.Item
name={['cite']}
label={t('cite')}
initialValue={true}
valuePropName="checked"
tooltip={t('citeTip')}
>
<Switch />
</Form.Item>
</Form>
);
};

export default GenerateForm;

+ 63
- 50
web/src/pages/flow/hooks.ts ファイルの表示

@@ -1,19 +1,26 @@
import { useSetModalState } from '@/hooks/commonHooks';
import React, {
Dispatch,
KeyboardEventHandler,
SetStateAction,
useCallback,
useState,
} from 'react';
import {
Node,
Position,
ReactFlowInstance,
useOnSelectionChange,
useReactFlow,
} from 'reactflow';
import { useFetchFlowTemplates } from '@/hooks/flow-hooks';
import { useFetchLlmList } from '@/hooks/llmHooks';
import React, { KeyboardEventHandler, useCallback, useState } from 'react';
import { Node, Position, ReactFlowInstance } from 'reactflow';
import { v4 as uuidv4 } from 'uuid';
import useStore, { RFState } from './store';
import { buildDslComponentsByGraph } from './utils';

const selector = (state: RFState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
setNodes: state.setNodes,
onSelectionChange: state.onSelectionChange,
});

export const useSelectCanvasData = () => {
// return useStore(useShallow(selector)); throw error
return useStore(selector);
};

export const useHandleDrag = () => {
const handleDragStart = useCallback(
@@ -27,7 +34,8 @@ export const useHandleDrag = () => {
return { handleDragStart };
};

export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
export const useHandleDrop = () => {
const addNode = useStore((state) => state.addNode);
const [reactFlowInstance, setReactFlowInstance] =
useState<ReactFlowInstance<any, any>>();

@@ -66,59 +74,40 @@ export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
targetPosition: Position.Left,
};

setNodes((nds) => nds.concat(newNode));
addNode(newNode);
},
[reactFlowInstance, setNodes],
[reactFlowInstance, addNode],
);

return { onDrop, onDragOver, setReactFlowInstance };
};

export const useShowDrawer = () => {
const [clickedNode, setClickedNode] = useState<Node>();
const {
visible: drawerVisible,
hideModal: hideDrawer,
showModal: showDrawer,
} = useSetModalState();

const handleShow = useCallback(
(node: Node) => {
setClickedNode(node);
showDrawer();
},
[showDrawer],
);

return {
drawerVisible,
hideDrawer,
showDrawer,
showDrawer: handleShow,
clickedNode,
};
};

export const useHandleSelectionChange = () => {
const [selectedNodes, setSelectedNodes] = useState<string[]>([]);
const [selectedEdges, setSelectedEdges] = useState<string[]>([]);

useOnSelectionChange({
onChange: ({ nodes, edges }) => {
setSelectedNodes(nodes.map((node) => node.id));
setSelectedEdges(edges.map((edge) => edge.id));
},
});

return { selectedEdges, selectedNodes };
};

export const useDeleteEdge = (selectedEdges: string[]) => {
const { setEdges } = useReactFlow();

const deleteEdge = useCallback(() => {
setEdges((edges) =>
edges.filter((edge) => selectedEdges.every((x) => x !== edge.id)),
);
}, [setEdges, selectedEdges]);

return deleteEdge;
};

export const useHandleKeyUp = (
selectedEdges: string[],
selectedNodes: string[],
) => {
const deleteEdge = useDeleteEdge(selectedEdges);
export const useHandleKeyUp = () => {
const deleteEdge = useStore((state) => state.deleteEdge);
const handleKeyUp: KeyboardEventHandler = useCallback(
(e) => {
if (e.code === 'Delete') {
@@ -132,7 +121,31 @@ export const useHandleKeyUp = (
};

export const useSaveGraph = () => {
const saveGraph = useCallback(() => {}, []);
const { nodes, edges } = useStore((state) => state);
const saveGraph = useCallback(() => {
const x = buildDslComponentsByGraph(nodes, edges);
console.info('components:', x);
}, [nodes, edges]);

return { saveGraph };
};

export const useHandleFormValuesChange = (id?: string) => {
const updateNodeForm = useStore((state) => state.updateNodeForm);
const handleValuesChange = useCallback(
(changedValues: any, values: any) => {
console.info(changedValues, values);
if (id) {
updateNodeForm(id, values);
}
},
[updateNodeForm, id],
);

return { handleValuesChange };
};

export const useFetchDataOnMount = () => {
useFetchFlowTemplates();
useFetchLlmList();
};

+ 4
- 1
web/src/pages/flow/index.tsx ファイルの表示

@@ -4,19 +4,22 @@ import { ReactFlowProvider } from 'reactflow';
import FlowCanvas from './canvas';
import Sider from './flow-sider';
import FlowHeader from './header';
import { useFetchDataOnMount } from './hooks';

const { Content } = Layout;

function RagFlow() {
const [collapsed, setCollapsed] = useState(false);

useFetchDataOnMount();

return (
<Layout>
<ReactFlowProvider>
<Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
<Layout>
<FlowHeader></FlowHeader>
<Content style={{ margin: '0 16px' }}>
<Content style={{ margin: 0 }}>
<FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
</Content>
</Layout>

+ 58
- 0
web/src/pages/flow/interface.ts ファイルの表示

@@ -1,4 +1,62 @@
import { Edge, Node } from 'reactflow';

export interface DSLComponentList {
id: string;
name: string;
}

export interface IOperatorForm {
onValuesChange?(changedValues: any, values: any): void;
}

export interface IBeginForm {
prologue?: string;
}

export interface IRetrievalForm {
similarity_threshold?: number;
keywords_similarity_weight?: number;
top_n?: number;
top_k?: number;
rerank_id?: string;
empty_response?: string;
kb_ids: string[];
}

export interface IGenerateForm {
max_tokens?: number;
temperature?: number;
top_p?: number;
presence_penalty?: number;
frequency_penalty?: number;
cite?: boolean;
prompt: number;
llm_id: string;
parameters: { key: string; component_id: string };
}

export type NodeData = {
label: string;
color: string;
form: IBeginForm | IRetrievalForm | IGenerateForm;
};

export interface IFlow {
avatar: null;
canvas_type: null;
create_date: string;
create_time: number;
description: null;
dsl: {
answer: any[];
components: DSLComponentList;
graph: { nodes: Node[]; edges: Edge[] };
history: any[];
path: string[];
};
id: string;
title: string;
update_date: string;
update_time: number;
user_id: string;
}

+ 78
- 0
web/src/pages/flow/list/flow-card/index.less ファイルの表示

@@ -0,0 +1,78 @@
.container {
height: 251px;
display: flex;
flex-direction: column;
justify-content: space-between;

.delete {
height: 24px;
}

.content {
display: flex;
justify-content: space-between;

.context {
flex: 1;
}
}

.footer {
// text-align: left;
}
.footerTop {
padding-bottom: 2px;
}
}

.card {
border-radius: 12px;
border: 1px solid rgba(234, 236, 240, 1);
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
padding: 24px;
width: 300px;
cursor: pointer;

.titleWrapper {
// flex: 1;
.title {
font-size: 24px;
line-height: 32px;
font-weight: 600;
color: rgba(0, 0, 0, 0.88);
word-break: break-all;
}
.description {
font-size: 12px;
font-weight: 600;
line-height: 20px;
color: rgba(0, 0, 0, 0.45);
}
}

:global {
.ant-card-body {
padding: 0;
margin: 0;
}
}
.bottom {
display: flex;
align-items: center;
justify-content: space-between;
}
.bottomLeft {
vertical-align: middle;
}
.leftIcon {
margin-right: 10px;
font-size: 18px;
vertical-align: middle;
}
.rightText {
font-size: 12px;
font-weight: 600;
color: rgba(0, 0, 0, 0.65);
vertical-align: middle;
}
}

+ 94
- 0
web/src/pages/flow/list/flow-card/index.tsx ファイルの表示

@@ -0,0 +1,94 @@
import { ReactComponent as MoreIcon } from '@/assets/svg/more.svg';
import { useShowDeleteConfirm } from '@/hooks/commonHooks';
import { formatDate } from '@/utils/date';
import {
CalendarOutlined,
DeleteOutlined,
UserOutlined,
} from '@ant-design/icons';
import { Avatar, Card, Dropdown, MenuProps, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';

import { useDeleteFlow } from '@/hooks/flow-hooks';
import { IFlow } from '../../interface';
import styles from './index.less';

interface IProps {
item: IFlow;
}

const FlowCard = ({ item }: IProps) => {
const navigate = useNavigate();
const showDeleteConfirm = useShowDeleteConfirm();
const { t } = useTranslation();
const { deleteFlow } = useDeleteFlow();

const removeKnowledge = () => {
return deleteFlow([item.id]);
};

const handleDelete = () => {
showDeleteConfirm({ onOk: removeKnowledge });
};

const items: MenuProps['items'] = [
{
key: '1',
label: (
<Space>
{t('common.delete')}
<DeleteOutlined />
</Space>
),
},
];

const handleDropdownMenuClick: MenuProps['onClick'] = ({ domEvent, key }) => {
domEvent.preventDefault();
domEvent.stopPropagation();
if (key === '1') {
handleDelete();
}
};

const handleCardClick = () => {
navigate(`/flow/${item.id}`);
};

return (
<Card className={styles.card} onClick={handleCardClick}>
<div className={styles.container}>
<div className={styles.content}>
<Avatar size={34} icon={<UserOutlined />} src={item.avatar} />
<Dropdown
menu={{
items,
onClick: handleDropdownMenuClick,
}}
>
<span className={styles.delete}>
<MoreIcon />
</span>
</Dropdown>
</div>
<div className={styles.titleWrapper}>
<span className={styles.title}>{item.title}</span>
<p>{item.description}</p>
</div>
<div className={styles.footer}>
<div className={styles.bottom}>
<div className={styles.bottomLeft}>
<CalendarOutlined className={styles.leftIcon} />
<span className={styles.rightText}>
{formatDate(item.update_time)}
</span>
</div>
</div>
</div>
</div>
</Card>
);
};

export default FlowCard;

+ 48
- 0
web/src/pages/flow/list/hooks.ts ファイルの表示

@@ -0,0 +1,48 @@
import { useSetModalState } from '@/hooks/commonHooks';
import { useFetchFlowList, useSetFlow } from '@/hooks/flow-hooks';
import { useCallback, useState } from 'react';
import { dsl } from '../mock';

export const useFetchDataOnMount = () => {
const { data, loading } = useFetchFlowList();

return { list: data, loading };
};

export const useSaveFlow = () => {
const [currentFlow, setCurrentFlow] = useState({});
const {
visible: flowSettingVisible,
hideModal: hideFlowSettingModal,
showModal: showFileRenameModal,
} = useSetModalState();
const { loading, setFlow } = useSetFlow();

const onFlowOk = useCallback(
async (title: string) => {
const ret = await setFlow({ title, dsl });

if (ret === 0) {
hideFlowSettingModal();
}
},
[setFlow, hideFlowSettingModal],
);

const handleShowFlowSettingModal = useCallback(
async (record: any) => {
setCurrentFlow(record);
showFileRenameModal();
},
[showFileRenameModal],
);

return {
flowSettingLoading: loading,
initialFlowName: '',
onFlowOk,
flowSettingVisible,
hideFlowSettingModal,
showFlowSettingModal: handleShowFlowSettingModal,
};
};

+ 48
- 0
web/src/pages/flow/list/index.less ファイルの表示

@@ -0,0 +1,48 @@
.flowListWrapper {
padding: 48px;
}

.topWrapper {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0 60px 72px;

.title {
font-family: Inter;
font-size: 30px;
font-style: normal;
font-weight: @fontWeight600;
line-height: 38px;
color: rgba(16, 24, 40, 1);
}
.description {
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
color: rgba(71, 84, 103, 1);
}

.topButton {
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: @fontWeight600;
line-height: 20px;
}

.filterButton {
display: flex;
align-items: center;
.topButton();
}
}
.flowCardContainer {
padding: 0 60px;
overflow: auto;
.knowledgeEmpty {
width: 100%;
}
}

+ 53
- 0
web/src/pages/flow/list/index.tsx ファイルの表示

@@ -0,0 +1,53 @@
import RenameModal from '@/components/rename-modal';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Empty, Flex, Spin } from 'antd';
import FlowCard from './flow-card';
import { useFetchDataOnMount, useSaveFlow } from './hooks';

import styles from './index.less';

const FlowList = () => {
const {
showFlowSettingModal,
hideFlowSettingModal,
flowSettingVisible,
flowSettingLoading,
onFlowOk,
} = useSaveFlow();

const { list, loading } = useFetchDataOnMount();

return (
<Flex className={styles.flowListWrapper} vertical flex={1} gap={'large'}>
<Flex justify={'end'}>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={showFlowSettingModal}
>
create canvas
</Button>
</Flex>
<Spin spinning={loading}>
<Flex gap={'large'} wrap="wrap" className={styles.flowCardContainer}>
{list.length > 0 ? (
list.map((item: any) => {
return <FlowCard item={item} key={item.name}></FlowCard>;
})
) : (
<Empty className={styles.knowledgeEmpty}></Empty>
)}
</Flex>
</Spin>
<RenameModal
visible={flowSettingVisible}
onOk={onFlowOk}
loading={flowSettingLoading}
hideModal={hideFlowSettingModal}
initialName=""
></RenameModal>
</Flex>
);
};

export default FlowList;

+ 15
- 8
web/src/pages/flow/mock.tsx ファイルの表示

@@ -1,12 +1,7 @@
import {
MergeCellsOutlined,
RocketOutlined,
SendOutlined,
} from '@ant-design/icons';
import { MergeCellsOutlined, RocketOutlined } from '@ant-design/icons';
import { Position } from 'reactflow';

export const componentList = [
{ name: 'Begin', icon: <SendOutlined />, description: '' },
{ name: 'Retrieval', icon: <RocketOutlined />, description: '' },
{ name: 'Generate', icon: <MergeCellsOutlined />, description: '' },
];
@@ -159,7 +154,14 @@ export const dsl = {
'Retrieval:China': {
obj: {
component_name: 'Retrieval',
params: {},
params: {
similarity_threshold: 0.2,
keywords_similarity_weight: 0.3,
top_n: 6,
top_k: 1024,
rerank_id: 'BAAI/bge-reranker-v2-m3',
kb_ids: ['568aa82603b611efa9d9fa163e197198'],
},
},
downstream: ['Generate:China'],
upstream: ['Answer:China'],
@@ -167,7 +169,12 @@ export const dsl = {
'Generate:China': {
obj: {
component_name: 'Generate',
params: {},
params: {
llm_id: 'deepseek-chat',
prompt:
'You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence "The answer you are looking for is not found in the knowledge base!" Answers need to consider chat history.\n Here is the knowledge base:\n {input}\n The above is the knowledge base.',
temperature: 0.2,
},
},
downstream: ['Answer:China'],
upstream: ['Retrieval:China'],

+ 43
- 0
web/src/pages/flow/retrieval-form/index.tsx ファイルの表示

@@ -0,0 +1,43 @@
import KnowledgeBaseItem from '@/components/knowledge-base-item';
import Rerank from '@/components/rerank';
import SimilaritySlider from '@/components/similarity-slider';
import TopNItem from '@/components/top-n-item';
import type { FormProps } from 'antd';
import { Form } from 'antd';
import { IOperatorForm } from '../interface';

type FieldType = {
top_n?: number;
};

const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
console.log('Success:', values);
};

const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => {
console.log('Failed:', errorInfo);
};

const RetrievalForm = ({ onValuesChange }: IOperatorForm) => {
const [form] = Form.useForm();

return (
<Form
name="basic"
labelCol={{ span: 12 }}
wrapperCol={{ span: 12 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
onValuesChange={onValuesChange}
form={form}
>
<SimilaritySlider isTooltipShown></SimilaritySlider>
<TopNItem></TopNItem>
<Rerank></Rerank>
<KnowledgeBaseItem></KnowledgeBaseItem>
</Form>
);
};

export default RetrievalForm;

+ 106
- 0
web/src/pages/flow/store.ts ファイルの表示

@@ -0,0 +1,106 @@
import type {} from '@redux-devtools/extension';
import {
Connection,
Edge,
EdgeChange,
Node,
NodeChange,
OnConnect,
OnEdgesChange,
OnNodesChange,
OnSelectionChangeFunc,
OnSelectionChangeParams,
addEdge,
applyEdgeChanges,
applyNodeChanges,
} from 'reactflow';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { NodeData } from './interface';
import { dsl } from './mock';

const { nodes: initialNodes, edges: initialEdges } = dsl.graph;

export type RFState = {
nodes: Node<NodeData>[];
edges: Edge[];
selectedNodeIds: string[];
selectedEdgeIds: string[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
onConnect: OnConnect;
setNodes: (nodes: Node[]) => void;
setEdges: (edges: Edge[]) => void;
updateNodeForm: (nodeId: string, values: any) => void;
onSelectionChange: OnSelectionChangeFunc;
addNode: (nodes: Node) => void;
deleteEdge: () => void;
deleteEdgeById: (id: string) => void;
};

// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useStore = create<RFState>()(
devtools((set, get) => ({
nodes: initialNodes as Node[],
edges: initialEdges as Edge[],
selectedNodeIds: [],
selectedEdgeIds: [],
onNodesChange: (changes: NodeChange[]) => {
set({
nodes: applyNodeChanges(changes, get().nodes),
});
},
onEdgesChange: (changes: EdgeChange[]) => {
set({
edges: applyEdgeChanges(changes, get().edges),
});
},
onConnect: (connection: Connection) => {
set({
edges: addEdge(connection, get().edges),
});
},
onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => {
set({
selectedEdgeIds: edges.map((x) => x.id),
selectedNodeIds: nodes.map((x) => x.id),
});
},
setNodes: (nodes: Node[]) => {
set({ nodes });
},
setEdges: (edges: Edge[]) => {
set({ edges });
},
addNode: (node: Node) => {
set({ nodes: get().nodes.concat(node) });
},
deleteEdge: () => {
const { edges, selectedEdgeIds } = get();
set({
edges: edges.filter((edge) =>
selectedEdgeIds.every((x) => x !== edge.id),
),
});
},
deleteEdgeById: (id: string) => {
const { edges } = get();
set({
edges: edges.filter((edge) => edge.id !== id),
});
},
updateNodeForm: (nodeId: string, values: any) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
node.data = { ...node.data, form: values };
}

return node;
}),
});
},
})),
);

export default useStore;

+ 33
- 0
web/src/pages/flow/utils.ts ファイルの表示

@@ -2,6 +2,7 @@ import { DSLComponents } from '@/interfaces/database/flow';
import dagre from 'dagre';
import { Edge, MarkerType, Node, Position } from 'reactflow';
import { v4 as uuidv4 } from 'uuid';
import { NodeData } from './interface';

const buildEdges = (
operatorIds: string[],
@@ -96,3 +97,35 @@ export const getLayoutedElements = (

return { nodes, edges };
};

const buildComponentDownstreamOrUpstream = (
edges: Edge[],
nodeId: string,
isBuildDownstream = true,
) => {
return edges
.filter((y) => y[isBuildDownstream ? 'source' : 'target'] === nodeId)
.map((y) => y[isBuildDownstream ? 'target' : 'source']);
};

// construct a dsl based on the node information of the graph
export const buildDslComponentsByGraph = (
nodes: Node<NodeData>[],
edges: Edge[],
): DSLComponents => {
const components: DSLComponents = {};

nodes.forEach((x) => {
const id = x.id;
components[id] = {
obj: {
component_name: x.data.label,
params: x.data.form as Record<string, unknown>,
},
downstream: buildComponentDownstreamOrUpstream(edges, id, true),
upstream: buildComponentDownstreamOrUpstream(edges, id, false),
};
});

return components;
};

+ 4
- 0
web/src/routes.ts ファイルの表示

@@ -90,6 +90,10 @@ const routes = [
},
{
path: '/flow',
component: '@/pages/flow/list',
},
{
path: '/flow/:id',
component: '@/pages/flow',
},
],

+ 43
- 0
web/src/services/flow-service.ts ファイルの表示

@@ -0,0 +1,43 @@
import api from '@/utils/api';
import registerServer from '@/utils/registerServer';
import request from '@/utils/request';

const {
getCanvas,
setCanvas,
listCanvas,
resetCanvas,
removeCanvas,
listTemplates,
} = api;

const methods = {
getCanvas: {
url: getCanvas,
method: 'get',
},
setCanvas: {
url: setCanvas,
method: 'post',
},
listCanvas: {
url: listCanvas,
method: 'get',
},
resetCanvas: {
url: resetCanvas,
method: 'post',
},
removeCanvas: {
url: removeCanvas,
method: 'post',
},
listTemplates: {
url: listTemplates,
method: 'get',
},
} as const;

const chatService = registerServer<keyof typeof methods>(methods, request);

export default chatService;

+ 8
- 0
web/src/utils/api.ts ファイルの表示

@@ -81,4 +81,12 @@ export default {
// system
getSystemVersion: `${api_host}/system/version`,
getSystemStatus: `${api_host}/system/status`,
// flow
listTemplates: `${api_host}/canvas/templates`,
listCanvas: `${api_host}/canvas/list`,
getCanvas: `${api_host}/canvas/get`,
removeCanvas: `${api_host}/canvas/rm`,
setCanvas: `${api_host}/canvas/set`,
resetCanvas: `${api_host}/canvas/reset`,
};

+ 1
- 1
web/src/utils/registerServer.ts ファイルの表示

@@ -1,7 +1,7 @@
import omit from 'lodash/omit';
import { RequestMethod } from 'umi-request';
type Service<T extends string> = Record<T, (params: any) => any>;
type Service<T extends string> = Record<T, (params?: any) => any>;
const registerServer = <T extends string>(
opt: Record<T, { url: string; method: string }>,

読み込み中…
キャンセル
保存