feat(web): workflow run time ui
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-02 15:15:11
|
* @Date: 2026-02-02 15:15:11
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-02-02 15:15:11
|
* @Last Modified time: 2026-04-07 14:04:33
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* CodeBlock Component
|
* CodeBlock Component
|
||||||
@@ -27,6 +27,7 @@ type ICodeBlockProps = {
|
|||||||
needCopy?: boolean;
|
needCopy?: boolean;
|
||||||
size?: 'small' | 'default';
|
size?: 'small' | 'default';
|
||||||
showLineNumbers?: boolean;
|
showLineNumbers?: boolean;
|
||||||
|
background?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Code block component for displaying formatted code with optional copy functionality */
|
/** Code block component for displaying formatted code with optional copy functionality */
|
||||||
@@ -34,7 +35,8 @@ const CodeBlock: FC<ICodeBlockProps> = ({
|
|||||||
value,
|
value,
|
||||||
needCopy = true,
|
needCopy = true,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
showLineNumbers = false
|
showLineNumbers = false,
|
||||||
|
background = '#F0F3F8'
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +45,7 @@ const CodeBlock: FC<ICodeBlockProps> = ({
|
|||||||
style={atelierHeathLight}
|
style={atelierHeathLight}
|
||||||
customStyle={{
|
customStyle={{
|
||||||
padding: '8px 12px 8px 12px',
|
padding: '8px 12px 8px 12px',
|
||||||
backgroundColor: '#F0F3F8',
|
backgroundColor: background,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
fontSize: size === 'small' ? 12 : 14,
|
fontSize: size === 'small' ? 12 : 14,
|
||||||
wordBreak: 'break-all'
|
wordBreak: 'break-all'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* @Author: ZhaoYing
|
* @Author: ZhaoYing
|
||||||
* @Date: 2026-02-24 17:57:08
|
* @Date: 2026-02-24 17:57:08
|
||||||
* @Last Modified by: ZhaoYing
|
* @Last Modified by: ZhaoYing
|
||||||
* @Last Modified time: 2026-03-12 13:39:24
|
* @Last Modified time: 2026-04-07 14:05:50
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
* Runtime Component
|
* Runtime Component
|
||||||
@@ -18,13 +18,15 @@
|
|||||||
import { type FC, useState } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Space, Button, Collapse, Flex } from 'antd'
|
import { App, Button, Collapse, Flex } from 'antd'
|
||||||
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, RightOutlined, ArrowLeftOutlined } from '@ant-design/icons'
|
import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined, RightOutlined, ArrowLeftOutlined } from '@ant-design/icons'
|
||||||
|
import copy from 'copy-to-clipboard'
|
||||||
|
|
||||||
import styles from './chat.module.css'
|
import styles from './chat.module.css'
|
||||||
import type { ChatItem } from '@/components/Chat/types'
|
import type { ChatItem } from '@/components/Chat/types'
|
||||||
import Markdown from '@/components/Markdown'
|
import Markdown from '@/components/Markdown'
|
||||||
import CodeBlock from '@/components/Markdown/CodeBlock'
|
import CodeBlock from '@/components/Markdown/CodeBlock'
|
||||||
|
import RbAlert from '@/components/RbAlert'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime component props
|
* Runtime component props
|
||||||
@@ -36,10 +38,12 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
index
|
index
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { message } = App.useApp()
|
||||||
// Stores the currently selected detail view (for nested loop/iteration exploration)
|
// Stores the currently selected detail view (for nested loop/iteration exploration)
|
||||||
const [detail, setDetail] = useState<any>(null)
|
const [detail, setDetail] = useState<any>(null)
|
||||||
// Tracks whether the current detail view is for a loop (true) or iteration (false)
|
// Tracks whether the current detail view is for a loop (true) or iteration (false)
|
||||||
const [loop, setLoop] = useState<boolean | null>(null)
|
const [loop, setLoop] = useState<boolean | null>(null)
|
||||||
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles navigation into nested loop/iteration details
|
* Handles navigation into nested loop/iteration details
|
||||||
@@ -57,7 +61,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
* @returns Tailwind CSS class for appropriate color
|
* @returns Tailwind CSS class for appropriate color
|
||||||
*/
|
*/
|
||||||
const getStatus = (status?: string) => {
|
const getStatus = (status?: string) => {
|
||||||
return status === 'completed' ? 'rb:text-[#369F21]' : status === 'failed' ? 'rb:text-[#FF5D34]' : 'rb:text-[#5B6167]'
|
return status === 'completed' ? 'rb:text-[#369F21]!' : status === 'failed' ? 'rb:text-[#FF5D34]!' : 'rb:text-[#5B6167]!'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +80,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size={8} direction="vertical" className="rb:w-full!">
|
<Flex gap={8} vertical>
|
||||||
{Object.entries(groupedByCycle).map(([cycleIdx, items]: [string, any]) => {
|
{Object.entries(groupedByCycle).map(([cycleIdx, items]: [string, any]) => {
|
||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
@@ -92,7 +96,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +107,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
*/
|
*/
|
||||||
const renderChild = (list: any) => {
|
const renderChild = (list: any) => {
|
||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
return <Space size={8} direction="vertical" className="rb:w-full!">
|
return <Flex gap={8} vertical>
|
||||||
{list?.map(vo => {
|
{list?.map(vo => {
|
||||||
const isLoop = vo.node_type === 'loop';
|
const isLoop = vo.node_type === 'loop';
|
||||||
// Render cycle variables for loop nodes without node_name
|
// Render cycle variables for loop nodes without node_name
|
||||||
@@ -114,6 +118,7 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
<Button
|
<Button
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
size="small"
|
size="small"
|
||||||
|
onClick={() => handleCopy(typeof vo.content === 'object' && vo.content?.input ? JSON.stringify(vo.content.input, null, 2) : '{}')}
|
||||||
>{t('common.copy')}</Button>
|
>{t('common.copy')}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rb:max-h-40 rb:overflow-auto">
|
<div className="rb:max-h-40 rb:overflow-auto">
|
||||||
@@ -133,35 +138,44 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
return (
|
return (
|
||||||
<Collapse
|
<Collapse
|
||||||
key={vo.node_id}
|
key={vo.node_id}
|
||||||
|
bordered={false}
|
||||||
|
className="rb:bg-[#F6F6F6]"
|
||||||
items={[{
|
items={[{
|
||||||
key: vo.node_id,
|
key: vo.node_id,
|
||||||
label: <div className={clsx("rb:flex rb:justify-between rb:items-center", getStatus(vo.status))}>
|
label: <div className={clsx("rb:flex rb:justify-between rb:items-center")}>
|
||||||
<div className="rb:flex rb:items-center rb:gap-1 rb:flex-1">
|
<Flex gap={6} align="center" className="rb:flex-1!">
|
||||||
{vo.icon && <div className={`rb:size-4 rb:bg-cover ${vo.icon}`} />}
|
{vo.icon && <div className={`rb:size-6 rb:bg-cover ${vo.icon}`} />}
|
||||||
<div className="rb:wrap-break-word rb:line-clamp-1">{vo.node_name}</div>
|
<div className="rb:wrap-break-word rb:line-clamp-1 rb:font-medium">{vo.node_name}</div>
|
||||||
</div>
|
</Flex>
|
||||||
<span>
|
<Flex align="center" gap={8} className="rb:text-[12px]">
|
||||||
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms</>}
|
{typeof vo.elapsed_time == 'number' && <>{vo.elapsed_time?.toFixed(3)}ms</>}
|
||||||
{vo.status === 'completed' ? <CheckCircleFilled className="rb:ml-1" /> : vo.status === 'failed' ? <CloseCircleFilled className="rb:ml-1" /> : <LoadingOutlined className="rb:ml-1" />}
|
{vo.status === 'completed'
|
||||||
</span>
|
? <CheckCircleFilled className={`rb:mr-1 ${getStatus(vo.status)}`} />
|
||||||
|
: vo.status === 'failed'
|
||||||
|
? <CloseCircleFilled className={`rb:mr-1 ${getStatus(vo.status)}`} />
|
||||||
|
: <LoadingOutlined className={`rb:mr-1 ${getStatus(vo.status)}`} />
|
||||||
|
}
|
||||||
|
</Flex>
|
||||||
</div>,
|
</div>,
|
||||||
className: styles.collapseItem,
|
className: styles.collapseItem,
|
||||||
children: (
|
children: (
|
||||||
<Space size={8} direction="vertical" className="rb:w-full!">
|
<Flex gap={8} vertical>
|
||||||
{/* Display error message for failed nodes */}
|
{/* Display error message for failed nodes */}
|
||||||
{vo.status === 'failed' &&
|
|
||||||
<div className={clsx("rb:bg-[#F0F3F8] rb:rounded-md", getStatus(vo.status))}>
|
{item.error &&
|
||||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
<RbAlert color="orange" className="rb:pb-0!">
|
||||||
{t(`workflow.error`)}
|
<Flex vertical className="rb:w-full!">
|
||||||
<Button
|
<Flex align="center" justify="space-between">
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
{t(`workflow.error`)}
|
||||||
size="small"
|
<Button
|
||||||
>{t('common.copy')}</Button>
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
</div>
|
size="small"
|
||||||
<div className="rb:pb-2 rb:px-3 rb:max-h-40 rb:overflow-auto">
|
onClick={() => handleCopy(vo.content?.error || '')}
|
||||||
|
>{t('common.copy')}</Button>
|
||||||
|
</Flex>
|
||||||
<Markdown content={vo.content?.error || ''} />
|
<Markdown content={vo.content?.error || ''} />
|
||||||
</div>
|
</Flex>
|
||||||
</div>
|
</RbAlert>
|
||||||
}
|
}
|
||||||
{/* Display navigation to nested cycles if subContent exists */}
|
{/* Display navigation to nested cycles if subContent exists */}
|
||||||
{vo.subContent?.length > 0 && (
|
{vo.subContent?.length > 0 && (
|
||||||
@@ -172,12 +186,13 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
)}
|
)}
|
||||||
{/* Display input and output data as JSON code blocks */}
|
{/* Display input and output data as JSON code blocks */}
|
||||||
{['input', 'output'].map(key => (
|
{['input', 'output'].map(key => (
|
||||||
<div key={key} className="rb:bg-[#F0F3F8] rb:rounded-md">
|
<div key={key} className="rb:bg-[#EBEBEB] rb:rounded-lg">
|
||||||
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
<div className="rb:py-2 rb:px-3 rb:flex rb:justify-between rb:items-center rb:text-[12px]">
|
||||||
{isLoop ? t(`workflow.runtime.${key}_cycle_vars`) : t(`workflow.${key}`)}
|
{isLoop ? t(`workflow.runtime.${key}_cycle_vars`) : t(`workflow.${key}`)}
|
||||||
<Button
|
<Button
|
||||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||||
size="small"
|
size="small"
|
||||||
|
onClick={() => handleCopy(typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}')}
|
||||||
>{t('common.copy')}</Button>
|
>{t('common.copy')}</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="rb:max-h-40 rb:overflow-auto">
|
<div className="rb:max-h-40 rb:overflow-auto">
|
||||||
@@ -186,55 +201,80 @@ const Runtime: FC<{ item: ChatItem; index: number;}> = ({
|
|||||||
value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'}
|
value={typeof vo.content === 'object' && vo.content?.[key] ? JSON.stringify(vo.content[key], null, 2) : '{}'}
|
||||||
needCopy={false}
|
needCopy={false}
|
||||||
showLineNumbers={true}
|
showLineNumbers={true}
|
||||||
|
background="#EBEBEB"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</Flex>
|
||||||
)
|
)
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Flex>
|
||||||
}
|
}
|
||||||
return <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
|
return <div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 ", getStatus('failed'))}>
|
||||||
<Markdown content={list || ''} />
|
<Markdown content={list || ''} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Copy value to clipboard and show success message */
|
||||||
|
const handleCopy = (value: string) => {
|
||||||
|
copy(value)
|
||||||
|
message.success(t('common.copySuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={index} className="rb:min-w-100 rb:max-w-full rb:mb-2">
|
<div
|
||||||
<Collapse
|
key={index}
|
||||||
className={styles[item.status || 'default']}
|
className={clsx("rb:mb-4 rb-border rb:rounded-xl rb:px-4 rb:pt-3 rb:bg-white rb:max-w-full", {
|
||||||
items={[{
|
'rb:hover:bg-[#F6F6F6] rb:w-64': !expanded
|
||||||
key: 0,
|
})}
|
||||||
label: <div className={getStatus(item.status)}>
|
>
|
||||||
{item.status === 'completed' ? <CheckCircleFilled className="rb:mr-1" /> : item.status === 'failed' ? <CloseCircleFilled className="rb:mr-1" /> : <LoadingOutlined className="rb:mr-1" />}
|
<Flex align="center" justify="space-between" className="rb:font-medium rb:pb-3!">
|
||||||
{t('application.workflow')}
|
<span className="rb:font-medium rb:leading-5">
|
||||||
</div>,
|
{item.status === 'completed'
|
||||||
className: styles.collapseItem,
|
? <CheckCircleFilled className={`rb:mr-1 ${getStatus(item.status)}`} />
|
||||||
children: (
|
: item.status === 'failed'
|
||||||
detail
|
? <CloseCircleFilled className={`rb:mr-1 ${getStatus(item.status)}`} />
|
||||||
? (
|
: <LoadingOutlined className={`rb:mr-1 ${getStatus(item.status)}`} />
|
||||||
<div className="rb:bg-[#FBFDFF] rb:rounded-md">
|
}
|
||||||
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
{t('application.workflow')}
|
||||||
{t('common.return')}
|
</span>
|
||||||
</Button>
|
<Flex
|
||||||
{renderDetailChild(detail.subContent)}
|
align="center"
|
||||||
</div>
|
justify="center"
|
||||||
)
|
className={clsx("rb:size-6.5 rb:cursor-pointer rb-border rb:rounded-lg", {
|
||||||
: <>
|
'rb:hover:bg-[#F6F6F6]!': expanded
|
||||||
{item.error &&
|
})}
|
||||||
<div className={clsx("rb:bg-[#FBFDFF] rb:rounded-md rb:py-2 rb:px-3 rb:mb-2 rb:-mt-4", getStatus('failed'))}>
|
onClick={() => { setExpanded(v => !v); setDetail(null) }}
|
||||||
<Markdown content={item.error} />
|
>
|
||||||
</div>
|
<div
|
||||||
}
|
className={clsx("rb:size-4 rb:bg-cover", {
|
||||||
{renderChild(item.subContent)}
|
'rb:bg-[url("@/assets/images/conversation/compress.svg")]': expanded,
|
||||||
</>
|
'rb:bg-[url("@/assets/images/conversation/expand.svg")]': !expanded
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
{expanded && (
|
||||||
|
detail
|
||||||
|
? (
|
||||||
|
<div className="rb:bg-[#FBFDFF] rb:rounded-md rb:mb-4">
|
||||||
|
<Button type="link" icon={<ArrowLeftOutlined />} onClick={() => setDetail(null)} className="rb:px-0! rb:text-[12px]!">
|
||||||
|
{t('common.return')}
|
||||||
|
</Button>
|
||||||
|
{renderDetailChild(detail.subContent)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}]}
|
: <div className="rb:mb-4">
|
||||||
/>
|
{item.error &&
|
||||||
|
<RbAlert color="orange" className="rb:pb-0! rb:mb-2!"><Markdown content={item.error} /></RbAlert>
|
||||||
|
}
|
||||||
|
{renderChild(item.subContent)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header) {
|
.collapse-item:global(.ant-collapse-item>.ant-collapse-header) {
|
||||||
padding: 8px 12px;
|
padding: 8px 16px 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) {
|
.collapse-item:global(.ant-collapse-item>.ant-collapse-header .ant-collapse-expand-icon) {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -45,4 +47,7 @@
|
|||||||
}
|
}
|
||||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||||
padding: 0 4px 4px 4px;
|
padding: 0 4px 4px 4px;
|
||||||
|
}
|
||||||
|
:global(.ant-collapse-borderless)>.collapse-item:global(.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box) {
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user