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:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@@ -0,0 +1,103 @@
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useStore } from '../../store'
import UserInput from './user-input'
import Chat from '@/app/components/base/chat/chat'
import type { ChatItem } from '@/app/components/base/chat/types'
import { fetchConvesationMessages } from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
import Loading from '@/app/components/base/loading'
const ChatRecord = () => {
const [fetched, setFetched] = useState(false)
const [chatList, setChatList] = useState([])
const appDetail = useAppStore(s => s.appDetail)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const currentConversationID = historyWorkflowData?.conversation_id
const chatMessageList = useMemo(() => {
const res: ChatItem[] = []
if (chatList.length) {
chatList.forEach((item: any) => {
res.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
res.push({
id: item.id,
content: item.answer,
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
workflow_run_id: item.workflow_run_id,
})
})
}
return res
}, [chatList])
const handleFetchConvesationMessages = useCallback(async () => {
if (appDetail && currentConversationID) {
try {
setFetched(false)
const res = await fetchConvesationMessages(appDetail.id, currentConversationID)
setFetched(true)
setChatList((res as any).data)
}
catch (e) {
}
}
}, [appDetail, currentConversationID])
useEffect(() => {
handleFetchConvesationMessages()
}, [currentConversationID, appDetail, handleFetchConvesationMessages])
return (
<div
className={`
flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02] shadow-xl
`}
style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',
}}
>
{!fetched && (
<div className='flex items-center justify-center h-full'>
<Loading />
</div>
)}
{fetched && (
<>
<div className='shrink-0 flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
{`TEST CHAT#${historyWorkflowData?.sequence_number}`}
</div>
<div className='grow h-0'>
<Chat
config={{} as any}
chatList={chatMessageList}
chatContainerclassName='px-4'
chatContainerInnerClassName='pt-6'
chatFooterClassName='px-4 rounded-b-2xl'
chatFooterInnerClassName='pb-4'
chatNode={<UserInput />}
noChatInput
allToolIcons={{}}
showPromptLog
/>
</div>
</>
)}
</div>
)
}
export default memo(ChatRecord)

View File

@@ -0,0 +1,56 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
const UserInput = () => {
const { t } = useTranslation()
const [expanded, setExpanded] = useState(true)
const variables: any = []
if (!variables.length)
return null
return (
<div
className={`
rounded-xl border
${!expanded ? 'bg-indigo-25 border-indigo-100 shadow-none' : 'bg-white shadow-xs border-transparent'}
`}
>
<div
className={`
flex items-center px-2 pt-4 h-[18px] text-[13px] font-semibold cursor-pointer
${!expanded ? 'text-indigo-800' : 'text-gray-800'}
`}
onClick={() => setExpanded(!expanded)}
>
<ChevronDown
className={`mr-1 w-3 h-3 ${!expanded ? '-rotate-90 text-indigo-600' : 'text-gray-300'}`}
/>
{t('workflow.panel.userInputField').toLocaleUpperCase()}
</div>
<div className='px-2 pt-1 pb-3'>
{
expanded && (
<div className='py-2 text-[13px] text-gray-900'>
{
variables.map((variable: any) => (
<div
key={variable.variable}
className='mb-2 last-of-type:mb-0'
>
</div>
))
}
</div>
)
}
</div>
</div>
)
}
export default memo(UserInput)

View File

@@ -0,0 +1,122 @@
import {
forwardRef,
memo,
useCallback,
useImperativeHandle,
useMemo,
} from 'react'
import { useNodes } from 'reactflow'
import { BlockEnum } from '../../types'
import {
useStore,
useWorkflowStore,
} from '../../store'
import type { StartNodeType } from '../../nodes/start/types'
import Empty from './empty'
import UserInput from './user-input'
import { useChat } from './hooks'
import type { ChatWrapperRefType } from './index'
import Chat from '@/app/components/base/chat/chat'
import type { OnSend } from '@/app/components/base/chat/types'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import {
fetchSuggestedQuestions,
stopChatMessageResponding,
} from '@/service/debug'
import { useStore as useAppStore } from '@/app/components/app/store'
const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables
const appDetail = useAppStore(s => s.appDetail)
const workflowStore = useWorkflowStore()
const featuresStore = useFeaturesStore()
const inputs = useStore(s => s.inputs)
const features = featuresStore!.getState().features
const config = useMemo(() => {
return {
opening_statement: features.opening?.opening_statement || '',
suggested_questions: features.opening?.suggested_questions || [],
suggested_questions_after_answer: features.suggested,
text_to_speech: features.text2speech,
speech_to_text: features.speech2text,
retriever_resource: features.citation,
sensitive_word_avoidance: features.moderation,
file_upload: features.file,
}
}, [features])
const {
conversationId,
chatList,
handleStop,
isResponding,
suggestedQuestions,
handleSend,
handleRestart,
} = useChat(
config,
{
inputs,
promptVariables: (startVariables as any) || [],
},
[],
taskId => stopChatMessageResponding(appDetail!.id, taskId),
)
const doSend = useCallback<OnSend>((query, files) => {
handleSend(
{
query,
files,
inputs: workflowStore.getState().inputs,
conversation_id: conversationId,
},
{
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
},
)
}, [conversationId, handleSend, workflowStore, appDetail])
useImperativeHandle(ref, () => {
return {
handleRestart,
}
}, [handleRestart])
return (
<Chat
config={{
...config,
supportCitationHitInfo: true,
} as any}
chatList={chatList}
isResponding={isResponding}
chatContainerclassName='px-4'
chatContainerInnerClassName='pt-6'
chatFooterClassName='px-4 rounded-bl-2xl'
chatFooterInnerClassName='pb-4'
onSend={doSend}
onStopResponding={handleStop}
chatNode={(
<>
<UserInput />
{
!chatList.length && (
<Empty />
)
}
</>
)}
suggestedQuestions={suggestedQuestions}
showPromptLog
chatAnswerContainerInner='!pr-2'
/>
)
})
ChatWrapper.displayName = 'ChatWrapper'
export default memo(ChatWrapper)

View File

@@ -0,0 +1,19 @@
import { useTranslation } from 'react-i18next'
import { ChatBotSlim } from '@/app/components/base/icons/src/vender/line/communication'
const Empty = () => {
const { t } = useTranslation()
return (
<div className='absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'>
<div className='flex justify-center mb-2'>
<ChatBotSlim className='w-12 h-12 text-gray-300' />
</div>
<div className='w-[256px] text-center text-[13px] text-gray-400'>
{t('workflow.common.previewPlaceholder')}
</div>
</div>
)
}
export default Empty

View File

@@ -0,0 +1,339 @@
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { produce, setAutoFreeze } from 'immer'
import { useWorkflowRun } from '../../hooks'
import { WorkflowRunningStatus } from '../../types'
import type {
ChatItem,
Inputs,
PromptVariable,
} from '@/app/components/base/chat/types'
import { useToastContext } from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import type { VisionFile } from '@/types/app'
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
type GetAbortController = (abortController: AbortController) => void
type SendCallback = {
onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
}
export const useChat = (
config: any,
promptVariablesConfig?: {
inputs: Inputs
promptVariables: PromptVariable[]
},
prevChatList?: ChatItem[],
stopChat?: (taskId: string) => void,
) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { handleRun } = useWorkflowRun()
const hasStopResponded = useRef(false)
const connversationId = useRef('')
const taskIdRef = useRef('')
const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
const chatListRef = useRef<ChatItem[]>(prevChatList || [])
const [isResponding, setIsResponding] = useState(false)
const isRespondingRef = useRef(false)
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
useEffect(() => {
setAutoFreeze(false)
return () => {
setAutoFreeze(true)
}
}, [])
const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
setChatList(newChatList)
chatListRef.current = newChatList
}, [])
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
}, [])
const getIntroduction = useCallback((str: string) => {
return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {})
}, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables])
useEffect(() => {
if (config?.opening_statement) {
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const index = draft.findIndex(item => item.isOpeningStatement)
if (index > -1) {
draft[index] = {
...draft[index],
content: getIntroduction(config.opening_statement),
suggestedQuestions: config.suggested_questions,
}
}
else {
draft.unshift({
id: `${Date.now()}`,
content: getIntroduction(config.opening_statement),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
})
}
}))
}
}, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
const handleStop = useCallback(() => {
hasStopResponded.current = true
handleResponding(false)
if (stopChat && taskIdRef.current)
stopChat(taskIdRef.current)
if (suggestedQuestionsAbortControllerRef.current)
suggestedQuestionsAbortControllerRef.current.abort()
}, [handleResponding, stopChat])
const handleRestart = useCallback(() => {
connversationId.current = ''
taskIdRef.current = ''
handleStop()
const newChatList = config?.opening_statement
? [{
id: `${Date.now()}`,
content: config.opening_statement,
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions: config.suggested_questions,
}]
: []
handleUpdateChatList(newChatList)
setSuggestQuestions([])
}, [
config,
handleStop,
handleUpdateChatList,
])
const updateCurrentQA = useCallback(({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
}: {
responseItem: ChatItem
questionId: string
placeholderAnswerId: string
questionItem: ChatItem
}) => {
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
}, [handleUpdateChatList])
const handleSend = useCallback((
params: any,
{
onGetSuggestedQuestions,
}: SendCallback,
) => {
if (isRespondingRef.current) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
return false
}
const questionId = `question-${Date.now()}`
const questionItem = {
id: questionId,
content: params.query,
isAnswer: false,
message_files: params.files,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
const placeholderAnswerItem = {
id: placeholderAnswerId,
content: '',
isAnswer: true,
}
const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
handleUpdateChatList(newList)
// answer
const responseItem: ChatItem = {
id: `${Date.now()}`,
content: '',
agent_thoughts: [],
message_files: [],
isAnswer: true,
}
handleResponding(true)
const bodyParams = {
conversation_id: connversationId.current,
...params,
}
if (bodyParams?.files?.length) {
bodyParams.files = bodyParams.files.map((item: VisionFile) => {
if (item.transfer_method === TransferMethod.local_file) {
return {
...item,
url: '',
}
}
return item
})
}
let hasSetResponseId = false
handleRun(
params,
{
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
responseItem.content = responseItem.content + message
if (messageId && !hasSetResponseId) {
responseItem.id = messageId
hasSetResponseId = true
}
if (isFirstMessage && newConversationId)
connversationId.current = newConversationId
taskIdRef.current = taskId
if (messageId)
responseItem.id = messageId
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
async onCompleted(hasError?: boolean, errorMessage?: string) {
handleResponding(false)
if (hasError) {
if (errorMessage) {
responseItem.content = errorMessage
responseItem.isError = true
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
}
return
}
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
const { data }: any = await onGetSuggestedQuestions(
responseItem.id,
newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
)
setSuggestQuestions(data)
}
},
onMessageEnd: (messageEnd) => {
responseItem.citation = messageEnd.metadata?.retriever_resources || []
const newListWithAnswer = produce(
chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
handleUpdateChatList(newListWithAnswer)
},
onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer
},
onError() {
handleResponding(false)
},
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
taskIdRef.current = task_id
responseItem.workflow_run_id = workflow_run_id
responseItem.workflowProcess = {
status: WorkflowRunningStatus.Running,
tracing: [],
}
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onWorkflowFinished: ({ data }) => {
responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onNodeStarted: ({ data }) => {
responseItem.workflowProcess!.tracing!.push(data as any)
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onNodeFinished: ({ data }) => {
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
responseItem.workflowProcess!.tracing[currentIndex] = {
...(responseItem.workflowProcess!.tracing[currentIndex].extras
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
: {}),
...data,
} as any
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
},
)
}, [handleRun, handleResponding, handleUpdateChatList, notify, t, updateCurrentQA, config.suggested_questions_after_answer?.enabled])
return {
conversationId: connversationId.current,
chatList,
handleSend,
handleStop,
handleRestart,
isResponding,
suggestedQuestions,
}
}

View File

@@ -0,0 +1,52 @@
import {
memo,
useRef,
} from 'react'
import { useKeyPress } from 'ahooks'
import { useTranslation } from 'react-i18next'
import ChatWrapper from './chat-wrapper'
import Button from '@/app/components/base/button'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
export type ChatWrapperRefType = {
handleRestart: () => void
}
const DebugAndPreview = () => {
const { t } = useTranslation()
const chatRef = useRef({ handleRestart: () => {} })
useKeyPress('shift.r', () => {
chatRef.current.handleRestart()
}, {
exactMatch: true,
})
return (
<div
className={`
flex flex-col w-[400px] rounded-l-2xl h-full border border-black/[0.02] shadow-xl
`}
style={{
background: 'linear-gradient(156deg, rgba(242, 244, 247, 0.80) 0%, rgba(242, 244, 247, 0.00) 99.43%), var(--white, #FFF)',
}}
>
<div className='shrink-0 flex items-center justify-between px-4 pt-3 pb-2 font-semibold text-gray-900'>
{t('workflow.common.debugAndPreview').toLocaleUpperCase()}
<Button
className='pl-2.5 pr-[7px] h-8 bg-white border-[0.5px] border-gray-200 shadow-xs rounded-lg text-[13px] text-primary-600 font-semibold'
onClick={() => chatRef.current.handleRestart()}
>
<RefreshCcw01 className='mr-1 w-3.5 h-3.5' />
{t('common.operation.refresh')}
<div className='ml-2 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>Shift</div>
<div className='ml-0.5 px-1 leading-[18px] rounded-md border border-gray-200 bg-gray-50 text-[11px] text-gray-500 font-medium'>R</div>
</Button>
</div>
<div className='grow rounded-b-2xl overflow-y-auto'>
<ChatWrapper ref={chatRef} />
</div>
</div>
)
}
export default memo(DebugAndPreview)

View File

@@ -0,0 +1,80 @@
import {
memo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import FormItem from '../../nodes/_base/components/before-run-form/form-item'
import { BlockEnum } from '../../types'
import {
useStore,
useWorkflowStore,
} from '../../store'
import type { StartNodeType } from '../../nodes/start/types'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
const UserInput = () => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
const [expanded, setExpanded] = useState(true)
const inputs = useStore(s => s.inputs)
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || []
const handleValueChange = (variable: string, v: string) => {
workflowStore.getState().setInputs({
...inputs,
[variable]: v,
})
}
if (!variables.length)
return null
return (
<div
className={`
relative rounded-xl border z-[1]
${!expanded ? 'bg-indigo-25 border-indigo-100 shadow-none' : 'bg-white shadow-xs border-transparent'}
`}
>
<div
className={`
flex items-center px-2 pt-4 h-[18px] text-[13px] font-semibold cursor-pointer
${!expanded ? 'text-indigo-800' : 'text-gray-800'}
`}
onClick={() => setExpanded(!expanded)}
>
<ChevronDown
className={`mr-1 w-3 h-3 ${!expanded ? '-rotate-90 text-indigo-600' : 'text-gray-300'}`}
/>
{t('workflow.panel.userInputField').toLocaleUpperCase()}
</div>
<div className='px-2 pt-1 pb-3'>
{
expanded && (
<div className='py-2 text-[13px] text-gray-900'>
{
variables.map(variable => (
<div
key={variable.variable}
className='mb-2 last-of-type:mb-0'
>
<FormItem
payload={variable}
value={inputs[variable.variable]}
onChange={v => handleValueChange(variable.variable, v)}
/>
</div>
))
}
</div>
)
}
</div>
</div>
)
}
export default memo(UserInput)

View File

@@ -0,0 +1,88 @@
import type { FC } from 'react'
import {
memo,
useMemo,
} from 'react'
import { useNodes } from 'reactflow'
import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
import { useStore } from '../store'
import { useIsChatMode } from '../hooks'
import DebugAndPreview from './debug-and-preview'
import Record from './record'
import WorkflowPreview from './workflow-preview'
import ChatRecord from './chat-record'
import { useStore as useAppStore } from '@/app/components/app/store'
import MessageLogModal from '@/app/components/base/message-log-modal'
const Panel: FC = () => {
const nodes = useNodes<CommonNodeType>()
const isChatMode = useIsChatMode()
const selectedNode = nodes.find(node => node.data.selected)
const showInputsPanel = useStore(s => s.showInputsPanel)
const workflowRunningData = useStore(s => s.workflowRunningData)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal } = useAppStore()
const {
showNodePanel,
showDebugAndPreviewPanel,
showWorkflowPreview,
} = useMemo(() => {
return {
showNodePanel: !!selectedNode && !workflowRunningData && !historyWorkflowData && !showInputsPanel,
showDebugAndPreviewPanel: isChatMode && workflowRunningData && !historyWorkflowData,
showWorkflowPreview: !isChatMode && !historyWorkflowData && (workflowRunningData || showInputsPanel),
}
}, [
showInputsPanel,
selectedNode,
isChatMode,
workflowRunningData,
historyWorkflowData,
])
return (
<div className='absolute top-14 right-0 bottom-2 flex z-10'>
{
showMessageLogModal && (
<MessageLogModal
fixedWidth
width={400}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
/>
)
}
{
historyWorkflowData && !isChatMode && (
<Record />
)
}
{
historyWorkflowData && isChatMode && (
<ChatRecord />
)
}
{
showDebugAndPreviewPanel && (
<DebugAndPreview />
)
}
{
showWorkflowPreview && (
<WorkflowPreview />
)
}
{
showNodePanel && (
<NodePanel {...selectedNode!} />
)
}
</div>
)
}
export default memo(Panel)

View File

@@ -0,0 +1,111 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import FormItem from '../nodes/_base/components/before-run-form/form-item'
import {
BlockEnum,
InputVarType,
WorkflowRunningStatus,
} from '../types'
import {
useStore,
useWorkflowStore,
} from '../store'
import { useWorkflowRun } from '../hooks'
import type { StartNodeType } from '../nodes/start/types'
import Button from '@/app/components/base/button'
import { useFeatures } from '@/app/components/base/features/hooks'
type Props = {
onRun: () => void
}
const InputsPanel = ({ onRun }: Props) => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
const fileSettings = useFeatures(s => s.features.file)
const nodes = useNodes<StartNodeType>()
const inputs = useStore(s => s.inputs)
const files = useStore(s => s.files)
const workflowRunningData = useStore(s => s.workflowRunningData)
const {
handleRun,
handleRunSetting,
} = useWorkflowRun()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables
const variables = useMemo(() => {
const data = startVariables || []
if (fileSettings?.image?.enabled) {
return [
...data,
{
type: InputVarType.files,
variable: '__image',
required: true,
label: 'files',
},
]
}
return data
}, [fileSettings?.image?.enabled, startVariables])
const handleValueChange = (variable: string, v: any) => {
if (variable === '__image') {
workflowStore.setState({
files: v,
})
}
else {
workflowStore.getState().setInputs({
...inputs,
[variable]: v,
})
}
}
const doRun = () => {
onRun()
handleRunSetting()
handleRun({ inputs, files })
}
return (
<>
<div className='px-4 pb-2'>
{
variables.map(variable => (
<div
key={variable.variable}
className='mb-2 last-of-type:mb-0'
>
<FormItem
className='!block'
payload={variable}
value={inputs[variable.variable]}
onChange={v => handleValueChange(variable.variable, v)}
/>
</div>
))
}
</div>
<div className='flex items-center justify-between px-4 py-2'>
<Button
type='primary'
disabled={workflowRunningData?.result?.status === WorkflowRunningStatus.Running}
className='py-0 w-full h-8 rounded-lg text-[13px] font-medium'
onClick={doRun}
>
{t('workflow.singleRun.startRun')}
</Button>
</div>
</>
)
}
export default memo(InputsPanel)

View File

@@ -0,0 +1,18 @@
import { memo } from 'react'
import Run from '../run'
import { useStore } from '../store'
const Record = () => {
const historyWorkflowData = useStore(s => s.historyWorkflowData)
return (
<div className='flex flex-col w-[400px] h-full rounded-l-2xl border-[0.5px] border-gray-200 shadow-xl bg-white'>
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
{`Test Run#${historyWorkflowData?.sequence_number}`}
</div>
<Run runID={historyWorkflowData?.id || ''} />
</div>
)
}
export default memo(Record)

View File

@@ -0,0 +1,155 @@
import {
memo,
useEffect,
useRef,
useState,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import OutputPanel from '../run/output-panel'
import ResultPanel from '../run/result-panel'
import TracingPanel from '../run/tracing-panel'
import {
useWorkflowRun,
} from '../hooks'
import { useStore } from '../store'
import {
WorkflowRunningStatus,
} from '../types'
import InputsPanel from './inputs-panel'
import Loading from '@/app/components/base/loading'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
const WorkflowPreview = () => {
const { t } = useTranslation()
const { handleRunSetting } = useWorkflowRun()
const showInputsPanel = useStore(s => s.showInputsPanel)
const workflowRunningData = useStore(s => s.workflowRunningData)
const [currentTab, setCurrentTab] = useState<string>(showInputsPanel ? 'INPUT' : 'TRACING')
const switchTab = async (tab: string) => {
setCurrentTab(tab)
}
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()
}, [])
return (
<div className={`
flex flex-col w-[420px] h-full rounded-l-2xl border-[0.5px] border-gray-200 shadow-xl bg-white
`}>
<div className='flex items-center justify-between p-4 pb-1 text-base font-semibold text-gray-900'>
{`Test Run${!workflowRunningData?.result.sequence_number ? '' : `#${workflowRunningData?.result.sequence_number}`}`}
{showInputsPanel && workflowRunningData?.result?.status !== WorkflowRunningStatus.Running && (
<div className='p-1 cursor-pointer' onClick={() => handleRunSetting(true)}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
)}
</div>
<div className='grow relative flex flex-col'>
<div className='shrink-0 flex items-center px-4 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
{showInputsPanel && (
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'INPUT' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('INPUT')}
>{t('runLog.input')}</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 === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
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',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
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',
!workflowRunningData && 'opacity-30 !cursor-not-allowed',
)}
onClick={() => {
if (!workflowRunningData)
return
switchTab('TRACING')
}}
>{t('runLog.tracing')}</div>
</div>
<div ref={ref} className={cn(
'grow bg-white h-0 overflow-y-auto rounded-b-2xl',
(currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50',
)}>
{currentTab === 'INPUT' && (
<InputsPanel onRun={() => switchTab('RESULT')} />
)}
{currentTab === 'RESULT' && (
<OutputPanel
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
outputs={workflowRunningData?.result?.outputs}
error={workflowRunningData?.result?.error}
height={height}
/>
)}
{currentTab === 'DETAIL' && (
<ResultPanel
inputs={workflowRunningData?.result?.inputs}
outputs={workflowRunningData?.result?.outputs}
status={workflowRunningData?.result?.status || ''}
error={workflowRunningData?.result?.error}
elapsed_time={workflowRunningData?.result?.elapsed_time}
total_tokens={workflowRunningData?.result?.total_tokens}
created_at={workflowRunningData?.result?.created_at}
created_by={(workflowRunningData?.result?.created_by as any)?.name}
steps={workflowRunningData?.result?.total_steps}
/>
)}
{currentTab === 'DETAIL' && !workflowRunningData?.result && (
<div className='flex h-full items-center justify-center bg-white'>
<Loading />
</div>
)}
{currentTab === 'TRACING' && (
<TracingPanel
list={workflowRunningData?.tracing || []}
/>
)}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
<div className='flex h-full items-center justify-center bg-gray-50'>
<Loading />
</div>
)}
</div>
</div>
</div>
)
}
export default memo(WorkflowPreview)