From 29b8baecca8c87f45ae73e0d9138330fb3835192 Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 28 Oct 2024 14:31:19 +0800 Subject: [PATCH] feat: Add hint for operators, round to square, input variable, readable operator ID. #3056 (#3057) ### What problem does this PR solve? feat: Add hint for operators, round to square, input variable, readable operator ID. #3056 ### Type of change - [ ] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --- web/src/assets/svg/begin.svg | 9 + web/src/assets/svg/concentrator.svg | 4 +- web/src/assets/svg/keyword.svg | 4 +- web/src/assets/svg/llm/baai.svg | 12 + web/src/assets/svg/llm/nomic-ai.svg | 7 + .../assets/svg/llm/sentence-transformers.svg | 29 +++ web/src/assets/svg/llm/youdao.svg | 13 ++ web/src/assets/svg/plus-circle-fill.svg | 2 + web/src/assets/svg/plus.svg | 5 + web/src/assets/svg/resize.svg | 6 + web/src/assets/svg/switch.svg | 2 +- web/src/components/knowledge-base-item.tsx | 10 +- web/src/components/llm-select/index.less | 3 + web/src/components/llm-select/index.tsx | 4 +- web/src/components/llm-select/llm-label.tsx | 31 +++ web/src/components/svg-icon.tsx | 25 +- web/src/constants/setting.ts | 50 ++++ web/src/hooks/{llm-hooks.ts => llm-hooks.tsx} | 23 +- web/src/locales/en.ts | 6 +- web/src/locales/zh-traditional.ts | 7 +- web/src/locales/zh.ts | 11 +- web/src/pages/flow/canvas/index.tsx | 12 + web/src/pages/flow/canvas/node/begin-node.tsx | 29 +-- .../flow/canvas/node/categorize-node.tsx | 85 +++---- web/src/pages/flow/canvas/node/dropdown.tsx | 2 +- .../pages/flow/canvas/node/generate-node.tsx | 74 ++++++ .../pages/flow/canvas/node/handle-icon.tsx | 20 ++ web/src/pages/flow/canvas/node/hooks.ts | 146 ++++++------ web/src/pages/flow/canvas/node/index.less | 174 +++++++++----- web/src/pages/flow/canvas/node/index.tsx | 40 +--- .../pages/flow/canvas/node/keyword-node.tsx | 54 +++++ web/src/pages/flow/canvas/node/logic-node.tsx | 57 +---- .../pages/flow/canvas/node/message-node.tsx | 63 +++++ .../pages/flow/canvas/node/node-header.tsx | 35 +++ web/src/pages/flow/canvas/node/note-node.tsx | 83 +++++-- .../pages/flow/canvas/node/relevant-node.tsx | 66 +++--- .../pages/flow/canvas/node/retrieval-node.tsx | 85 +++++++ .../pages/flow/canvas/node/rewrite-node.tsx | 54 +++++ .../pages/flow/canvas/node/switch-node.tsx | 112 +++++++++ web/src/pages/flow/constant.tsx | 29 +-- web/src/pages/flow/flow-drawer/index.less | 6 + web/src/pages/flow/flow-drawer/index.tsx | 44 ++-- web/src/pages/flow/flow-sider/index.tsx | 7 +- web/src/pages/flow/form-hooks.ts | 19 +- .../categorize-form/dynamic-categorize.tsx | 26 ++- .../flow/form/categorize-form/index.less | 11 + .../form/generate-form/dynamic-parameters.tsx | 1 + .../pages/flow/form/switch-form/index.less | 21 ++ web/src/pages/flow/form/switch-form/index.tsx | 218 ++++++++++-------- web/src/pages/flow/hooks.ts | 26 ++- web/src/pages/flow/interface.ts | 8 +- web/src/pages/flow/operator-icon/index.tsx | 9 +- web/src/pages/flow/store.ts | 3 +- web/src/pages/flow/utils.ts | 4 + .../user-setting/setting-model/constant.ts | 45 ---- .../user-setting/setting-model/index.tsx | 20 +- 56 files changed, 1367 insertions(+), 584 deletions(-) create mode 100644 web/src/assets/svg/begin.svg create mode 100644 web/src/assets/svg/llm/baai.svg create mode 100644 web/src/assets/svg/llm/nomic-ai.svg create mode 100644 web/src/assets/svg/llm/sentence-transformers.svg create mode 100644 web/src/assets/svg/llm/youdao.svg create mode 100644 web/src/assets/svg/plus.svg create mode 100644 web/src/assets/svg/resize.svg create mode 100644 web/src/components/llm-select/index.less create mode 100644 web/src/components/llm-select/llm-label.tsx rename web/src/hooks/{llm-hooks.ts => llm-hooks.tsx} (93%) create mode 100644 web/src/pages/flow/canvas/node/generate-node.tsx create mode 100644 web/src/pages/flow/canvas/node/handle-icon.tsx create mode 100644 web/src/pages/flow/canvas/node/keyword-node.tsx create mode 100644 web/src/pages/flow/canvas/node/message-node.tsx create mode 100644 web/src/pages/flow/canvas/node/node-header.tsx create mode 100644 web/src/pages/flow/canvas/node/retrieval-node.tsx create mode 100644 web/src/pages/flow/canvas/node/rewrite-node.tsx create mode 100644 web/src/pages/flow/canvas/node/switch-node.tsx create mode 100644 web/src/pages/flow/form/categorize-form/index.less create mode 100644 web/src/pages/flow/form/switch-form/index.less diff --git a/web/src/assets/svg/begin.svg b/web/src/assets/svg/begin.svg new file mode 100644 index 00000000000..c1e77892f07 --- /dev/null +++ b/web/src/assets/svg/begin.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/web/src/assets/svg/concentrator.svg b/web/src/assets/svg/concentrator.svg index d1ecdfad1b7..a5ddc57968a 100644 --- a/web/src/assets/svg/concentrator.svg +++ b/web/src/assets/svg/concentrator.svg @@ -1,7 +1,7 @@ - + + p-id="4355" fill="#32d2a3"> \ No newline at end of file diff --git a/web/src/assets/svg/keyword.svg b/web/src/assets/svg/keyword.svg index 0ab63a10181..9d0350dcf15 100644 --- a/web/src/assets/svg/keyword.svg +++ b/web/src/assets/svg/keyword.svg @@ -2,8 +2,8 @@ p-id="11640" width="200" height="200"> + p-id="11641" fill="#0f0e0f"> + p-id="11642" fill="#0f0e0f"> \ No newline at end of file diff --git a/web/src/assets/svg/llm/baai.svg b/web/src/assets/svg/llm/baai.svg new file mode 100644 index 00000000000..7e365e37707 --- /dev/null +++ b/web/src/assets/svg/llm/baai.svg @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/web/src/assets/svg/llm/nomic-ai.svg b/web/src/assets/svg/llm/nomic-ai.svg new file mode 100644 index 00000000000..26e624a88b8 --- /dev/null +++ b/web/src/assets/svg/llm/nomic-ai.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/web/src/assets/svg/llm/sentence-transformers.svg b/web/src/assets/svg/llm/sentence-transformers.svg new file mode 100644 index 00000000000..f777b3d26cc --- /dev/null +++ b/web/src/assets/svg/llm/sentence-transformers.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/svg/llm/youdao.svg b/web/src/assets/svg/llm/youdao.svg new file mode 100644 index 00000000000..5af58851fd3 --- /dev/null +++ b/web/src/assets/svg/llm/youdao.svg @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/web/src/assets/svg/plus-circle-fill.svg b/web/src/assets/svg/plus-circle-fill.svg index b0fc908a53a..118318d2a50 100644 --- a/web/src/assets/svg/plus-circle-fill.svg +++ b/web/src/assets/svg/plus-circle-fill.svg @@ -1,5 +1,7 @@ + + diff --git a/web/src/assets/svg/plus.svg b/web/src/assets/svg/plus.svg new file mode 100644 index 00000000000..782d281ae03 --- /dev/null +++ b/web/src/assets/svg/plus.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/web/src/assets/svg/resize.svg b/web/src/assets/svg/resize.svg new file mode 100644 index 00000000000..1f193f52081 --- /dev/null +++ b/web/src/assets/svg/resize.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/web/src/assets/svg/switch.svg b/web/src/assets/svg/switch.svg index 33a6babc3bf..2ca5b92a562 100644 --- a/web/src/assets/svg/switch.svg +++ b/web/src/assets/svg/switch.svg @@ -2,5 +2,5 @@ width="200" height="200"> + fill="#b548f8" p-id="4300"> \ No newline at end of file diff --git a/web/src/components/knowledge-base-item.tsx b/web/src/components/knowledge-base-item.tsx index 155b09af7d4..331da8e68c9 100644 --- a/web/src/components/knowledge-base-item.tsx +++ b/web/src/components/knowledge-base-item.tsx @@ -1,6 +1,7 @@ import { useTranslate } from '@/hooks/common-hooks'; import { useNextFetchKnowledgeList } from '@/hooks/knowledge-hooks'; -import { Form, Select } from 'antd'; +import { UserOutlined } from '@ant-design/icons'; +import { Avatar, Form, Select, Space } from 'antd'; const KnowledgeBaseItem = () => { const { t } = useTranslate('chat'); @@ -8,7 +9,12 @@ const KnowledgeBaseItem = () => { const { list: knowledgeList } = useNextFetchKnowledgeList(true); const knowledgeOptions = knowledgeList.map((x) => ({ - label: x.name, + label: ( + + } src={x.avatar} /> + {x.name} + + ), value: x.id, })); diff --git a/web/src/components/llm-select/index.less b/web/src/components/llm-select/index.less new file mode 100644 index 00000000000..341768ae13a --- /dev/null +++ b/web/src/components/llm-select/index.less @@ -0,0 +1,3 @@ +.llmLabel { + font-size: 14px; +} diff --git a/web/src/components/llm-select/index.tsx b/web/src/components/llm-select/index.tsx index 1100390b81f..6804f1bd1c0 100644 --- a/web/src/components/llm-select/index.tsx +++ b/web/src/components/llm-select/index.tsx @@ -7,9 +7,10 @@ interface IProps { id?: string; value?: string; onChange?: (value: string) => void; + disabled?: boolean; } -const LLMSelect = ({ id, value, onChange }: IProps) => { +const LLMSelect = ({ id, value, onChange, disabled }: IProps) => { const modelOptions = useComposeLlmOptionsByModelTypes([ LlmModelType.Chat, LlmModelType.Image2text, @@ -38,6 +39,7 @@ const LLMSelect = ({ id, value, onChange }: IProps) => { id={id} value={value} onChange={onChange} + disabled={disabled} /> ); diff --git a/web/src/components/llm-select/llm-label.tsx b/web/src/components/llm-select/llm-label.tsx new file mode 100644 index 00000000000..dcdffab5e21 --- /dev/null +++ b/web/src/components/llm-select/llm-label.tsx @@ -0,0 +1,31 @@ +import { LlmModelType } from '@/constants/knowledge'; +import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks'; +import { useMemo } from 'react'; + +interface IProps { + id?: string; + value?: string; + onChange?: (value: string) => void; + disabled?: boolean; +} + +const LLMLabel = ({ value }: IProps) => { + const modelOptions = useComposeLlmOptionsByModelTypes([ + LlmModelType.Chat, + LlmModelType.Image2text, + ]); + + const label = useMemo(() => { + for (const item of modelOptions) { + for (const option of item.options) { + if (option.value === value) { + return option.label; + } + } + } + }, [modelOptions, value]); + + return
{label}
; +}; + +export default LLMLabel; diff --git a/web/src/components/svg-icon.tsx b/web/src/components/svg-icon.tsx index 2171fc42cc1..b329177aca1 100644 --- a/web/src/components/svg-icon.tsx +++ b/web/src/components/svg-icon.tsx @@ -1,5 +1,8 @@ -import Icon from '@ant-design/icons'; +import { IconMap } from '@/constants/setting'; +import Icon, { UserOutlined } from '@ant-design/icons'; import { IconComponentProps } from '@ant-design/icons/lib/components/Icon'; +import { Avatar } from 'antd'; +import { AvatarSize } from 'antd/es/avatar/AvatarContext'; const importAll = (requireContext: __WebpackModuleApi.RequireContext) => { const list = requireContext.keys().map((key) => { @@ -36,4 +39,24 @@ const SvgIcon = ({ name, width, height, ...restProps }: IProps) => { ); }; +export const LlmIcon = ({ + name, + height = 48, + width = 48, + size = 'large', +}: { + name: string; + height?: number; + width?: number; + size?: AvatarSize; +}) => { + const icon = IconMap[name as keyof typeof IconMap]; + + return icon ? ( + + ) : ( + } /> + ); +}; + export default SvgIcon; diff --git a/web/src/constants/setting.ts b/web/src/constants/setting.ts index e09dc4c9fc0..22c47358039 100644 --- a/web/src/constants/setting.ts +++ b/web/src/constants/setting.ts @@ -19,6 +19,56 @@ export const UserSettingRouteMap = { [UserSettingRouteKey.Logout]: 'Log out', }; +// Please lowercase the file name +export const IconMap = { + 'Tongyi-Qianwen': 'tongyi', + Moonshot: 'moonshot', + OpenAI: 'openai', + 'ZHIPU-AI': 'zhipu', + 文心一言: 'wenxin', + Ollama: 'ollama', + Xinference: 'xinference', + DeepSeek: 'deepseek', + VolcEngine: 'volc_engine', + BaiChuan: 'baichuan', + Jina: 'jina', + MiniMax: 'chat-minimax', + Mistral: 'mistral', + 'Azure-OpenAI': 'azure', + Bedrock: 'bedrock', + Gemini: 'gemini', + Groq: 'groq-next', + OpenRouter: 'open-router', + LocalAI: 'local-ai', + StepFun: 'stepfun', + NVIDIA: 'nvidia', + 'LM-Studio': 'lm-studio', + 'OpenAI-API-Compatible': 'openai-api', + cohere: 'cohere', + LeptonAI: 'lepton-ai', + TogetherAI: 'together-ai', + PerfXCloud: 'perfx-cloud', + Upstage: 'upstage', + 'novita.ai': 'novita-ai', + SILICONFLOW: 'siliconflow', + '01.AI': 'yi', + Replicate: 'replicate', + 'Tencent Hunyuan': 'hunyuan', + 'XunFei Spark': 'spark', + BaiduYiyan: 'yiyan', + 'Fish Audio': 'fish-audio', + 'Tencent Cloud': 'tencent-cloud', + Anthropic: 'anthropic', + 'Voyage AI': 'voyage', + 'Google Cloud': 'google-cloud', + HuggingFace: 'huggingface', + Youdao: 'youdao', + BAAI: 'baai', + 'nomic-ai': 'nomic-ai', + jinaai: 'jina', + 'sentence-transformers': 'sentence-transformers', +}; + export const TimezoneList = [ 'UTC-11\tPacific/Midway', 'UTC-11\tPacific/Niue', diff --git a/web/src/hooks/llm-hooks.ts b/web/src/hooks/llm-hooks.tsx similarity index 93% rename from web/src/hooks/llm-hooks.ts rename to web/src/hooks/llm-hooks.tsx index 534b896ddd2..2ee3236c1d9 100644 --- a/web/src/hooks/llm-hooks.ts +++ b/web/src/hooks/llm-hooks.tsx @@ -1,3 +1,4 @@ +import { LlmIcon } from '@/components/svg-icon'; import { LlmModelType } from '@/constants/knowledge'; import { ResponseGetType } from '@/interfaces/database/base'; import { @@ -13,7 +14,7 @@ import { import userService from '@/services/user-service'; import { sortLLmFactoryListBySpecifiedOrder } from '@/utils/common-util'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { message } from 'antd'; +import { Flex, message } from 'antd'; import { DefaultOptionType } from 'antd/es/select'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -53,6 +54,14 @@ export const useSelectLlmOptions = () => { return embeddingModelOptions; }; +const getLLMIconName = (fid: string, llm_name: string) => { + if (fid === 'FastEmbed') { + return llm_name.split('/').at(0) ?? ''; + } + + return fid; +}; + export const useSelectLlmOptionsByModelType = () => { const llmInfo: IThirdOAIModelCollection = useFetchLlmList(); @@ -71,7 +80,17 @@ export const useSelectLlmOptionsByModelType = () => { x.available, ) .map((x) => ({ - label: x.llm_name, + label: ( + + + {x.llm_name} + + ), value: `${x.llm_name}@${x.fid}`, disabled: !x.available, })), diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 5d8cfcf3782..402f9b2c856 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -728,18 +728,20 @@ The above is the content you need to summarize.`, 'The window size of conversation history that needed to be seen by LLM. The larger the better. But be careful with the maximum content length of LLM.', wikipedia: 'Wikipedia', pubMed: 'PubMed', + pubMedDescription: + 'This component is used to get search result from https://pubmed.ncbi.nlm.nih.gov/. Typically, it performs as a supplement to knowledgebases. Top N specifies the number of search results you need to adapt. E-mail is a required field.', email: 'Email', emailTip: 'This component is used to get search result from https://pubmed.ncbi.nlm.nih.gov/. Typically, it performs as a supplement to knowledgebases. Top N specifies the number of search results you need to adapt. E-mail is a required field.', arXiv: 'ArXiv', - arXivTip: + arXivDescription: 'This component is used to get search result from https://arxiv.org/. Typically, it performs as a supplement to knowledgebases. Top N specifies the number of search results you need to adapt.', sortBy: 'Sort by', submittedDate: 'Submitted date', lastUpdatedDate: 'Last updated date', relevance: 'Relevance', google: 'Google', - googleTip: + googleDescription: 'This component is used to get search result fromhttps://www.google.com/ . Typically, it performs as a supplement to knowledgebases. Top N and SerpApi API key specifies the number of search results you need to adapt.', bing: 'Bing', bingTip: diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 4157c298e09..9335c3619db 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -680,18 +680,21 @@ export default { messageHistoryWindowSizeTip: 'LLM需要查看的對話記錄的視窗大小。越大越好。但要注意LLM的最大內容長度。', wikipedia: '維基百科', + pubMed: 'PubMed', + pubMedDescription: + '此元件用於從 https://pubmed.ncbi.nlm.nih.gov/ 取得搜尋結果。通常,它充當知識庫的補充。 Top N 指定您需要適應的搜尋結果的數量。電子郵件是必填欄位。', email: '信箱', emailTip: '此元件用於從 https://pubmed.ncbi.nlm.nih.gov/ 取得搜尋結果。通常,它充當知識庫的補充。 Top N 指定您需要適應的搜尋結果的數量。電子郵件是必填欄位。', arXiv: 'ArXiv', - arXivTip: + arXivDescription: '此元件用於從 https://arxiv.org/ 取得搜尋結果。通常,它充當知識庫的補充。 Top N 指定您需要適應的搜尋結果的數量。', sortBy: '排序方式', submittedDate: '提交日期', lastUpdatedDate: '最後更新日期', relevance: '關聯', google: 'Google', - googleTip: + googleDescription: '此元件用於從https://www.google.com/取得搜尋結果。通常,它作為知識庫的補充。 Top N 和 SerpApi API 金鑰指定您需要調整的搜尋結果數量。', bing: 'Bing', bingTip: diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 693929d9fef..e0a32171cc0 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -688,7 +688,7 @@ export default { keywordExtract: '关键词', keywordExtractDescription: `该组件用于从用户的问题中提取关键词。Top N指定需要提取的关键词数量。`, baidu: '百度', - baiduDescription: `此元件用於取得www.baidu.com的搜尋結果。通常作為知識庫的補充。 Top N指定您需要適配的搜尋結果數。`, + baiduDescription: `此组件用于从 www.baidu.com 获取搜索结果。通常,它作为知识库的补充。Top N 指定您需要调整的搜索结果数量。`, duckDuckGo: 'DuckDuckGo', duckDuckGoDescription: '此元件用於從 www.duckduckgo.com 取得搜尋結果。通常,它作為知識庫的補充。 Top N 指定您需要調整的搜尋結果數。', @@ -700,18 +700,21 @@ export default { messageHistoryWindowSizeTip: 'LLM 需要查看的对话历史窗口大小。越大越好。但要注意 LLM 的最大内容长度。', wikipedia: '维基百科', - email: '邮箱', emailTip: '此组件用于从 https://pubmed.ncbi.nlm.nih.gov/ 获取搜索结果。通常,它作为知识库的补充。Top N 指定您需要调整的搜索结果数。电子邮件是必填字段。', + email: '邮箱', + pubMed: 'PubMed', + pubMedDescription: + '此组件用于从 https://pubmed.ncbi.nlm.nih.gov/ 获取搜索结果。通常,它作为知识库的补充。Top N 指定您需要调整的搜索结果数。电子邮件是必填字段。', arXiv: 'ArXiv', - arXivTip: + arXivDescription: '此组件用于从 https://arxiv.org/ 获取搜索结果。通常,它作为知识库的补充。Top N 指定您需要调整的搜索结果数量。', sortBy: '排序方式', submittedDate: '提交日期', lastUpdatedDate: '最后更新日期', relevance: '关联', google: 'Google', - googleTip: + googleDescription: '此组件用于从https://www.google.com/获取搜索结果。通常,它作为知识库的补充。Top N 和 SerpApi API 密钥指定您需要调整的搜索结果数量。', bing: 'Bing', bingTip: diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx index 39cce7096ff..8d6751c0790 100644 --- a/web/src/pages/flow/canvas/index.tsx +++ b/web/src/pages/flow/canvas/index.tsx @@ -22,9 +22,15 @@ import styles from './index.less'; import { RagNode } from './node'; import { BeginNode } from './node/begin-node'; import { CategorizeNode } from './node/categorize-node'; +import { GenerateNode } from './node/generate-node'; +import { KeywordNode } from './node/keyword-node'; import { LogicNode } from './node/logic-node'; +import { MessageNode } from './node/message-node'; import NoteNode from './node/note-node'; import { RelevantNode } from './node/relevant-node'; +import { RetrievalNode } from './node/retrieval-node'; +import { RewriteNode } from './node/rewrite-node'; +import { SwitchNode } from './node/switch-node'; const nodeTypes = { ragNode: RagNode, @@ -33,6 +39,12 @@ const nodeTypes = { relevantNode: RelevantNode, logicNode: LogicNode, noteNode: NoteNode, + switchNode: SwitchNode, + generateNode: GenerateNode, + retrievalNode: RetrievalNode, + messageNode: MessageNode, + rewriteNode: RewriteNode, + keywordNode: KeywordNode, }; const edgeTypes = { diff --git a/web/src/pages/flow/canvas/node/begin-node.tsx b/web/src/pages/flow/canvas/node/begin-node.tsx index a802cc84d88..2506870471d 100644 --- a/web/src/pages/flow/canvas/node/begin-node.tsx +++ b/web/src/pages/flow/canvas/node/begin-node.tsx @@ -1,25 +1,24 @@ -import { useTranslate } from '@/hooks/common-hooks'; import { Flex } from 'antd'; import classNames from 'classnames'; -import lowerFirst from 'lodash/lowerFirst'; +import { useTranslation } from 'react-i18next'; import { Handle, NodeProps, Position } from 'reactflow'; import { Operator, operatorMap } from '../../constant'; import { NodeData } from '../../interface'; +import OperatorIcon from '../../operator-icon'; +import { RightHandleStyle } from './handle-icon'; import styles from './index.less'; // TODO: do not allow other nodes to connect to this node -export function BeginNode({ id, data, selected }: NodeProps) { - const { t } = useTranslate('flow'); +export function BeginNode({ selected, data }: NodeProps) { + const { t } = useTranslation(); + return (
) { position={Position.Right} isConnectable className={styles.handle} + style={RightHandleStyle} > - - {t(lowerFirst(data.label))} + + + +
{t(`flow.begin`)}
-
-
{data.name}
-
); } diff --git a/web/src/pages/flow/canvas/node/categorize-node.tsx b/web/src/pages/flow/canvas/node/categorize-node.tsx index fcdace773b7..b367bd93f00 100644 --- a/web/src/pages/flow/canvas/node/categorize-node.tsx +++ b/web/src/pages/flow/canvas/node/categorize-node.tsx @@ -1,22 +1,17 @@ -import { useTranslate } from '@/hooks/common-hooks'; +import LLMLabel from '@/components/llm-select/llm-label'; import { Flex } from 'antd'; import classNames from 'classnames'; -import lowerFirst from 'lodash/lowerFirst'; +import { get } from 'lodash'; import { Handle, NodeProps, Position } from 'reactflow'; -import { Operator, SwitchElseTo, operatorMap } from '../../constant'; import { NodeData } from '../../interface'; -import OperatorIcon from '../../operator-icon'; -import CategorizeHandle from './categorize-handle'; -import NodeDropdown from './dropdown'; +import { RightHandleStyle } from './handle-icon'; import { useBuildCategorizeHandlePositions } from './hooks'; import styles from './index.less'; +import NodeHeader from './node-header'; import NodePopover from './popover'; export function CategorizeNode({ id, data, selected }: NodeProps) { - const style = operatorMap[data.label as Operator]; - const { t } = useTranslate('flow'); const { positions } = useBuildCategorizeHandlePositions({ data, id }); - const operatorName = data.label; return ( @@ -24,10 +19,6 @@ export function CategorizeNode({ id, data, selected }: NodeProps) { className={classNames(styles.logicNode, { [styles.selectedNode]: selected, })} - style={{ - backgroundColor: style.backgroundColor, - color: style.color, - }} > ) { className={styles.handle} id={'a'} > - - - {operatorName === Operator.Switch && ( - - To - - )} - {positions.map((position, idx) => { - return ( - - ); - })} - - - {t(lowerFirst(data.label))} - + + + + +
+ +
+ {positions.map((position, idx) => { + return ( +
+
{position.text}
+ +
+ ); + })}
-
-
{data.name}
-
); diff --git a/web/src/pages/flow/canvas/node/dropdown.tsx b/web/src/pages/flow/canvas/node/dropdown.tsx index f2b77509be5..aefa8147541 100644 --- a/web/src/pages/flow/canvas/node/dropdown.tsx +++ b/web/src/pages/flow/canvas/node/dropdown.tsx @@ -38,7 +38,7 @@ const NodeDropdown = ({ id, iconFontColor }: IProps) => { return ( ) { + const parameters: IGenerateParameter[] = get(data, 'form.parameters', []); + const getLabel = useGetComponentLabelByValue(id); + + return ( + +
+ + + + + +
+ +
+ + {parameters.map((x) => ( + + + + {getLabel(x.component_id)} + + + ))} + +
+
+ ); +} diff --git a/web/src/pages/flow/canvas/node/handle-icon.tsx b/web/src/pages/flow/canvas/node/handle-icon.tsx new file mode 100644 index 00000000000..c71f05c3bf8 --- /dev/null +++ b/web/src/pages/flow/canvas/node/handle-icon.tsx @@ -0,0 +1,20 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { CSSProperties } from 'react'; + +export const HandleIcon = () => { + return ( + + ); +}; + +export const RightHandleStyle: CSSProperties = { + right: -5, +}; + +export const LeftHandleStyle: CSSProperties = { + left: -7, +}; + +export default HandleIcon; diff --git a/web/src/pages/flow/canvas/node/hooks.ts b/web/src/pages/flow/canvas/node/hooks.ts index cdfecd7fe11..c1bd80025c7 100644 --- a/web/src/pages/flow/canvas/node/hooks.ts +++ b/web/src/pages/flow/canvas/node/hooks.ts @@ -1,14 +1,13 @@ import get from 'lodash/get'; -import pick from 'lodash/pick'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { useUpdateNodeInternals } from 'reactflow'; -import { Operator } from '../../constant'; -import { IPosition, NodeData } from '../../interface'; +import { SwitchElseTo } from '../../constant'; import { - buildNewPositionMap, - generateSwitchHandleText, - isKeysEqual, -} from '../../utils'; + ICategorizeItemResult, + ISwitchCondition, + NodeData, +} from '../../interface'; +import { generateSwitchHandleText } from '../../utils'; export const useBuildCategorizeHandlePositions = ({ data, @@ -17,85 +16,86 @@ export const useBuildCategorizeHandlePositions = ({ id: string; data: NodeData; }) => { - const operatorName = data.label as Operator; const updateNodeInternals = useUpdateNodeInternals(); - const [positionMap, setPositionMap] = useState>({}); - const categoryData = useMemo(() => { - if (operatorName === Operator.Categorize) { - return get(data, `form.category_description`, {}); - } else if (operatorName === Operator.Switch) { - return get(data, 'form.conditions', []); - } - return {}; - }, [data, operatorName]); + const categoryData: ICategorizeItemResult = useMemo(() => { + return get(data, `form.category_description`, {}); + }, [data]); const positions = useMemo(() => { - return Object.keys(categoryData) - .map((x, idx) => { - const position = positionMap[x]; - let text = x; - if (operatorName === Operator.Switch) { - text = generateSwitchHandleText(idx); - } - return { text, ...position }; - }) - .filter((x) => typeof x?.right === 'number'); - }, [categoryData, positionMap, operatorName]); - - useEffect(() => { - // Cache used coordinates - setPositionMap((state) => { - const categoryDataKeys = Object.keys(categoryData); - const stateKeys = Object.keys(state); - if (!isKeysEqual(categoryDataKeys, stateKeys)) { - const { newPositionMap, intersectionKeys } = buildNewPositionMap( - categoryDataKeys, - state, - ); - - const nextPositionMap = { - ...pick(state, intersectionKeys), - ...newPositionMap, - }; + const list: Array<{ + text: string; + top: number; + idx: number; + }> = []; - return nextPositionMap; - } - return state; + Object.keys(categoryData).forEach((x, idx) => { + list.push({ + text: x, + idx, + top: idx === 0 ? 98 : list[idx - 1].top + 8 + 26, + }); }); + + return list; }, [categoryData]); useEffect(() => { updateNodeInternals(id); - }, [id, updateNodeInternals, positionMap]); + }, [id, updateNodeInternals, categoryData]); return { positions }; }; -// export const useBuildSwitchHandlePositions = ({ -// data, -// id, -// }: { -// id: string; -// data: NodeData; -// }) => { -// const [positionMap, setPositionMap] = useState>({}); -// const conditions = useMemo(() => get(data, 'form.conditions', []), [data]); -// const updateNodeInternals = useUpdateNodeInternals(); +export const useBuildSwitchHandlePositions = ({ + data, + id, +}: { + id: string; + data: NodeData; +}) => { + const updateNodeInternals = useUpdateNodeInternals(); -// const positions = useMemo(() => { -// return conditions -// .map((x, idx) => { -// const text = `Item ${idx}`; -// const position = positionMap[text]; -// return { text: text, ...position }; -// }) -// .filter((x) => typeof x?.right === 'number'); -// }, [conditions, positionMap]); + const conditions: ISwitchCondition[] = useMemo(() => { + return get(data, 'form.conditions', []); + }, [data]); -// useEffect(() => { -// updateNodeInternals(id); -// }, [id, updateNodeInternals, positionMap]); + const positions = useMemo(() => { + const list: Array<{ + text: string; + top: number; + idx: number; + condition?: ISwitchCondition; + }> = []; + + [...conditions, ''].forEach((x, idx) => { + let top = idx === 0 ? 58 : list[idx - 1].top + 32; // case number (Case 1) height + flex gap + if (idx - 1 >= 0) { + const previousItems = conditions[idx - 1]?.items ?? []; + if (previousItems.length > 0) { + top += 12; // ConditionBlock padding + top += previousItems.length * 22; // condition variable height + top += (previousItems.length - 1) * 25; // operator height + } + } + + list.push({ + text: + idx < conditions.length + ? generateSwitchHandleText(idx) + : SwitchElseTo, + idx, + top, + condition: typeof x === 'string' ? undefined : x, + }); + }); -// return { positions }; -// }; + return list; + }, [conditions]); + + useEffect(() => { + updateNodeInternals(id); + }, [id, updateNodeInternals, conditions]); + + return { positions }; +}; diff --git a/web/src/pages/flow/canvas/node/index.less b/web/src/pages/flow/canvas/node/index.less index e41ab176c54..a9907df2dcd 100644 --- a/web/src/pages/flow/canvas/node/index.less +++ b/web/src/pages/flow/canvas/node/index.less @@ -3,22 +3,16 @@ -6px 0 12px 0 rgba(179, 177, 177, 0.08), -3px 0 6px -4px rgba(0, 0, 0, 0.12), -6px 0 16px 6px rgba(0, 0, 0, 0.05); + + padding: 10px; + border-radius: 10px; + background: white; + width: 200px; } .ragNode { - position: relative; .commonNode(); - padding: 5px; - border-radius: 5px; - background: white; - width: 50px; - height: 50px; - border-radius: 50%; - display: flex; - // align-items: center; - // justify-self: center; - justify-content: center; .nodeName { font-size: 10px; color: black; @@ -28,23 +22,10 @@ color: #777; font-size: 12px; } - .type { - // font-size: 12px; - } .description { font-size: 10px; } - .bottomBox { - position: absolute; - bottom: -34px; - background: white; - padding: 2px 5px; - border-radius: 5px; - box-shadow: - -6px 0 12px 0 rgba(179, 177, 177, 0.08), - -3px 0 6px -4px rgba(0, 0, 0, 0.12), - -6px 0 16px 6px rgba(0, 0, 0, 0.05); - } + .categorizeAnchorPointText { position: absolute; top: -4px; @@ -53,14 +34,25 @@ } } +@lightBackgroundColor: rgba(150, 150, 150, 0.1); +@darkBackgroundColor: rgba(150, 150, 150, 0.2); + .selectedNode { - border: 1px solid rgb(59, 118, 244); + border: 1.5px solid rgb(59, 118, 244); } .handle { display: inline-flex; - text-align: center; - // align-items: center; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + background: rgb(59, 88, 253); + border: 1px solid white; + z-index: 1; + background-image: url('@/assets/svg/plus.svg'); + background-size: cover; + background-position: center; } .jsonView { @@ -71,19 +63,8 @@ } .logicNode { - position: relative; .commonNode(); - padding: 5px; - border-radius: 5px; - background: white; - width: 100px; - height: 100px; - border-radius: 50%; - display: flex; - // align-items: center; - // justify-self: center; - justify-content: center; .nodeName { font-size: 10px; color: black; @@ -93,41 +74,122 @@ color: #777; font-size: 12px; } - .type { - // font-size: 12px; - } + .description { font-size: 10px; } - .bottomBox { - position: absolute; - bottom: -34px; - background: white; - padding: 2px 5px; - border-radius: 5px; - box-shadow: - -6px 0 12px 0 rgba(179, 177, 177, 0.08), - -3px 0 6px -4px rgba(0, 0, 0, 0.12), - -6px 0 16px 6px rgba(0, 0, 0, 0.05); - } + .categorizeAnchorPointText { position: absolute; top: -4px; left: 8px; white-space: nowrap; } + .relevantSourceLabel { + font-size: 10px; + } } .noteNode { .commonNode(); - width: 140px; - padding: 4px 6px 6px; + min-width: 140px; + width: auto; + height: 100%; + padding: 0; border-radius: 10px; - background-color: #dbf8f4; + min-height: 128px; .noteTitle { + background-color: #edfcff; font-size: 12px; + padding: 6px 6px 4px; + border-top-left-radius: 10px; + border-top-right-radius: 10px; } .noteForm { margin-top: 4px; + height: calc(100% - 50px); + } + .noteName { + padding: 0px 4px; + } + .noteTextarea { + resize: none; + border: 0; + border-radius: 0; + height: 100%; + &:focus { + border: none; + box-shadow: none; + } + } +} + +.nodeText { + padding-inline: 0.4em; + padding-block: 0.2em 0.1em; + background: @lightBackgroundColor; + border-radius: 3px; + min-height: 22px; + .textEllipsis(); +} + +.nodeTitle { + font-weight: 600; + text-align: center; + .textEllipsis(); +} + +.nodeHeader { + padding-bottom: 12px; +} + +.zeroDivider { + margin: 0 !important; +} + +.conditionBlock { + border-radius: 4px; + padding: 6px; + background: @lightBackgroundColor; +} + +.conditionLine { + border-radius: 4px; + padding: 0 4px; + background: @darkBackgroundColor; + .textEllipsis(); +} + +.conditionKey { + flex: 1; +} + +.conditionOperator { + padding: 0 2px; + text-align: center; +} + +.relevantLabel { + text-align: right; +} + +.knowledgeNodeName { + .textEllipsis(); +} + +.messageNodeContainer { + overflow-y: auto; + max-height: 300px; +} + +.generateParameters { + padding-top: 8px; + label { + flex: 2; + .textEllipsis(); + } + .parameterValue { + flex: 3; + .conditionLine; } } diff --git a/web/src/pages/flow/canvas/node/index.tsx b/web/src/pages/flow/canvas/node/index.tsx index 5ba34693380..0001507fb55 100644 --- a/web/src/pages/flow/canvas/node/index.tsx +++ b/web/src/pages/flow/canvas/node/index.tsx @@ -1,12 +1,9 @@ -import { Flex } from 'antd'; import classNames from 'classnames'; -import pick from 'lodash/pick'; import { Handle, NodeProps, Position } from 'reactflow'; -import { Operator, operatorMap } from '../../constant'; import { NodeData } from '../../interface'; -import OperatorIcon from '../../operator-icon'; -import NodeDropdown from './dropdown'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import styles from './index.less'; +import NodeHeader from './node-header'; import NodePopover from './popover'; export function RagNode({ @@ -15,17 +12,12 @@ export function RagNode({ isConnectable = true, selected, }: NodeProps) { - const style = operatorMap[data.label as Operator]; - return (
- - - - - - - - - - - - - - - -
-
{data.name}
-
+
); diff --git a/web/src/pages/flow/canvas/node/keyword-node.tsx b/web/src/pages/flow/canvas/node/keyword-node.tsx new file mode 100644 index 00000000000..6c74a0b97f4 --- /dev/null +++ b/web/src/pages/flow/canvas/node/keyword-node.tsx @@ -0,0 +1,54 @@ +import LLMLabel from '@/components/llm-select/llm-label'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { Handle, NodeProps, Position } from 'reactflow'; +import { NodeData } from '../../interface'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; +import NodePopover from './popover'; + +export function KeywordNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + return ( + +
+ + + + + +
+ +
+
+
+ ); +} diff --git a/web/src/pages/flow/canvas/node/logic-node.tsx b/web/src/pages/flow/canvas/node/logic-node.tsx index e155694b63f..3501917d90f 100644 --- a/web/src/pages/flow/canvas/node/logic-node.tsx +++ b/web/src/pages/flow/canvas/node/logic-node.tsx @@ -1,38 +1,23 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Flex } from 'antd'; import classNames from 'classnames'; -import lowerFirst from 'lodash/lowerFirst'; -import pick from 'lodash/pick'; import { Handle, NodeProps, Position } from 'reactflow'; -import { Operator, operatorMap } from '../../constant'; import { NodeData } from '../../interface'; -import OperatorIcon from '../../operator-icon'; -import NodeDropdown from './dropdown'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import styles from './index.less'; +import NodeHeader from './node-header'; import NodePopover from './popover'; -const ZeroGapOperators = [ - Operator.RewriteQuestion, - Operator.KeywordExtract, - Operator.ArXiv, -]; - export function LogicNode({ id, data, isConnectable = true, selected, }: NodeProps) { - const style = operatorMap[data.label as Operator]; - const { t } = useTranslate('flow'); - return (
- - - x === data.label) ? 0 : 6} - > - - - - - - - {t(lowerFirst(data.label))} - - - - - - - -
-
{data.name}
-
+
); diff --git a/web/src/pages/flow/canvas/node/message-node.tsx b/web/src/pages/flow/canvas/node/message-node.tsx new file mode 100644 index 00000000000..a0db834919d --- /dev/null +++ b/web/src/pages/flow/canvas/node/message-node.tsx @@ -0,0 +1,63 @@ +import { Flex } from 'antd'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { Handle, NodeProps, Position } from 'reactflow'; +import { NodeData } from '../../interface'; +import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; +import styles from './index.less'; +import NodeHeader from './node-header'; +import NodePopover from './popover'; + +export function MessageNode({ + id, + data, + isConnectable = true, + selected, +}: NodeProps) { + const messages: string[] = get(data, 'form.messages', []); + + return ( + +
+ + + 0, + })} + > + + + {messages.map((message, idx) => { + return ( +
+ {message} +
+ ); + })} +
+
+
+ ); +} diff --git a/web/src/pages/flow/canvas/node/node-header.tsx b/web/src/pages/flow/canvas/node/node-header.tsx new file mode 100644 index 00000000000..4386f9c4752 --- /dev/null +++ b/web/src/pages/flow/canvas/node/node-header.tsx @@ -0,0 +1,35 @@ +import { Flex } from 'antd'; + +import { Operator, operatorMap } from '../../constant'; +import OperatorIcon from '../../operator-icon'; +import NodeDropdown from './dropdown'; +import styles from './index.less'; + +interface IProps { + id: string; + label: string; + name: string; + gap?: number; + className?: string; +} + +const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => { + return ( + + + {name} + + + ); +}; + +export default NodeHeader; diff --git a/web/src/pages/flow/canvas/node/note-node.tsx b/web/src/pages/flow/canvas/node/note-node.tsx index 1686255e08c..5028baa1d89 100644 --- a/web/src/pages/flow/canvas/node/note-node.tsx +++ b/web/src/pages/flow/canvas/node/note-node.tsx @@ -1,20 +1,33 @@ -import { Flex, Form, Input, Space } from 'antd'; -import { NodeProps } from 'reactflow'; +import { Flex, Form, Input } from 'antd'; +import classNames from 'classnames'; +import { NodeProps, NodeResizeControl } from 'reactflow'; import { NodeData } from '../../interface'; import NodeDropdown from './dropdown'; import SvgIcon from '@/components/svg-icon'; -import { useEffect } from 'react'; +import { memo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useHandleFormValuesChange } from '../../hooks'; +import { + useHandleFormValuesChange, + useHandleNodeNameChange, +} from '../../hooks'; import styles from './index.less'; const { TextArea } = Input; +const controlStyle = { + background: 'transparent', + border: 'none', +}; + function NoteNode({ data, id }: NodeProps) { const { t } = useTranslation(); const [form] = Form.useForm(); + const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({ + id, + data, + }); const { handleValuesChange } = useHandleFormValuesChange(id); useEffect(() => { @@ -22,25 +35,51 @@ function NoteNode({ data, id }: NodeProps) { }, [form, data?.form]); return ( -
- - + <> + + + +
+ - {t('flow.note')} - - - -
- -