diff --git a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-text-chunk.ts b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-text-chunk.ts index dfca7f61a6..b016cabc4d 100644 --- a/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-text-chunk.ts +++ b/web/app/components/workflow/hooks/use-workflow-run-event/use-workflow-text-chunk.ts @@ -1,13 +1,25 @@ import type { TextChunkResponse } from '@/types/workflow' import { produce } from 'immer' -import { useCallback } from 'react' +import { useCallback, useRef } from 'react' +import { v4 as uuidV4 } from 'uuid' import { useWorkflowStore } from '@/app/components/workflow/store' export const useWorkflowTextChunk = () => { const workflowStore = useWorkflowStore() + const toolCallIdRef = useRef(null) const handleWorkflowTextChunk = useCallback((params: TextChunkResponse) => { - const { data: { text } } = params + const { data: { + text, + chunk_type, + tool_name, + tool_arguments, + tool_icon, + tool_icon_dark, + tool_error, + tool_elapsed_time, + tool_files, + } } = params const { workflowRunningData, setWorkflowRunningData, @@ -15,7 +27,87 @@ export const useWorkflowTextChunk = () => { setWorkflowRunningData(produce(workflowRunningData!, (draft) => { draft.resultTabActive = true - draft.resultText += text + + if (chunk_type === 'text') { + draft.resultText += text + + if (!draft.resultLLMGenerationItems) + draft.resultLLMGenerationItems = [] + + const isNotCompletedTextItemIndex = draft.resultLLMGenerationItems?.findIndex(item => item.type === 'text' && !item.textCompleted) + + if (isNotCompletedTextItemIndex > -1) { + draft.resultLLMGenerationItems![isNotCompletedTextItemIndex].text += text + } + else { + draft.resultLLMGenerationItems?.push({ + id: uuidV4(), + type: 'text', + text, + }) + } + } + + if (chunk_type === 'tool_call') { + if (!draft.resultLLMGenerationItems) + draft.resultLLMGenerationItems = [] + + const isNotCompletedTextItemIndex = draft.resultLLMGenerationItems?.findIndex(item => item.type === 'text' && !item.textCompleted) + if (isNotCompletedTextItemIndex > -1) { + draft.resultLLMGenerationItems![isNotCompletedTextItemIndex].textCompleted = true + } + toolCallIdRef.current = uuidV4() + draft.resultLLMGenerationItems?.push({ + id: toolCallIdRef.current, + type: 'tool', + toolName: tool_name, + toolArguments: tool_arguments, + toolIcon: tool_icon, + toolIconDark: tool_icon_dark, + }) + } + + if (chunk_type === 'tool_result') { + const currentToolCallIndex = draft.resultLLMGenerationItems?.findIndex(item => item.id === toolCallIdRef.current) ?? -1 + + if (currentToolCallIndex > -1) { + draft.resultLLMGenerationItems![currentToolCallIndex].toolError = tool_error + draft.resultLLMGenerationItems![currentToolCallIndex].toolDuration = tool_elapsed_time + draft.resultLLMGenerationItems![currentToolCallIndex].toolFiles = tool_files + draft.resultLLMGenerationItems![currentToolCallIndex].toolOutput = text + } + } + + if (chunk_type === 'thought_start') { + if (!draft.resultLLMGenerationItems) + draft.resultLLMGenerationItems = [] + + const isNotCompletedTextItemIndex = draft.resultLLMGenerationItems?.findIndex(item => item.type === 'text' && !item.textCompleted) + if (isNotCompletedTextItemIndex > -1) { + draft.resultLLMGenerationItems![isNotCompletedTextItemIndex].textCompleted = true + } + toolCallIdRef.current = uuidV4() + draft.resultLLMGenerationItems?.push({ + id: toolCallIdRef.current, + type: 'thought', + thoughtOutput: '', + }) + } + + if (chunk_type === 'thought') { + const currentThoughtIndex = draft.resultLLMGenerationItems?.findIndex(item => item.id === toolCallIdRef.current) ?? -1 + if (currentThoughtIndex > -1) { + draft.resultLLMGenerationItems![currentThoughtIndex].thoughtOutput += text + } + } + + if (chunk_type === 'thought_end') { + const currentThoughtIndex = draft.resultLLMGenerationItems?.findIndex(item => item.id === toolCallIdRef.current) ?? -1 + if (currentThoughtIndex > -1) { + draft.resultLLMGenerationItems![currentThoughtIndex].thoughtOutput += text + draft.resultLLMGenerationItems![currentThoughtIndex].thoughtCompleted = true + } + } })) }, [workflowStore]) diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index bb90f77748..3fcec92471 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -178,6 +178,7 @@ const WorkflowPreview = () => { switchTab('DETAIL')} diff --git a/web/app/components/workflow/run/result-text.tsx b/web/app/components/workflow/run/result-text.tsx index 026e5b7c05..bada24b701 100644 --- a/web/app/components/workflow/run/result-text.tsx +++ b/web/app/components/workflow/run/result-text.tsx @@ -1,6 +1,8 @@ 'use client' import type { FC } from 'react' +import type { LLMGenerationItem } from '@/types/workflow' import { useTranslation } from 'react-i18next' +import GenerationContent from '@/app/components/base/chat/chat/answer/generation-content' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' import { FileList } from '@/app/components/base/file-uploader' import { ImageIndentLeft } from '@/app/components/base/icons/src/vender/line/editor' @@ -10,6 +12,7 @@ import StatusContainer from '@/app/components/workflow/run/status-container' type ResultTextProps = { isRunning?: boolean outputs?: any + llmGenerationItems?: LLMGenerationItem[] error?: string onClick?: () => void allFiles?: any[] @@ -18,11 +21,16 @@ type ResultTextProps = { const ResultText: FC = ({ isRunning, outputs, + llmGenerationItems, error, onClick, allFiles, }) => { const { t } = useTranslation() + const generationContentRenderIsUsed = llmGenerationItems?.length && llmGenerationItems.some((item) => { + return item.type === 'tool' || item.type === 'thought' + }) + return (
{isRunning && !outputs && ( @@ -50,11 +58,18 @@ const ResultText: FC = ({ )} {(outputs || !!allFiles?.length) && ( <> - {outputs && ( + {outputs && !generationContentRenderIsUsed && (
)} + { + generationContentRenderIsUsed && ( +
+ +
+ ) + } {!!allFiles?.length && allFiles.map(item => (
{item.varName}
diff --git a/web/app/components/workflow/store/workflow/workflow-slice.ts b/web/app/components/workflow/store/workflow/workflow-slice.ts index aeb7ee394a..d498eef100 100644 --- a/web/app/components/workflow/store/workflow/workflow-slice.ts +++ b/web/app/components/workflow/store/workflow/workflow-slice.ts @@ -5,10 +5,12 @@ import type { WorkflowRunningData, } from '@/app/components/workflow/types' import type { FileUploadConfigResponse } from '@/models/common' +import type { LLMGenerationItem } from '@/types/workflow' type PreviewRunningData = WorkflowRunningData & { resultTabActive?: boolean resultText?: string + resultLLMGenerationItems?: LLMGenerationItem[] } type MousePosition = { diff --git a/web/types/workflow.ts b/web/types/workflow.ts index 5c3bf31754..f831b3f862 100644 --- a/web/types/workflow.ts +++ b/web/types/workflow.ts @@ -369,6 +369,16 @@ export type TextChunkResponse = { event: string data: { text: string + chunk_type?: 'text' | 'tool_call' | 'tool_result' | 'thought' | 'thought_start' | 'thought_end' + tool_call_id?: string + tool_name?: string + tool_arguments?: string + tool_icon?: string | IconObject + tool_icon_dark?: string | IconObject + + tool_files?: string[] + tool_error?: string + tool_elapsed_time?: number } }