diff --git a/web/src/assets/svg/begin.svg b/web/src/assets/svg/begin.svg new file mode 100644 index 0000000000000000000000000000000000000000..c1e77892f07047387c425673121c184391145aa8 --- /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 d1ecdfad1b7318c414a7e73decb6d63767fde4bc..a5ddc57968a34987c2997cc14ddfa4178d66f7b0 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 0ab63a101815231c538343b01d7178a1032d989c..9d0350dcf158365f6684ae29ee4a806f147d2a9a 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 0000000000000000000000000000000000000000..7e365e377071ffa14c1b1061a4ebc4fc6223bb06 --- /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 0000000000000000000000000000000000000000..26e624a88b8b0848c5d4e357d00e4b1977b90d81 --- /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 0000000000000000000000000000000000000000..f777b3d26ccfc3156731ec99c860b0b414279903 --- /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 0000000000000000000000000000000000000000..5af58851fd330ee4f545f2abcaebb1feb6524328 --- /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 b0fc908a53a618028f8cf8ab68254aac9347bfd4..118318d2a5042ec78140d1fefe303d3d8ef4e307 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 0000000000000000000000000000000000000000..782d281ae034f25b3170b0468ccf9b1fce96f44d --- /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 0000000000000000000000000000000000000000..1f193f52081bc7945f9db191b3c601ec33b6e5c7 --- /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 33a6babc3bf016fb67332ca061a1428a9b6a02b6..2ca5b92a5625da3debc3b5de0d443c4b66ffd567 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 155b09af7d4c235386c75ce24f7cf11c636e7a20..331da8e68c9070a7d1f43584e4e7055b1f22f472 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 0000000000000000000000000000000000000000..341768ae13a67310582f7f388b9159dde10e61a8 --- /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 1100390b81fcd98c9e90c1666ec182b72e0efbc1..6804f1bd1c0f253636927b837ceea61d82fba184 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 0000000000000000000000000000000000000000..dcdffab5e219ef43ab55da213a71f638f62e122b --- /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 2171fc42cc17898e59a55b1f0a57acc4bbdb1eea..b329177aca1b1c3e4988fa8546b309f30c834fa8 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 e09dc4c9fc08b606fa4f289d088d45062f64761b..22c473580398db5d4781511262089b257487132b 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 534b896ddd2a252bb8785071e92b49e4c2f3813c..2ee3236c1d96e58788cfb5e0d408cdab0eeca265 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 5d8cfcf3782730551b69c018f15dd4b57e3814a0..402f9b2c856e42a141ae89d18c96d845f96dd30e 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 4157c298e094ddc940968d84312e54386d90e88b..9335c3619db132cb9e2f8a58cc6a2241150e2c08 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 693929d9fef9b186c7e1d7bef4ea7a3af8264165..e0a32171cc0410e96424bfbf64e10e493e208083 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 39cce7096ffcb7eb147e727f3bedf5d192c412eb..8d6751c0790ec6a43fa1ab8590e7cedc55add4ae 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 a802cc84d881b6e4d12657df7ec4ec4a62bf0227..2506870471d76b39dd67845daef6f5ee3a496a61 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 fcdace773b7436c92c90cd84582556614620efe2..b367bd93f002201a439bcd1b3aa7872739d998d7 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 f2b77509be52a364eed16b85d5ee52f780610abe..aefa8147541da7f528d3b212b1346e0fd3f41036 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 0000000000000000000000000000000000000000..c71f05c3bf868dd6ded55443a53cee1780320a8e --- /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 cdfecd7fe110a80aaa4212f82cbfcc75d10681b6..c1bd80025c7f690ca362811c1f5507dff9b9d2ae 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 e41ab176c548c6b6ab66a4a4a8cab4444ab26860..a9907df2dcd86d32d28d18e155aa4a752630aa6b 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 5ba3469338092f2c9bd3a6aba10e831d3e25c0fd..0001507fb55f810913ea4a1454869db2fd6acb60 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 0000000000000000000000000000000000000000..6c74a0b97f483da1912fb376ade78bf2fa49bd37 --- /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 e155694b63fb351b9e2e90a543712049c90c6a82..3501917d90fab9ad2359b14b15a5757046515ef5 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 0000000000000000000000000000000000000000..a0db834919dc552798040ed346e530f441227f45 --- /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 0000000000000000000000000000000000000000..4386f9c475283bdea479821fae797616a89c4993 --- /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 1686255e08c3a7a8f77f9853a485e2a89af61361..5028baa1d892be7a8f1286e7d4b7fbcb4a4a9ecf 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')} - - - -
- -