| </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')} |
| 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( | ||||
| <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 | |||||
| files={data?.files} | |||||
| showDeleteAction={false} | |||||
| showDownloadAction | |||||
| canPreview | |||||
| /> | |||||
| <div className='flex flex-col gap-2'> | |||||
| {data?.files.map((item: any) => ( | |||||
| <div key={item.varName} className='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> | |||||
| )} | )} | ||||
| </> | </> | ||||
| )} | )} |
| 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' | ||||
| 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 [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) | 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'> | |||||
| {fileList.map((file) => { | |||||
| <div className='flex gap-1'> | |||||
| {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 ( | ||||
| </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'> | |||||
| {fileList.map(file => ( | |||||
| <FileItem | |||||
| key={file.id} | |||||
| file={file} | |||||
| showDeleteAction={false} | |||||
| showDownloadAction | |||||
| /> | |||||
| <div className='flex flex-col gap-3'> | |||||
| {fileList.map(item => ( | |||||
| <div key={item.varName} className='flex flex-col gap-1 system-xs-regular'> | |||||
| <div className='py-1 text-text-tertiary '>{item.varName}</div> | |||||
| {item.list.map(file => ( | |||||
| <FileItem | |||||
| key={file.id} | |||||
| file={file} | |||||
| showDeleteAction={false} | |||||
| showDownloadAction | |||||
| canPreview | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| )} | )} |
| 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, | ||||
| 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 | ||||
| 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, | ||||
| 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', | |||||
| progress === -1 && 'bg-state-destructive-hover border-state-destructive-border', | |||||
| )}> | |||||
| <div className='flex items-center justify-center w-12 h-12'> | |||||
| { | |||||
| isImageFile && ( | |||||
| <FileImageRender | |||||
| className='w-8 h-8' | |||||
| imageUrl={base64Url || url || ''} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| !isImageFile && ( | |||||
| <FileTypeIcon | |||||
| 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 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', | |||||
| progress === -1 && 'bg-state-destructive-hover border-state-destructive-border', | |||||
| )}> | |||||
| <div className='flex items-center justify-center w-12 h-12'> | |||||
| { | |||||
| isImageFile && ( | |||||
| <FileImageRender | |||||
| className='w-8 h-8' | |||||
| imageUrl={base64Url || url || ''} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| !isImageFile && ( | |||||
| <FileTypeIcon | |||||
| type={getFileAppearanceType(name, type)} | |||||
| size='lg' | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </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'> | |||||
| { | |||||
| progress >= 0 && !fileIsUploaded(file) && ( | |||||
| <ProgressCircle | |||||
| className='mr-2.5' | |||||
| percentage={progress} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | { | ||||
| ext && ( | |||||
| <span>{ext.toLowerCase()}</span> | |||||
| progress === -1 && ( | |||||
| <ActionButton | |||||
| className='mr-1' | |||||
| onClick={() => onReUpload?.(id)} | |||||
| > | |||||
| <ReplayLine className='w-4 h-4 text-text-tertiary' /> | |||||
| </ActionButton> | |||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| ext && ( | |||||
| <span className='mx-1 system-2xs-medium'>•</span> | |||||
| showDeleteAction && ( | |||||
| <ActionButton onClick={() => onRemove?.(id)}> | |||||
| <RiDeleteBinLine className='w-4 h-4' /> | |||||
| </ActionButton> | |||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| !!file.size && ( | |||||
| <span>{formatFileSize(file.size)}</span> | |||||
| 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'> | |||||
| { | |||||
| progress >= 0 && !fileIsUploaded(file) && ( | |||||
| <ProgressCircle | |||||
| className='mr-2.5' | |||||
| percentage={progress} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| 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> | |||||
| { | |||||
| imagePreviewUrl && canPreview && ( | |||||
| <ImagePreview | |||||
| title={name} | |||||
| url={imagePreviewUrl} | |||||
| onCancel={() => setImagePreviewUrl('')} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </> | |||||
| ) | ) | ||||
| } | } | ||||
| 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 | ||||
| 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' /> |
| 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' | ||||
| } | } | ||||
| export const getFilesInLogs = (rawData: any) => { | 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 | return undefined | ||||
| }).filter(Boolean)).filter(item => item?.model_identity === '__dify__file__') | |||||
| return getProcessedFilesFromResponse(originalFiles) | |||||
| }).filter(Boolean) | |||||
| return result | |||||
| } | } | ||||
| export const fileIsUploaded = (file: FileEntity) => { | export const fileIsUploaded = (file: FileEntity) => { |
| 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 = { | ||||
| } | } | ||||
| 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('') |
| 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 = () => { | ||||
| 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 |
| 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 | ||||
| } | } |
| 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} | ||||
| > | > |
| }, [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]) | ||||
| '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 | ||||
| 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 && ( | ||||
| <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> | ||||
| )} | )} |
| 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> = ({ | ||||
| }) => { | }) => { | ||||
| 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> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {outputs && ( | |||||
| <div className='px-4 py-2'> | |||||
| <Markdown content={outputs} /> | |||||
| {!!allFiles?.length && ( | |||||
| <FileList | |||||
| files={allFiles} | |||||
| showDeleteAction={false} | |||||
| showDownloadAction | |||||
| canPreview | |||||
| /> | |||||
| {(outputs || !!allFiles?.length) && ( | |||||
| <> | |||||
| {outputs && ( | |||||
| <div className='px-4 py-2'> | |||||
| <Markdown content={outputs} /> | |||||
| </div> | |||||
| )} | )} | ||||
| </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> | ||||
| ) | ) |
| 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', |
| runDetail: { | runDetail: { | ||||
| title: '对话日志', | title: '对话日志', | ||||
| workflowTitle: '日志详情', | workflowTitle: '日志详情', | ||||
| fileListLabel: '文件详情', | |||||
| fileListDetail: '详情', | |||||
| }, | }, | ||||
| promptLog: 'Prompt 日志', | promptLog: 'Prompt 日志', | ||||
| agentLog: 'Agent 日志', | agentLog: 'Agent 日志', |