Severian's picture
initial commit
a8b3f00
raw
history blame
8.74 kB
'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