You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

panel.tsx 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import type {
  2. FC,
  3. ReactNode,
  4. } from 'react'
  5. import {
  6. cloneElement,
  7. memo,
  8. useCallback,
  9. } from 'react'
  10. import {
  11. RiCloseLine,
  12. RiPlayLargeLine,
  13. } from '@remixicon/react'
  14. import { useShallow } from 'zustand/react/shallow'
  15. import { useTranslation } from 'react-i18next'
  16. import NextStep from './components/next-step'
  17. import PanelOperator from './components/panel-operator'
  18. import HelpLink from './components/help-link'
  19. import NodePosition from './components/node-position'
  20. import {
  21. DescriptionInput,
  22. TitleInput,
  23. } from './components/title-description-input'
  24. import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel'
  25. import RetryOnPanel from './components/retry/retry-on-panel'
  26. import { useResizePanel } from './hooks/use-resize-panel'
  27. import cn from '@/utils/classnames'
  28. import BlockIcon from '@/app/components/workflow/block-icon'
  29. import Split from '@/app/components/workflow/nodes/_base/components/split'
  30. import {
  31. WorkflowHistoryEvent,
  32. useAvailableBlocks,
  33. useNodeDataUpdate,
  34. useNodesInteractions,
  35. useNodesReadOnly,
  36. useNodesSyncDraft,
  37. useToolIcon,
  38. useWorkflow,
  39. useWorkflowHistory,
  40. } from '@/app/components/workflow/hooks'
  41. import {
  42. canRunBySingle,
  43. hasErrorHandleNode,
  44. hasRetryNode,
  45. } from '@/app/components/workflow/utils'
  46. import Tooltip from '@/app/components/base/tooltip'
  47. import type { Node } from '@/app/components/workflow/types'
  48. import { useStore as useAppStore } from '@/app/components/app/store'
  49. import { useStore } from '@/app/components/workflow/store'
  50. type BasePanelProps = {
  51. children: ReactNode
  52. } & Node
  53. const BasePanel: FC<BasePanelProps> = ({
  54. id,
  55. data,
  56. children,
  57. position,
  58. width,
  59. height,
  60. }) => {
  61. const { t } = useTranslation()
  62. const { showMessageLogModal } = useAppStore(useShallow(state => ({
  63. showMessageLogModal: state.showMessageLogModal,
  64. })))
  65. const showSingleRunPanel = useStore(s => s.showSingleRunPanel)
  66. const panelWidth = localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
  67. const {
  68. setPanelWidth,
  69. } = useWorkflow()
  70. const { handleNodeSelect } = useNodesInteractions()
  71. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  72. const { nodesReadOnly } = useNodesReadOnly()
  73. const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
  74. const toolIcon = useToolIcon(data)
  75. const handleResize = useCallback((width: number) => {
  76. setPanelWidth(width)
  77. }, [setPanelWidth])
  78. const {
  79. triggerRef,
  80. containerRef,
  81. } = useResizePanel({
  82. direction: 'horizontal',
  83. triggerDirection: 'left',
  84. minWidth: 420,
  85. maxWidth: 720,
  86. onResize: handleResize,
  87. })
  88. const { saveStateToHistory } = useWorkflowHistory()
  89. const {
  90. handleNodeDataUpdate,
  91. handleNodeDataUpdateWithSyncDraft,
  92. } = useNodeDataUpdate()
  93. const handleTitleBlur = useCallback((title: string) => {
  94. handleNodeDataUpdateWithSyncDraft({ id, data: { title } })
  95. saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange)
  96. }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
  97. const handleDescriptionChange = useCallback((desc: string) => {
  98. handleNodeDataUpdateWithSyncDraft({ id, data: { desc } })
  99. saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange)
  100. }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
  101. return (
  102. <div className={cn(
  103. 'relative mr-2 h-full',
  104. showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
  105. )}>
  106. <div
  107. ref={triggerRef}
  108. className='absolute -left-2 top-1/2 h-6 w-3 -translate-y-1/2 cursor-col-resize resize-x'>
  109. <div className='h-6 w-1 rounded-sm bg-divider-regular'></div>
  110. </div>
  111. <div
  112. ref={containerRef}
  113. className={cn('h-full rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
  114. style={{
  115. width: `${panelWidth}px`,
  116. }}
  117. >
  118. <div className='sticky top-0 z-10 border-b-[0.5px] border-divider-regular bg-components-panel-bg'>
  119. <div className='flex items-center px-4 pb-1 pt-4'>
  120. <BlockIcon
  121. className='mr-1 shrink-0'
  122. type={data.type}
  123. toolIcon={toolIcon}
  124. size='md'
  125. />
  126. <TitleInput
  127. value={data.title || ''}
  128. onBlur={handleTitleBlur}
  129. />
  130. <div className='flex shrink-0 items-center text-text-tertiary'>
  131. {
  132. canRunBySingle(data.type) && !nodesReadOnly && (
  133. <Tooltip
  134. popupContent={t('workflow.panel.runThisStep')}
  135. popupClassName='mr-1'
  136. >
  137. <div
  138. className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
  139. onClick={() => {
  140. handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
  141. handleSyncWorkflowDraft(true)
  142. }}
  143. >
  144. <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
  145. </div>
  146. </Tooltip>
  147. )
  148. }
  149. <NodePosition nodePosition={position} nodeWidth={width} nodeHeight={height}></NodePosition>
  150. <HelpLink nodeType={data.type} />
  151. <PanelOperator id={id} data={data} showHelpLink={false} />
  152. <div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
  153. <div
  154. className='flex h-6 w-6 cursor-pointer items-center justify-center'
  155. onClick={() => handleNodeSelect(id, true)}
  156. >
  157. <RiCloseLine className='h-4 w-4 text-text-tertiary' />
  158. </div>
  159. </div>
  160. </div>
  161. <div className='p-2'>
  162. <DescriptionInput
  163. value={data.desc || ''}
  164. onChange={handleDescriptionChange}
  165. />
  166. </div>
  167. </div>
  168. <div>
  169. {cloneElement(children as any, { id, data })}
  170. </div>
  171. <Split />
  172. {
  173. hasRetryNode(data.type) && (
  174. <RetryOnPanel
  175. id={id}
  176. data={data}
  177. />
  178. )
  179. }
  180. {
  181. hasErrorHandleNode(data.type) && (
  182. <ErrorHandleOnPanel
  183. id={id}
  184. data={data}
  185. />
  186. )
  187. }
  188. {
  189. !!availableNextBlocks.length && (
  190. <div className='border-t-[0.5px] border-divider-regular p-4'>
  191. <div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
  192. {t('workflow.panel.nextStep').toLocaleUpperCase()}
  193. </div>
  194. <div className='system-xs-regular mb-2 text-text-tertiary'>
  195. {t('workflow.panel.addNextStep')}
  196. </div>
  197. <NextStep selectedNode={{ id, data } as Node} />
  198. </div>
  199. )
  200. }
  201. </div>
  202. </div>
  203. )
  204. }
  205. export default memo(BasePanel)