Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
zxhlyh
2024-10-21 10:32:37 +08:00
committed by GitHub
parent 4fd2743efa
commit 7a1d6fe509
445 changed files with 11759 additions and 6922 deletions

View File

@@ -1,31 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import GroupName from '@/app/components/app/configuration/base/group-name'
export type IFeatureGroupProps = {
title: string
description?: string
children: React.ReactNode
}
const FeatureGroup: FC<IFeatureGroupProps> = ({
title,
description,
children,
}) => {
return (
<div className='mb-6'>
<div className='mb-2'>
<GroupName name={title} />
{description && (
<div className='text-xs font-normal text-gray-500'>{description}</div>
)}
</div>
<div className='space-y-2'>
{children}
</div>
</div>
)
}
export default React.memo(FeatureGroup)

View File

@@ -1,96 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import produce from 'immer'
import s from './style.module.css'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'
import { FeatureEnum } from '@/app/components/base/features/types'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { useModalContext } from '@/context/modal-context'
export type IFeatureItemProps = {
icon: React.ReactNode
previewImgClassName?: string
title: string
description: string
value: boolean
onChange: (type: FeatureEnum, value: boolean) => void
type: FeatureEnum
}
const FeatureItem: FC<IFeatureItemProps> = ({
icon,
previewImgClassName,
title,
description,
value,
onChange,
type,
}) => {
const featuresStore = useFeaturesStore()
const { setShowModerationSettingModal } = useModalContext()
const handleChange = useCallback((newValue: boolean) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
if (newValue && !features.moderation?.type && type === FeatureEnum.moderation) {
setShowModerationSettingModal({
payload: {
enabled: true,
type: 'keywords',
config: {
keywords: '',
inputs_config: {
enabled: true,
preset_response: '',
},
},
},
onSaveCallback: (newModeration) => {
setFeatures(produce(features, (draft) => {
draft.moderation = newModeration
}))
},
onCancelCallback: () => {
setFeatures(produce(features, (draft) => {
draft.moderation = { enabled: false }
}))
},
})
return
}
onChange(type, newValue)
}, [type, onChange, featuresStore, setShowModerationSettingModal])
return (
<div className={cn(s.wrap, 'relative flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer')}>
<div className='flex space-x-3 mr-2'>
{/* icon */}
<div
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white'
style={{
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}}
>
{icon}
</div>
<div>
<div className='text-sm font-semibold text-gray-800'>{title}</div>
<div className='text-xs font-normal text-gray-500'>{description}</div>
</div>
</div>
<Switch onChange={handleChange} defaultValue={value} />
{
previewImgClassName && (
<div className={cn(s.preview, s[previewImgClassName])}>
</div>)
}
</div>
)
}
export default React.memo(FeatureItem)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 211 KiB

View File

@@ -1,41 +0,0 @@
.preview {
display: none;
position: absolute;
top: 0;
left: 100%;
transform: translate(32px, -54px);
width: 280px;
height: 360px;
background: center center no-repeat;
background-size: contain;
border-radius: 8px;
}
.wrap:hover .preview {
display: block;
}
.openingStatementPreview {
background-image: url(~@/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-statement.png);
}
.suggestedQuestionsAfterAnswerPreview {
background-image: url(~@/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.png);
}
.moreLikeThisPreview {
background-image: url(~@/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.png);
}
.speechToTextPreview {
background-image: url(~@/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.png);
}
.textToSpeechPreview {
@apply shadow-lg rounded-lg;
background-image: url(~@/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-assistant@2x.png);
}
.citationPreview {
background-image: url(~@/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.png);
}

View File

@@ -1,147 +0,0 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import {
useFeatures,
useFeaturesStore,
} from '../hooks'
import type { OnFeaturesChange } from '../types'
import FeatureGroup from './feature-group'
import FeatureItem from './feature-item'
import Modal from '@/app/components/base/modal'
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
import { Microphone01, Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
import { MessageHeartCircle } from '@/app/components/base/icons/src/vender/solid/communication'
import { FeatureEnum } from '@/app/components/base/features/types'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
export type FeatureModalProps = {
onChange?: OnFeaturesChange
}
const FeatureModal: FC<FeatureModalProps> = ({
onChange,
}) => {
const { t } = useTranslation()
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const featuresStore = useFeaturesStore()
const setShowFeaturesModal = useFeatures(s => s.setShowFeaturesModal)
const features = useFeatures(s => s.features)
const handleCancelModal = useCallback(() => {
setShowFeaturesModal(false)
}, [setShowFeaturesModal])
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 (
<Modal
isShow
onClose={handleCancelModal}
className='w-[400px]'
title={t('appDebug.operation.addFeature')}
closable
overflowVisible
>
<div className='pt-5 pb-10'>
{/* Chat Feature */}
<FeatureGroup
title={t('appDebug.feature.groupChat.title')}
description={t('appDebug.feature.groupChat.description') as string}
>
<>
<FeatureItem
icon={<MessageHeartCircle className='w-4 h-4 text-[#DD2590]' />}
previewImgClassName='openingStatementPreview'
title={t('appDebug.feature.conversationOpener.title')}
description={t('appDebug.feature.conversationOpener.description')}
value={!!features.opening?.enabled}
onChange={handleChange}
type={FeatureEnum.opening}
/>
<FeatureItem
icon={<SuggestedQuestionsAfterAnswerIcon />}
previewImgClassName='suggestedQuestionsAfterAnswerPreview'
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
value={!!features.suggested?.enabled}
onChange={handleChange}
type={FeatureEnum.suggested}
/>
{
!!text2speechDefaultModel && (
<FeatureItem
icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='textToSpeechPreview'
title={t('appDebug.feature.textToSpeech.title')}
description={t('appDebug.feature.textToSpeech.description')}
value={!!features.text2speech?.enabled}
onChange={handleChange}
type={FeatureEnum.text2speech}
/>
)
}
{
!!speech2textDefaultModel && (
<FeatureItem
icon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
previewImgClassName='speechToTextPreview'
title={t('appDebug.feature.speechToText.title')}
description={t('appDebug.feature.speechToText.description')}
value={!!features.speech2text?.enabled}
onChange={handleChange}
type={FeatureEnum.speech2text}
/>
)
}
<FeatureItem
icon={<Citations className='w-4 h-4 text-[#FD853A]' />}
previewImgClassName='citationPreview'
title={t('appDebug.feature.citation.title')}
description={t('appDebug.feature.citation.description')}
value={!!features.citation?.enabled}
onChange={handleChange}
type={FeatureEnum.citation}
/>
</>
</FeatureGroup>
<FeatureGroup title={t('appDebug.feature.toolbox.title')}>
<>
<FeatureItem
icon={<FileSearch02 className='w-4 h-4 text-[#039855]' />}
previewImgClassName=''
title={t('appDebug.feature.moderation.title')}
description={t('appDebug.feature.moderation.description')}
value={!!features.moderation?.enabled}
onChange={handleChange}
type={FeatureEnum.moderation}
/>
</>
</FeatureGroup>
</div>
</Modal>
)
}
export default React.memo(FeatureModal)

View File

@@ -1,42 +0,0 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAddLine,
} from '@remixicon/react'
import { useFeatures } from '../hooks'
import type { OnFeaturesChange } from '../types'
import FeatureModal from './feature-modal'
import Button from '@/app/components/base/button'
type ChooseFeatureProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const ChooseFeature = ({
onChange,
disabled,
}: ChooseFeatureProps) => {
const { t } = useTranslation()
const showFeaturesModal = useFeatures(s => s.showFeaturesModal)
const setShowFeaturesModal = useFeatures(s => s.setShowFeaturesModal)
return (
<>
<Button
className={`
border-primary-100 bg-primary-25 text-xs font-semibold text-primary-600
`}
onClick={() => !disabled && setShowFeaturesModal(true)}
>
<RiAddLine className='mr-1 w-4 h-4' />
{t('appDebug.operation.addFeature')}
</Button>
{
showFeaturesModal && (
<FeatureModal onChange={onChange} />
)
}
</>
)
}
export default React.memo(ChooseFeature)

View File

@@ -1,25 +0,0 @@
'use client'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import Panel from '@/app/components/app/configuration/base/feature-panel'
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
const Citation: FC = () => {
const { t } = useTranslation()
return (
<Panel
title={
<div className='flex items-center gap-2'>
<div>{t('appDebug.feature.citation.title')}</div>
</div>
}
headerIcon={<Citations className='w-4 h-4 text-[#FD853A]' />}
headerRight={
<div className='text-xs text-gray-500'>{t('appDebug.feature.citation.resDes')}</div>
}
noBodySpacing
/>
)
}
export default React.memo(Citation)

View File

@@ -1,63 +0,0 @@
'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 { File05 } from '@/app/components/base/icons/src/vender/solid/files'
type FileUploadProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const FileUpload = ({
onChange,
disabled,
}: 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) => {
if (draft.file?.image)
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'>
<File05 className='shrink-0 w-4 h-4 text-[#6938EF]' />
</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} disabled={disabled} />
<div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
<Switch
defaultValue={file?.image?.enabled}
onChange={handleSwitch}
disabled={disabled}
size='md'
/>
</div>
</div>
)
}
export default React.memo(FileUpload)

View File

@@ -1,119 +0,0 @@
'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 (draft.file?.image) {
if (value === 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) => {
if (draft.file?.image)
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('common.operation.settings')}</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

@@ -1,49 +0,0 @@
'use client'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { OnFeaturesChange } from '../../types'
import ParamConfigContent from './param-config-content'
import cn from '@/utils/classnames'
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
disabled?: boolean
}
const ParamsConfig = ({
onChange,
disabled,
}: ParamsConfigProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => !disabled && 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', disabled && 'cursor-not-allowed opacity-50')}>
<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

@@ -1,40 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import s from './style.module.css'
import cn from '@/utils/classnames'
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

@@ -1,24 +0,0 @@
.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

@@ -1,108 +0,0 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import produce from 'immer'
import { useContext } from 'use-context-selector'
import {
useFeatures,
useFeaturesStore,
} from '../../hooks'
import type { OnFeaturesChange } from '../../types'
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { useModalContext } from '@/context/modal-context'
import { fetchCodeBasedExtensionList } from '@/service/common'
import I18n from '@/context/i18n'
type ModerationProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const Moderation = ({
onChange,
disabled,
}: ModerationProps) => {
const { t } = useTranslation()
const { setShowModerationSettingModal } = useModalContext()
const { locale } = useContext(I18n)
const featuresStore = useFeaturesStore()
const moderation = useFeatures(s => s.features.moderation)
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)
const handleOpenModerationSettingModal = () => {
if (disabled)
return
const {
features,
setFeatures,
} = featuresStore!.getState()
setShowModerationSettingModal({
payload: moderation as any,
onSaveCallback: (newModeration) => {
const newFeatures = produce(features, (draft) => {
draft.moderation = newModeration
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
},
})
}
const renderInfo = () => {
let prefix = ''
let suffix = ''
if (moderation?.type === 'openai_moderation')
prefix = t('appDebug.feature.moderation.modal.provider.openai')
else if (moderation?.type === 'keywords')
prefix = t('appDebug.feature.moderation.modal.provider.keywords')
else if (moderation?.type === 'api')
prefix = t('common.apiBasedExtension.selector.title')
else
prefix = codeBasedExtensionList?.data.find(item => item.name === moderation?.type)?.label[locale] || ''
if (moderation?.config?.inputs_config?.enabled && moderation.config?.outputs_config?.enabled)
suffix = t('appDebug.feature.moderation.allEnabled')
else if (moderation?.config?.inputs_config?.enabled)
suffix = t('appDebug.feature.moderation.inputEnabled')
else if (moderation?.config?.outputs_config?.enabled)
suffix = t('appDebug.feature.moderation.outputEnabled')
return `${prefix} · ${suffix}`
}
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('appDebug.feature.moderation.title')}
</div>
<div
className='grow block w-0 text-right text-xs text-gray-500 truncate'
title={renderInfo()}>
{renderInfo()}
</div>
<div className='shrink-0 ml-4 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
<div
className={`
shrink-0 flex items-center px-3 h-7 cursor-pointer rounded-md
text-xs text-gray-700 font-medium hover:bg-gray-200
${disabled && '!cursor-not-allowed'}
`}
onClick={handleOpenModerationSettingModal}
>
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
{t('common.operation.settings')}
</div>
</div>
)
}
export default memo(Moderation)

View File

@@ -1,22 +0,0 @@
'use client'
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
const SpeechToTextConfig: FC = () => {
const { t } = useTranslation()
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'>
<Microphone01 className='w-4 h-4 text-[#7839EE]' />
</div>
<div className='shrink-0 mr-2 flex items-center whitespace-nowrap text-sm text-gray-800 font-semibold'>
<div>{t('appDebug.feature.speechToText.title')}</div>
</div>
<div className='grow'></div>
<div className='text-xs text-gray-500'>{t('appDebug.feature.speechToText.resDes')}</div>
</div>
)
}
export default React.memo(SpeechToTextConfig)

View File

@@ -1,25 +0,0 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { MessageSmileSquare } from '@/app/components/base/icons/src/vender/solid/communication'
import Tooltip from '@/app/components/base/tooltip'
const SuggestedQuestionsAfterAnswer: FC = () => {
const { t } = useTranslation()
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'>
<MessageSmileSquare className='w-4 h-4 text-[#06AED4]' />
</div>
<div className='shrink-0 mr-2 flex items-center whitespace-nowrap text-sm text-gray-800 font-semibold'>
<div className='mr-2'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}</div>
<Tooltip popupContent={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}/>
</div>
<div className='grow'></div>
<div className='text-xs text-gray-500'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.resDes')}</div>
</div>
)
}
export default React.memo(SuggestedQuestionsAfterAnswer)

View File

@@ -1,62 +0,0 @@
'use client'
import useSWR from 'swr'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { usePathname } from 'next/navigation'
import { useFeatures } from '../../hooks'
import type { OnFeaturesChange } from '../../types'
import ParamsConfig from './params-config'
import { Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import { languages } from '@/i18n/language'
import { fetchAppVoices } from '@/service/apps'
import AudioBtn from '@/app/components/base/audio-btn'
type TextToSpeechProps = {
onChange?: OnFeaturesChange
disabled?: boolean
}
const TextToSpeech = ({
onChange,
disabled,
}: TextToSpeechProps) => {
const { t } = useTranslation()
const textToSpeech = useFeatures(s => s.features.text2speech)
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const language = textToSpeech?.language
const languageInfo = languages.find(i => i.value === textToSpeech?.language)
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
const voiceItem = voiceItems?.find(item => item.value === textToSpeech?.voice)
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'>
<Speaker className='w-4 h-4 text-[#7839EE]' />
</div>
<div className='shrink-0 mr-2 whitespace-nowrap text-sm text-gray-800 font-semibold'>
{t('appDebug.feature.textToSpeech.title')}
</div>
<div
className='grow '>
</div>
<div className='shrink-0 text-xs text-gray-500 inline-flex items-center gap-2'>
{languageInfo && (`${languageInfo?.name} - `)}{voiceItem?.name ?? t('appDebug.voice.defaultDisplay')}
{ languageInfo?.example && (
<AudioBtn
value={languageInfo?.example}
voice={voiceItem?.value}
noCache={false}
isAudition={true}
/>
)}
</div>
<div className='shrink-0 flex items-center'>
<ParamsConfig onChange={onChange} disabled={disabled} />
</div>
</div>
)
}
export default React.memo(TextToSpeech)

View File

@@ -1,241 +0,0 @@
'use client'
import useSWR from 'swr'
import produce from 'immer'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import {
useFeatures,
useFeaturesStore,
} from '../../hooks'
import type { OnFeaturesChange } from '../../types'
import classNames from '@/utils/classnames'
import type { Item } from '@/app/components/base/select'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import { languages } from '@/i18n/language'
import RadioGroup from '@/app/components/app/configuration/config-vision/radio-group'
import { TtsAutoPlay } from '@/types/app'
type VoiceParamConfigProps = {
onChange?: OnFeaturesChange
}
const VoiceParamConfig = ({
onChange,
}: VoiceParamConfigProps) => {
const { t } = useTranslation()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const text2speech = useFeatures(state => state.features.text2speech)
const featuresStore = useFeaturesStore()
let languageItem = languages.find(item => item.value === text2speech?.language)
if (languages && !languageItem)
languageItem = languages[0]
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
const language = languageItem?.value
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
if (voiceItems && !voiceItem)
voiceItem = voiceItems[0]
const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
const handleChange = (value: Record<string, string>) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.text2speech = {
...draft.text2speech,
...value,
}
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
}
return (
<div>
<div>
<div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.title')}</div>
<div className='pt-3 space-y-6'>
<div>
<div className='mb-2 flex items-center space-x-1'>
<div
className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.language')}</div>
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}
</div>
))}
</div>
}
/>
</div>
<Listbox
value={languageItem}
onChange={(value: Item) => {
handleChange({
language: String(value.value),
})
}}
>
<div className={'relative h-9'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span className={classNames('block truncate text-left', !languageItem?.name && 'text-gray-400')}>
{languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{languages.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span
className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
{(selected || item.value === text2speech?.language) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div>
<div
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
<Listbox
value={voiceItem ?? {}}
disabled={!languageItem}
onChange={(value: Item) => {
handleChange({
voice: String(value.value),
})
}}
>
<div className={'relative h-9'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span
className={classNames('block truncate text-left', !voiceItem?.name && 'text-gray-400')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{voiceItems?.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{(selected || item.value === text2speech?.voice) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div>
<div
className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.autoPlay')}</div>
<RadioGroup
className='space-x-3'
options={[
{
label: t('appDebug.voice.voiceSettings.autoPlayEnabled'),
value: TtsAutoPlay.enabled,
},
{
label: t('appDebug.voice.voiceSettings.autoPlayDisabled'),
value: TtsAutoPlay.disabled,
},
]}
value={text2speech?.autoPlay ? text2speech?.autoPlay : TtsAutoPlay.disabled}
onChange={(value: TtsAutoPlay) => {
handleChange({
autoPlay: value,
})
}}
/>
</div>
</div>
</div>
</div>
)
}
export default React.memo(VoiceParamConfig)

View File

@@ -1,48 +0,0 @@
'use client'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { OnFeaturesChange } from '../../types'
import ParamConfigContent from './param-config-content'
import cn from '@/utils/classnames'
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
disabled?: boolean
}
const ParamsConfig = ({
onChange,
disabled,
}: ParamsConfigProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={() => !disabled && 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

@@ -1,3 +1 @@
export { default as FeaturesPanel } from './feature-panel'
export { default as FeaturesChoose } from './feature-choose'
export { FeaturesProvider } from './context'

View File

@@ -0,0 +1,135 @@
'use client'
import type { FC } from 'react'
import React, { useRef, useState } from 'react'
import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next'
import cn from '@/utils/classnames'
import { MessageCheckRemove, MessageFastPlus } from '@/app/components/base/icons/src/vender/line/communication'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import { Edit04 } from '@/app/components/base/icons/src/vender/line/general'
import RemoveAnnotationConfirmModal from '@/app/components/app/annotation/remove-annotation-confirm-modal'
import Tooltip from '@/app/components/base/tooltip'
import { addAnnotation, delAnnotation } from '@/service/annotation'
import Toast from '@/app/components/base/toast'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
type Props = {
appId: string
messageId?: string
annotationId?: string
className?: string
cached: boolean
query: string
answer: string
onAdded: (annotationId: string, authorName: string) => void
onEdit: () => void
onRemoved: () => void
}
const CacheCtrlBtn: FC<Props> = ({
className,
cached,
query,
answer,
appId,
messageId,
annotationId,
onAdded,
onEdit,
onRemoved,
}) => {
const { t } = useTranslation()
const { plan, enableBilling } = useProviderContext()
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
const { setShowAnnotationFullModal } = useModalContext()
const [showModal, setShowModal] = useState(false)
const cachedBtnRef = useRef<HTMLDivElement>(null)
const isCachedBtnHovering = useHover(cachedBtnRef)
const handleAdd = async () => {
if (isAnnotationFull) {
setShowAnnotationFullModal()
return
}
const res: any = await addAnnotation(appId, {
message_id: messageId,
question: query,
answer,
})
Toast.notify({
message: t('common.api.actionSuccess') as string,
type: 'success',
})
onAdded(res.id, res.account?.name)
}
const handleRemove = async () => {
await delAnnotation(appId, annotationId!)
Toast.notify({
message: t('common.api.actionSuccess') as string,
type: 'success',
})
onRemoved()
setShowModal(false)
}
return (
<div className={cn('inline-block', className)}>
<div className='inline-flex p-0.5 space-x-0.5 rounded-lg bg-white border border-gray-100 shadow-md text-gray-500 cursor-pointer'>
{cached
? (
<div>
<div
ref={cachedBtnRef}
className={cn(isCachedBtnHovering ? 'bg-[#FEF3F2] text-[#D92D20]' : 'bg-[#EEF4FF] text-[#444CE7]', 'flex p-1 space-x-1 items-center rounded-md leading-4 text-xs font-medium')}
onClick={() => setShowModal(true)}
>
{!isCachedBtnHovering
? (
<>
<MessageFast className='w-4 h-4' />
<div>{t('appDebug.feature.annotation.cached')}</div>
</>
)
: <>
<MessageCheckRemove className='w-4 h-4' />
<div>{t('appDebug.feature.annotation.remove')}</div>
</>}
</div>
</div>
)
: answer
? (
<Tooltip
popupContent={t('appDebug.feature.annotation.add')}
>
<div
className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
onClick={handleAdd}
>
<MessageFastPlus className='w-4 h-4' />
</div>
</Tooltip>
)
: null
}
<Tooltip
popupContent={t('appDebug.feature.annotation.edit')}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
onClick={onEdit}
>
<Edit04 className='w-4 h-4' />
</div>
</Tooltip>
</div>
<RemoveAnnotationConfirmModal
isShow={showModal}
onHide={() => setShowModal(false)}
onRemove={handleRemove}
/>
</div>
)
}
export default React.memo(CacheCtrlBtn)

View File

@@ -0,0 +1,139 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ScoreSlider from './score-slider'
import { Item } from './config-param'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import type { AnnotationReplyConfig } from '@/models/debug'
import { ANNOTATION_DEFAULT } from '@/config'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type Props = {
appId: string
isShow: boolean
onHide: () => void
onSave: (embeddingModel: {
embedding_provider_name: string
embedding_model_name: string
}, score: number) => void
isInit?: boolean
annotationConfig: AnnotationReplyConfig
}
const ConfigParamModal: FC<Props> = ({
isShow,
onHide: doHide,
onSave,
isInit,
annotationConfig: oldAnnotationConfig,
}) => {
const { t } = useTranslation()
const {
modelList: embeddingsModelList,
defaultModel: embeddingsDefaultModel,
currentModel: isEmbeddingsDefaultModelValid,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textEmbedding)
const [annotationConfig, setAnnotationConfig] = useState(oldAnnotationConfig)
const [isLoading, setLoading] = useState(false)
const [embeddingModel, setEmbeddingModel] = useState(oldAnnotationConfig.embedding_model
? {
providerName: oldAnnotationConfig.embedding_model.embedding_provider_name,
modelName: oldAnnotationConfig.embedding_model.embedding_model_name,
}
: (embeddingsDefaultModel
? {
providerName: embeddingsDefaultModel.provider.provider,
modelName: embeddingsDefaultModel.model,
}
: undefined))
const onHide = () => {
if (!isLoading)
doHide()
}
const handleSave = async () => {
if (!embeddingModel || !embeddingModel.modelName || (embeddingModel.modelName === embeddingsDefaultModel?.model && !isEmbeddingsDefaultModelValid)) {
Toast.notify({
message: t('common.modelProvider.embeddingModel.required'),
type: 'error',
})
return
}
setLoading(true)
await onSave({
embedding_provider_name: embeddingModel.providerName,
embedding_model_name: embeddingModel.modelName,
}, annotationConfig.score_threshold)
setLoading(false)
}
return (
<Modal
isShow={isShow}
onClose={onHide}
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
>
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
{t(`appAnnotation.initSetup.${isInit ? 'title' : 'configTitle'}`)}
</div>
<div className='mt-6 space-y-3'>
<Item
title={t('appDebug.feature.annotation.scoreThreshold.title')}
tooltip={t('appDebug.feature.annotation.scoreThreshold.description')}
>
<ScoreSlider
className='mt-1'
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
onChange={(val) => {
setAnnotationConfig({
...annotationConfig,
score_threshold: val / 100,
})
}}
/>
</Item>
<Item
title={t('common.modelProvider.embeddingModel.key')}
tooltip={t('appAnnotation.embeddingModelSwitchTip')}
>
<div className='pt-1'>
<ModelSelector
defaultModel={embeddingModel && {
provider: embeddingModel.providerName,
model: embeddingModel.modelName,
}}
modelList={embeddingsModelList}
onSelect={(val) => {
setEmbeddingModel({
providerName: val.provider,
modelName: val.model,
})
}}
/>
</div>
</Item>
</div>
<div className='mt-6 flex gap-2 justify-end'>
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button
variant='primary'
onClick={handleSave}
loading={isLoading}
>
<div></div>
<div>{t(`appAnnotation.initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`)}</div>
</Button >
</div >
</Modal >
)
}
export default React.memo(ConfigParamModal)

View File

@@ -0,0 +1,24 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Tooltip from '@/app/components/base/tooltip'
export const Item: FC<{ title: string; tooltip: string; children: JSX.Element }> = ({
title,
tooltip,
children,
}) => {
return (
<div>
<div className='flex items-center space-x-1'>
<div>{title}</div>
<Tooltip
popupContent={
<div className='max-w-[200px] leading-[18px] text-[13px] font-medium text-gray-800'>{tooltip}</div>
}
/>
</div>
<div>{children}</div>
</div>
)
}

View File

@@ -0,0 +1,152 @@
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePathname, useRouter } from 'next/navigation'
import produce from 'immer'
import { RiEqualizer2Line, RiExternalLinkLine } from '@remixicon/react'
import { MessageFast } 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 } from '@/app/components/base/features/types'
import useAnnotationConfig from '@/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { ANNOTATION_DEFAULT } from '@/config'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
}
const AnnotationReply = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const featuresStore = useFeaturesStore()
const annotationReply = useFeatures(s => s.features.annotationReply)
const updateAnnotationReply = useCallback((newConfig: any) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.annotationReply = newConfig
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
}, [featuresStore, onChange])
const {
handleEnableAnnotation,
handleDisableAnnotation,
isShowAnnotationConfigInit,
setIsShowAnnotationConfigInit,
isShowAnnotationFullModal,
setIsShowAnnotationFullModal,
} = useAnnotationConfig({
appId,
annotationConfig: annotationReply as any || {
id: '',
enabled: false,
score_threshold: ANNOTATION_DEFAULT.score_threshold,
embedding_model: {
embedding_provider_name: '',
embedding_model_name: '',
},
},
setAnnotationConfig: updateAnnotationReply,
})
const handleSwitch = useCallback((enabled: boolean) => {
if (enabled)
setIsShowAnnotationConfigInit(true)
else
handleDisableAnnotation(annotationReply?.embedding_model as any)
}, [annotationReply?.embedding_model, handleDisableAnnotation, setIsShowAnnotationConfigInit])
const [isHovering, setIsHovering] = useState(false)
return (
<>
<FeatureCard
icon={
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
<MessageFast className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.annotation.title')}
value={!!annotationReply?.enabled}
onChange={state => handleSwitch(state)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<>
{!annotationReply?.enabled && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.annotation.description')}</div>
)}
{!!annotationReply?.enabled && (
<>
{!isHovering && (
<div className='pt-0.5 flex items-center gap-4'>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.annotation.scoreThreshold.title')}</div>
<div className='text-text-secondary system-xs-regular'>{annotationReply.score_threshold || '-'}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('common.modelProvider.embeddingModel.key')}</div>
<div className='text-text-secondary system-xs-regular'>{annotationReply.embedding_model?.embedding_model_name}</div>
</div>
</div>
)}
{isHovering && (
<div className='flex items-center justify-between'>
<Button className='w-[178px]' onClick={() => setIsShowAnnotationConfigInit(true)} disabled={disabled}>
<RiEqualizer2Line className='mr-1 w-4 h-4' />
{t('common.operation.params')}
</Button>
<Button className='w-[178px]' onClick={() => {
router.push(`/app/${appId}/annotations`)
}}>
<RiExternalLinkLine className='mr-1 w-4 h-4' />
{t('appDebug.feature.annotation.cacheManagement')}
</Button>
</div>
)}
</>
)}
</>
</FeatureCard>
<ConfigParamModal
appId={appId}
isInit
isShow={isShowAnnotationConfigInit}
onHide={() => {
setIsShowAnnotationConfigInit(false)
// showChooseFeatureTrue()
}}
onSave={async (embeddingModel, score) => {
await handleEnableAnnotation(embeddingModel, score)
setIsShowAnnotationConfigInit(false)
}}
annotationConfig={annotationReply as any}
/>
{isShowAnnotationFullModal && (
<AnnotationFullModal
show={isShowAnnotationFullModal}
onHide={() => setIsShowAnnotationFullModal(false)}
/>
)}
</>
)
}
export default AnnotationReply

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Slider from '@/app/components/app/configuration/toolbox/score-slider/base-slider'
import Slider from '@/app/components/base/features/new-feature-panel/annotation-reply/score-slider/base-slider'
type Props = {
className?: string

View File

@@ -0,0 +1,4 @@
export enum PageType {
log = 'log',
annotation = 'annotation',
}

View File

@@ -0,0 +1,89 @@
import React, { useState } from 'react'
import produce from 'immer'
import type { AnnotationReplyConfig } from '@/models/debug'
import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation'
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
import { AnnotationEnableStatus, JobStatus } from '@/app/components/app/annotation/type'
import { sleep } from '@/utils'
import { ANNOTATION_DEFAULT } from '@/config'
import { useProviderContext } from '@/context/provider-context'
type Params = {
appId: string
annotationConfig: AnnotationReplyConfig
setAnnotationConfig: (annotationConfig: AnnotationReplyConfig) => void
}
const useAnnotationConfig = ({
appId,
annotationConfig,
setAnnotationConfig,
}: Params) => {
const { plan, enableBilling } = useProviderContext()
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
const [isShowAnnotationFullModal, setIsShowAnnotationFullModal] = useState(false)
const [isShowAnnotationConfigInit, doSetIsShowAnnotationConfigInit] = React.useState(false)
const setIsShowAnnotationConfigInit = (isShow: boolean) => {
if (isShow) {
if (isAnnotationFull) {
setIsShowAnnotationFullModal(true)
return
}
}
doSetIsShowAnnotationConfigInit(isShow)
}
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
let isCompleted = false
while (!isCompleted) {
const res: any = await queryAnnotationJobStatus(appId, status, jobId)
isCompleted = res.job_status === JobStatus.completed
if (isCompleted)
break
await sleep(2000)
}
}
const handleEnableAnnotation = async (embeddingModel: EmbeddingModelConfig, score?: number) => {
if (isAnnotationFull)
return
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.enable, embeddingModel, score)
await ensureJobCompleted(jobId, AnnotationEnableStatus.enable)
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
draft.enabled = true
draft.embedding_model = embeddingModel
if (!draft.score_threshold)
draft.score_threshold = ANNOTATION_DEFAULT.score_threshold
}))
}
const setScore = (score: number, embeddingModel?: EmbeddingModelConfig) => {
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
draft.score_threshold = score
if (embeddingModel)
draft.embedding_model = embeddingModel
}))
}
const handleDisableAnnotation = async (embeddingModel: EmbeddingModelConfig) => {
if (!annotationConfig.enabled)
return
await updateAnnotationStatus(appId, AnnotationEnableStatus.disable, embeddingModel)
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
draft.enabled = false
}))
}
return {
handleEnableAnnotation,
handleDisableAnnotation,
isShowAnnotationConfigInit,
setIsShowAnnotationConfigInit,
isShowAnnotationFullModal,
setIsShowAnnotationFullModal,
setScore,
}
}
export default useAnnotationConfig

View File

@@ -0,0 +1,56 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { Citations } from '@/app/components/base/icons/src/vender/features'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
}
const Citation = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
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-warning-warning-500'>
<Citations className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.citation.title')}
value={!!features.citation?.enabled}
description={t('appDebug.feature.citation.description')!}
onChange={state => handleChange(FeatureEnum.citation, state)}
disabled={disabled}
/>
)
}
export default Citation

View File

@@ -0,0 +1,119 @@
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 } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
import { useModalContext } from '@/context/modal-context'
import type { PromptVariable } from '@/models/debug'
import type { InputVar } from '@/app/components/workflow/types'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}
const ConversationOpener = ({
disabled,
onChange,
promptVariables,
workflowVariables,
onAutoAddPromptVariable,
}: 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,
promptVariables,
workflowVariables,
onAutoAddPromptVariable,
},
onSaveCallback: (newOpening) => {
const newFeatures = produce(features, (draft) => {
draft.opening = newOpening
})
setFeatures(newFeatures)
if (onChange)
onChange()
},
onCancelCallback: () => {
if (onChange)
onChange()
},
})
}, [disabled, featuresStore, onAutoAddPromptVariable, onChange, opening, promptVariables, 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()
}, [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)}
disabled={disabled}
>
<>
{!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} disabled={disabled}>
<RiEditLine className='mr-1 w-4 h-4' />
{t('appDebug.openingStatement.writeOpener')}
</Button>
)}
</>
)}
</>
</FeatureCard>
)
}
export default ConversationOpener

View File

@@ -0,0 +1,206 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
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 ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
import type { OpeningStatement } from '@/app/components/base/features/types'
import { getInputKeys } from '@/app/components/base/block-input'
import type { PromptVariable } from '@/models/debug'
import type { InputVar } from '@/app/components/workflow/types'
import { getNewVar } from '@/utils/var'
type OpeningSettingModalProps = {
data: OpeningStatement
onSave: (newState: OpeningStatement) => void
onCancel: () => void
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}
const MAX_QUESTION_NUM = 5
const OpeningSettingModal = ({
data,
onSave,
onCancel,
promptVariables = [],
workflowVariables = [],
onAutoAddPromptVariable,
}: 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 [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
const handleSave = useCallback((ignoreVariablesCheck?: boolean) => {
if (!ignoreVariablesCheck) {
const keys = getInputKeys(tempValue)
const promptKeys = promptVariables.map(item => item.key)
const workflowVariableKeys = workflowVariables.map(item => item.variable)
let notIncludeKeys: string[] = []
if (promptKeys.length === 0 && workflowVariables.length === 0) {
if (keys.length > 0)
notIncludeKeys = keys
}
else {
if (workflowVariables.length > 0)
notIncludeKeys = keys.filter(key => !workflowVariableKeys.includes(key))
else notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
}
if (notIncludeKeys.length > 0) {
setNotIncludeKeys(notIncludeKeys)
showConfirmAddVar()
return
}
}
const newOpening = produce(data, (draft) => {
if (draft) {
draft.opening_statement = tempValue
draft.suggested_questions = tempSuggestedQuestions
}
})
onSave(newOpening)
}, [data, onSave, promptVariables, workflowVariables, showConfirmAddVar, tempSuggestedQuestions, tempValue])
const cancelAutoAddVar = useCallback(() => {
hideConfirmAddVar()
handleSave(true)
}, [handleSave, hideConfirmAddVar])
const autoAddVar = useCallback(() => {
onAutoAddPromptVariable?.([
...notIncludeKeys.map(key => getNewVar(key, 'string')),
])
hideConfirmAddVar()
handleSave(true)
}, [handleSave, hideConfirmAddVar, notIncludeKeys, onAutoAddPromptVariable])
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.variableConfig.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>
{isShowConfirmAddVar && (
<ConfirmAddVar
varNameArr={notIncludeKeys}
onConfirm={autoAddVar}
onCancel={cancelAutoAddVar}
onHide={hideConfirmAddVar}
/>
)}
</Modal>
)
}
export default OpeningSettingModal

View File

@@ -0,0 +1,59 @@
import { Fragment, useCallback } from 'react'
import type { ReactNode } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import cn from '@/utils/classnames'
type DialogProps = {
className?: string
children: ReactNode
show: boolean
onClose?: () => void
inWorkflow?: boolean
}
const DialogWrapper = ({
className,
children,
show,
onClose,
inWorkflow = true,
}: DialogProps) => {
const close = useCallback(() => onClose?.(), [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={close}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0">
<div className={cn('flex flex-col items-end justify-center min-h-full pb-2', inWorkflow ? 'pt-[112px]' : 'pt-[64px] pr-2')}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={cn('grow flex relative w-[420px] h-0 p-0 overflow-hidden text-left align-middle transition-all transform bg-components-panel-bg-alt border-components-panel-border shadow-xl', inWorkflow ? 'border-t-[0.5px] border-l-[0.5px] border-b-[0.5px] rounded-l-2xl' : 'border-[0.5px] rounded-2xl', className)}>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition >
)
}
export default DialogWrapper

View File

@@ -0,0 +1,145 @@
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiApps2AddLine, RiArrowRightLine, RiSparklingFill } from '@remixicon/react'
import { Citations, ContentModeration, FolderUpload, LoveMessage, MessageFast, Microphone01, TextToAudio, VirtualAssistant } from '@/app/components/base/icons/src/vender/features'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings'
import { useFeatures } from '@/app/components/base/features/hooks'
import cn from '@/utils/classnames'
type Props = {
isChatMode?: boolean
showFileUpload?: boolean
disabled?: boolean
onFeatureBarClick?: (state: boolean) => void
}
const FeatureBar = ({
isChatMode = true,
showFileUpload = true,
disabled,
onFeatureBarClick,
}: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
const [modalOpen, setModalOpen] = useState(false)
const noFeatureEnabled = useMemo(() => {
// completion app citation is always true but not enabled for setting
const data = {
...features,
citation: { enabled: isChatMode ? features.citation?.enabled : false },
file: showFileUpload ? features.file! : { enabled: false },
}
return !Object.values(data).some(f => f.enabled)
}, [features, isChatMode, showFileUpload])
return (
<div className='-translate-y-2 m-1 mt-0 px-2.5 py-2 pt-4 bg-util-colors-indigo-indigo-50 rounded-b-[10px] border-l border-b border-r border-components-panel-border-subtle'>
{noFeatureEnabled && (
<div className='flex items-end gap-1 cursor-pointer' onClick={() => onFeatureBarClick?.(true)}>
<RiApps2AddLine className='w-3.5 h-3.5 text-text-accent' />
<div className='text-text-accent body-xs-medium'>{t('appDebug.feature.bar.empty')}</div>
<RiArrowRightLine className='w-3.5 h-3.5 text-text-accent' />
</div>
)}
{!noFeatureEnabled && (
<div className='flex items-center gap-2'>
<div className='shrink-0 flex items-center gap-0.5'>
{!!features.moreLikeThis?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.moreLikeThis.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-light-blue-light-500'>
<RiSparklingFill className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{!!features.opening?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.conversationOpener.title')}
>
<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-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{!!features.moderation?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.moderation.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-text-success'>
<ContentModeration className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{!!features.speech2text?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.speechToText.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-violet-violet-600'>
<Microphone01 className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{!!features.text2speech?.enabled && (
<VoiceSettings placementLeft={false} open={modalOpen && !disabled} onOpen={setModalOpen}>
<Tooltip
popupContent={t('appDebug.feature.textToSpeech.title')}
>
<div className={cn('shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-violet-violet-600', !disabled && 'cursor-pointer')}>
<TextToAudio className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
</VoiceSettings>
)}
{showFileUpload && !!features.file?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.fileUpload.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-blue-600'>
<FolderUpload className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{!!features.suggested?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-light-blue-light-500'>
<VirtualAssistant className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{isChatMode && !!features.citation?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.citation.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-warning-warning-500'>
<Citations className='w-4 h-4 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
{isChatMode && !!features.annotationReply?.enabled && (
<Tooltip
popupContent={t('appDebug.feature.annotation.title')}
>
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
<MessageFast className='w-3.5 h-3.5 text-text-primary-on-surface' />
</div>
</Tooltip>
)}
</div>
<div className='grow text-text-tertiary body-xs-regular'>{t('appDebug.feature.bar.enableText')}</div>
<Button className='shrink-0' variant='ghost-accent' size='small' onClick={() => onFeatureBarClick?.(true)}>
<div className='mx-1'>{t('appDebug.feature.bar.manage')}</div>
<RiArrowRightLine className='w-3.5 h-3.5 text-text-accent' />
</Button>
</div>
)}
</div>
)
}
export default FeatureBar

View File

@@ -0,0 +1,61 @@
import React from 'react'
import {
RiQuestionLine,
} from '@remixicon/react'
import Switch from '@/app/components/base/switch'
import Tooltip from '@/app/components/base/tooltip'
type Props = {
icon: any
title: any
tooltip?: any
value: any
description?: string
children?: React.ReactNode
disabled?: boolean
onChange?: (state: any) => void
onMouseEnter?: () => void
onMouseLeave?: () => void
}
const FeatureCard = ({
icon,
title,
tooltip,
value,
description,
children,
disabled,
onChange,
onMouseEnter,
onMouseLeave,
}: Props) => {
return (
<div
className='mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<div className='mb-2 flex items-center gap-2'>
{icon}
<div className='grow flex items-center text-text-secondary system-sm-semibold'>
{title}
{tooltip && (
<Tooltip
popupContent={tooltip}
>
<div className='ml-0.5 p-px'><RiQuestionLine className='w-3.5 h-3.5 text-text-quaternary' /></div>
</Tooltip>
)}
</div>
<Switch disabled={disabled} className='shrink-0' onChange={state => onChange?.(state)} defaultValue={value} />
</div>
{description && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{description}</div>
)}
{children}
</div>
)
}
export default FeatureCard

View File

@@ -0,0 +1,105 @@
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { RiEqualizer2Line } from '@remixicon/react'
import { FolderUpload } from '@/app/components/base/icons/src/vender/features'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import SettingModal from '@/app/components/base/features/new-feature-panel/file-upload/setting-modal'
import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
type Props = {
disabled: boolean
onChange?: OnFeaturesChange
}
const FileUpload = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const [modalOpen, setModalOpen] = useState(false)
const [isHovering, setIsHovering] = useState(false)
const supportedTypes = useMemo(() => {
return file?.allowed_file_types?.join(',') || '-'
}, [file?.allowed_file_types])
const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft[type] = {
...draft[type],
enabled,
image: { enabled },
}
})
setFeatures(newFeatures)
if (onChange)
onChange()
}, [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-blue-600'>
<FolderUpload className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.fileUpload.title')}
value={file?.enabled}
onChange={state => handleChange(FeatureEnum.file, state)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<>
{!file?.enabled && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.fileUpload.description')}</div>
)}
{file?.enabled && (
<>
{!isHovering && !modalOpen && (
<div className='pt-0.5 flex items-center gap-4'>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.fileUpload.supportedTypes')}</div>
<div className='text-text-secondary system-xs-regular'>{supportedTypes}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.fileUpload.numberLimit')}</div>
<div className='text-text-secondary system-xs-regular'>{file?.number_limits}</div>
</div>
</div>
)}
{(isHovering || modalOpen) && (
<SettingModal
open={modalOpen && !disabled}
onOpen={(v) => {
setModalOpen(v)
setIsHovering(v)
}}
onChange={onChange}
>
<Button className='w-full' disabled={disabled}>
<RiEqualizer2Line className='mr-1 w-4 h-4' />
{t('common.operation.settings')}
</Button>
</SettingModal>
)}
</>
)}
</>
</FeatureCard>
)
}
export default FileUpload

View File

@@ -0,0 +1,89 @@
import React, { useCallback, useMemo, useState } from 'react'
import produce from 'immer'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import type { UploadFileSetting } from '@/app/components/workflow/types'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
type SettingContentProps = {
imageUpload?: boolean
onClose: () => void
onChange?: OnFeaturesChange
}
const SettingContent = ({
imageUpload,
onClose,
onChange,
}: SettingContentProps) => {
const { t } = useTranslation()
const featuresStore = useFeaturesStore()
const file = useFeatures(state => state.features.file)
const fileSettingPayload = useMemo(() => {
return {
allowed_file_upload_methods: file?.allowed_file_upload_methods || ['local_file', 'remote_url'],
allowed_file_types: file?.allowed_file_types || [SupportUploadFileTypes.image],
allowed_file_extensions: file?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image],
max_length: file?.number_limits || 3,
} as UploadFileSetting
}, [file])
const [tempPayload, setTempPayload] = useState<UploadFileSetting>(fileSettingPayload)
const handleChange = useCallback(() => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.file = {
...draft.file,
allowed_file_upload_methods: tempPayload.allowed_file_upload_methods,
number_limits: tempPayload.max_length,
allowed_file_types: tempPayload.allowed_file_types,
allowed_file_extensions: tempPayload.allowed_file_extensions,
}
})
setFeatures(newFeatures)
if (onChange)
onChange()
}, [featuresStore, onChange, tempPayload])
return (
<>
<div className='mb-4 flex items-center justify-between'>
<div className='text-text-primary system-xl-semibold'>{!imageUpload ? t('appDebug.feature.fileUpload.modalTitle') : t('appDebug.feature.imageUpload.modalTitle')}</div>
<div className='p-1 cursor-pointer' onClick={onClose}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
</div>
<FileUploadSetting
isMultiple
inFeaturePanel
hideSupportFileType={imageUpload}
payload={tempPayload}
onChange={(p: UploadFileSetting) => setTempPayload(p)}
/>
<div className='mt-4 flex items-center justify-end'>
<Button
onClick={onClose}
className='mr-2'
>
{t('common.operation.cancel')}
</Button>
<Button
variant='primary'
onClick={handleChange}
disabled={tempPayload.allowed_file_types.length === 0}
>
{t('common.operation.save')}
</Button>
</div>
</>
)
}
export default React.memo(SettingContent)

View File

@@ -0,0 +1,53 @@
'use client'
import { memo } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import SettingContent from '@/app/components/base/features/new-feature-panel/file-upload/setting-content'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
type FileUploadSettingsProps = {
open: boolean
onOpen: (state: any) => void
onChange?: OnFeaturesChange
disabled?: boolean
children?: React.ReactNode
imageUpload?: boolean
}
const FileUploadSettings = ({
open,
onOpen,
onChange,
disabled,
children,
imageUpload,
}: FileUploadSettingsProps) => {
return (
<PortalToFollowElem
open={open}
onOpenChange={onOpen}
placement='left'
offset={{
mainAxis: 32,
}}
>
<PortalToFollowElemTrigger className='flex' onClick={() => !disabled && onOpen((open: boolean) => !open)}>
{children}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-[360px] p-4 bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-2xl'>
<SettingContent
imageUpload={imageUpload}
onClose={() => onOpen(false)}
onChange={(v) => {
onChange?.(v)
onOpen(false)
}} />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(FileUploadSettings)

View File

@@ -0,0 +1,56 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { VirtualAssistant } from '@/app/components/base/icons/src/vender/features'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
}
const FollowUp = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
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'>
<VirtualAssistant className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
value={!!features.suggested?.enabled}
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')!}
onChange={state => handleChange(FeatureEnum.suggested, state)}
disabled={disabled}
/>
)
}
export default FollowUp

View File

@@ -0,0 +1,114 @@
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { RiEqualizer2Line, RiImage2Fill } from '@remixicon/react'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import SettingModal from '@/app/components/base/features/new-feature-panel/file-upload/setting-modal'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
type Props = {
disabled: boolean
onChange?: OnFeaturesChange
}
const FileUpload = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const file = useFeatures(s => s.features.file)
const featuresStore = useFeaturesStore()
const [modalOpen, setModalOpen] = useState(false)
const [isHovering, setIsHovering] = useState(false)
const supportedTypes = useMemo(() => {
return file?.allowed_file_types?.join(',') || '-'
}, [file?.allowed_file_types])
const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft[type] = {
...draft[type],
enabled,
image: { enabled },
}
})
setFeatures(newFeatures)
if (onChange)
onChange()
}, [featuresStore, onChange])
return (
<FeatureCard
icon={
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
<RiImage2Fill className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={
<div className='flex items-center'>
{t('appDebug.feature.imageUpload.title')}
<Badge
text='LEGACY'
className='shrink-0 mx-1 border-text-accent-secondary text-text-accent-secondary'
/>
</div>
}
value={file?.enabled}
onChange={state => handleChange(FeatureEnum.file, state)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<>
{!file?.enabled && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.imageUpload.description')}</div>
)}
{file?.enabled && (
<>
{!isHovering && !modalOpen && (
<div className='pt-0.5 flex items-center gap-4'>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.imageUpload.supportedTypes')}</div>
<div className='text-text-secondary system-xs-regular'>{supportedTypes}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.imageUpload.numberLimit')}</div>
<div className='text-text-secondary system-xs-regular'>{file?.number_limits}</div>
</div>
</div>
)}
{(isHovering || modalOpen) && (
<SettingModal
imageUpload
open={modalOpen && !disabled}
onOpen={(v) => {
setModalOpen(v)
setIsHovering(v)
}}
onChange={onChange}
>
<Button className='w-full' disabled={disabled}>
<RiEqualizer2Line className='mr-1 w-4 h-4' />
{t('common.operation.settings')}
</Button>
</SettingModal>
)}
</>
)}
</>
</FeatureCard>
)
}
export default FileUpload

View File

@@ -0,0 +1,126 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { RiCloseLine, RiInformation2Fill } from '@remixicon/react'
import DialogWrapper from '@/app/components/base/features/new-feature-panel/dialog-wrapper'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
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 FollowUp from '@/app/components/base/features/new-feature-panel/follow-up'
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'
import FileUpload from '@/app/components/base/features/new-feature-panel/file-upload'
import Citation from '@/app/components/base/features/new-feature-panel/citation'
import ImageUpload from '@/app/components/base/features/new-feature-panel/image-upload'
import Moderation from '@/app/components/base/features/new-feature-panel/moderation'
import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply'
import type { PromptVariable } from '@/models/debug'
import type { InputVar } from '@/app/components/workflow/types'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
type Props = {
show: boolean
isChatMode: boolean
disabled: boolean
onChange?: OnFeaturesChange
onClose: () => void
inWorkflow?: boolean
showFileUpload?: boolean
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}
const NewFeaturePanel = ({
show,
isChatMode,
disabled,
onChange,
onClose,
inWorkflow = true,
showFileUpload = true,
promptVariables,
workflowVariables,
onAutoAddPromptVariable,
}: Props) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
return (
<DialogWrapper
show={show}
onClose={onClose}
inWorkflow={inWorkflow}
>
<div className='grow flex flex-col h-full'>
{/* header */}
<div className='shrink-0 flex justify-between p-4 pb-3'>
<div>
<div className='text-text-primary system-xl-semibold'>{t('workflow.common.features')}</div>
<div className='text-text-tertiary body-xs-regular'>{t('workflow.common.featuresDescription')}</div>
</div>
<div className='w-8 h-8 p-2 cursor-pointer' onClick={onClose}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
</div>
{/* list */}
<div className='grow basis-0 overflow-y-auto px-4 pb-4'>
{showFileUpload && (
<div className='relative mb-1 p-2 rounded-xl border border-components-panel-border shadow-xs'>
<div className='absolute top-0 left-0 w-full h-full rounded-xl opacity-40' style={{ background: 'linear-gradient(92deg, rgba(11, 165, 236, 0.25) 18.12%, rgba(255, 255, 255, 0.00) 167.31%)' }}></div>
<div className='relative flex items-start w-full h-full'>
<div className='shrink-0 mr-0.5 p-0.5'>
<RiInformation2Fill className='w-5 h-5 text-text-accent' />
</div>
<div className='p-1 text-text-primary system-xs-medium'>
<span>{isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')}</span>
<a
className='text-text-accent'
href={`https://docs.dify.ai/${locale === LanguagesSupported[1] && 'v/zh-hans/'}'guides/workflow/bulletin`}
target='_blank' rel='noopener noreferrer'
>{t('workflow.common.featuresDocLink')}</a>
</div>
</div>
</div>
)}
{!isChatMode && !inWorkflow && (
<MoreLikeThis disabled={disabled} onChange={onChange} />
)}
{isChatMode && (
<ConversationOpener
disabled={disabled}
onChange={onChange}
promptVariables={promptVariables}
workflowVariables={workflowVariables}
onAutoAddPromptVariable={onAutoAddPromptVariable}
/>
)}
{isChatMode && (
<FollowUp disabled={disabled} onChange={onChange} />
)}
{text2speechDefaultModel && (isChatMode || !inWorkflow) && (
<TextToSpeech disabled={disabled} onChange={onChange} />
)}
{isChatMode && speech2textDefaultModel && (
<SpeechToText disabled={disabled} onChange={onChange} />
)}
{showFileUpload && isChatMode && <FileUpload disabled={disabled} onChange={onChange} />}
{showFileUpload && !isChatMode && <ImageUpload disabled={disabled} onChange={onChange} />}
{isChatMode && (
<Citation disabled={disabled} onChange={onChange} />
)}
{(isChatMode || !inWorkflow) && <Moderation disabled={disabled} onChange={onChange} />}
{!inWorkflow && isChatMode && (
<AnnotationReply disabled={disabled} onChange={onChange} />
)}
</div>
</div>
</DialogWrapper>
)
}
export default NewFeaturePanel

View File

@@ -1,9 +1,9 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useContext } from 'use-context-selector'
import type { CodeBasedExtensionForm } from '@/models/common'
import I18n from '@/context/i18n'
import { PortalSelect } from '@/app/components/base/select'
import Textarea from '@/app/components/base/textarea'
import type { ModerationConfig } from '@/models/debug'
type FormGenerationProps = {
@@ -45,10 +45,10 @@ const FormGeneration: FC<FormGenerationProps> = ({
}
{
form.type === 'paragraph' && (
<div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
<textarea
<div className='relative'>
<Textarea
className='resize-none'
value={value?.[form.variable] || ''}
className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
placeholder={form.placeholder}
onChange={e => handleFormChange(form.variable, e.target.value)}
/>
@@ -77,4 +77,4 @@ const FormGeneration: FC<FormGenerationProps> = ({
)
}
export default memo(FormGeneration)
export default FormGeneration

View File

@@ -0,0 +1,176 @@
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import produce from 'immer'
import { useContext } from 'use-context-selector'
import { RiEqualizer2Line } from '@remixicon/react'
import { ContentModeration } 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 } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
import { fetchCodeBasedExtensionList } from '@/service/common'
import { useModalContext } from '@/context/modal-context'
import I18n from '@/context/i18n'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
}
const Moderation = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const { setShowModerationSettingModal } = useModalContext()
const { locale } = useContext(I18n)
const featuresStore = useFeaturesStore()
const moderation = useFeatures(s => s.features.moderation)
const { data: codeBasedExtensionList } = useSWR(
'/code-based-extension?module=moderation',
fetchCodeBasedExtensionList,
)
const [isHovering, setIsHovering] = useState(false)
const handleOpenModerationSettingModal = () => {
if (disabled)
return
const {
features,
setFeatures,
} = featuresStore!.getState()
setShowModerationSettingModal({
payload: moderation as any,
onSaveCallback: (newModeration) => {
const newFeatures = produce(features, (draft) => {
draft.moderation = newModeration
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
},
onCancelCallback: () => {
if (onChange)
onChange()
},
})
}
const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
if (enabled && !features.moderation?.type && type === FeatureEnum.moderation) {
setShowModerationSettingModal({
payload: {
enabled: true,
type: 'keywords',
config: {
keywords: '',
inputs_config: {
enabled: true,
preset_response: '',
},
},
},
onSaveCallback: (newModeration) => {
const newFeatures = produce(features, (draft) => {
draft.moderation = newModeration
})
setFeatures(newFeatures)
if (onChange)
onChange(newFeatures)
},
onCancelCallback: () => {
const newFeatures = produce(features, (draft) => {
draft.moderation = { enabled: false }
})
setFeatures(newFeatures)
if (onChange)
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')
return t('appDebug.feature.moderation.modal.provider.openai')
else if (moderation?.type === 'keywords')
return t('appDebug.feature.moderation.modal.provider.keywords')
else if (moderation?.type === 'api')
return t('common.apiBasedExtension.selector.title')
else
return codeBasedExtensionList?.data.find(item => item.name === moderation?.type)?.label[locale] || '-'
}, [codeBasedExtensionList?.data, locale, moderation?.type, t])
const enableContent = useMemo(() => {
if (moderation?.config?.inputs_config?.enabled && moderation.config?.outputs_config?.enabled)
return t('appDebug.feature.moderation.allEnabled')
else if (moderation?.config?.inputs_config?.enabled)
return t('appDebug.feature.moderation.inputEnabled')
else if (moderation?.config?.outputs_config?.enabled)
return t('appDebug.feature.moderation.outputEnabled')
}, [moderation?.config?.inputs_config?.enabled, moderation?.config?.outputs_config?.enabled, t])
return (
<FeatureCard
icon={
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-text-success'>
<ContentModeration className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.moderation.title')}
value={!!moderation?.enabled}
onChange={state => handleChange(FeatureEnum.moderation, state)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<>
{!moderation?.enabled && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.moderation.description')}</div>
)}
{!!moderation?.enabled && (
<>
{!isHovering && (
<div className='pt-0.5 flex items-center gap-4'>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.moderation.modal.provider.title')}</div>
<div className='text-text-secondary system-xs-regular'>{providerContent}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.feature.moderation.contentEnableLabel')}</div>
<div className='text-text-secondary system-xs-regular'>{enableContent}</div>
</div>
</div>
)}
{isHovering && (
<Button className='w-full' onClick={handleOpenModerationSettingModal} disabled={disabled}>
<RiEqualizer2Line className='mr-1 w-4 h-4' />
{t('common.operation.settings')}
</Button>
)}
</>
)}
</>
</FeatureCard>
)
}
export default Moderation

View File

@@ -1,5 +1,4 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import Switch from '@/app/components/base/switch'
import type { ModerationContentConfig } from '@/models/debug'
@@ -70,4 +69,4 @@ const ModerationContent: FC<ModerationContentProps> = ({
)
}
export default memo(ModerationContent)
export default ModerationContent

View File

@@ -1,11 +1,9 @@
import type { ChangeEvent, FC } from 'react'
import {
memo,
useState,
} from 'react'
import { useState } from 'react'
import useSWR from 'swr'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import ModerationContent from './moderation-content'
import FormGeneration from './form-generation'
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
@@ -240,10 +238,11 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
<Modal
isShow
onClose={() => { }}
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
className='!p-6 !mt-14 !max-w-none !w-[600px]'
>
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
{t('appDebug.feature.moderation.modal.title')}
<div className='flex items-center justify-between'>
<div className='text-text-primary title-2xl-semi-bold'>{t('appDebug.feature.moderation.modal.title')}</div>
<div className='p-1 cursor-pointer' onClick={onCancel}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
</div>
<div className='py-2'>
<div className='leading-9 text-sm font-medium text-gray-900'>
@@ -373,4 +372,4 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
)
}
export default memo(ModerationSettingModal)
export default ModerationSettingModal

View File

@@ -0,0 +1,57 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { RiSparklingFill } from '@remixicon/react'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
type Props = {
disabled?: boolean
onChange?: OnFeaturesChange
}
const MoreLikeThis = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
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()
}, [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'>
<RiSparklingFill className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.moreLikeThis.title')}
tooltip={t('appDebug.feature.moreLikeThis.tip')}
value={!!features.moreLikeThis?.enabled}
description={t('appDebug.feature.moreLikeThis.description')!}
onChange={state => handleChange(FeatureEnum.moreLikeThis, state)}
disabled={disabled}
/>
)
}
export default MoreLikeThis

View File

@@ -0,0 +1,56 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { Microphone01 } from '@/app/components/base/icons/src/vender/features'
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
type Props = {
disabled: boolean
onChange?: OnFeaturesChange
}
const SpeechToText = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
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()
}, [featuresStore, onChange])
return (
<FeatureCard
icon={
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-violet-violet-600'>
<Microphone01 className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.speechToText.title')}
value={!!features.speech2text?.enabled}
description={t('appDebug.feature.speechToText.description')!}
onChange={state => handleChange(FeatureEnum.speech2text, state)}
disabled={disabled}
/>
)
}
export default SpeechToText

View File

@@ -0,0 +1,102 @@
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import { RiEqualizer2Line } from '@remixicon/react'
import { TextToAudio } 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 VoiceSettings from '@/app/components/base/features/new-feature-panel/text-to-speech/voice-settings'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import { FeatureEnum } from '@/app/components/base/features/types'
import { languages } from '@/i18n/language'
import { TtsAutoPlay } from '@/types/app'
type Props = {
disabled: boolean
onChange?: OnFeaturesChange
}
const TextToSpeech = ({
disabled,
onChange,
}: Props) => {
const { t } = useTranslation()
const textToSpeech = useFeatures(s => s.features.text2speech) // .language .voice .autoPlay
const languageInfo = languages.find(i => i.value === textToSpeech?.language)
const [modalOpen, setModalOpen] = useState(false)
const [isHovering, setIsHovering] = useState(false)
const features = useFeatures(s => s.features)
const featuresStore = useFeaturesStore()
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()
}, [featuresStore, onChange])
return (
<FeatureCard
icon={
<div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-violet-violet-600'>
<TextToAudio className='w-4 h-4 text-text-primary-on-surface' />
</div>
}
title={t('appDebug.feature.textToSpeech.title')}
value={!!features.text2speech?.enabled}
onChange={state => handleChange(FeatureEnum.text2speech, state)}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
disabled={disabled}
>
<>
{!features.text2speech?.enabled && (
<div className='min-h-8 text-text-tertiary system-xs-regular line-clamp-2'>{t('appDebug.feature.textToSpeech.description')}</div>
)}
{!!features.text2speech?.enabled && (
<>
{!isHovering && !modalOpen && (
<div className='pt-0.5 flex items-center gap-4'>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.voice.voiceSettings.language')}</div>
<div className='text-text-secondary system-xs-regular'>{languageInfo?.name || '-'}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.voice.voiceSettings.voice')}</div>
<div className='text-text-secondary system-xs-regular'>{features.text2speech?.voice || t('appDebug.voice.defaultDisplay')}</div>
</div>
<div className='w-px h-[27px] bg-divider-subtle rotate-12'></div>
<div className=''>
<div className='mb-0.5 text-text-tertiary system-2xs-medium-uppercase'>{t('appDebug.voice.voiceSettings.autoPlay')}</div>
<div className='text-text-secondary system-xs-regular'>{features.text2speech?.autoPlay === TtsAutoPlay.enabled ? t('appDebug.voice.voiceSettings.autoPlayEnabled') : t('appDebug.voice.voiceSettings.autoPlayDisabled')}</div>
</div>
</div>
)}
{(isHovering || modalOpen) && (
<VoiceSettings open={modalOpen && !disabled} onOpen={setModalOpen} onChange={onChange}>
<Button className='w-full' disabled={disabled}>
<RiEqualizer2Line className='mr-1 w-4 h-4' />
{t('appDebug.voice.voiceSettings.title')}
</Button>
</VoiceSettings>
)}
</>
)}
</>
</FeatureCard>
)
}
export default TextToSpeech

View File

@@ -0,0 +1,242 @@
'use client'
import useSWR from 'swr'
import produce from 'immer'
import React, { Fragment } from 'react'
import { usePathname } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import { Listbox, Transition } from '@headlessui/react'
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
import type { Item } from '@/app/components/base/select'
import { fetchAppVoices } from '@/service/apps'
import Tooltip from '@/app/components/base/tooltip'
import Switch from '@/app/components/base/switch'
import AudioBtn from '@/app/components/base/audio-btn'
import { languages } from '@/i18n/language'
import { TtsAutoPlay } from '@/types/app'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
import classNames from '@/utils/classnames'
type VoiceParamConfigProps = {
onClose: () => void
onChange?: OnFeaturesChange
}
const VoiceParamConfig = ({
onClose,
onChange,
}: VoiceParamConfigProps) => {
const { t } = useTranslation()
const pathname = usePathname()
const matched = pathname.match(/\/app\/([^/]+)/)
const appId = (matched?.length && matched[1]) ? matched[1] : ''
const text2speech = useFeatures(state => state.features.text2speech)
const featuresStore = useFeaturesStore()
let languageItem = languages.find(item => item.value === text2speech?.language)
if (languages && !languageItem)
languageItem = languages[0]
const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
const language = languageItem?.value
const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
let voiceItem = voiceItems?.find(item => item.value === text2speech?.voice)
if (voiceItems && !voiceItem)
voiceItem = voiceItems[0]
const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
const handleChange = (value: Record<string, string>) => {
const {
features,
setFeatures,
} = featuresStore!.getState()
const newFeatures = produce(features, (draft) => {
draft.text2speech = {
...draft.text2speech,
...value,
}
})
setFeatures(newFeatures)
if (onChange)
onChange()
}
return (
<>
<div className='mb-4 flex items-center justify-between'>
<div className='text-text-primary system-xl-semibold'>{t('appDebug.voice.voiceSettings.title')}</div>
<div className='p-1 cursor-pointer' onClick={onClose}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
</div>
<div className='mb-3'>
<div className='mb-1 py-1 flex items-center text-text-secondary system-sm-semibold'>
{t('appDebug.voice.voiceSettings.language')}
<Tooltip
popupContent={
<div className='w-[180px]'>
{t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
<div key={item}>{item}
</div>
))}
</div>
}
/>
</div>
<Listbox
value={languageItem}
onChange={(value: Item) => {
handleChange({
language: String(value.value),
})
}}
>
<div className='relative h-8'>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span className={classNames('block truncate text-left', !languageItem?.name && 'text-gray-400')}>
{languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{languages.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span
className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
{(selected || item.value === text2speech?.language) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
<div className='mb-3'>
<div className='mb-1 py-1 text-text-secondary system-sm-semibold'>
{t('appDebug.voice.voiceSettings.voice')}
</div>
<div className='flex items-center gap-1'>
<Listbox
value={voiceItem ?? {}}
disabled={!languageItem}
onChange={(value: Item) => {
handleChange({
voice: String(value.value),
})
}}
>
<div className={'grow relative h-8'}>
<Listbox.Button
className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
<span
className={classNames('block truncate text-left', !voiceItem?.name && 'text-gray-400')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
{voiceItems?.map((item: Item) => (
<Listbox.Option
key={item.value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
}`
}
value={item}
disabled={false}
>
{({ /* active, */ selected }) => (
<>
<span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
{(selected || item.value === text2speech?.voice) && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
)}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
{languageItem?.example && (
<div className='shrink-0 h-8 p-1 rounded-lg bg-components-button-tertiary-bg'>
<AudioBtn
value={languageItem?.example}
isAudition
voice={text2speech?.voice}
noCache
/>
</div>
)}
</div>
</div>
<div>
<div className='mb-1 py-1 text-text-secondary system-sm-semibold'>
{t('appDebug.voice.voiceSettings.autoPlay')}
</div>
<Switch className='shrink-0'
defaultValue={text2speech?.autoPlay === TtsAutoPlay.enabled}
onChange={(value: boolean) => {
handleChange({
autoPlay: value ? TtsAutoPlay.enabled : TtsAutoPlay.disabled,
})
}}
/>
</div>
</>
)
}
export default React.memo(VoiceParamConfig)

View File

@@ -0,0 +1,47 @@
'use client'
import { memo } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import ParamConfigContent from '@/app/components/base/features/new-feature-panel/text-to-speech/param-config-content'
import type { OnFeaturesChange } from '@/app/components/base/features/types'
type VoiceSettingsProps = {
open: boolean
onOpen: (state: any) => void
onChange?: OnFeaturesChange
disabled?: boolean
children?: React.ReactNode
placementLeft?: boolean
}
const VoiceSettings = ({
open,
onOpen,
onChange,
disabled,
children,
placementLeft = true,
}: VoiceSettingsProps) => {
return (
<PortalToFollowElem
open={open}
onOpenChange={onOpen}
placement={placementLeft ? 'left' : 'top'}
offset={{
mainAxis: placementLeft ? 32 : 4,
}}
>
<PortalToFollowElemTrigger className='flex' onClick={() => !disabled && onOpen((open: boolean) => !open)}>
{children}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 50 }}>
<div className='w-[360px] p-4 bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-2xl'>
<ParamConfigContent onClose={() => onOpen(false)} onChange={onChange} />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(VoiceSettings)

View File

@@ -1,6 +1,6 @@
import { createStore } from 'zustand'
import type { Features } from './types'
import { TransferMethod } from '@/types/app'
import { Resolution, TransferMethod } from '@/types/app'
export type FeaturesModal = {
showFeaturesModal: boolean
@@ -22,6 +22,9 @@ export type FeaturesStore = ReturnType<typeof createFeaturesStore>
export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
const DEFAULT_PROPS: FeaturesState = {
features: {
moreLikeThis: {
enabled: false,
},
opening: {
enabled: false,
},
@@ -43,10 +46,14 @@ export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
file: {
image: {
enabled: false,
detail: Resolution.high,
number_limits: 3,
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
},
},
annotationReply: {
enabled: false,
},
},
}
return createStore<FeatureStoreState>()(set => ({

View File

@@ -1,9 +1,11 @@
import type { TransferMethod, TtsAutoPlay } from '@/types/app'
import type { Resolution, TransferMethod, TtsAutoPlay } from '@/types/app'
export type EnabledOrDisabled = {
enabled?: boolean
}
export type MoreLikeThis = EnabledOrDisabled
export type OpeningStatement = EnabledOrDisabled & {
opening_statement?: string
suggested_questions?: string[]
@@ -28,12 +30,28 @@ export type SensitiveWordAvoidance = EnabledOrDisabled & {
export type FileUpload = {
image?: EnabledOrDisabled & {
detail?: Resolution
number_limits?: number
transfer_methods?: TransferMethod[]
}
allowed_file_types?: string[]
allowed_file_extensions?: string[]
allowed_file_upload_methods?: TransferMethod[]
number_limits?: number
} & EnabledOrDisabled
export type AnnotationReplyConfig = {
enabled: boolean
id?: string
score_threshold?: number
embedding_model?: {
embedding_provider_name: string
embedding_model_name: string
}
}
export enum FeatureEnum {
moreLikeThis = 'moreLikeThis',
opening = 'opening',
suggested = 'suggested',
text2speech = 'text2speech',
@@ -41,9 +59,11 @@ export enum FeatureEnum {
citation = 'citation',
moderation = 'moderation',
file = 'file',
annotationReply = 'annotationReply',
}
export type Features = {
[FeatureEnum.moreLikeThis]?: MoreLikeThis
[FeatureEnum.opening]?: OpeningStatement
[FeatureEnum.suggested]?: SuggestedQuestionsAfterAnswer
[FeatureEnum.text2speech]?: TextToSpeech
@@ -51,6 +71,7 @@ export type Features = {
[FeatureEnum.citation]?: RetrieverResource
[FeatureEnum.moderation]?: SensitiveWordAvoidance
[FeatureEnum.file]?: FileUpload
[FeatureEnum.annotationReply]?: AnnotationReplyConfig
}
export type OnFeaturesChange = (features: Features) => void
export type OnFeaturesChange = (features?: Features) => void