Spaces:
Paused
Paused
| 'use client' | |
| import type { FC } from 'react' | |
| import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' | |
| import { useContext } from 'use-context-selector' | |
| import { useTranslation } from 'react-i18next' | |
| import { useBoolean } from 'ahooks' | |
| import { BlockEnum } from '../types' | |
| import OutputPanel from './output-panel' | |
| import ResultPanel from './result-panel' | |
| import TracingPanel from './tracing-panel' | |
| import IterationResultPanel from './iteration-result-panel' | |
| import cn from '@/utils/classnames' | |
| import { ToastContext } from '@/app/components/base/toast' | |
| import Loading from '@/app/components/base/loading' | |
| import { fetchRunDetail, fetchTracingList } from '@/service/log' | |
| import type { NodeTracing } from '@/types/workflow' | |
| import type { WorkflowRunDetailResponse } from '@/models/log' | |
| import { useStore as useAppStore } from '@/app/components/app/store' | |
| export type RunProps = { | |
| hideResult?: boolean | |
| activeTab?: 'RESULT' | 'DETAIL' | 'TRACING' | |
| runID: string | |
| getResultCallback?: (result: WorkflowRunDetailResponse) => void | |
| } | |
| const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getResultCallback }) => { | |
| const { t } = useTranslation() | |
| const { notify } = useContext(ToastContext) | |
| const [currentTab, setCurrentTab] = useState<string>(activeTab) | |
| const appDetail = useAppStore(state => state.appDetail) | |
| const [loading, setLoading] = useState<boolean>(true) | |
| const [runDetail, setRunDetail] = useState<WorkflowRunDetailResponse>() | |
| const [list, setList] = useState<NodeTracing[]>([]) | |
| const executor = useMemo(() => { | |
| if (runDetail?.created_by_role === 'account') | |
| return runDetail.created_by_account?.name || '' | |
| if (runDetail?.created_by_role === 'end_user') | |
| return runDetail.created_by_end_user?.session_id || '' | |
| return 'N/A' | |
| }, [runDetail]) | |
| const getResult = useCallback(async (appID: string, runID: string) => { | |
| try { | |
| const res = await fetchRunDetail({ | |
| appID, | |
| runID, | |
| }) | |
| setRunDetail(res) | |
| if (getResultCallback) | |
| getResultCallback(res) | |
| } | |
| catch (err) { | |
| notify({ | |
| type: 'error', | |
| message: `${err}`, | |
| }) | |
| } | |
| }, [notify, getResultCallback]) | |
| const formatNodeList = useCallback((list: NodeTracing[]) => { | |
| const allItems = [...list].reverse() | |
| const result: NodeTracing[] = [] | |
| const groupMap = new Map<string, NodeTracing[]>() | |
| const processIterationNode = (item: NodeTracing) => { | |
| result.push({ | |
| ...item, | |
| details: [], | |
| }) | |
| } | |
| const updateParallelModeGroup = (runId: string, item: NodeTracing, iterationNode: NodeTracing) => { | |
| if (!groupMap.has(runId)) | |
| groupMap.set(runId, [item]) | |
| else | |
| groupMap.get(runId)!.push(item) | |
| if (item.status === 'failed') { | |
| iterationNode.status = 'failed' | |
| iterationNode.error = item.error | |
| } | |
| iterationNode.details = Array.from(groupMap.values()) | |
| } | |
| const updateSequentialModeGroup = (index: number, item: NodeTracing, iterationNode: NodeTracing) => { | |
| const { details } = iterationNode | |
| if (details) { | |
| if (!details[index]) | |
| details[index] = [item] | |
| else | |
| details[index].push(item) | |
| } | |
| if (item.status === 'failed') { | |
| iterationNode.status = 'failed' | |
| iterationNode.error = item.error | |
| } | |
| } | |
| const processNonIterationNode = (item: NodeTracing) => { | |
| const { execution_metadata } = item | |
| if (!execution_metadata?.iteration_id) { | |
| result.push(item) | |
| return | |
| } | |
| const iterationNode = result.find(node => node.node_id === execution_metadata.iteration_id) | |
| if (!iterationNode || !Array.isArray(iterationNode.details)) | |
| return | |
| const { parallel_mode_run_id, iteration_index = 0 } = execution_metadata | |
| if (parallel_mode_run_id) | |
| updateParallelModeGroup(parallel_mode_run_id, item, iterationNode) | |
| else | |
| updateSequentialModeGroup(iteration_index, item, iterationNode) | |
| } | |
| allItems.forEach((item) => { | |
| item.node_type === BlockEnum.Iteration | |
| ? processIterationNode(item) | |
| : processNonIterationNode(item) | |
| }) | |
| return result | |
| }, []) | |
| const getTracingList = useCallback(async (appID: string, runID: string) => { | |
| try { | |
| const { data: nodeList } = await fetchTracingList({ | |
| url: `/apps/${appID}/workflow-runs/${runID}/node-executions`, | |
| }) | |
| setList(formatNodeList(nodeList)) | |
| } | |
| catch (err) { | |
| notify({ | |
| type: 'error', | |
| message: `${err}`, | |
| }) | |
| } | |
| }, [notify]) | |
| const getData = async (appID: string, runID: string) => { | |
| setLoading(true) | |
| await getResult(appID, runID) | |
| await getTracingList(appID, runID) | |
| setLoading(false) | |
| } | |
| const switchTab = async (tab: string) => { | |
| setCurrentTab(tab) | |
| if (tab === 'RESULT') | |
| appDetail?.id && await getResult(appDetail.id, runID) | |
| appDetail?.id && await getTracingList(appDetail.id, runID) | |
| } | |
| useEffect(() => { | |
| // fetch data | |
| if (appDetail && runID) | |
| getData(appDetail.id, runID) | |
| }, [appDetail, runID]) | |
| const [height, setHeight] = useState(0) | |
| const ref = useRef<HTMLDivElement>(null) | |
| const adjustResultHeight = () => { | |
| if (ref.current) | |
| setHeight(ref.current?.clientHeight - 16 - 16 - 2 - 1) | |
| } | |
| useEffect(() => { | |
| adjustResultHeight() | |
| }, [loading]) | |
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | |
| const [isShowIterationDetail, { | |
| setTrue: doShowIterationDetail, | |
| setFalse: doHideIterationDetail, | |
| }] = useBoolean(false) | |
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][]) => { | |
| setIterationRunResult(detail) | |
| doShowIterationDetail() | |
| }, [doShowIterationDetail]) | |
| if (isShowIterationDetail) { | |
| return ( | |
| <div className='grow relative flex flex-col'> | |
| <IterationResultPanel | |
| list={iterationRunResult} | |
| onHide={doHideIterationDetail} | |
| onBack={doHideIterationDetail} | |
| /> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className='grow relative flex flex-col'> | |
| {/* tab */} | |
| <div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-divider-subtle'> | |
| {!hideResult && ( | |
| <div | |
| className={cn( | |
| 'mr-6 py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer', | |
| currentTab === 'RESULT' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary', | |
| )} | |
| onClick={() => switchTab('RESULT')} | |
| >{t('runLog.result')}</div> | |
| )} | |
| <div | |
| className={cn( | |
| 'mr-6 py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer', | |
| currentTab === 'DETAIL' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary', | |
| )} | |
| onClick={() => switchTab('DETAIL')} | |
| >{t('runLog.detail')}</div> | |
| <div | |
| className={cn( | |
| 'mr-6 py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer', | |
| currentTab === 'TRACING' && '!border-util-colors-blue-brand-blue-brand-600 text-text-primary', | |
| )} | |
| onClick={() => switchTab('TRACING')} | |
| >{t('runLog.tracing')}</div> | |
| </div> | |
| {/* panel detail */} | |
| <div ref={ref} className={cn('grow bg-components-panel-bg h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-background-section-burn')}> | |
| {loading && ( | |
| <div className='flex h-full items-center justify-center bg-components-panel-bg'> | |
| <Loading /> | |
| </div> | |
| )} | |
| {!loading && currentTab === 'RESULT' && runDetail && ( | |
| <OutputPanel | |
| outputs={runDetail.outputs} | |
| error={runDetail.error} | |
| height={height} | |
| /> | |
| )} | |
| {!loading && currentTab === 'DETAIL' && runDetail && ( | |
| <ResultPanel | |
| inputs={runDetail.inputs} | |
| outputs={runDetail.outputs} | |
| status={runDetail.status} | |
| error={runDetail.error} | |
| elapsed_time={runDetail.elapsed_time} | |
| total_tokens={runDetail.total_tokens} | |
| created_at={runDetail.created_at} | |
| created_by={executor} | |
| steps={runDetail.total_steps} | |
| /> | |
| )} | |
| {!loading && currentTab === 'TRACING' && ( | |
| <TracingPanel | |
| className='bg-background-section-burn' | |
| list={list} | |
| onShowIterationDetail={handleShowIterationDetail} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export default RunPanel | |