mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-19 11:05:57 +08:00
conversation opener
This commit is contained in:
parent
2f658de155
commit
d69b453729
@ -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 (
|
||||
<FeatureCard
|
||||
icon={
|
||||
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-light-blue-light-500'>
|
||||
<LoveMessage className='w-4 h-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
}
|
||||
title={t('appDebug.feature.conversationOpener.title')}
|
||||
value={!!opening?.enabled}
|
||||
onChange={state => handleChange(FeatureEnum.opening, state)}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
>
|
||||
<>
|
||||
{!opening?.enabled && (
|
||||
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.conversationOpener.description')}</div>
|
||||
)}
|
||||
{!!opening?.enabled && (
|
||||
<>
|
||||
{!isHovering && (
|
||||
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>
|
||||
{opening.opening_statement || t('appDebug.openingStatement.placeholder')}
|
||||
</div>
|
||||
)}
|
||||
{isHovering && (
|
||||
<Button className='w-full' onClick={handleOpenOpeningModal}>
|
||||
<RiEditLine className='mr-1 w-4 h-4' />
|
||||
{t('appDebug.openingStatement.writeOpener')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</FeatureCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConversationOpener
|
@ -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 (
|
||||
<div>
|
||||
<div className='flex items-center py-2'>
|
||||
<div className='shrink-0 flex space-x-0.5 leading-[18px] text-xs font-medium text-gray-500'>
|
||||
<div className='uppercase'>{t('appDebug.openingStatement.openingQuestion')}</div>
|
||||
<div>·</div>
|
||||
<div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
|
||||
</div>
|
||||
<div className='ml-3 grow w-0 h-px bg-[#243, 244, 246]'></div>
|
||||
</div>
|
||||
<ReactSortable
|
||||
className="space-y-1"
|
||||
list={tempSuggestedQuestions.map((name, index) => {
|
||||
return {
|
||||
id: index,
|
||||
name,
|
||||
}
|
||||
})}
|
||||
setList={list => setTempSuggestedQuestions(list.map(item => item.name))}
|
||||
handle='.handle'
|
||||
ghostClass="opacity-50"
|
||||
animation={150}
|
||||
>
|
||||
{tempSuggestedQuestions.map((question, index) => {
|
||||
return (
|
||||
<div className='group relative rounded-lg border border-gray-200 flex items-center pl-2.5 hover:border-gray-300 hover:bg-white' key={index}>
|
||||
<RiDraggable className='handle w-4 h-4 cursor-grab' />
|
||||
<input
|
||||
type="input"
|
||||
value={question || ''}
|
||||
onChange={(e) => {
|
||||
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'}
|
||||
/>
|
||||
|
||||
<div
|
||||
className='block absolute top-1/2 translate-y-[-50%] right-1.5 p-1 rounded-md cursor-pointer hover:bg-[#FEE4E2] hover:text-[#D92D20]'
|
||||
onClick={() => {
|
||||
setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i))
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}</ReactSortable>
|
||||
{tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
|
||||
<div
|
||||
onClick={() => { 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'>
|
||||
<RiAddLine className='w-4 h-4' />
|
||||
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => { }}
|
||||
className='!p-6 !mt-14 !max-w-none !w-[640px] !bg-components-panel-bg-blur'
|
||||
>
|
||||
<div className='flex items-center justify-between mb-6'>
|
||||
<div className='text-text-primary title-2xl-semi-bold'>{t('appDebug.feature.conversationOpener.title')}</div>
|
||||
<div className='p-1 cursor-pointer' onClick={onCancel}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
|
||||
</div>
|
||||
<div className='flex gap-2 mb-8'>
|
||||
<div className='shrink-0 mt-1.5 w-8 h-8 p-1.5 rounded-lg border-components-panel-border bg-util-colors-orange-dark-orange-dark-500'>
|
||||
<RiAsterisk className='w-5 h-5 text-text-primary-on-surface' />
|
||||
</div>
|
||||
<div className='grow p-3 bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle shadow-xs'>
|
||||
<textarea
|
||||
value={tempValue}
|
||||
rows={3}
|
||||
onChange={e => setTempValue(e.target.value)}
|
||||
className="w-full px-0 text-text-secondary system-md-regular border-0 bg-transparent focus:outline-none"
|
||||
placeholder={t('appDebug.openingStatement.placeholder') as string}
|
||||
/>
|
||||
{renderQuestions()}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-end'>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className='mr-2'
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default OpeningSettingModal
|
@ -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 && (
|
||||
<MoreLikeThis onChange={onChange} />
|
||||
)}
|
||||
{isChatMode && (
|
||||
<ConversationOpener onChange={onChange} />
|
||||
)}
|
||||
<Moderation onChange={onChange} />
|
||||
{isChatMode && speech2textDefaultModel && (
|
||||
<SpeechToText onChange={onChange} />
|
||||
|
@ -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 (
|
||||
<FeatureCard
|
||||
|
@ -18,7 +18,8 @@ const Features = () => {
|
||||
|
||||
const handleFeaturesChange = useCallback(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
}, [handleSyncWorkflowDraft])
|
||||
setShowFeaturesPanel(true)
|
||||
}, [handleSyncWorkflowDraft, setShowFeaturesPanel])
|
||||
|
||||
return (
|
||||
<NewFeaturePanel
|
||||
|
@ -26,6 +26,8 @@ import type {
|
||||
import ModelLoadBalancingEntryModal from '@/app/components/header/account-setting/model-provider-page/model-modal/model-load-balancing-entry-modal'
|
||||
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
|
||||
import ModelLoadBalancingModal from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
|
||||
import OpeningSettingModal from '@/app/components/base/features/new-feature-panel/conversation-opener/modal'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
|
||||
export type ModalState<T> = {
|
||||
payload: T
|
||||
@ -54,6 +56,7 @@ export type ModalContextState = {
|
||||
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
|
||||
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
|
||||
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
|
||||
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement> | null>>
|
||||
}
|
||||
const ModalContext = createContext<ModalContextState>({
|
||||
setShowAccountSettingModal: () => { },
|
||||
@ -65,6 +68,7 @@ const ModalContext = createContext<ModalContextState>({
|
||||
setShowModelModal: () => { },
|
||||
setShowModelLoadBalancingModal: () => { },
|
||||
setShowModelLoadBalancingEntryModal: () => { },
|
||||
setShowOpeningModal: () => { },
|
||||
})
|
||||
|
||||
export const useModalContext = () => useContext(ModalContext)
|
||||
@ -88,6 +92,7 @@ export const ModalContextProvider = ({
|
||||
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
|
||||
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
|
||||
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
|
||||
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement> | 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 (
|
||||
<ModalContext.Provider value={{
|
||||
setShowAccountSettingModal,
|
||||
@ -175,6 +192,7 @@ export const ModalContextProvider = ({
|
||||
setShowModelModal,
|
||||
setShowModelLoadBalancingModal,
|
||||
setShowModelLoadBalancingEntryModal,
|
||||
setShowOpeningModal,
|
||||
}}>
|
||||
<>
|
||||
{children}
|
||||
@ -263,6 +281,12 @@ export const ModalContextProvider = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showOpeningModal && (
|
||||
<OpeningSettingModal
|
||||
data={showOpeningModal.payload}
|
||||
onSave={handleSaveOpeningModal}
|
||||
onCancel={handleCancelOpeningModal} />
|
||||
)}
|
||||
</>
|
||||
</ModalContext.Provider>
|
||||
)
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user