mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-18 21:26:02 +08:00
file upload
This commit is contained in:
parent
d69b453729
commit
b3529d3ccc
@ -0,0 +1,102 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
import { RiEqualizer2Line } from '@remixicon/react'
|
||||
import { FolderUpload } from '@/app/components/base/icons/src/vender/features'
|
||||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import SettingModal from '@/app/components/base/features/new-feature-panel/file-upload/setting-modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import { FeatureEnum } from '@/app/components/base/features/types'
|
||||
|
||||
type Props = {
|
||||
onChange?: OnFeaturesChange
|
||||
}
|
||||
|
||||
const FileUpload = ({
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
|
||||
const supportedTypes = useMemo(() => {
|
||||
return file?.allowed_file_types?.join(',') || '-'
|
||||
}, [file?.allowed_file_types])
|
||||
|
||||
const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft[type] = {
|
||||
...draft[type],
|
||||
enabled,
|
||||
image: { enabled },
|
||||
}
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
}, [featuresStore, onChange])
|
||||
|
||||
return (
|
||||
<FeatureCard
|
||||
icon={
|
||||
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-blue-600'>
|
||||
<FolderUpload className='w-4 h-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
}
|
||||
title={t('appDebug.feature.fileUpload.title')}
|
||||
value={file?.enabled}
|
||||
onChange={state => handleChange(FeatureEnum.file, state)}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<>
|
||||
{!file?.enabled && (
|
||||
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.fileUpload.description')}</div>
|
||||
)}
|
||||
{file?.enabled && (
|
||||
<>
|
||||
{!isHovering && !modalOpen && (
|
||||
<div className='pt-0.5 flex items-center gap-4'>
|
||||
<div className=''>
|
||||
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.fileUpload.supportedTypes')}</div>
|
||||
<div className='text-text-secondary system-xs-regular'>{supportedTypes}</div>
|
||||
</div>
|
||||
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
|
||||
<div className=''>
|
||||
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.fileUpload.numberLimit')}</div>
|
||||
<div className='text-text-secondary system-xs-regular'>{file?.number_limits}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(isHovering || modalOpen) && (
|
||||
<SettingModal
|
||||
open={modalOpen}
|
||||
onOpen={(v) => {
|
||||
setModalOpen(v)
|
||||
setIsHovering(v)
|
||||
}}
|
||||
onChange={onChange}
|
||||
>
|
||||
<Button className='w-full'>
|
||||
<RiEqualizer2Line className='mr-1 w-4 h-4' />
|
||||
{t('common.operation.settings')}
|
||||
</Button>
|
||||
</SettingModal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</FeatureCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileUpload
|
@ -0,0 +1,86 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
import type { UploadFileSetting } from '@/app/components/workflow/types'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
|
||||
type SettingContentProps = {
|
||||
onClose: () => void
|
||||
onChange?: OnFeaturesChange
|
||||
}
|
||||
const SettingContent = ({
|
||||
onClose,
|
||||
onChange,
|
||||
}: SettingContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const file = useFeatures(state => state.features.file)
|
||||
const fileSettingPayload = useMemo(() => {
|
||||
return {
|
||||
allowed_file_upload_methods: file?.allowed_file_upload_methods || ['local_file', 'remote_url'],
|
||||
allowed_file_types: file?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: file?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image],
|
||||
max_length: file?.number_limits || 3,
|
||||
} as UploadFileSetting
|
||||
}, [file])
|
||||
const [tempPayload, setTempPayload] = useState<UploadFileSetting>(fileSettingPayload)
|
||||
|
||||
const handleChange = useCallback(() => {
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.file = {
|
||||
...draft.file,
|
||||
allowed_file_upload_methods: tempPayload.allowed_file_upload_methods,
|
||||
number_limits: tempPayload.max_length,
|
||||
allowed_file_types: tempPayload.allowed_file_types,
|
||||
allowed_file_extensions: tempPayload.allowed_file_extensions,
|
||||
}
|
||||
})
|
||||
|
||||
setFeatures(newFeatures)
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
}, [featuresStore, onChange, tempPayload])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='mb-4 flex items-center justify-between'>
|
||||
<div className='text-text-primary system-xl-semibold'>{t('appDebug.feature.fileUpload.modalTitle')}</div>
|
||||
<div className='p-1 cursor-pointer' onClick={onClose}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
|
||||
</div>
|
||||
<FileUploadSetting
|
||||
isMultiple
|
||||
inFeaturePanel
|
||||
payload={tempPayload}
|
||||
onChange={(p: UploadFileSetting) => setTempPayload(p)}
|
||||
/>
|
||||
<div className='mt-4 flex items-center justify-end'>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
className='mr-2'
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleChange}
|
||||
disabled={tempPayload.allowed_file_types.length === 0}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SettingContent)
|
@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import SettingContent from '@/app/components/base/features/new-feature-panel/file-upload/setting-content'
|
||||
import type { OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
|
||||
type FileUploadSettingsProps = {
|
||||
open: boolean
|
||||
onOpen: (state: any) => void
|
||||
onChange?: OnFeaturesChange
|
||||
disabled?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
const FileUploadSettings = ({
|
||||
open,
|
||||
onOpen,
|
||||
onChange,
|
||||
disabled,
|
||||
children,
|
||||
}: FileUploadSettingsProps) => {
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={onOpen}
|
||||
placement='left'
|
||||
offset={{
|
||||
mainAxis: 32,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger className='flex' onClick={() => !disabled && onOpen((open: boolean) => !open)}>
|
||||
{children}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 50 }}>
|
||||
<div className='w-[360px] p-4 bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-2xl'>
|
||||
<SettingContent
|
||||
onClose={() => onOpen(false)}
|
||||
onChange={(v) => {
|
||||
onChange?.(v)
|
||||
onOpen(false)
|
||||
}} />
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default memo(FileUploadSettings)
|
@ -7,10 +7,7 @@ import {
|
||||
RiEqualizer2Line,
|
||||
RiExternalLinkLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
FolderUpload,
|
||||
MessageFast,
|
||||
} from '@/app/components/base/icons/src/vender/features'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/features'
|
||||
import DialogWrapper from '@/app/components/base/features/new-feature-panel/dialog-wrapper'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
@ -25,6 +22,7 @@ import ConversationOpener from '@/app/components/base/features/new-feature-panel
|
||||
import Moderation from '@/app/components/base/features/new-feature-panel/moderation'
|
||||
import SpeechToText from '@/app/components/base/features/new-feature-panel/speech-to-text'
|
||||
import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-to-speech'
|
||||
import FileUpload from '@/app/components/base/features/new-feature-panel/file-upload'
|
||||
import FollowUp from '@/app/components/base/features/new-feature-panel/follow-up'
|
||||
import Citation from '@/app/components/base/features/new-feature-panel/citation'
|
||||
|
||||
@ -100,18 +98,7 @@ const NewFeaturePanel = ({
|
||||
{text2speechDefaultModel && (
|
||||
<TextToSpeech onChange={onChange} />
|
||||
)}
|
||||
{/* file upload ##TODO## */}
|
||||
<div className='group mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-blue-600'>
|
||||
<FolderUpload className='w-4 h-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='grow flex items-center text-text-secondary system-sm-semibold'>
|
||||
File upload
|
||||
</div>
|
||||
<Switch className='shrink-0' onChange={value => handleChange(FeatureEnum.text2speech, value)} defaultValue={!!features.text2speech?.enabled} />
|
||||
</div>
|
||||
</div>
|
||||
<FileUpload onChange={onChange} />
|
||||
{isChatMode && (
|
||||
<FollowUp onChange={onChange} />
|
||||
)}
|
||||
|
@ -33,7 +33,11 @@ export type FileUpload = {
|
||||
number_limits?: number
|
||||
transfer_methods?: TransferMethod[]
|
||||
}
|
||||
}
|
||||
allowed_file_types?: string[]
|
||||
allowed_file_extensions?: string[]
|
||||
allowed_file_upload_methods?: string[]
|
||||
number_limits?: number
|
||||
} & EnabledOrDisabled
|
||||
|
||||
export enum FeatureEnum {
|
||||
moreLikeThis = 'moreLikeThis',
|
||||
|
@ -35,6 +35,7 @@ import type {
|
||||
} from './types'
|
||||
import {
|
||||
ControlMode,
|
||||
SupportUploadFileTypes,
|
||||
} from './types'
|
||||
import { WorkflowContextProvider } from './context'
|
||||
import {
|
||||
@ -88,6 +89,7 @@ import type { Features as FeaturesData } from '@/app/components/base/features/ty
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
|
||||
const nodeTypes = {
|
||||
[CUSTOM_NODE]: CustomNode,
|
||||
@ -404,6 +406,11 @@ const WorkflowWrap = memo(() => {
|
||||
number_limits: features.file_upload?.image.number_limits || 3,
|
||||
transfer_methods: features.file_upload?.image.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
|
||||
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: features.file_upload?.number_limits || features.file_upload?.image.number_limits || 3,
|
||||
},
|
||||
opening: {
|
||||
enabled: !!features.opening_statement,
|
||||
|
@ -14,12 +14,14 @@ import { TransferMethod } from '@/types/app'
|
||||
type Props = {
|
||||
payload: UploadFileSetting
|
||||
isMultiple: boolean
|
||||
inFeaturePanel?: boolean
|
||||
onChange: (payload: UploadFileSetting) => void
|
||||
}
|
||||
|
||||
const FileUploadSetting: FC<Props> = ({
|
||||
payload,
|
||||
isMultiple,
|
||||
inFeaturePanel = false,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -83,29 +85,31 @@ const FileUploadSetting: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Field
|
||||
title={t('appDebug.variableConig.file.supportFileTypes')}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.image, SupportUploadFileTypes.document, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
{!inFeaturePanel && (
|
||||
<Field
|
||||
title={t('appDebug.variableConig.file.supportFileTypes')}
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.image, SupportUploadFileTypes.document, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
<Field
|
||||
title='Upload File Types'
|
||||
className='mt-4'
|
||||
@ -144,6 +148,32 @@ const FileUploadSetting: FC<Props> = ({
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
{inFeaturePanel && (
|
||||
<Field
|
||||
title={t('appDebug.variableConig.file.supportFileTypes')}
|
||||
className='mt-4'
|
||||
>
|
||||
<div className='space-y-1'>
|
||||
{
|
||||
[SupportUploadFileTypes.image, SupportUploadFileTypes.document, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
|
@ -199,6 +199,13 @@ const translation = {
|
||||
},
|
||||
},
|
||||
},
|
||||
fileUpload: {
|
||||
title: 'File Upload',
|
||||
description: 'The chat input box allows uploading of images, documents, and other files.',
|
||||
supportedTypes: 'Support File Types',
|
||||
numberLimit: 'Max uploads',
|
||||
modalTitle: 'File Upload Setting',
|
||||
},
|
||||
},
|
||||
generate: {
|
||||
title: 'Prompt Generator',
|
||||
|
@ -199,6 +199,13 @@ const translation = {
|
||||
},
|
||||
},
|
||||
},
|
||||
fileUpload: {
|
||||
title: '文件上传',
|
||||
description: '聊天输入框支持上传文件。类型包括图片、文档以及其它类型',
|
||||
supportedTypes: '支持的文件类型',
|
||||
numberLimit: '最大上传数',
|
||||
modalTitle: '文件上传设置',
|
||||
},
|
||||
},
|
||||
generate: {
|
||||
title: '提示词生成器',
|
||||
|
Loading…
x
Reference in New Issue
Block a user