diff --git a/web/app/components/base/features/feature-panel/file-upload/index.tsx b/web/app/components/base/features/feature-panel/file-upload/index.tsx
new file mode 100644
index 0000000000..d002f0005d
--- /dev/null
+++ b/web/app/components/base/features/feature-panel/file-upload/index.tsx
@@ -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 (
+
+
+
+
+
+ {t('common.imageUploader.imageUpload')}
+
+
+
+
+ )
+}
+export default React.memo(FileUpload)
diff --git a/web/app/components/base/features/feature-panel/file-upload/param-config-content.tsx b/web/app/components/base/features/feature-panel/file-upload/param-config-content.tsx
new file mode 100644
index 0000000000..1a2184c6b4
--- /dev/null
+++ b/web/app/components/base/features/feature-panel/file-upload/param-config-content.tsx
@@ -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 (
+
+
+
{t('appDebug.vision.visionSettings.title')}
+
+
+
{t('appDebug.vision.visionSettings.uploadMethod')}
+
+
+
+
+
+
+ )
+}
+
+export default React.memo(ParamConfigContent)
diff --git a/web/app/components/base/features/feature-panel/file-upload/param-config.tsx b/web/app/components/base/features/feature-panel/file-upload/param-config.tsx
new file mode 100644
index 0000000000..ed3a1b434c
--- /dev/null
+++ b/web/app/components/base/features/feature-panel/file-upload/param-config.tsx
@@ -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 (
+
+ setOpen(v => !v)}>
+
+
+
{t('appDebug.voice.settings')}
+
+
+
+
+
+
+ )
+}
+export default memo(ParamsConfig)
diff --git a/web/app/components/base/features/feature-panel/file-upload/radio-group/index.tsx b/web/app/components/base/features/feature-panel/file-upload/radio-group/index.tsx
new file mode 100644
index 0000000000..77e4d02184
--- /dev/null
+++ b/web/app/components/base/features/feature-panel/file-upload/radio-group/index.tsx
@@ -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 = ({
+ className = '',
+ options,
+ value,
+ onChange,
+}) => {
+ return (
+
+ {options.map(item => (
+
onChange(item.value)}
+ >
+
+
{item.label}
+
+ ))}
+
+ )
+}
+export default React.memo(RadioGroup)
diff --git a/web/app/components/base/features/feature-panel/file-upload/radio-group/style.module.css b/web/app/components/base/features/feature-panel/file-upload/radio-group/style.module.css
new file mode 100644
index 0000000000..22c29c6a42
--- /dev/null
+++ b/web/app/components/base/features/feature-panel/file-upload/radio-group/style.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/web/app/components/base/features/feature-panel/index.tsx b/web/app/components/base/features/feature-panel/index.tsx
index 6d21f76a56..cefeaac29c 100644
--- a/web/app/components/base/features/feature-panel/index.tsx
+++ b/web/app/components/base/features/feature-panel/index.tsx
@@ -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 (
+
{
showAdvanceFeature && (
diff --git a/web/app/components/base/features/store.ts b/web/app/components/base/features/store.ts
index 2e8517b6c0..bdb2f70db6 100644
--- a/web/app/components/base/features/store.ts
+++ b/web/app/components/base/features/store.ts
@@ -39,6 +39,13 @@ export const createFeaturesStore = (initProps?: Partial
) => {
moderation: {
enabled: false,
},
+ file: {
+ image: {
+ enabled: false,
+ number_limits: 3,
+ transfer_methods: ['local_file', 'remote_url'],
+ },
+ },
},
}
return createStore()(set => ({
diff --git a/web/app/components/base/features/types.ts b/web/app/components/base/features/types.ts
index 60f273fbb2..1912eb6609 100644
--- a/web/app/components/base/features/types.ts
+++ b/web/app/components/base/features/types.ts
@@ -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
diff --git a/web/app/components/workflow/features.tsx b/web/app/components/workflow/features.tsx
index 3da33f8c14..1cd9b5a48b 100644
--- a/web/app/components/workflow/features.tsx
+++ b/web/app/components/workflow/features.tsx
@@ -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 = () => {
{t('workflow.common.features')}
-
-
-
-
setShowFeaturesPanel(false)}
- >
-
-
-
+ {
+ isChatMode && (
+
+
+
+
setShowFeaturesPanel(false)}
+ >
+
+
+
+ )
+ }
{
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 = () => {
}
- {
- isChatMode && (
-
- )
- }
+
diff --git a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts
index 717942a029..dd7319532f 100644
--- a/web/app/components/workflow/hooks/use-nodes-sync-draft.ts
+++ b/web/app/components/workflow/hooks/use-nodes-sync-draft.ts
@@ -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) => {
diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx
index 56d4b272e8..280a410d4f 100644
--- a/web/app/components/workflow/index.tsx
+++ b/web/app/components/workflow/index.tsx
@@ -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,
diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
index 12113995d9..de87839001 100644
--- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
+++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx
@@ -35,6 +35,7 @@ const ChatWrapper = forwardRef((_, ref) => {
speech_to_text: features.speech2text,
retriever_resource: features.citation,
sensitive_word_avoidance: features.moderation,
+ file_upload: features.file,
}
}, [features])