| '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(() => { |
| |
| 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 |
|
|