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

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