From b68de4fa6d6548ea8bb9d9d2fc7281afacecaaaf Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Thu, 5 Dec 2024 16:58:39 +0800 Subject: [PATCH] Fix: support file download in workflow result (#11338) --- .../app/text-generate/item/index.tsx | 2 +- .../app/text-generate/item/result-tab.tsx | 27 ++- .../base/file-uploader/file-list-in-log.tsx | 54 ++++-- .../file-uploader-in-attachment/file-item.tsx | 171 ++++++++++-------- .../file-uploader-in-chat-input/file-item.tsx | 4 +- .../components/base/file-uploader/utils.ts | 21 ++- .../share/text-generation/result/index.tsx | 4 +- .../workflow/hooks/use-workflow-run.ts | 4 +- .../nodes/_base/components/editor/base.tsx | 5 +- .../components/editor/code-editor/index.tsx | 2 +- .../workflow/panel/workflow-preview.tsx | 2 +- .../components/workflow/run/output-panel.tsx | 44 ++++- .../components/workflow/run/result-text.tsx | 38 ++-- web/i18n/en-US/app-log.ts | 2 + web/i18n/zh-Hans/app-log.ts | 2 + 15 files changed, 247 insertions(+), 135 deletions(-) diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index e10350acc4161b..0c4f62282ef579 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -334,7 +334,7 @@ const GenerationItem: FC = ({ ) } - {(currentTab === 'RESULT' || !isWorkflow) && ( + {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && ( { - if (data?.resultText) + if (data?.resultText || !!data?.files?.length) switchTab('RESULT') else switchTab('DETAIL') - }, [data?.resultText]) + }, [data?.files?.length, data?.resultText]) return (
- {data?.resultText && ( + {(data?.resultText || !!data?.files?.length) && (
{currentTab === 'RESULT' && ( <> - + {data?.resultText && } {!!data?.files?.length && ( - +
+ {data?.files.map((item: any) => ( +
+
{item.varName}
+ +
+ ))} +
)} )} diff --git a/web/app/components/base/file-uploader/file-list-in-log.tsx b/web/app/components/base/file-uploader/file-list-in-log.tsx index 9c28fc0eaafed9..e76d84bacee2f3 100644 --- a/web/app/components/base/file-uploader/file-list-in-log.tsx +++ b/web/app/components/base/file-uploader/file-list-in-log.tsx @@ -1,4 +1,5 @@ -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' import { RiArrowRightSLine } from '@remixicon/react' import FileImageRender from './file-image-render' import FileTypeIcon from './file-type-icon' @@ -12,23 +13,36 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types' import cn from '@/utils/classnames' type Props = { - fileList: FileEntity[] + fileList: { + varName: string + list: FileEntity[] + }[] + isExpanded?: boolean + noBorder?: boolean + noPadding?: boolean } -const FileListInLog = ({ fileList }: Props) => { - const [expanded, setExpanded] = useState(false) +const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPadding = false }: Props) => { + const { t } = useTranslation() + const [expanded, setExpanded] = useState(isExpanded) + const fullList = useMemo(() => { + return fileList.reduce((acc: FileEntity[], { list }) => { + return [...acc, ...list] + }, []) + }, [fileList]) if (!fileList.length) return null + return ( -
+
{expanded && ( -
+
setExpanded(!expanded)}>{t('appLog.runDetail.fileListLabel')}
)} {!expanded && ( -
- {fileList.map((file) => { +
+ {fullList.map((file) => { const { id, name, type, supportFileType, base64Url, url } = file const isImageFile = supportFileType === SupportUploadFileTypes.image return ( @@ -63,19 +77,25 @@ const FileListInLog = ({ fileList }: Props) => {
)}
setExpanded(!expanded)}> - {!expanded &&
DETAIL
} + {!expanded &&
{t('appLog.runDetail.fileListDetail')}
}
{expanded && ( -
- {fileList.map(file => ( - +
+ {fileList.map(item => ( +
+
{item.varName}
+ {item.list.map(file => ( + + ))} +
))}
)} diff --git a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx index 2a042bab403df9..722ef64a68b32e 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx @@ -1,12 +1,15 @@ import { memo, + useState, } from 'react' import { RiDeleteBinLine, RiDownloadLine, + RiEyeLine, } from '@remixicon/react' import FileTypeIcon from '../file-type-icon' import { + downloadFile, fileIsUploaded, getFileAppearanceType, getFileExtension, @@ -19,6 +22,7 @@ import { formatFileSize } from '@/utils/format' import cn from '@/utils/classnames' import { ReplayLine } from '@/app/components/base/icons/src/vender/other' import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import ImagePreview from '@/app/components/base/image-uploader/image-preview' type FileInAttachmentItemProps = { file: FileEntity @@ -26,6 +30,7 @@ type FileInAttachmentItemProps = { showDownloadAction?: boolean onRemove?: (fileId: string) => void onReUpload?: (fileId: string) => void + canPreview?: boolean } const FileInAttachmentItem = ({ file, @@ -33,96 +38,116 @@ const FileInAttachmentItem = ({ showDownloadAction = true, onRemove, onReUpload, + canPreview, }: FileInAttachmentItemProps) => { const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file const ext = getFileExtension(name, type, isRemote) const isImageFile = supportFileType === SupportUploadFileTypes.image - + const [imagePreviewUrl, setImagePreviewUrl] = useState('') return ( -
-
- { - isImageFile && ( - - ) - } - { - !isImageFile && ( - - ) - } -
-
-
-
{name}
+ <> +
+
+ { + isImageFile && ( + + ) + } + { + !isImageFile && ( + + ) + }
-
+
+
+
{name}
+
+
+ { + ext && ( + {ext.toLowerCase()} + ) + } + { + ext && ( + + ) + } + { + !!file.size && ( + {formatFileSize(file.size)} + ) + } +
+
+
+ { + progress >= 0 && !fileIsUploaded(file) && ( + + ) + } { - ext && ( - {ext.toLowerCase()} + progress === -1 && ( + onReUpload?.(id)} + > + + ) } { - ext && ( - + showDeleteAction && ( + onRemove?.(id)}> + + ) } { - !!file.size && ( - {formatFileSize(file.size)} + canPreview && isImageFile && ( + setImagePreviewUrl(url || '')}> + + + ) + } + { + showDownloadAction && ( + { + e.stopPropagation() + downloadFile(url || base64Url || '', name) + }}> + + ) }
-
- { - progress >= 0 && !fileIsUploaded(file) && ( - - ) - } - { - progress === -1 && ( - onReUpload?.(id)} - > - - - ) - } - { - showDeleteAction && ( - onRemove?.(id)}> - - - ) - } - { - showDownloadAction && ( - - - - ) - } -
-
+ { + imagePreviewUrl && canPreview && ( + setImagePreviewUrl('')} + /> + ) + } + ) } diff --git a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx index dcf408278053b1..b6ecc276dbabea 100644 --- a/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx +++ b/web/app/components/base/file-uploader/file-uploader-in-chat-input/file-item.tsx @@ -31,7 +31,7 @@ const FileItem = ({ onRemove, onReUpload, }: FileItemProps) => { - const { id, name, type, progress, url, isRemote } = file + const { id, name, type, progress, url, base64Url, isRemote } = file const ext = getFileExtension(name, type, isRemote) const uploadError = progress === -1 @@ -86,7 +86,7 @@ const FileItem = ({ className='hidden group-hover/file-item:flex absolute -right-1 -top-1' onClick={(e) => { e.stopPropagation() - downloadFile(url || '', name) + downloadFile(url || base64Url || '', name) }} > diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts index aa8625f2217e62..8c752fde8ab858 100644 --- a/web/app/components/base/file-uploader/utils.ts +++ b/web/app/components/base/file-uploader/utils.ts @@ -1,5 +1,4 @@ import mime from 'mime' -import { flatten } from 'lodash-es' import { FileAppearanceTypeEnum } from './types' import type { FileEntity } from './types' import { upload } from '@/service/base' @@ -158,12 +157,22 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a } export const getFilesInLogs = (rawData: any) => { - const originalFiles = flatten(Object.keys(rawData || {}).map((key) => { - if (typeof rawData[key] === 'object' || Array.isArray(rawData[key])) - return rawData[key] + const result = Object.keys(rawData || {}).map((key) => { + if (typeof rawData[key] === 'object' && rawData[key].dify_model_identity === '__dify__file__') { + return { + varName: key, + list: getProcessedFilesFromResponse([rawData[key]]), + } + } + if (Array.isArray(rawData[key]) && rawData[key].some(item => item.dify_model_identity === '__dify__file__')) { + return { + varName: key, + list: getProcessedFilesFromResponse(rawData[key]), + } + } return undefined - }).filter(Boolean)).filter(item => item?.model_identity === '__dify__file__') - return getProcessedFilesFromResponse(originalFiles) + }).filter(Boolean) + return result } export const fileIsUploaded = (file: FileEntity) => { diff --git a/web/app/components/share/text-generation/result/index.tsx b/web/app/components/share/text-generation/result/index.tsx index 6b881f1fd27b22..cd4ed5d2877a9a 100644 --- a/web/app/components/share/text-generation/result/index.tsx +++ b/web/app/components/share/text-generation/result/index.tsx @@ -21,7 +21,7 @@ import { sleep } from '@/utils' import type { SiteInfo } from '@/models/share' import { TEXT_GENERATION_TIMEOUT_MS } from '@/config' import { - getProcessedFilesFromResponse, + getFilesInLogs, } from '@/app/components/base/file-uploader/utils' export type IResultProps = { @@ -288,7 +288,7 @@ const Result: FC = ({ } setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { draft.status = WorkflowRunningStatus.Succeeded - draft.files = getProcessedFilesFromResponse(data.files || []) + draft.files = getFilesInLogs(data.outputs || []) as any[] })) if (!data.outputs) { setCompletionRes('') diff --git a/web/app/components/workflow/hooks/use-workflow-run.ts b/web/app/components/workflow/hooks/use-workflow-run.ts index 24b20b52740bd6..5fbca27791397e 100644 --- a/web/app/components/workflow/hooks/use-workflow-run.ts +++ b/web/app/components/workflow/hooks/use-workflow-run.ts @@ -26,7 +26,7 @@ import { import { useFeaturesStore } from '@/app/components/base/features/hooks' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' import { - getProcessedFilesFromResponse, + getFilesInLogs, } from '@/app/components/base/file-uploader/utils' export const useWorkflowRun = () => { @@ -213,7 +213,7 @@ export const useWorkflowRun = () => { draft.result = { ...draft.result, ...data, - files: getProcessedFilesFromResponse(data.files || []), + files: getFilesInLogs(data.outputs), } as any if (isStringOutput) { draft.resultTabActive = true diff --git a/web/app/components/workflow/nodes/_base/components/editor/base.tsx b/web/app/components/workflow/nodes/_base/components/editor/base.tsx index 44930427ae5ec0..18ec5ea4a354e3 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/base.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/base.tsx @@ -27,7 +27,10 @@ type Props = { isInNode?: boolean onGenerated?: (prompt: string) => void codeLanguages?: CodeLanguage - fileList?: FileEntity[] + fileList?: { + varName: string + list: FileEntity[] + }[] showFileList?: boolean showCodeGenerator?: boolean } diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 2d75679b08be13..ed32dfb492010f 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -208,7 +208,7 @@ const CodeEditor: FC = ({ isInNode={isInNode} onGenerated={onGenerated} codeLanguages={language} - fileList={fileList} + fileList={fileList as any} showFileList={showFileList} showCodeGenerator={showCodeGenerator} > diff --git a/web/app/components/workflow/panel/workflow-preview.tsx b/web/app/components/workflow/panel/workflow-preview.tsx index d560c0b2cb6c10..1171051efc6252 100644 --- a/web/app/components/workflow/panel/workflow-preview.tsx +++ b/web/app/components/workflow/panel/workflow-preview.tsx @@ -48,7 +48,7 @@ const WorkflowPreview = () => { }, [showDebugAndPreviewPanel, showInputsPanel]) useEffect(() => { - if ((workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded || workflowRunningData?.result.status === WorkflowRunningStatus.Failed) && !workflowRunningData.resultText) + if ((workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded || workflowRunningData?.result.status === WorkflowRunningStatus.Failed) && !workflowRunningData.resultText && !workflowRunningData.result.files?.length) switchTab('DETAIL') }, [workflowRunningData]) diff --git a/web/app/components/workflow/run/output-panel.tsx b/web/app/components/workflow/run/output-panel.tsx index 6b8d4cde1ab9ab..9904079eda5bb4 100644 --- a/web/app/components/workflow/run/output-panel.tsx +++ b/web/app/components/workflow/run/output-panel.tsx @@ -1,10 +1,13 @@ 'use client' import type { FC } from 'react' +import { useMemo } from 'react' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { Markdown } from '@/app/components/base/markdown' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' +import { FileList } from '@/app/components/base/file-uploader' import StatusContainer from '@/app/components/workflow/run/status-container' +import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' type OutputPanelProps = { isRunning?: boolean @@ -19,6 +22,30 @@ const OutputPanel: FC = ({ error, height, }) => { + const isTextOutput = useMemo(() => { + return outputs && Object.keys(outputs).length === 1 && typeof outputs[Object.keys(outputs)[0]] === 'string' + }, [outputs]) + + const fileList = useMemo(() => { + const fileList: any[] = [] + if (!outputs) + return fileList + if (Object.keys(outputs).length > 1) + return fileList + for (const key in outputs) { + if (Array.isArray(outputs[key])) { + outputs[key].map((output: any) => { + if (output.dify_model_identity === '__dify__file__') + fileList.push(output) + return null + }) + } + else if (outputs[key].dify_model_identity === '__dify__file__') { + fileList.push(outputs[key]) + } + } + return getProcessedFilesFromResponse(fileList) + }, [outputs]) return (
{isRunning && ( @@ -36,20 +63,31 @@ const OutputPanel: FC = ({
)} - {outputs && Object.keys(outputs).length === 1 && ( + {isTextOutput && (
)} + {fileList.length > 0 && ( +
+ +
+ )} {outputs && Object.keys(outputs).length > 1 && height! > 0 && ( -
+
} language={CodeLanguage.json} value={outputs} isJSONStringifyBeauty - height={height} + height={height ? (height - 16) / 2 : undefined} />
)} diff --git a/web/app/components/workflow/run/result-text.tsx b/web/app/components/workflow/run/result-text.tsx index 1369d49c61dfe6..27b1f2cd8c1b2c 100644 --- a/web/app/components/workflow/run/result-text.tsx +++ b/web/app/components/workflow/run/result-text.tsx @@ -6,14 +6,13 @@ import { Markdown } from '@/app/components/base/markdown' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' import StatusContainer from '@/app/components/workflow/run/status-container' import { FileList } from '@/app/components/base/file-uploader' -import type { FileEntity } from '@/app/components/base/file-uploader/types' type ResultTextProps = { isRunning?: boolean outputs?: any error?: string onClick?: () => void - allFiles?: FileEntity[] + allFiles?: any[] } const ResultText: FC = ({ @@ -25,20 +24,20 @@ const ResultText: FC = ({ }) => { const { t } = useTranslation() return ( -
+
{isRunning && !outputs && (
)} {!isRunning && error && ( -
+
{error}
)} - {!isRunning && !outputs && !error && ( + {!isRunning && !outputs && !error && !allFiles?.length && (
{t('runLog.resultEmpty.title')}
@@ -49,18 +48,25 @@ const ResultText: FC = ({
)} - {outputs && ( -
- - {!!allFiles?.length && ( - + {(outputs || !!allFiles?.length) && ( + <> + {outputs && ( +
+ +
)} -
+ {!!allFiles?.length && allFiles.map(item => ( +
+
{item.varName}
+ +
+ ))} + )}
) diff --git a/web/i18n/en-US/app-log.ts b/web/i18n/en-US/app-log.ts index a53da966bed39f..587c305e8da26b 100644 --- a/web/i18n/en-US/app-log.ts +++ b/web/i18n/en-US/app-log.ts @@ -79,6 +79,8 @@ const translation = { runDetail: { title: 'Conversation Log', workflowTitle: 'Log Detail', + fileListLabel: 'File Details', + fileListDetail: 'Detail', }, promptLog: 'Prompt Log', agentLog: 'Agent Log', diff --git a/web/i18n/zh-Hans/app-log.ts b/web/i18n/zh-Hans/app-log.ts index 0d2118a6841fa6..52b93d378ce88d 100644 --- a/web/i18n/zh-Hans/app-log.ts +++ b/web/i18n/zh-Hans/app-log.ts @@ -79,6 +79,8 @@ const translation = { runDetail: { title: '对话日志', workflowTitle: '日志详情', + fileListLabel: '文件详情', + fileListDetail: '详情', }, promptLog: 'Prompt 日志', agentLog: 'Agent 日志',