conversation opener

This commit is contained in:
JzoNg 2024-08-28 18:37:02 +08:00
parent 2f658de155
commit d69b453729
7 changed files with 299 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,8 @@ const Features = () => {
const handleFeaturesChange = useCallback(() => {
handleSyncWorkflowDraft()
}, [handleSyncWorkflowDraft])
setShowFeaturesPanel(true)
}, [handleSyncWorkflowDraft, setShowFeaturesPanel])
return (
<NewFeaturePanel

View File

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

View File

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