mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-13 16:59:05 +08:00
Fix: support file download in workflow result (#11338)
This commit is contained in:
parent
a5d6082418
commit
0b25c0b677
@ -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')}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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' />
|
||||||
|
@ -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) => {
|
||||||
|
@ -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('')
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
>
|
>
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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',
|
||||||
|
@ -79,6 +79,8 @@ const translation = {
|
|||||||
runDetail: {
|
runDetail: {
|
||||||
title: '对话日志',
|
title: '对话日志',
|
||||||
workflowTitle: '日志详情',
|
workflowTitle: '日志详情',
|
||||||
|
fileListLabel: '文件详情',
|
||||||
|
fileListDetail: '详情',
|
||||||
},
|
},
|
||||||
promptLog: 'Prompt 日志',
|
promptLog: 'Prompt 日志',
|
||||||
agentLog: 'Agent 日志',
|
agentLog: 'Agent 日志',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user