mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-09 19:06:51 +08:00
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:
103
web/app/components/workflow/panel/chat-record/index.tsx
Normal file
103
web/app/components/workflow/panel/chat-record/index.tsx
Normal 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)
|
||||
56
web/app/components/workflow/panel/chat-record/user-input.tsx
Normal file
56
web/app/components/workflow/panel/chat-record/user-input.tsx
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
339
web/app/components/workflow/panel/debug-and-preview/hooks.ts
Normal file
339
web/app/components/workflow/panel/debug-and-preview/hooks.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
88
web/app/components/workflow/panel/index.tsx
Normal file
88
web/app/components/workflow/panel/index.tsx
Normal 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)
|
||||
111
web/app/components/workflow/panel/inputs-panel.tsx
Normal file
111
web/app/components/workflow/panel/inputs-panel.tsx
Normal 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)
|
||||
18
web/app/components/workflow/panel/record.tsx
Normal file
18
web/app/components/workflow/panel/record.tsx
Normal 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)
|
||||
155
web/app/components/workflow/panel/workflow-preview.tsx
Normal file
155
web/app/components/workflow/panel/workflow-preview.tsx
Normal 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)
|
||||
Reference in New Issue
Block a user