Fix: support file download in workflow result (#11338)

This commit is contained in:
KVOJJJin 2024-12-05 16:58:39 +08:00 committed by GitHub
parent a5d6082418
commit 0b25c0b677
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 247 additions and 135 deletions

View File

@ -334,7 +334,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</SimpleBtn> </SimpleBtn>
) )
} }
{(currentTab === 'RESULT' || !isWorkflow) && ( {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
<SimpleBtn <SimpleBtn
isDisabled={isError || !messageId} isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1')} className={cn(isMobile && '!px-1.5', 'space-x-1')}

View File

@ -27,15 +27,15 @@ const ResultTab = ({
onCurrentTabChange(tab) onCurrentTabChange(tab)
} }
useEffect(() => { useEffect(() => {
if (data?.resultText) if (data?.resultText || !!data?.files?.length)
switchTab('RESULT') switchTab('RESULT')
else else
switchTab('DETAIL') switchTab('DETAIL')
}, [data?.resultText]) }, [data?.files?.length, data?.resultText])
return ( return (
<div className='grow relative flex flex-col'> <div className='grow relative flex flex-col'>
{data?.resultText && ( {(data?.resultText || !!data?.files?.length) && (
<div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'> <div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
<div <div
className={cn( className={cn(
@ -56,14 +56,21 @@ const ResultTab = ({
<div className={cn('grow bg-white')}> <div className={cn('grow bg-white')}>
{currentTab === 'RESULT' && ( {currentTab === 'RESULT' && (
<> <>
<Markdown content={data?.resultText || ''} /> {data?.resultText && <Markdown content={data?.resultText || ''} />}
{!!data?.files?.length && ( {!!data?.files?.length && (
<FileList <div className='flex flex-col gap-2'>
files={data?.files} {data?.files.map((item: any) => (
showDeleteAction={false} <div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
showDownloadAction <div className='py-1 text-text-tertiary '>{item.varName}</div>
canPreview <FileList
/> files={item.list}
showDeleteAction={false}
showDownloadAction
canPreview
/>
</div>
))}
</div>
)} )}
</> </>
)} )}

View File

@ -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 { RiArrowRightSLine } from '@remixicon/react'
import FileImageRender from './file-image-render' import FileImageRender from './file-image-render'
import FileTypeIcon from './file-type-icon' import FileTypeIcon from './file-type-icon'
@ -12,23 +13,36 @@ import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
fileList: FileEntity[] fileList: {
varName: string
list: FileEntity[]
}[]
isExpanded?: boolean
noBorder?: boolean
noPadding?: boolean
} }
const FileListInLog = ({ fileList }: Props) => { const FileListInLog = ({ fileList, isExpanded = false, noBorder = false, noPadding = false }: Props) => {
const [expanded, setExpanded] = useState(false) const { t } = useTranslation()
const [expanded, setExpanded] = useState(isExpanded)
const fullList = useMemo(() => {
return fileList.reduce((acc: FileEntity[], { list }) => {
return [...acc, ...list]
}, [])
}, [fileList])
if (!fileList.length) if (!fileList.length)
return null return null
return ( return (
<div className={cn('border-t border-divider-subtle px-3 py-2', expanded && 'py-3')}> <div className={cn('px-3 py-2', expanded && 'py-3', !noBorder && 'border-t border-divider-subtle', noPadding && '!p-0')}>
<div className='flex justify-between gap-1'> <div className='flex justify-between gap-1'>
{expanded && ( {expanded && (
<div></div> <div className='grow py-1 text-text-secondary system-xs-semibold-uppercase cursor-pointer' onClick={() => setExpanded(!expanded)}>{t('appLog.runDetail.fileListLabel')}</div>
)} )}
{!expanded && ( {!expanded && (
<div className='flex'> <div className='flex gap-1'>
{fileList.map((file) => { {fullList.map((file) => {
const { id, name, type, supportFileType, base64Url, url } = file const { id, name, type, supportFileType, base64Url, url } = file
const isImageFile = supportFileType === SupportUploadFileTypes.image const isImageFile = supportFileType === SupportUploadFileTypes.image
return ( return (
@ -63,19 +77,25 @@ const FileListInLog = ({ fileList }: Props) => {
</div> </div>
)} )}
<div className='flex items-center gap-1 cursor-pointer' onClick={() => setExpanded(!expanded)}> <div className='flex items-center gap-1 cursor-pointer' onClick={() => setExpanded(!expanded)}>
{!expanded && <div className='text-text-tertiary system-xs-medium-uppercase'>DETAIL</div>} {!expanded && <div className='text-text-tertiary system-xs-medium-uppercase'>{t('appLog.runDetail.fileListDetail')}</div>}
<RiArrowRightSLine className={cn('w-4 h-4 text-text-tertiary', expanded && 'rotate-90')} /> <RiArrowRightSLine className={cn('w-4 h-4 text-text-tertiary', expanded && 'rotate-90')} />
</div> </div>
</div> </div>
{expanded && ( {expanded && (
<div className='flex flex-col gap-1'> <div className='flex flex-col gap-3'>
{fileList.map(file => ( {fileList.map(item => (
<FileItem <div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
key={file.id} <div className='py-1 text-text-tertiary '>{item.varName}</div>
file={file} {item.list.map(file => (
showDeleteAction={false} <FileItem
showDownloadAction key={file.id}
/> file={file}
showDeleteAction={false}
showDownloadAction
canPreview
/>
))}
</div>
))} ))}
</div> </div>
)} )}

View File

@ -1,12 +1,15 @@
import { import {
memo, memo,
useState,
} from 'react' } from 'react'
import { import {
RiDeleteBinLine, RiDeleteBinLine,
RiDownloadLine, RiDownloadLine,
RiEyeLine,
} from '@remixicon/react' } from '@remixicon/react'
import FileTypeIcon from '../file-type-icon' import FileTypeIcon from '../file-type-icon'
import { import {
downloadFile,
fileIsUploaded, fileIsUploaded,
getFileAppearanceType, getFileAppearanceType,
getFileExtension, getFileExtension,
@ -19,6 +22,7 @@ import { formatFileSize } from '@/utils/format'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other' import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
type FileInAttachmentItemProps = { type FileInAttachmentItemProps = {
file: FileEntity file: FileEntity
@ -26,6 +30,7 @@ type FileInAttachmentItemProps = {
showDownloadAction?: boolean showDownloadAction?: boolean
onRemove?: (fileId: string) => void onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void onReUpload?: (fileId: string) => void
canPreview?: boolean
} }
const FileInAttachmentItem = ({ const FileInAttachmentItem = ({
file, file,
@ -33,96 +38,116 @@ const FileInAttachmentItem = ({
showDownloadAction = true, showDownloadAction = true,
onRemove, onRemove,
onReUpload, onReUpload,
canPreview,
}: FileInAttachmentItemProps) => { }: FileInAttachmentItemProps) => {
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote) const ext = getFileExtension(name, type, isRemote)
const isImageFile = supportFileType === SupportUploadFileTypes.image const isImageFile = supportFileType === SupportUploadFileTypes.image
const [imagePreviewUrl, setImagePreviewUrl] = useState('')
return ( return (
<div className={cn( <>
'flex items-center pr-3 h-12 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs', <div className={cn(
progress === -1 && 'bg-state-destructive-hover border-state-destructive-border', 'flex items-center pr-3 h-12 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs',
)}> progress === -1 && 'bg-state-destructive-hover border-state-destructive-border',
<div className='flex items-center justify-center w-12 h-12'> )}>
{ <div className='flex items-center justify-center w-12 h-12'>
isImageFile && ( {
<FileImageRender isImageFile && (
className='w-8 h-8' <FileImageRender
imageUrl={base64Url || url || ''} className='w-8 h-8'
/> imageUrl={base64Url || url || ''}
) />
} )
{ }
!isImageFile && ( {
<FileTypeIcon !isImageFile && (
type={getFileAppearanceType(name, type)} <FileTypeIcon
size='lg' type={getFileAppearanceType(name, type)}
/> size='lg'
) />
} )
</div> }
<div className='grow w-0 mr-1'>
<div
className='flex items-center mb-0.5 system-xs-medium text-text-secondary truncate'
title={file.name}
>
<div className='truncate'>{name}</div>
</div> </div>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'> <div className='grow w-0 mr-1'>
<div
className='flex items-center mb-0.5 system-xs-medium text-text-secondary truncate'
title={file.name}
>
<div className='truncate'>{name}</div>
</div>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
{
ext && (
<span>{ext.toLowerCase()}</span>
)
}
{
ext && (
<span className='mx-1 system-2xs-medium'></span>
)
}
{
!!file.size && (
<span>{formatFileSize(file.size)}</span>
)
}
</div>
</div>
<div className='shrink-0 flex items-center'>
{ {
ext && ( progress >= 0 && !fileIsUploaded(file) && (
<span>{ext.toLowerCase()}</span> <ProgressCircle
className='mr-2.5'
percentage={progress}
/>
) )
} }
{ {
ext && ( progress === -1 && (
<span className='mx-1 system-2xs-medium'></span> <ActionButton
className='mr-1'
onClick={() => onReUpload?.(id)}
>
<ReplayLine className='w-4 h-4 text-text-tertiary' />
</ActionButton>
) )
} }
{ {
!!file.size && ( showDeleteAction && (
<span>{formatFileSize(file.size)}</span> <ActionButton onClick={() => onRemove?.(id)}>
<RiDeleteBinLine className='w-4 h-4' />
</ActionButton>
)
}
{
canPreview && isImageFile && (
<ActionButton className='mr-1' onClick={() => setImagePreviewUrl(url || '')}>
<RiEyeLine className='w-4 h-4' />
</ActionButton>
)
}
{
showDownloadAction && (
<ActionButton onClick={(e) => {
e.stopPropagation()
downloadFile(url || base64Url || '', name)
}}>
<RiDownloadLine className='w-4 h-4' />
</ActionButton>
) )
} }
</div> </div>
</div> </div>
<div className='shrink-0 flex items-center'> {
{ imagePreviewUrl && canPreview && (
progress >= 0 && !fileIsUploaded(file) && ( <ImagePreview
<ProgressCircle title={name}
className='mr-2.5' url={imagePreviewUrl}
percentage={progress} onCancel={() => setImagePreviewUrl('')}
/> />
) )
} }
{ </>
progress === -1 && (
<ActionButton
className='mr-1'
onClick={() => onReUpload?.(id)}
>
<ReplayLine className='w-4 h-4 text-text-tertiary' />
</ActionButton>
)
}
{
showDeleteAction && (
<ActionButton onClick={() => onRemove?.(id)}>
<RiDeleteBinLine className='w-4 h-4' />
</ActionButton>
)
}
{
showDownloadAction && (
<ActionButton
size='xs'
>
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
)
}
</div>
</div>
) )
} }

View File

@ -31,7 +31,7 @@ const FileItem = ({
onRemove, onRemove,
onReUpload, onReUpload,
}: FileItemProps) => { }: 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 ext = getFileExtension(name, type, isRemote)
const uploadError = progress === -1 const uploadError = progress === -1
@ -86,7 +86,7 @@ const FileItem = ({
className='hidden group-hover/file-item:flex absolute -right-1 -top-1' className='hidden group-hover/file-item:flex absolute -right-1 -top-1'
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
downloadFile(url || '', name) downloadFile(url || base64Url || '', name)
}} }}
> >
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' /> <RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />

View File

@ -1,5 +1,4 @@
import mime from 'mime' import mime from 'mime'
import { flatten } from 'lodash-es'
import { FileAppearanceTypeEnum } from './types' import { FileAppearanceTypeEnum } from './types'
import type { FileEntity } from './types' import type { FileEntity } from './types'
import { upload } from '@/service/base' import { upload } from '@/service/base'
@ -158,12 +157,22 @@ export const isAllowedFileExtension = (fileName: string, fileMimetype: string, a
} }
export const getFilesInLogs = (rawData: any) => { export const getFilesInLogs = (rawData: any) => {
const originalFiles = flatten(Object.keys(rawData || {}).map((key) => { const result = Object.keys(rawData || {}).map((key) => {
if (typeof rawData[key] === 'object' || Array.isArray(rawData[key])) if (typeof rawData[key] === 'object' && rawData[key].dify_model_identity === '__dify__file__') {
return rawData[key] 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 return undefined
}).filter(Boolean)).filter(item => item?.model_identity === '__dify__file__') }).filter(Boolean)
return getProcessedFilesFromResponse(originalFiles) return result
} }
export const fileIsUploaded = (file: FileEntity) => { export const fileIsUploaded = (file: FileEntity) => {

View File

@ -21,7 +21,7 @@ import { sleep } from '@/utils'
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import { TEXT_GENERATION_TIMEOUT_MS } from '@/config' import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
import { import {
getProcessedFilesFromResponse, getFilesInLogs,
} from '@/app/components/base/file-uploader/utils' } from '@/app/components/base/file-uploader/utils'
export type IResultProps = { export type IResultProps = {
@ -288,7 +288,7 @@ const Result: FC<IResultProps> = ({
} }
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => { setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.status = WorkflowRunningStatus.Succeeded draft.status = WorkflowRunningStatus.Succeeded
draft.files = getProcessedFilesFromResponse(data.files || []) draft.files = getFilesInLogs(data.outputs || []) as any[]
})) }))
if (!data.outputs) { if (!data.outputs) {
setCompletionRes('') setCompletionRes('')

View File

@ -26,7 +26,7 @@ import {
import { useFeaturesStore } from '@/app/components/base/features/hooks' import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
import { import {
getProcessedFilesFromResponse, getFilesInLogs,
} from '@/app/components/base/file-uploader/utils' } from '@/app/components/base/file-uploader/utils'
export const useWorkflowRun = () => { export const useWorkflowRun = () => {
@ -213,7 +213,7 @@ export const useWorkflowRun = () => {
draft.result = { draft.result = {
...draft.result, ...draft.result,
...data, ...data,
files: getProcessedFilesFromResponse(data.files || []), files: getFilesInLogs(data.outputs),
} as any } as any
if (isStringOutput) { if (isStringOutput) {
draft.resultTabActive = true draft.resultTabActive = true

View File

@ -27,7 +27,10 @@ type Props = {
isInNode?: boolean isInNode?: boolean
onGenerated?: (prompt: string) => void onGenerated?: (prompt: string) => void
codeLanguages?: CodeLanguage codeLanguages?: CodeLanguage
fileList?: FileEntity[] fileList?: {
varName: string
list: FileEntity[]
}[]
showFileList?: boolean showFileList?: boolean
showCodeGenerator?: boolean showCodeGenerator?: boolean
} }

View File

@ -208,7 +208,7 @@ const CodeEditor: FC<Props> = ({
isInNode={isInNode} isInNode={isInNode}
onGenerated={onGenerated} onGenerated={onGenerated}
codeLanguages={language} codeLanguages={language}
fileList={fileList} fileList={fileList as any}
showFileList={showFileList} showFileList={showFileList}
showCodeGenerator={showCodeGenerator} showCodeGenerator={showCodeGenerator}
> >

View File

@ -48,7 +48,7 @@ const WorkflowPreview = () => {
}, [showDebugAndPreviewPanel, showInputsPanel]) }, [showDebugAndPreviewPanel, showInputsPanel])
useEffect(() => { 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') switchTab('DETAIL')
}, [workflowRunningData]) }, [workflowRunningData])

View File

@ -1,10 +1,13 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useMemo } from 'react'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' 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 StatusContainer from '@/app/components/workflow/run/status-container'
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
type OutputPanelProps = { type OutputPanelProps = {
isRunning?: boolean isRunning?: boolean
@ -19,6 +22,30 @@ const OutputPanel: FC<OutputPanelProps> = ({
error, error,
height, 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 ( return (
<div className='py-2'> <div className='py-2'>
{isRunning && ( {isRunning && (
@ -36,20 +63,31 @@ const OutputPanel: FC<OutputPanelProps> = ({
<Markdown content='No Output' /> <Markdown content='No Output' />
</div> </div>
)} )}
{outputs && Object.keys(outputs).length === 1 && ( {isTextOutput && (
<div className='px-4 py-2'> <div className='px-4 py-2'>
<Markdown content={outputs[Object.keys(outputs)[0]] || ''} /> <Markdown content={outputs[Object.keys(outputs)[0]] || ''} />
</div> </div>
)} )}
{fileList.length > 0 && (
<div className='px-4 py-2'>
<FileList
files={fileList}
showDeleteAction={false}
showDownloadAction
canPreview
/>
</div>
)}
{outputs && Object.keys(outputs).length > 1 && height! > 0 && ( {outputs && Object.keys(outputs).length > 1 && height! > 0 && (
<div className='px-4 py-2 flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<CodeEditor <CodeEditor
showFileList
readOnly readOnly
title={<div></div>} title={<div></div>}
language={CodeLanguage.json} language={CodeLanguage.json}
value={outputs} value={outputs}
isJSONStringifyBeauty isJSONStringifyBeauty
height={height} height={height ? (height - 16) / 2 : undefined}
/> />
</div> </div>
)} )}

View File

@ -6,14 +6,13 @@ import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim' import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
import StatusContainer from '@/app/components/workflow/run/status-container' import StatusContainer from '@/app/components/workflow/run/status-container'
import { FileList } from '@/app/components/base/file-uploader' import { FileList } from '@/app/components/base/file-uploader'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
type ResultTextProps = { type ResultTextProps = {
isRunning?: boolean isRunning?: boolean
outputs?: any outputs?: any
error?: string error?: string
onClick?: () => void onClick?: () => void
allFiles?: FileEntity[] allFiles?: any[]
} }
const ResultText: FC<ResultTextProps> = ({ const ResultText: FC<ResultTextProps> = ({
@ -25,20 +24,20 @@ const ResultText: FC<ResultTextProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='bg-background-section-burn py-2'> <div className='bg-background-section-burn'>
{isRunning && !outputs && ( {isRunning && !outputs && (
<div className='pt-4 pl-[26px]'> <div className='pt-4 pl-[26px]'>
<LoadingAnim type='text' /> <LoadingAnim type='text' />
</div> </div>
)} )}
{!isRunning && error && ( {!isRunning && error && (
<div className='px-4'> <div className='px-4 py-2'>
<StatusContainer status='failed'> <StatusContainer status='failed'>
{error} {error}
</StatusContainer> </StatusContainer>
</div> </div>
)} )}
{!isRunning && !outputs && !error && ( {!isRunning && !outputs && !error && !allFiles?.length && (
<div className='mt-[120px] px-4 py-2 flex flex-col items-center text-[13px] leading-[18px] text-gray-500'> <div className='mt-[120px] px-4 py-2 flex flex-col items-center text-[13px] leading-[18px] text-gray-500'>
<ImageIndentLeft className='w-6 h-6 text-gray-400' /> <ImageIndentLeft className='w-6 h-6 text-gray-400' />
<div className='mr-2'>{t('runLog.resultEmpty.title')}</div> <div className='mr-2'>{t('runLog.resultEmpty.title')}</div>
@ -49,18 +48,25 @@ const ResultText: FC<ResultTextProps> = ({
</div> </div>
</div> </div>
)} )}
{outputs && ( {(outputs || !!allFiles?.length) && (
<div className='px-4 py-2'> <>
<Markdown content={outputs} /> {outputs && (
{!!allFiles?.length && ( <div className='px-4 py-2'>
<FileList <Markdown content={outputs} />
files={allFiles} </div>
showDeleteAction={false}
showDownloadAction
canPreview
/>
)} )}
</div> {!!allFiles?.length && allFiles.map(item => (
<div key={item.varName} className='px-4 py-2 flex flex-col gap-1 system-xs-regular'>
<div className='py-1 text-text-tertiary '>{item.varName}</div>
<FileList
files={item.list}
showDeleteAction={false}
showDownloadAction
canPreview
/>
</div>
))}
</>
)} )}
</div> </div>
) )

View File

@ -79,6 +79,8 @@ const translation = {
runDetail: { runDetail: {
title: 'Conversation Log', title: 'Conversation Log',
workflowTitle: 'Log Detail', workflowTitle: 'Log Detail',
fileListLabel: 'File Details',
fileListDetail: 'Detail',
}, },
promptLog: 'Prompt Log', promptLog: 'Prompt Log',
agentLog: 'Agent Log', agentLog: 'Agent Log',

View File

@ -79,6 +79,8 @@ const translation = {
runDetail: { runDetail: {
title: '对话日志', title: '对话日志',
workflowTitle: '日志详情', workflowTitle: '日志详情',
fileListLabel: '文件详情',
fileListDetail: '详情',
}, },
promptLog: 'Prompt 日志', promptLog: 'Prompt 日志',
agentLog: 'Agent 日志', agentLog: 'Agent 日志',