chat upload file

This commit is contained in:
StyleZhang 2024-03-18 14:49:17 +08:00
parent 25949338cb
commit 788550affa
13 changed files with 344 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 => ({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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