file upload

This commit is contained in:
JzoNg 2024-08-29 20:18:51 +08:00
parent d69b453729
commit b3529d3ccc
9 changed files with 320 additions and 40 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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} />
)}

View File

@ -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',

View File

@ -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,

View File

@ -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,6 +85,7 @@ const FileUploadSetting: FC<Props> = ({
return (
<div>
{!inFeaturePanel && (
<Field
title={t('appDebug.variableConig.file.supportFileTypes')}
>
@ -106,6 +109,7 @@ const FileUploadSetting: FC<Props> = ({
/>
</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>
)

View File

@ -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',

View File

@ -199,6 +199,13 @@ const translation = {
},
},
},
fileUpload: {
title: '文件上传',
description: '聊天输入框支持上传文件。类型包括图片、文档以及其它类型',
supportedTypes: '支持的文件类型',
numberLimit: '最大上传数',
modalTitle: '文件上传设置',
},
},
generate: {
title: '提示词生成器',