panel.tsx 6.9 KB

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