mirror of
https://github.com/langgenius/webapp-conversation.git
synced 2025-12-08 17:32:27 +08:00
add workflow process
This commit is contained in:
119
app/components/workflow/block-icon.tsx
Normal file
119
app/components/workflow/block-icon.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { BlockEnum } from '@/types/app'
|
||||
import {
|
||||
Answer,
|
||||
Code,
|
||||
End,
|
||||
Home,
|
||||
Http,
|
||||
IfElse,
|
||||
KnowledgeRetrieval,
|
||||
Llm,
|
||||
QuestionClassifier,
|
||||
TemplatingTransform,
|
||||
VariableX,
|
||||
} from '@/app/components/base/icons/workflow'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
type BlockIconProps = {
|
||||
type: BlockEnum
|
||||
size?: string
|
||||
className?: string
|
||||
toolIcon?: string | { content: string; background: string }
|
||||
}
|
||||
const ICON_CONTAINER_CLASSNAME_SIZE_MAP: Record<string, string> = {
|
||||
xs: 'w-4 h-4 rounded-[5px] shadow-xs',
|
||||
sm: 'w-5 h-5 rounded-md shadow-xs',
|
||||
md: 'w-6 h-6 rounded-lg shadow-md',
|
||||
}
|
||||
const getIcon = (type: BlockEnum, className: string) => {
|
||||
return {
|
||||
[BlockEnum.Start]: <Home className={className} />,
|
||||
[BlockEnum.LLM]: <Llm className={className} />,
|
||||
[BlockEnum.Code]: <Code className={className} />,
|
||||
[BlockEnum.End]: <End className={className} />,
|
||||
[BlockEnum.IfElse]: <IfElse className={className} />,
|
||||
[BlockEnum.HttpRequest]: <Http className={className} />,
|
||||
[BlockEnum.Answer]: <Answer className={className} />,
|
||||
[BlockEnum.KnowledgeRetrieval]: <KnowledgeRetrieval className={className} />,
|
||||
[BlockEnum.QuestionClassifier]: <QuestionClassifier className={className} />,
|
||||
[BlockEnum.TemplateTransform]: <TemplatingTransform className={className} />,
|
||||
[BlockEnum.VariableAssigner]: <VariableX className={className} />,
|
||||
[BlockEnum.Tool]: <VariableX className={className} />,
|
||||
}[type]
|
||||
}
|
||||
const ICON_CONTAINER_BG_COLOR_MAP: Record<string, string> = {
|
||||
[BlockEnum.Start]: 'bg-[#2970FF]',
|
||||
[BlockEnum.LLM]: 'bg-[#6172F3]',
|
||||
[BlockEnum.Code]: 'bg-[#2E90FA]',
|
||||
[BlockEnum.End]: 'bg-[#F79009]',
|
||||
[BlockEnum.IfElse]: 'bg-[#06AED4]',
|
||||
[BlockEnum.HttpRequest]: 'bg-[#875BF7]',
|
||||
[BlockEnum.Answer]: 'bg-[#F79009]',
|
||||
[BlockEnum.KnowledgeRetrieval]: 'bg-[#16B364]',
|
||||
[BlockEnum.QuestionClassifier]: 'bg-[#16B364]',
|
||||
[BlockEnum.TemplateTransform]: 'bg-[#2E90FA]',
|
||||
[BlockEnum.VariableAssigner]: 'bg-[#2E90FA]',
|
||||
}
|
||||
const BlockIcon: FC<BlockIconProps> = ({
|
||||
type,
|
||||
size = 'sm',
|
||||
className,
|
||||
toolIcon,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`
|
||||
flex items-center justify-center border-[0.5px] border-white/[0.02] text-white
|
||||
${ICON_CONTAINER_CLASSNAME_SIZE_MAP[size]}
|
||||
${ICON_CONTAINER_BG_COLOR_MAP[type]}
|
||||
${toolIcon && '!shadow-none'}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{
|
||||
type !== BlockEnum.Tool && (
|
||||
getIcon(type, size === 'xs' ? 'w-3 h-3' : 'w-3.5 h-3.5')
|
||||
)
|
||||
}
|
||||
{
|
||||
type === BlockEnum.Tool && toolIcon && (
|
||||
<>
|
||||
{
|
||||
typeof toolIcon === 'string'
|
||||
? (
|
||||
<div
|
||||
className='shrink-0 w-full h-full bg-cover bg-center rounded-md'
|
||||
style={{
|
||||
backgroundImage: `url(${toolIcon})`,
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
: (
|
||||
<AppIcon
|
||||
className='shrink-0 !w-full !h-full'
|
||||
size='tiny'
|
||||
icon={toolIcon?.content}
|
||||
background={toolIcon?.background}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const VarBlockIcon: FC<BlockIconProps> = ({
|
||||
type,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{getIcon(type, `w-3 h-3 ${className}`)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BlockIcon)
|
||||
122
app/components/workflow/code-editor/index.tsx
Normal file
122
app/components/workflow/code-editor/index.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Editor, { loader } from '@monaco-editor/react'
|
||||
import React, { useRef } from 'react'
|
||||
import Base from '../editor/base'
|
||||
import { CodeLanguage } from '@/types/app'
|
||||
import './style.css'
|
||||
|
||||
// load file from local instead of cdn https://github.com/suren-atoyan/monaco-react/issues/482
|
||||
loader.config({ paths: { vs: '/vs' } })
|
||||
|
||||
type Props = {
|
||||
value?: string | object
|
||||
onChange?: (value: string) => void
|
||||
title: JSX.Element
|
||||
language: CodeLanguage
|
||||
headerRight?: JSX.Element
|
||||
readOnly?: boolean
|
||||
isJSONStringifyBeauty?: boolean
|
||||
height?: number
|
||||
}
|
||||
|
||||
const languageMap = {
|
||||
[CodeLanguage.javascript]: 'javascript',
|
||||
[CodeLanguage.python3]: 'python',
|
||||
[CodeLanguage.json]: 'json',
|
||||
}
|
||||
|
||||
const CodeEditor: FC<Props> = ({
|
||||
value = '',
|
||||
onChange = () => { },
|
||||
title,
|
||||
headerRight,
|
||||
language,
|
||||
readOnly,
|
||||
isJSONStringifyBeauty,
|
||||
height,
|
||||
}) => {
|
||||
const [isFocus, setIsFocus] = React.useState(false)
|
||||
|
||||
const handleEditorChange = (value: string | undefined) => {
|
||||
onChange(value || '')
|
||||
}
|
||||
|
||||
const editorRef = useRef(null)
|
||||
const handleEditorDidMount = (editor: any, monaco: any) => {
|
||||
editorRef.current = editor
|
||||
editor.onDidFocusEditorText(() => {
|
||||
setIsFocus(true)
|
||||
})
|
||||
editor.onDidBlurEditorText(() => {
|
||||
setIsFocus(false)
|
||||
})
|
||||
|
||||
monaco.editor.defineTheme('blur-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#F2F4F7',
|
||||
},
|
||||
})
|
||||
|
||||
monaco.editor.defineTheme('focus-theme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#ffffff',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const outPutValue = (() => {
|
||||
if (!isJSONStringifyBeauty)
|
||||
return value as string
|
||||
try {
|
||||
return JSON.stringify(value as object, null, 2)
|
||||
}
|
||||
catch (e) {
|
||||
return value as string
|
||||
}
|
||||
})()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Base
|
||||
title={title}
|
||||
value={outPutValue}
|
||||
headerRight={headerRight}
|
||||
isFocus={isFocus && !readOnly}
|
||||
minHeight={height || 200}
|
||||
>
|
||||
<>
|
||||
{/* https://www.npmjs.com/package/@monaco-editor/react */}
|
||||
<Editor
|
||||
className='h-full'
|
||||
// language={language === CodeLanguage.javascript ? 'javascript' : 'python'}
|
||||
language={languageMap[language] || 'javascript'}
|
||||
theme={isFocus ? 'focus-theme' : 'blur-theme'}
|
||||
value={outPutValue}
|
||||
onChange={handleEditorChange}
|
||||
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IEditorOptions.html
|
||||
options={{
|
||||
readOnly,
|
||||
domReadOnly: true,
|
||||
quickSuggestions: false,
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 1, // would change line num width
|
||||
wordWrap: 'on', // auto line wrap
|
||||
// lineNumbers: (num) => {
|
||||
// return <div>{num}</div>
|
||||
// }
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
</>
|
||||
</Base>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CodeEditor)
|
||||
8
app/components/workflow/code-editor/style.css
Normal file
8
app/components/workflow/code-editor/style.css
Normal file
@ -0,0 +1,8 @@
|
||||
.margin-view-overlays {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
/* hide readonly tooltip */
|
||||
.monaco-editor-overlaymessage {
|
||||
display: none !important;
|
||||
}
|
||||
81
app/components/workflow/editor/base.tsx
Normal file
81
app/components/workflow/editor/base.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import cn from 'classnames'
|
||||
import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
|
||||
import ToggleExpandBtn from './toggle-expand-btn'
|
||||
import useToggleExpend from './use-toggle-expend'
|
||||
import { Clipboard, ClipboardCheck } from '@/app/components/base/icons/line/files'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
title: JSX.Element | string
|
||||
headerRight?: JSX.Element
|
||||
children: JSX.Element
|
||||
minHeight?: number
|
||||
value: string
|
||||
isFocus: boolean
|
||||
}
|
||||
|
||||
const Base: FC<Props> = ({
|
||||
className,
|
||||
title,
|
||||
headerRight,
|
||||
children,
|
||||
minHeight = 120,
|
||||
value,
|
||||
isFocus,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const {
|
||||
wrapClassName,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
editorExpandHeight,
|
||||
} = useToggleExpend({ ref, hasFooter: false })
|
||||
|
||||
const editorContentMinHeight = minHeight - 28
|
||||
const [editorContentHeight, setEditorContentHeight] = useState(editorContentMinHeight)
|
||||
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
const handleCopy = useCallback(() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className={cn(wrapClassName)}>
|
||||
<div ref={ref} className={cn(className, isExpand && 'h-full', 'rounded-lg border', isFocus ? 'bg-white border-gray-200' : 'bg-gray-100 border-gray-100 overflow-hidden')}>
|
||||
<div className='flex justify-between items-center h-7 pt-1 pl-3 pr-2'>
|
||||
<div className='text-xs font-semibold text-gray-700'>{title}</div>
|
||||
<div className='flex items-center'>
|
||||
{headerRight}
|
||||
{!isCopied
|
||||
? (
|
||||
<Clipboard className='mx-1 w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={handleCopy} />
|
||||
)
|
||||
: (
|
||||
<ClipboardCheck className='mx-1 w-3.5 h-3.5 text-gray-500' />
|
||||
)
|
||||
}
|
||||
<div className='ml-1'>
|
||||
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PromptEditorHeightResizeWrap
|
||||
height={isExpand ? editorExpandHeight : editorContentHeight}
|
||||
minHeight={editorContentMinHeight}
|
||||
onHeightChange={setEditorContentHeight}
|
||||
hideResize={isExpand}
|
||||
>
|
||||
<div className='h-full pb-2'>
|
||||
{children}
|
||||
</div>
|
||||
</PromptEditorHeightResizeWrap>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Base)
|
||||
@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
height: number
|
||||
minHeight: number
|
||||
onHeightChange: (height: number) => void
|
||||
children: JSX.Element
|
||||
footer?: JSX.Element
|
||||
hideResize?: boolean
|
||||
}
|
||||
|
||||
const PromptEditorHeightResizeWrap: FC<Props> = ({
|
||||
className,
|
||||
height,
|
||||
minHeight,
|
||||
onHeightChange,
|
||||
children,
|
||||
footer,
|
||||
hideResize,
|
||||
}) => {
|
||||
const [clientY, setClientY] = useState(0)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const [prevUserSelectStyle, setPrevUserSelectStyle] = useState(getComputedStyle(document.body).userSelect)
|
||||
|
||||
const handleStartResize = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
setClientY(e.clientY)
|
||||
setIsResizing(true)
|
||||
setPrevUserSelectStyle(getComputedStyle(document.body).userSelect)
|
||||
document.body.style.userSelect = 'none'
|
||||
}, [])
|
||||
|
||||
const handleStopResize = useCallback(() => {
|
||||
setIsResizing(false)
|
||||
document.body.style.userSelect = prevUserSelectStyle
|
||||
}, [prevUserSelectStyle])
|
||||
|
||||
const { run: didHandleResize } = useDebounceFn((e) => {
|
||||
if (!isResizing)
|
||||
return
|
||||
|
||||
const offset = e.clientY - clientY
|
||||
let newHeight = height + offset
|
||||
setClientY(e.clientY)
|
||||
if (newHeight < minHeight)
|
||||
newHeight = minHeight
|
||||
onHeightChange(newHeight)
|
||||
}, {
|
||||
wait: 0,
|
||||
})
|
||||
|
||||
const handleResize = useCallback(didHandleResize, [isResizing, height, minHeight, clientY])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousemove', handleResize)
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleResize)
|
||||
}
|
||||
}, [handleResize])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', handleStopResize)
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleStopResize)
|
||||
}
|
||||
}, [handleStopResize])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='relative'
|
||||
>
|
||||
<div className={cn(className, 'overflow-y-auto')}
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{/* resize handler */}
|
||||
{footer}
|
||||
{!hideResize && (
|
||||
<div
|
||||
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize'
|
||||
onMouseDown={handleStartResize}>
|
||||
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PromptEditorHeightResizeWrap)
|
||||
25
app/components/workflow/editor/toggle-expand-btn.tsx
Normal file
25
app/components/workflow/editor/toggle-expand-btn.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Expand04 from '@/app/components/base/icons/solid/expand-04'
|
||||
import Collapse04 from '@/app/components/base/icons/line/arrows/collapse-04'
|
||||
|
||||
type Props = {
|
||||
isExpand: boolean
|
||||
onExpandChange: (isExpand: boolean) => void
|
||||
}
|
||||
|
||||
const ExpandBtn: FC<Props> = ({
|
||||
isExpand,
|
||||
onExpandChange,
|
||||
}) => {
|
||||
const handleToggle = useCallback(() => {
|
||||
onExpandChange(!isExpand)
|
||||
}, [isExpand])
|
||||
|
||||
const Icon = isExpand ? Collapse04 : Expand04
|
||||
return (
|
||||
<Icon className='w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={handleToggle} />
|
||||
)
|
||||
}
|
||||
export default React.memo(ExpandBtn)
|
||||
26
app/components/workflow/editor/use-toggle-expend.ts
Normal file
26
app/components/workflow/editor/use-toggle-expend.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
type Params = {
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
hasFooter?: boolean
|
||||
}
|
||||
|
||||
const useToggleExpend = ({ ref, hasFooter = true }: Params) => {
|
||||
const [isExpand, setIsExpand] = useState(false)
|
||||
const [wrapHeight, setWrapHeight] = useState(ref.current?.clientHeight)
|
||||
const editorExpandHeight = isExpand ? wrapHeight! - (hasFooter ? 56 : 29) : 0
|
||||
useEffect(() => {
|
||||
setWrapHeight(ref.current?.clientHeight)
|
||||
}, [isExpand])
|
||||
|
||||
const wrapClassName = isExpand && 'absolute z-10 left-4 right-6 top-[52px] bottom-0 pb-4 bg-white'
|
||||
|
||||
return {
|
||||
wrapClassName,
|
||||
editorExpandHeight,
|
||||
isExpand,
|
||||
setIsExpand,
|
||||
}
|
||||
}
|
||||
|
||||
export default useToggleExpend
|
||||
132
app/components/workflow/node.tsx
Normal file
132
app/components/workflow/node.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import BlockIcon from './block-icon'
|
||||
import CodeEditor from './code-editor'
|
||||
import { CodeLanguage } from '@/types/app'
|
||||
import AlertCircle from '@/app/components/base/icons/line/alert-circle'
|
||||
import AlertTriangle from '@/app/components/base/icons/line/alert-triangle'
|
||||
import Loading02 from '@/app/components/base/icons/line/loading-02'
|
||||
import CheckCircle from '@/app/components/base/icons/line/check-circle'
|
||||
import ChevronRight from '@/app/components/base/icons/line/chevron-right'
|
||||
import type { NodeTracing } from '@/types/app'
|
||||
|
||||
type Props = {
|
||||
nodeInfo: NodeTracing
|
||||
hideInfo?: boolean
|
||||
}
|
||||
|
||||
const NodePanel: FC<Props> = ({ nodeInfo, hideInfo = false }) => {
|
||||
const [collapseState, setCollapseState] = useState<boolean>(true)
|
||||
|
||||
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 === '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
|
||||
104
app/components/workflow/workflow-process.tsx
Normal file
104
app/components/workflow/workflow-process.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import NodePanel from './node'
|
||||
import type { WorkflowProcess } from '@/types/app'
|
||||
import CheckCircle from '@/app/components/base/icons/solid/general/check-circle'
|
||||
import AlertCircle from '@/app/components/base/icons/solid/alert-circle'
|
||||
import Loading02 from '@/app/components/base/icons/line/loading-02'
|
||||
import ChevronRight from '@/app/components/base/icons/line/chevron-right'
|
||||
import { WorkflowRunningStatus } from '@/types/app'
|
||||
|
||||
type WorkflowProcessProps = {
|
||||
data: WorkflowProcess
|
||||
grayBg?: boolean
|
||||
expand?: boolean
|
||||
hideInfo?: boolean
|
||||
}
|
||||
const WorkflowProcessItem = ({
|
||||
data,
|
||||
grayBg,
|
||||
expand = false,
|
||||
hideInfo = false,
|
||||
}: WorkflowProcessProps) => {
|
||||
const [collapse, setCollapse] = useState(!expand)
|
||||
const running = data.status === WorkflowRunningStatus.Running
|
||||
const succeeded = data.status === WorkflowRunningStatus.Succeeded
|
||||
const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
|
||||
|
||||
const background = useMemo(() => {
|
||||
if (running && !collapse)
|
||||
return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
|
||||
|
||||
if (succeeded && !collapse)
|
||||
return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
|
||||
|
||||
if (failed && !collapse)
|
||||
return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
|
||||
}, [running, succeeded, failed, collapse])
|
||||
|
||||
useEffect(() => {
|
||||
setCollapse(!expand)
|
||||
}, [expand])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mb-2 rounded-xl border-[0.5px] border-black/[0.08]',
|
||||
collapse ? 'py-[7px]' : hideInfo ? 'pt-2 pb-1' : 'py-2',
|
||||
collapse && (!grayBg ? 'bg-white' : 'bg-gray-50'),
|
||||
hideInfo ? 'mx-[-8px] px-1' : 'w-full px-3',
|
||||
)}
|
||||
style={{
|
||||
background,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center h-[18px] cursor-pointer',
|
||||
hideInfo && 'px-[6px]',
|
||||
)}
|
||||
onClick={() => setCollapse(!collapse)}
|
||||
>
|
||||
{
|
||||
running && (
|
||||
<Loading02 className='shrink-0 mr-1 w-3 h-3 text-[#667085] animate-spin' />
|
||||
)
|
||||
}
|
||||
{
|
||||
succeeded && (
|
||||
<CheckCircle className='shrink-0 mr-1 w-3 h-3 text-[#12B76A]' />
|
||||
)
|
||||
}
|
||||
{
|
||||
failed && (
|
||||
<AlertCircle className='shrink-0 mr-1 w-3 h-3 text-[#F04438]' />
|
||||
)
|
||||
}
|
||||
<div className='grow text-xs font-medium text-gray-700 leading-[18px]'>Workflow Process</div>
|
||||
<ChevronRight className={`'ml-1 w-3 h-3 text-gray-500' ${collapse ? '' : 'rotate-90'}`} />
|
||||
</div>
|
||||
{
|
||||
!collapse && (
|
||||
<div className='mt-1.5'>
|
||||
{
|
||||
data.tracing.map(node => (
|
||||
<div key={node.id} className='mb-0.5 last-of-type:mb-0'>
|
||||
<NodePanel
|
||||
nodeInfo={node}
|
||||
hideInfo={hideInfo}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowProcessItem
|
||||
Reference in New Issue
Block a user