mirror of https://github.com/langgenius/dify.git
chat upload file
This commit is contained in:
parent
25949338cb
commit
788550affa
|
|
@ -0,0 +1,59 @@
|
|||
'use client'
|
||||
import produce from 'immer'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OnFeaturesChange } from '../../types'
|
||||
import {
|
||||
useFeatures,
|
||||
useFeaturesStore,
|
||||
} from '../../hooks'
|
||||
import ParamConfig from './param-config'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
|
||||
type FileUploadProps = {
|
||||
onChange?: OnFeaturesChange
|
||||
}
|
||||
const FileUpload = ({
|
||||
onChange,
|
||||
}: FileUploadProps) => {
|
||||
const { t } = useTranslation()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
|
||||
const handleSwitch = useCallback((value: boolean) => {
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.file.image.enabled = value
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
}, [featuresStore, onChange])
|
||||
|
||||
return (
|
||||
<div className='flex items-center px-3 h-12 bg-gray-50 rounded-xl overflow-hidden'>
|
||||
<div className='shrink-0 flex items-center justify-center mr-1 w-6 h-6'>
|
||||
<FileSearch02 className='shrink-0 w-4 h-4 text-[#039855]' />
|
||||
</div>
|
||||
<div className='shrink-0 mr-2 whitespace-nowrap text-sm text-gray-800 font-semibold'>
|
||||
{t('common.imageUploader.imageUpload')}
|
||||
</div>
|
||||
<div className='grow' />
|
||||
<div className='flex items-center'>
|
||||
<ParamConfig onChange={onChange} />
|
||||
<div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Switch
|
||||
defaultValue={file.image.enabled}
|
||||
onChange={handleSwitch}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FileUpload)
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
'use client'
|
||||
|
||||
import produce from 'immer'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OnFeaturesChange } from '../../types'
|
||||
import {
|
||||
useFeatures,
|
||||
useFeaturesStore,
|
||||
} from '../../hooks'
|
||||
import RadioGroup from './radio-group'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import ParamItem from '@/app/components/base/param-item'
|
||||
|
||||
const MIN = 1
|
||||
const MAX = 6
|
||||
type ParamConfigContentProps = {
|
||||
onChange?: OnFeaturesChange
|
||||
}
|
||||
const ParamConfigContent = ({
|
||||
onChange,
|
||||
}: ParamConfigContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const file = useFeatures(s => s.features.file)
|
||||
|
||||
const transferMethod = useMemo(() => {
|
||||
if (!file.image.transfer_methods || file.image.transfer_methods.length === 2)
|
||||
return TransferMethod.all
|
||||
|
||||
return file.image.transfer_methods[0]
|
||||
}, [file.image.transfer_methods])
|
||||
|
||||
const handleTransferMethodsChange = useCallback((value: TransferMethod) => {
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (TransferMethod.all)
|
||||
draft.file.image.transfer_methods = [TransferMethod.remote_url, TransferMethod.local_file]
|
||||
else
|
||||
draft.file.image.transfer_methods = [value]
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
}, [featuresStore, onChange])
|
||||
|
||||
const handleLimitsChange = useCallback((_key: string, value: number) => {
|
||||
if (!value)
|
||||
return
|
||||
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.file.image.number_limits = value
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
}, [featuresStore, onChange])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
|
||||
<div className='pt-3 space-y-6'>
|
||||
<div>
|
||||
<div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
|
||||
<RadioGroup
|
||||
className='space-x-3'
|
||||
options={[
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.both'),
|
||||
value: TransferMethod.all,
|
||||
},
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.localUpload'),
|
||||
value: TransferMethod.local_file,
|
||||
},
|
||||
{
|
||||
label: t('appDebug.vision.visionSettings.url'),
|
||||
value: TransferMethod.remote_url,
|
||||
},
|
||||
]}
|
||||
value={transferMethod}
|
||||
onChange={handleTransferMethodsChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ParamItem
|
||||
id='upload_limit'
|
||||
className=''
|
||||
name={t('appDebug.vision.visionSettings.uploadLimit')}
|
||||
noTooltip
|
||||
{...{
|
||||
default: 2,
|
||||
step: 1,
|
||||
min: MIN,
|
||||
max: MAX,
|
||||
}}
|
||||
value={file.image.number_limits || 3}
|
||||
enable={true}
|
||||
onChange={handleLimitsChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ParamConfigContent)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
'use client'
|
||||
|
||||
import { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { OnFeaturesChange } from '../../types'
|
||||
import ParamConfigContent from './param-config-content'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type ParamsConfigProps = {
|
||||
onChange?: OnFeaturesChange
|
||||
}
|
||||
const ParamsConfig = ({
|
||||
onChange,
|
||||
}: ParamsConfigProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
|
||||
<Settings01 className='w-3.5 h-3.5 ' />
|
||||
<div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent style={{ zIndex: 50 }}>
|
||||
<div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
|
||||
<ParamConfigContent onChange={onChange} />
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
export default memo(ParamsConfig)
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
type OPTION = {
|
||||
label: string
|
||||
value: any
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
options: OPTION[]
|
||||
value: any
|
||||
onChange: (value: any) => void
|
||||
}
|
||||
|
||||
const RadioGroup: FC<Props> = ({
|
||||
className = '',
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, 'flex')}>
|
||||
{options.map(item => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(s.item, item.value === value && s.checked)}
|
||||
onClick={() => onChange(item.value)}
|
||||
>
|
||||
<div className={s.radio}></div>
|
||||
<div className='text-[13px] font-medium text-gray-900'>{item.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(RadioGroup)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.item {
|
||||
@apply grow flex items-center h-8 px-2.5 rounded-lg bg-gray-25 border border-gray-100 cursor-pointer space-x-2;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: #ffffff;
|
||||
border-color: #B2CCFF;
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
|
||||
.item.checked {
|
||||
background-color: #ffffff;
|
||||
border-color: #528BFF;
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10);
|
||||
}
|
||||
|
||||
.radio {
|
||||
@apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
|
||||
}
|
||||
|
||||
.item.checked .radio {
|
||||
border-width: 5px;
|
||||
border-color: #155eef;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import {
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import type { OnFeaturesChange } from '../types'
|
||||
import { useFeatures } from '../hooks'
|
||||
import FileUpload from './file-upload'
|
||||
import OpeningStatement from './opening-statement'
|
||||
import type { OpeningStatementProps } from './opening-statement'
|
||||
import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
|
||||
|
|
@ -34,6 +35,7 @@ const FeaturePanel = ({
|
|||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<FileUpload onChange={onChange} />
|
||||
{
|
||||
showAdvanceFeature && (
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
|
|||
moderation: {
|
||||
enabled: false,
|
||||
},
|
||||
file: {
|
||||
image: {
|
||||
enabled: false,
|
||||
number_limits: 3,
|
||||
transfer_methods: ['local_file', 'remote_url'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return createStore<FeatureStoreState>()(set => ({
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ export type SensitiveWordAvoidance = EnabledOrDisabled & {
|
|||
config?: any
|
||||
}
|
||||
|
||||
export type FileUpload = {
|
||||
image: EnabledOrDisabled & {
|
||||
number_limits: number
|
||||
transfer_methods: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export enum FeatureEnum {
|
||||
opening = 'opening',
|
||||
suggested = 'suggested',
|
||||
|
|
@ -30,6 +37,7 @@ export enum FeatureEnum {
|
|||
speech2text = 'speech2text',
|
||||
citation = 'citation',
|
||||
moderation = 'moderation',
|
||||
file = 'file',
|
||||
}
|
||||
|
||||
export type Features = {
|
||||
|
|
@ -39,6 +47,7 @@ export type Features = {
|
|||
[FeatureEnum.speech2text]: SpeechToText
|
||||
[FeatureEnum.citation]: RetrieverResource
|
||||
[FeatureEnum.moderation]: SensitiveWordAvoidance
|
||||
[FeatureEnum.file]: FileUpload
|
||||
}
|
||||
|
||||
export type OnFeaturesChange = (features: Features) => void
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import {
|
|||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from './store'
|
||||
import { useNodesSyncDraft } from './hooks'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesSyncDraft,
|
||||
} from './hooks'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
FeaturesChoose,
|
||||
|
|
@ -13,6 +16,7 @@ import {
|
|||
|
||||
const Features = () => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
const setShowFeaturesPanel = useStore(state => state.setShowFeaturesPanel)
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
|
|
@ -24,16 +28,20 @@ const Features = () => {
|
|||
<div className='fixed top-16 left-2 bottom-2 w-[600px] rounded-2xl border-[0.5px] border-gray-200 bg-white shadow-xl z-10'>
|
||||
<div className='flex items-center justify-between px-4 pt-3'>
|
||||
{t('workflow.common.features')}
|
||||
<div className='flex items-center'>
|
||||
<FeaturesChoose onChange={handleFeaturesChange} />
|
||||
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => setShowFeaturesPanel(false)}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isChatMode && (
|
||||
<div className='flex items-center'>
|
||||
<FeaturesChoose onChange={handleFeaturesChange} />
|
||||
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => setShowFeaturesPanel(false)}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<FeaturesPanel
|
||||
|
|
|
|||
|
|
@ -8,10 +8,7 @@ import {
|
|||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import { useWorkflowRun } from '../hooks'
|
||||
import RunAndHistory from './run-and-history'
|
||||
import EditingTitle from './editing-title'
|
||||
import RunningTitle from './running-title'
|
||||
|
|
@ -26,7 +23,6 @@ const Header: FC = () => {
|
|||
const workflowStore = useWorkflowStore()
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
|
||||
const isChatMode = useIsChatMode()
|
||||
const runningStatus = useStore(s => s.runningStatus)
|
||||
const { handleRunSetting } = useWorkflowRun()
|
||||
|
||||
|
|
@ -78,21 +74,17 @@ const Header: FC = () => {
|
|||
}
|
||||
<RunAndHistory />
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
{
|
||||
isChatMode && (
|
||||
<Button
|
||||
className={`
|
||||
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
|
||||
border-[0.5px] border-gray-200 shadow-xs
|
||||
${runningStatus && '!cursor-not-allowed opacity-50'}
|
||||
`}
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<Grid01 className='mr-1 w-4 h-4 text-gray-500' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
className={`
|
||||
mr-2 px-3 py-0 h-8 bg-white text-[13px] font-medium text-gray-700
|
||||
border-[0.5px] border-gray-200 shadow-xs
|
||||
${runningStatus && '!cursor-not-allowed opacity-50'}
|
||||
`}
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<Grid01 className='mr-1 w-4 h-4 text-gray-500' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
<Publish />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export const useNodesSyncDraft = () => {
|
|||
speech_to_text: features.speech2text,
|
||||
retriever_resource: features.citation,
|
||||
sensitive_word_avoidance: features.moderation,
|
||||
file_upload: features.file,
|
||||
},
|
||||
},
|
||||
}).then((res) => {
|
||||
|
|
|
|||
|
|
@ -183,6 +183,13 @@ const WorkflowWrap = memo(() => {
|
|||
|
||||
const features = data.features || {}
|
||||
const initialFeatures: FeaturesData = {
|
||||
file: {
|
||||
image: {
|
||||
enabled: !!features.file_upload.image.enabled,
|
||||
number_limits: features.file_upload.image.number_limits || 3,
|
||||
transfer_methods: features.file_upload.image.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
},
|
||||
opening: {
|
||||
enabled: !!features.opening_statement,
|
||||
opening_statement: features.opening_statement,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const ChatWrapper = forwardRef<ChatWrapperRefType>((_, ref) => {
|
|||
speech_to_text: features.speech2text,
|
||||
retriever_resource: features.citation,
|
||||
sensitive_word_avoidance: features.moderation,
|
||||
file_upload: features.file,
|
||||
}
|
||||
}, [features])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue