add-nodes-sider.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import { ChatContext } from '@/app/chat-context';
  2. import { apiInterceptors, getFlowNodes } from '@/client/api';
  3. import { IFlowNode } from '@/types/flow';
  4. import { FLOW_NODES_KEY } from '@/utils';
  5. import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
  6. import type { CollapseProps } from 'antd';
  7. import { Badge, Collapse, Input, Layout, Space, Switch } from 'antd';
  8. import classnames from 'classnames';
  9. import React, { useContext, useEffect, useMemo, useState } from 'react';
  10. import { useTranslation } from 'react-i18next';
  11. import StaticNodes from './static-nodes';
  12. const { Search } = Input;
  13. const { Sider } = Layout;
  14. const TAGS = JSON.stringify({ order: 'higher-order' });
  15. type GroupType = {
  16. category: string;
  17. categoryLabel: string;
  18. nodes: IFlowNode[];
  19. };
  20. const zeroWidthTriggerDefaultStyle: React.CSSProperties = {
  21. display: 'flex',
  22. alignItems: 'center',
  23. justifyContent: 'center',
  24. width: 16,
  25. height: 48,
  26. position: 'absolute',
  27. top: '50%',
  28. transform: 'translateY(-50%)',
  29. border: '1px solid #d6d8da',
  30. borderRadius: 8,
  31. right: -8,
  32. };
  33. const AddNodesSider: React.FC = () => {
  34. const { t } = useTranslation();
  35. const { mode } = useContext(ChatContext);
  36. const [collapsed, setCollapsed] = useState<boolean>(false);
  37. const [searchValue, setSearchValue] = useState<string>('');
  38. const [operators, setOperators] = useState<Array<IFlowNode>>([]);
  39. const [resources, setResources] = useState<Array<IFlowNode>>([]);
  40. const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
  41. const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
  42. const [isAllNodesVisible, setIsAllNodesVisible] = useState<boolean>(false);
  43. useEffect(() => {
  44. getNodes(TAGS);
  45. }, []);
  46. // tags is optional, if tags is not passed, it will get all nodes
  47. async function getNodes(tags?: string) {
  48. const [_, data] = await apiInterceptors(getFlowNodes(tags));
  49. if (data && data.length > 0) {
  50. localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
  51. const operatorNodes = data.filter(node => node.flow_type === 'operator');
  52. const resourceNodes = data.filter(node => node.flow_type === 'resource');
  53. setOperators(operatorNodes);
  54. setResources(resourceNodes);
  55. setOperatorsGroup(groupNodes(operatorNodes));
  56. setResourcesGroup(groupNodes(resourceNodes));
  57. }
  58. }
  59. const triggerStyle: React.CSSProperties = useMemo(() => {
  60. if (collapsed) {
  61. return {
  62. ...zeroWidthTriggerDefaultStyle,
  63. right: -16,
  64. borderRadius: '0px 8px 8px 0',
  65. borderLeft: '1px solid #d5e5f6',
  66. };
  67. }
  68. return {
  69. ...zeroWidthTriggerDefaultStyle,
  70. borderLeft: '1px solid #d6d8da',
  71. };
  72. }, [collapsed]);
  73. function groupNodes(data: IFlowNode[]) {
  74. const groups: GroupType[] = [];
  75. const categoryMap: Record<string, { category: string; categoryLabel: string; nodes: IFlowNode[] }> = {};
  76. data.forEach(item => {
  77. const { category, category_label } = item;
  78. if (!categoryMap[category]) {
  79. categoryMap[category] = {
  80. category,
  81. categoryLabel: category_label,
  82. nodes: [],
  83. };
  84. groups.push(categoryMap[category]);
  85. }
  86. categoryMap[category].nodes.push(item);
  87. });
  88. return groups;
  89. }
  90. const operatorItems: CollapseProps['items'] = useMemo(() => {
  91. if (!searchValue) {
  92. return operatorsGroup.map(({ category, categoryLabel, nodes }) => ({
  93. key: category,
  94. label: categoryLabel,
  95. children: <StaticNodes nodes={nodes} />,
  96. extra: (
  97. <Badge
  98. showZero
  99. count={nodes.length || 0}
  100. style={{
  101. backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
  102. }}
  103. />
  104. ),
  105. }));
  106. } else {
  107. const searchedNodes = operators.filter(node => node.label.toLowerCase().includes(searchValue.toLowerCase()));
  108. return groupNodes(searchedNodes).map(({ category, categoryLabel, nodes }) => ({
  109. key: category,
  110. label: categoryLabel,
  111. children: <StaticNodes nodes={nodes} />,
  112. extra: (
  113. <Badge
  114. showZero
  115. count={nodes.length || 0}
  116. style={{
  117. backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
  118. }}
  119. />
  120. ),
  121. }));
  122. }
  123. }, [operatorsGroup, searchValue]);
  124. const resourceItems: CollapseProps['items'] = useMemo(() => {
  125. if (!searchValue) {
  126. return resourcesGroup.map(({ category, categoryLabel, nodes }) => ({
  127. key: category,
  128. label: categoryLabel,
  129. children: <StaticNodes nodes={nodes} />,
  130. extra: (
  131. <Badge
  132. showZero
  133. count={nodes.length || 0}
  134. style={{
  135. backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
  136. }}
  137. />
  138. ),
  139. }));
  140. } else {
  141. const searchedNodes = resources.filter(node => node.label.toLowerCase().includes(searchValue.toLowerCase()));
  142. return groupNodes(searchedNodes).map(({ category, categoryLabel, nodes }) => ({
  143. key: category,
  144. label: categoryLabel,
  145. children: <StaticNodes nodes={nodes} />,
  146. extra: (
  147. <Badge
  148. showZero
  149. count={nodes.length || 0}
  150. style={{
  151. backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
  152. }}
  153. />
  154. ),
  155. }));
  156. }
  157. }, [resourcesGroup, searchValue]);
  158. function searchNode(val: string) {
  159. setSearchValue(val);
  160. }
  161. function onModeChange() {
  162. if (isAllNodesVisible) {
  163. getNodes(TAGS);
  164. } else {
  165. getNodes();
  166. }
  167. setIsAllNodesVisible(!isAllNodesVisible);
  168. }
  169. return (
  170. <Sider
  171. className='flex justify-center items-start nodrag bg-[#ffffff80] border-r border-[#d5e5f6] dark:bg-[#ffffff29] dark:border-[#ffffff66]'
  172. theme={mode}
  173. width={280}
  174. collapsible={true}
  175. collapsed={collapsed}
  176. collapsedWidth={0}
  177. trigger={collapsed ? <CaretRightOutlined className='text-base' /> : <CaretLeftOutlined className='text-base' />}
  178. zeroWidthTriggerStyle={triggerStyle}
  179. onCollapse={collapsed => setCollapsed(collapsed)}
  180. >
  181. <Space direction='vertical' className='w-[280px] pt-4 px-4 overflow-hidden overflow-y-auto scrollbar-default'>
  182. <div className='flex justify-between align-middle'>
  183. <p className='w-full text-base font-semibold text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] line-clamp-1'>
  184. {t('add_node')}
  185. </p>
  186. <Switch
  187. checkedChildren='高阶'
  188. unCheckedChildren='全部'
  189. onClick={onModeChange}
  190. className={classnames('w-20', { 'bg-zinc-400': isAllNodesVisible })}
  191. defaultChecked
  192. />
  193. </div>
  194. <Search placeholder='Search node' onSearch={searchNode} allowClear />
  195. <h2 className='font-semibold'>{t('operators')}</h2>
  196. <Collapse
  197. size='small'
  198. bordered={false}
  199. className='max-h-[calc((100vh-156px)/2)] overflow-hidden overflow-y-auto scrollbar-default'
  200. defaultActiveKey={['']}
  201. items={operatorItems}
  202. />
  203. <h2 className='font-semibold'>{t('resource')}</h2>
  204. <Collapse
  205. size='small'
  206. bordered={false}
  207. className='max-h-[calc((100vh-156px)/2)] overflow-hidden overflow-y-auto scrollbar-default'
  208. defaultActiveKey={['']}
  209. items={resourceItems}
  210. />
  211. </Space>
  212. </Sider>
  213. );
  214. };
  215. export default AddNodesSider;