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.

workflow-preview.tsx 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import {
  2. memo,
  3. useCallback,
  4. useEffect,
  5. useState,
  6. } from 'react'
  7. import {
  8. RiClipboardLine,
  9. RiCloseLine,
  10. } from '@remixicon/react'
  11. import { useTranslation } from 'react-i18next'
  12. import copy from 'copy-to-clipboard'
  13. import ResultText from '../run/result-text'
  14. import ResultPanel from '../run/result-panel'
  15. import TracingPanel from '../run/tracing-panel'
  16. import {
  17. useWorkflowInteractions,
  18. } from '../hooks'
  19. import { useStore } from '../store'
  20. import {
  21. WorkflowRunningStatus,
  22. } from '../types'
  23. import { formatWorkflowRunIdentifier } from '../utils'
  24. import Toast from '../../base/toast'
  25. import InputsPanel from './inputs-panel'
  26. import cn from '@/utils/classnames'
  27. import Loading from '@/app/components/base/loading'
  28. import Button from '@/app/components/base/button'
  29. const WorkflowPreview = () => {
  30. const { t } = useTranslation()
  31. const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
  32. const workflowRunningData = useStore(s => s.workflowRunningData)
  33. const showInputsPanel = useStore(s => s.showInputsPanel)
  34. const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
  35. const [currentTab, setCurrentTab] = useState<string>(showInputsPanel ? 'INPUT' : 'TRACING')
  36. const switchTab = async (tab: string) => {
  37. setCurrentTab(tab)
  38. }
  39. useEffect(() => {
  40. if (showDebugAndPreviewPanel && showInputsPanel)
  41. setCurrentTab('INPUT')
  42. }, [showDebugAndPreviewPanel, showInputsPanel])
  43. useEffect(() => {
  44. if ((workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded || workflowRunningData?.result.status === WorkflowRunningStatus.Failed) && !workflowRunningData.resultText && !workflowRunningData.result.files?.length)
  45. switchTab('DETAIL')
  46. }, [workflowRunningData])
  47. const [panelWidth, setPanelWidth] = useState(420)
  48. const [isResizing, setIsResizing] = useState(false)
  49. const startResizing = useCallback((e: React.MouseEvent) => {
  50. e.preventDefault()
  51. setIsResizing(true)
  52. }, [])
  53. const stopResizing = useCallback(() => {
  54. setIsResizing(false)
  55. }, [])
  56. const resize = useCallback((e: MouseEvent) => {
  57. if (isResizing) {
  58. const newWidth = window.innerWidth - e.clientX
  59. if (newWidth > 420 && newWidth < 1024)
  60. setPanelWidth(newWidth)
  61. }
  62. }, [isResizing])
  63. useEffect(() => {
  64. window.addEventListener('mousemove', resize)
  65. window.addEventListener('mouseup', stopResizing)
  66. return () => {
  67. window.removeEventListener('mousemove', resize)
  68. window.removeEventListener('mouseup', stopResizing)
  69. }
  70. }, [resize, stopResizing])
  71. return (
  72. <div className={`
  73. relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl
  74. `}
  75. style={{ width: `${panelWidth}px` }}
  76. >
  77. <div
  78. className="absolute bottom-0 left-[3px] top-1/2 z-50 h-6 w-[3px] cursor-col-resize rounded bg-gray-300"
  79. onMouseDown={startResizing}
  80. />
  81. <div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary'>
  82. {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at)}`}
  83. <div className='cursor-pointer p-1' onClick={() => handleCancelDebugAndPreviewPanel()}>
  84. <RiCloseLine className='h-4 w-4 text-text-tertiary' />
  85. </div>
  86. </div>
  87. <div className='relative flex grow flex-col'>
  88. <div className='flex shrink-0 items-center border-b-[0.5px] border-divider-subtle px-4'>
  89. {showInputsPanel && (
  90. <div
  91. className={cn(
  92. 'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
  93. currentTab === 'INPUT' && '!border-[rgb(21,94,239)] text-text-secondary',
  94. )}
  95. onClick={() => switchTab('INPUT')}
  96. >{t('runLog.input')}</div>
  97. )}
  98. <div
  99. className={cn(
  100. 'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
  101. currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-text-secondary',
  102. !workflowRunningData && '!cursor-not-allowed opacity-30',
  103. )}
  104. onClick={() => {
  105. if (!workflowRunningData)
  106. return
  107. switchTab('RESULT')
  108. }}
  109. >{t('runLog.result')}</div>
  110. <div
  111. className={cn(
  112. 'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
  113. currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary',
  114. !workflowRunningData && '!cursor-not-allowed opacity-30',
  115. )}
  116. onClick={() => {
  117. if (!workflowRunningData)
  118. return
  119. switchTab('DETAIL')
  120. }}
  121. >{t('runLog.detail')}</div>
  122. <div
  123. className={cn(
  124. 'mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] font-semibold leading-[18px] text-text-tertiary',
  125. currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary',
  126. !workflowRunningData && '!cursor-not-allowed opacity-30',
  127. )}
  128. onClick={() => {
  129. if (!workflowRunningData)
  130. return
  131. switchTab('TRACING')
  132. }}
  133. >{t('runLog.tracing')}</div>
  134. </div>
  135. <div className={cn(
  136. 'h-0 grow overflow-y-auto rounded-b-2xl bg-components-panel-bg',
  137. (currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-background-section-burn',
  138. )}>
  139. {currentTab === 'INPUT' && showInputsPanel && (
  140. <InputsPanel onRun={() => switchTab('RESULT')} />
  141. )}
  142. {currentTab === 'RESULT' && (
  143. <>
  144. <ResultText
  145. isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
  146. outputs={workflowRunningData?.resultText}
  147. allFiles={workflowRunningData?.result?.files}
  148. error={workflowRunningData?.result?.error}
  149. onClick={() => switchTab('DETAIL')}
  150. />
  151. {(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && (
  152. <Button
  153. className={cn('mb-4 ml-4 space-x-1')}
  154. onClick={() => {
  155. const content = workflowRunningData?.resultText
  156. if (typeof content === 'string')
  157. copy(content)
  158. else
  159. copy(JSON.stringify(content))
  160. Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
  161. }}>
  162. <RiClipboardLine className='h-3.5 w-3.5' />
  163. <div>{t('common.operation.copy')}</div>
  164. </Button>
  165. )}
  166. </>
  167. )}
  168. {currentTab === 'DETAIL' && (
  169. <ResultPanel
  170. inputs={workflowRunningData?.result?.inputs}
  171. outputs={workflowRunningData?.result?.outputs}
  172. status={workflowRunningData?.result?.status || ''}
  173. error={workflowRunningData?.result?.error}
  174. elapsed_time={workflowRunningData?.result?.elapsed_time}
  175. total_tokens={workflowRunningData?.result?.total_tokens}
  176. created_at={workflowRunningData?.result?.created_at}
  177. created_by={(workflowRunningData?.result?.created_by as any)?.name}
  178. steps={workflowRunningData?.result?.total_steps}
  179. exceptionCounts={workflowRunningData?.result?.exceptions_count}
  180. />
  181. )}
  182. {currentTab === 'DETAIL' && !workflowRunningData?.result && (
  183. <div className='flex h-full items-center justify-center bg-components-panel-bg'>
  184. <Loading />
  185. </div>
  186. )}
  187. {currentTab === 'TRACING' && (
  188. <TracingPanel
  189. className='bg-background-section-burn'
  190. list={workflowRunningData?.tracing || []}
  191. />
  192. )}
  193. {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
  194. <div className='flex h-full items-center justify-center !bg-background-section-burn'>
  195. <Loading />
  196. </div>
  197. )}
  198. </div>
  199. </div>
  200. </div>
  201. )
  202. }
  203. export default memo(WorkflowPreview)