FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
171
web/app/components/workflow/run/index.tsx
Normal file
171
web/app/components/workflow/run/index.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
'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 cn from 'classnames'
|
||||
import OutputPanel from './output-panel'
|
||||
import ResultPanel from './result-panel'
|
||||
import TracingPanel from './tracing-panel'
|
||||
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()
|
||||
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 getTracingList = useCallback(async (appID: string, runID: string) => {
|
||||
try {
|
||||
const { data: nodeList } = await fetchTracingList({
|
||||
url: `/apps/${appID}/workflow-runs/${runID}/node-executions`,
|
||||
})
|
||||
setList(nodeList.reverse())
|
||||
}
|
||||
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, setHieght] = useState(0)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const adjustResultHeight = () => {
|
||||
if (ref.current)
|
||||
setHieght(ref.current?.clientHeight - 16 - 16 - 2 - 1)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
adjustResultHeight()
|
||||
}, [loading])
|
||||
|
||||
return (
|
||||
<div className='grow relative flex flex-col'>
|
||||
{/* tab */}
|
||||
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
|
||||
{!hideResult && (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
|
||||
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
|
||||
)}
|
||||
onClick={() => switchTab('RESULT')}
|
||||
>{t('runLog.result')}</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
|
||||
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700',
|
||||
)}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>{t('runLog.detail')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
|
||||
currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-gray-700',
|
||||
)}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
>{t('runLog.tracing')}</div>
|
||||
</div>
|
||||
{/* panel detal */}
|
||||
<div ref={ref} className={cn('grow bg-white h-0 overflow-y-auto rounded-b-2xl', currentTab !== 'DETAIL' && '!bg-gray-50')}>
|
||||
{loading && (
|
||||
<div className='flex h-full items-center justify-center bg-white'>
|
||||
<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
|
||||
list={list}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RunPanel
|
111
web/app/components/workflow/run/meta.tsx
Normal file
111
web/app/components/workflow/run/meta.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
// import cn from 'classnames'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
type Props = {
|
||||
status: string
|
||||
executor?: string
|
||||
startTime?: number
|
||||
time?: number
|
||||
tokens?: number
|
||||
steps?: number
|
||||
showSteps?: boolean
|
||||
}
|
||||
|
||||
const MetaData: FC<Props> = ({
|
||||
status,
|
||||
executor,
|
||||
startTime = 0,
|
||||
time,
|
||||
tokens,
|
||||
steps = 1,
|
||||
showSteps = true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className='h-6 leading-6 text-gray-500 text-xs font-medium'>{t('runLog.meta.title')}</div>
|
||||
<div className='py-1'>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.status')}</div>
|
||||
<div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
|
||||
{status === 'running' && (
|
||||
<div className='my-[5px] w-16 h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status === 'succeeded' && (
|
||||
<span>SUCCESS</span>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
<span>FAIL</span>
|
||||
)}
|
||||
{status === 'stopped' && (
|
||||
<span>STOP</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.executor')}</div>
|
||||
<div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
|
||||
{status === 'running' && (
|
||||
<div className='my-[5px] w-[88px] h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{executor || 'N/A'}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.startTime')}</div>
|
||||
<div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
|
||||
{status === 'running' && (
|
||||
<div className='my-[5px] w-[72px] h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{dayjs(startTime * 1000).format('YYYY-MM-DD hh:mm:ss')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.time')}</div>
|
||||
<div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
|
||||
{status === 'running' && (
|
||||
<div className='my-[5px] w-[72px] h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{`${time?.toFixed(3)}s`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.tokens')}</div>
|
||||
<div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
|
||||
{status === 'running' && (
|
||||
<div className='my-[5px] w-[48px] h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{`${tokens || 0} Tokens`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showSteps && (
|
||||
<div className='flex'>
|
||||
<div className='shrink-0 w-[104px] px-2 py-[5px] text-gray-500 text-xs leading-[18px] truncate'>{t('runLog.meta.steps')}</div>
|
||||
<div className='grow px-2 py-[5px] text-gray-900 text-xs leading-[18px]'>
|
||||
{status === 'running' && (
|
||||
<div className='my-[5px] w-[24px] h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{steps}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetaData
|
135
web/app/components/workflow/run/node.tsx
Normal file
135
web/app/components/workflow/run/node.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import BlockIcon from '../block-icon'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { AlertCircle, AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { CheckCircle, Loading02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type Props = {
|
||||
nodeInfo: NodeTracing
|
||||
hideInfo?: boolean
|
||||
}
|
||||
|
||||
const NodePanel: FC<Props> = ({ nodeInfo, hideInfo = false }) => {
|
||||
const [collapseState, setCollapseState] = useState<boolean>(true)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getTime = (time: number) => {
|
||||
if (time < 1)
|
||||
return `${(time * 1000).toFixed(3)} ms`
|
||||
if (time > 60)
|
||||
return `${parseInt(Math.round(time / 60).toString())} m ${(time % 60).toFixed(3)} s`
|
||||
return `${time.toFixed(3)} s`
|
||||
}
|
||||
|
||||
const getTokenCount = (tokens: number) => {
|
||||
if (tokens < 1000)
|
||||
return tokens
|
||||
if (tokens >= 1000 && tokens < 1000000)
|
||||
return `${parseFloat((tokens / 1000).toFixed(3))}K`
|
||||
if (tokens >= 1000000)
|
||||
return `${parseFloat((tokens / 1000000).toFixed(3))}M`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCollapseState(!nodeInfo.expand)
|
||||
}, [nodeInfo.expand])
|
||||
|
||||
return (
|
||||
<div className={cn('px-4 py-1', hideInfo && '!p-0')}>
|
||||
<div className={cn('group transition-all bg-white border border-gray-100 rounded-2xl shadow-xs hover:shadow-md', hideInfo && '!rounded-lg')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center pl-[6px] pr-3 cursor-pointer',
|
||||
hideInfo ? 'py-2' : 'py-3',
|
||||
!collapseState && (hideInfo ? '!pb-1' : '!pb-2'),
|
||||
)}
|
||||
onClick={() => setCollapseState(!collapseState)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'shrink-0 w-3 h-3 mr-1 text-gray-400 transition-all group-hover:text-gray-500',
|
||||
!collapseState && 'rotate-90',
|
||||
)}
|
||||
/>
|
||||
<BlockIcon size={hideInfo ? 'xs' : 'sm'} className={cn('shrink-0 mr-2', hideInfo && '!mr-1')} type={nodeInfo.node_type} toolIcon={nodeInfo.extras?.icon || nodeInfo.extras} />
|
||||
<div className={cn(
|
||||
'grow text-gray-700 text-[13px] leading-[16px] font-semibold truncate',
|
||||
hideInfo && '!text-xs',
|
||||
)} title={nodeInfo.title}>{nodeInfo.title}</div>
|
||||
{nodeInfo.status !== 'running' && !hideInfo && (
|
||||
<div className='shrink-0 text-gray-500 text-xs leading-[18px]'>{`${getTime(nodeInfo.elapsed_time || 0)} · ${getTokenCount(nodeInfo.execution_metadata?.total_tokens || 0)} tokens`}</div>
|
||||
)}
|
||||
{nodeInfo.status === 'succeeded' && (
|
||||
<CheckCircle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#12B76A]' />
|
||||
)}
|
||||
{nodeInfo.status === 'failed' && (
|
||||
<AlertCircle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F04438]' />
|
||||
)}
|
||||
{nodeInfo.status === 'stopped' && (
|
||||
<AlertTriangle className='shrink-0 ml-2 w-3.5 h-3.5 text-[#F79009]' />
|
||||
)}
|
||||
{nodeInfo.status === 'running' && (
|
||||
<div className='shrink-0 flex items-center text-primary-600 text-[13px] leading-[16px] font-medium'>
|
||||
<Loading02 className='mr-1 w-3.5 h-3.5 animate-spin' />
|
||||
<span>Running</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!collapseState && (
|
||||
<div className='pb-2'>
|
||||
<div className={cn('px-[10px] py-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
{nodeInfo.status === 'stopped' && (
|
||||
<div className='px-3 py-[10px] bg-[#fffaeb] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] text-xs leading-[18px] text-[#dc6803] shadow-xs'>{t('workflow.tracing.stopBy', { user: nodeInfo.created_by ? nodeInfo.created_by.name : 'N/A' })}</div>
|
||||
)}
|
||||
{nodeInfo.status === 'failed' && (
|
||||
<div className='px-3 py-[10px] bg-[#fef3f2] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] text-xs leading-[18px] text-[#d92d20] shadow-xs'>{nodeInfo.error}</div>
|
||||
)}
|
||||
</div>
|
||||
{nodeInfo.inputs && (
|
||||
<div className={cn('px-[10px] py-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>INPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.inputs}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nodeInfo.process_data && (
|
||||
<div className={cn('px-[10px] py-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>PROCESS DATA</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.process_data}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{nodeInfo.outputs && (
|
||||
<div className={cn('px-[10px] py-1', hideInfo && '!px-2 !py-0.5')}>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>OUTPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={nodeInfo.outputs}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodePanel
|
61
web/app/components/workflow/run/output-panel.tsx
Normal file
61
web/app/components/workflow/run/output-panel.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from '@/app/components/app/chat/loading-anim'
|
||||
|
||||
type OutputPanelProps = {
|
||||
isRunning?: boolean
|
||||
outputs?: any
|
||||
error?: string
|
||||
height?: number
|
||||
}
|
||||
|
||||
const OutputPanel: FC<OutputPanelProps> = ({
|
||||
isRunning,
|
||||
outputs,
|
||||
error,
|
||||
height,
|
||||
}) => {
|
||||
return (
|
||||
<div className='bg-gray-50 py-2'>
|
||||
{isRunning && (
|
||||
<div className='pt-4 pl-[26px]'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && error && (
|
||||
<div className='px-4'>
|
||||
<div className='px-3 py-[10px] rounded-lg !bg-[#fef3f2] border-[0.5px] border-[rbga(0,0,0,0.05)] shadow-xs'>
|
||||
<div className='text-xs leading-[18px] text-[#d92d20]'>{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isRunning && !outputs && (
|
||||
<div className='px-4 py-2'>
|
||||
<Markdown content='No Output' />
|
||||
</div>
|
||||
)}
|
||||
{outputs && Object.keys(outputs).length === 1 && (
|
||||
<div className='px-4 py-2'>
|
||||
<Markdown content={outputs[Object.keys(outputs)[0]] || ''} />
|
||||
</div>
|
||||
)}
|
||||
{outputs && Object.keys(outputs).length > 1 && height! > 0 && (
|
||||
<div className='px-4 py-2 flex flex-col gap-2'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div></div>}
|
||||
language={CodeLanguage.json}
|
||||
value={outputs}
|
||||
isJSONStringifyBeauty
|
||||
height={height}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OutputPanel
|
91
web/app/components/workflow/run/result-panel.tsx
Normal file
91
web/app/components/workflow/run/result-panel.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import StatusPanel from './status'
|
||||
import MetaData from './meta'
|
||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||
|
||||
type ResultPanelProps = {
|
||||
inputs?: string
|
||||
process_data?: string
|
||||
outputs?: string
|
||||
status: string
|
||||
error?: string
|
||||
elapsed_time?: number
|
||||
total_tokens?: number
|
||||
created_at?: number
|
||||
created_by?: string
|
||||
finished_at?: number
|
||||
steps?: number
|
||||
showSteps?: boolean
|
||||
}
|
||||
|
||||
const ResultPanel: FC<ResultPanelProps> = ({
|
||||
inputs,
|
||||
process_data,
|
||||
outputs,
|
||||
status,
|
||||
error,
|
||||
elapsed_time,
|
||||
total_tokens,
|
||||
created_at,
|
||||
created_by,
|
||||
steps,
|
||||
showSteps,
|
||||
}) => {
|
||||
return (
|
||||
<div className='bg-white py-2'>
|
||||
<div className='px-4 py-2'>
|
||||
<StatusPanel
|
||||
status={status}
|
||||
time={elapsed_time}
|
||||
tokens={total_tokens}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<div className='px-4 py-2 flex flex-col gap-2'>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>INPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={inputs}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
{process_data && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>PROCESS DATA</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={process_data}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)}
|
||||
{(outputs || status === 'running') && (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
title={<div>OUTPUT</div>}
|
||||
language={CodeLanguage.json}
|
||||
value={outputs}
|
||||
isJSONStringifyBeauty
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<div className='h-[0.5px] bg-black opacity-5' />
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<MetaData
|
||||
status={status}
|
||||
executor={created_by}
|
||||
startTime={created_at}
|
||||
time={elapsed_time}
|
||||
tokens={total_tokens}
|
||||
steps={steps}
|
||||
showSteps={showSteps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResultPanel
|
100
web/app/components/workflow/run/status.tsx
Normal file
100
web/app/components/workflow/run/status.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
|
||||
type ResultProps = {
|
||||
status: string
|
||||
time?: number
|
||||
tokens?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
const StatusPanel: FC<ResultProps> = ({
|
||||
status,
|
||||
time,
|
||||
tokens,
|
||||
error,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'px-3 py-[10px] rounded-lg border-[0.5px] border-[rbga(0,0,0,0.05)] shadow-xs',
|
||||
status === 'running' && '!bg-primary-50',
|
||||
status === 'succeeded' && '!bg-[#ecfdf3]',
|
||||
status === 'failed' && '!bg-[#fef3f2]',
|
||||
status === 'stopped' && '!bg-[#fffaeb]',
|
||||
)}
|
||||
>
|
||||
<div className='flex'>
|
||||
<div className='flex-[33%] max-w-[120px]'>
|
||||
<div className='text-xs leading-[18px] font-medium text-gray-400'>{t('runLog.resultPanel.status')}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 h-[18px] text-xs leading-3 font-semibold',
|
||||
status === 'running' && '!text-primary-600',
|
||||
status === 'succeeded' && '!text-[#039855]',
|
||||
status === 'failed' && '!text-[#d92d20]',
|
||||
status === 'stopped' && '!text-[#f79009]',
|
||||
)}
|
||||
>
|
||||
{status === 'running' && (
|
||||
<span>Running</span>
|
||||
)}
|
||||
{status === 'succeeded' && (
|
||||
<>
|
||||
<Indicator color={'green'} />
|
||||
<span>SUCCESS</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
<>
|
||||
<Indicator color={'red'} />
|
||||
<span>FAIL</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'stopped' && (
|
||||
<>
|
||||
<Indicator color={'yellow'} />
|
||||
<span>STOP</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-[33%] max-w-[152px]'>
|
||||
<div className='text-xs leading-[18px] font-medium text-gray-400'>{t('runLog.resultPanel.time')}</div>
|
||||
<div className='flex items-center gap-1 h-[18px] text-gray-700 text-xs leading-3 font-semibold'>
|
||||
{status === 'running' && (
|
||||
<div className='w-16 h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{`${time?.toFixed(3)}s`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-[33%]'>
|
||||
<div className='text-xs leading-[18px] font-medium text-gray-400'>{t('runLog.resultPanel.tokens')}</div>
|
||||
<div className='flex items-center gap-1 h-[18px] text-gray-700 text-xs leading-3 font-semibold'>
|
||||
{status === 'running' && (
|
||||
<div className='w-20 h-2 rounded-sm bg-[rgba(0,0,0,0.05)]'/>
|
||||
)}
|
||||
{status !== 'running' && (
|
||||
<span>{`${tokens || 0} Tokens`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{status === 'failed' && error && (
|
||||
<>
|
||||
<div className='my-2 h-[0.5px] bg-black opacity-5'/>
|
||||
<div className='text-xs leading-[18px] text-[#d92d20]'>{error}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusPanel
|
23
web/app/components/workflow/run/tracing-panel.tsx
Normal file
23
web/app/components/workflow/run/tracing-panel.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import NodePanel from './node'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type TracingPanelProps = {
|
||||
list: NodeTracing[]
|
||||
}
|
||||
|
||||
const TracingPanel: FC<TracingPanelProps> = ({ list }) => {
|
||||
return (
|
||||
<div className='bg-gray-50 py-2'>
|
||||
{list.map(node => (
|
||||
<NodePanel
|
||||
key={node.id}
|
||||
nodeInfo={node}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TracingPanel
|
Reference in New Issue
Block a user