diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx
new file mode 100644
index 0000000000..b84836cf55
--- /dev/null
+++ b/web/app/components/base/features/new-feature-panel/conversation-opener/index.tsx
@@ -0,0 +1,105 @@
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import produce from 'immer'
+import { RiEditLine } from '@remixicon/react'
+import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
+import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
+import Button from '@/app/components/base/button'
+import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
+import type { OnFeaturesChange, OpeningStatement } from '@/app/components/base/features/types'
+import { FeatureEnum } from '@/app/components/base/features/types'
+import { useModalContext } from '@/context/modal-context'
+
+type Props = {
+ disabled?: boolean
+ onChange?: OnFeaturesChange
+}
+
+const ConversationOpener = ({
+ disabled,
+ onChange,
+}: Props) => {
+ const { t } = useTranslation()
+ const { setShowOpeningModal } = useModalContext()
+ const opening = useFeatures(s => s.features.opening)
+ const featuresStore = useFeaturesStore()
+ const [isHovering, setIsHovering] = useState(false)
+ const handleOpenOpeningModal = useCallback(() => {
+ if (disabled)
+ return
+ const {
+ features,
+ setFeatures,
+ } = featuresStore!.getState()
+ setShowOpeningModal({
+ payload: opening as OpeningStatement,
+ onSaveCallback: (newOpening) => {
+ const newFeatures = produce(features, (draft) => {
+ draft.opening = newOpening
+ })
+ setFeatures(newFeatures)
+ if (onChange)
+ onChange(newFeatures)
+ },
+ onCancelCallback: () => {
+ if (onChange)
+ onChange(features)
+ },
+ })
+ }, [disabled, featuresStore, onChange, opening, setShowOpeningModal])
+
+ const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
+ const {
+ features,
+ setFeatures,
+ } = featuresStore!.getState()
+
+ const newFeatures = produce(features, (draft) => {
+ draft[type] = {
+ ...draft[type],
+ enabled,
+ }
+ })
+ setFeatures(newFeatures)
+ if (onChange)
+ onChange(newFeatures)
+ }, [featuresStore, onChange])
+
+ return (
+
+
+
+ }
+ title={t('appDebug.feature.conversationOpener.title')}
+ value={!!opening?.enabled}
+ onChange={state => handleChange(FeatureEnum.opening, state)}
+ onMouseEnter={() => setIsHovering(true)}
+ onMouseLeave={() => setIsHovering(false)}
+ >
+ <>
+ {!opening?.enabled && (
+ {t('appDebug.feature.conversationOpener.description')}
+ )}
+ {!!opening?.enabled && (
+ <>
+ {!isHovering && (
+
+ {opening.opening_statement || t('appDebug.openingStatement.placeholder')}
+
+ )}
+ {isHovering && (
+
+ )}
+ >
+ )}
+ >
+
+ )
+}
+
+export default ConversationOpener
diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx
new file mode 100644
index 0000000000..6706b3699b
--- /dev/null
+++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx
@@ -0,0 +1,149 @@
+import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import produce from 'immer'
+import { ReactSortable } from 'react-sortablejs'
+import { RiAddLine, RiAsterisk, RiCloseLine, RiDeleteBinLine, RiDraggable } from '@remixicon/react'
+import Modal from '@/app/components/base/modal'
+import Button from '@/app/components/base/button'
+import type { OpeningStatement } from '@/app/components/base/features/types'
+
+type OpeningSettingModalProps = {
+ data: OpeningStatement
+ onSave: (newState: OpeningStatement) => void
+ onCancel: () => void
+}
+
+const MAX_QUESTION_NUM = 5
+
+const OpeningSettingModal = ({
+ data,
+ onSave,
+ onCancel,
+}: OpeningSettingModalProps) => {
+ const { t } = useTranslation()
+ const [tempValue, setTempValue] = useState(data?.opening_statement || '')
+ useEffect(() => {
+ setTempValue(data.opening_statement || '')
+ }, [data.opening_statement])
+ const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(data.suggested_questions || [])
+
+ const handleSave = () => {
+ const newOpening = produce(data, (draft) => {
+ if (draft) {
+ draft.opening_statement = tempValue
+ draft.suggested_questions = tempSuggestedQuestions
+ }
+ })
+ onSave(newOpening)
+ }
+
+ const renderQuestions = () => {
+ return (
+
+
+
+
{t('appDebug.openingStatement.openingQuestion')}
+
ยท
+
{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}
+
+
+
+
{
+ return {
+ id: index,
+ name,
+ }
+ })}
+ setList={list => setTempSuggestedQuestions(list.map(item => item.name))}
+ handle='.handle'
+ ghostClass="opacity-50"
+ animation={150}
+ >
+ {tempSuggestedQuestions.map((question, index) => {
+ return (
+
+
+
{
+ const value = e.target.value
+ setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => {
+ if (index === i)
+ return value
+
+ return item
+ }))
+ }}
+ className={'w-full overflow-x-auto pl-1.5 pr-8 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer rounded-lg'}
+ />
+
+
{
+ setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i))
+ }}
+ >
+
+
+
+ )
+ })}
+ {tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
+
{ setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
+ className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100 hover:bg-gray-200'>
+
+
{t('appDebug.variableConig.addOption')}
+
+ )}
+
+ )
+ }
+
+ return (
+ { }}
+ className='!p-6 !mt-14 !max-w-none !w-[640px] !bg-components-panel-bg-blur'
+ >
+
+
{t('appDebug.feature.conversationOpener.title')}
+
+
+
+
+
+
+
+
+ )
+}
+
+export default OpeningSettingModal
diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx
index 165bd9e061..8b666ef363 100644
--- a/web/app/components/base/features/new-feature-panel/index.tsx
+++ b/web/app/components/base/features/new-feature-panel/index.tsx
@@ -21,6 +21,7 @@ import Switch from '@/app/components/base/switch'
import Button from '@/app/components/base/button'
import MoreLikeThis from '@/app/components/base/features/new-feature-panel/more-like-this'
+import ConversationOpener from '@/app/components/base/features/new-feature-panel/conversation-opener'
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'
@@ -89,6 +90,9 @@ const NewFeaturePanel = ({
{!isChatMode && (
)}
+ {isChatMode && (
+
+ )}
{isChatMode && speech2textDefaultModel && (
diff --git a/web/app/components/base/features/new-feature-panel/moderation/index.tsx b/web/app/components/base/features/new-feature-panel/moderation/index.tsx
index b9f7049136..172dba8d0f 100644
--- a/web/app/components/base/features/new-feature-panel/moderation/index.tsx
+++ b/web/app/components/base/features/new-feature-panel/moderation/index.tsx
@@ -8,7 +8,6 @@ import { ContentModeration } from '@/app/components/base/icons/src/vender/featur
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
-import { useStore } from '@/app/components/workflow/store'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
import { fetchCodeBasedExtensionList } from '@/service/common'
@@ -25,7 +24,6 @@ const Moderation = ({
onChange,
}: Props) => {
const { t } = useTranslation()
- const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
const { setShowModerationSettingModal } = useModalContext()
const { locale } = useContext(I18n)
const featuresStore = useFeaturesStore()
@@ -51,12 +49,12 @@ const Moderation = ({
draft.moderation = newModeration
})
setFeatures(newFeatures)
- setShowFeaturesPanel(true)
if (onChange)
onChange(newFeatures)
},
onCancelCallback: () => {
- setShowFeaturesPanel(true)
+ if (onChange)
+ onChange(features)
},
})
}
@@ -85,7 +83,6 @@ const Moderation = ({
draft.moderation = newModeration
})
setFeatures(newFeatures)
- setShowFeaturesPanel(true)
if (onChange)
onChange(newFeatures)
},
@@ -94,13 +91,21 @@ const Moderation = ({
draft.moderation = { enabled: false }
})
setFeatures(newFeatures)
- setShowFeaturesPanel(true)
if (onChange)
onChange(newFeatures)
},
})
}
- }, [featuresStore, onChange])
+
+ if (!enabled) {
+ const newFeatures = produce(features, (draft) => {
+ draft.moderation = { enabled: false }
+ })
+ setFeatures(newFeatures)
+ if (onChange)
+ onChange(newFeatures)
+ }
+ }, [featuresStore, onChange, setShowModerationSettingModal])
const providerContent = useMemo(() => {
if (moderation?.type === 'openai_moderation')
@@ -111,7 +116,7 @@ const Moderation = ({
return t('common.apiBasedExtension.selector.title')
else
return codeBasedExtensionList?.data.find(item => item.name === moderation?.type)?.label[locale] || '-'
- }, [codeBasedExtensionList, moderation])
+ }, [codeBasedExtensionList?.data, locale, moderation?.type, t])
const enableContent = useMemo(() => {
if (moderation?.config?.inputs_config?.enabled && moderation.config?.outputs_config?.enabled)
@@ -120,7 +125,7 @@ const Moderation = ({
return t('appDebug.feature.moderation.inputEnabled')
else if (moderation?.config?.outputs_config?.enabled)
return t('appDebug.feature.moderation.outputEnabled')
- }, [moderation])
+ }, [moderation?.config?.inputs_config?.enabled, moderation?.config?.outputs_config?.enabled, t])
return (
{
const handleFeaturesChange = useCallback(() => {
handleSyncWorkflowDraft()
- }, [handleSyncWorkflowDraft])
+ setShowFeaturesPanel(true)
+ }, [handleSyncWorkflowDraft, setShowFeaturesPanel])
return (
= {
payload: T
@@ -54,6 +56,7 @@ export type ModalContextState = {
setShowModelModal: Dispatch | null>>
setShowModelLoadBalancingModal: Dispatch>
setShowModelLoadBalancingEntryModal: Dispatch | null>>
+ setShowOpeningModal: Dispatch | null>>
}
const ModalContext = createContext({
setShowAccountSettingModal: () => { },
@@ -65,6 +68,7 @@ const ModalContext = createContext({
setShowModelModal: () => { },
setShowModelLoadBalancingModal: () => { },
setShowModelLoadBalancingEntryModal: () => { },
+ setShowOpeningModal: () => { },
})
export const useModalContext = () => useContext(ModalContext)
@@ -88,6 +92,7 @@ export const ModalContextProvider = ({
const [showModelModal, setShowModelModal] = useState | null>(null)
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState(null)
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState | null>(null)
+ const [showOpeningModal, setShowOpeningModal] = useState | null>(null)
const searchParams = useSearchParams()
const router = useRouter()
const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1')
@@ -127,6 +132,12 @@ export const ModalContextProvider = ({
setShowModelLoadBalancingEntryModal(null)
}, [showModelLoadBalancingEntryModal])
+ const handleCancelOpeningModal = useCallback(() => {
+ setShowOpeningModal(null)
+ if (showOpeningModal?.onCancelCallback)
+ showOpeningModal.onCancelCallback()
+ }, [showOpeningModal])
+
const handleSaveModelLoadBalancingEntryModal = useCallback((entry: ModelLoadBalancingConfigEntry) => {
showModelLoadBalancingEntryModal?.onSaveCallback?.({
...showModelLoadBalancingEntryModal.payload,
@@ -164,6 +175,12 @@ export const ModalContextProvider = ({
return true
}
+ const handleSaveOpeningModal = (newOpening: OpeningStatement) => {
+ if (showOpeningModal?.onSaveCallback)
+ showOpeningModal.onSaveCallback(newOpening)
+ setShowOpeningModal(null)
+ }
+
return (
<>
{children}
@@ -263,6 +281,12 @@ export const ModalContextProvider = ({
/>
)
}
+ {showOpeningModal && (
+
+ )}
>
)
diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts
index cedae63fc4..91e14c4f80 100644
--- a/web/i18n/en-US/app-debug.ts
+++ b/web/i18n/en-US/app-debug.ts
@@ -392,7 +392,7 @@ const translation = {
openingStatement: {
title: 'Conversation Opener',
add: 'Add',
- writeOpener: 'Write opener',
+ writeOpener: 'Edit opener',
placeholder: 'Write your opener message here, you can use variables, try type {{variable}}.',
openingQuestion: 'Opening Questions',
noDataPlaceHolder: