Feat/attachments (#9526)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: JzoNg <jzongcode@gmail.com>
@@ -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)
|
||||
@@ -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)
|
||||
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -1,3 +1 @@
|
||||
export { default as FeaturesPanel } from './feature-panel'
|
||||
export { default as FeaturesChoose } from './feature-choose'
|
||||
export { FeaturesProvider } from './context'
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum PageType {
|
||||
log = 'log',
|
||||
annotation = 'annotation',
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
126
web/app/components/base/features/new-feature-panel/index.tsx
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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
|
||||
|
||||