features
27
web/app/components/base/features/context.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
createContext,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import type {
|
||||
FeaturesState,
|
||||
FeaturesStore,
|
||||
} from './store'
|
||||
import { createFeaturesStore } from './store'
|
||||
|
||||
export const FeaturesContext = createContext<FeaturesStore | null>(null)
|
||||
|
||||
type FeaturesProviderProps = {
|
||||
children: React.ReactNode
|
||||
} & Partial<FeaturesState>
|
||||
export const FeaturesProvider = ({ children, ...props }: FeaturesProviderProps) => {
|
||||
const storeRef = useRef<FeaturesStore>()
|
||||
|
||||
if (!storeRef.current)
|
||||
storeRef.current = createFeaturesStore(props)
|
||||
|
||||
return (
|
||||
<FeaturesContext.Provider value={storeRef.current}>
|
||||
{children}
|
||||
</FeaturesContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'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)
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
export type IFeatureItemProps = {
|
||||
icon: React.ReactNode
|
||||
previewImgClassName?: string
|
||||
title: string
|
||||
description: string
|
||||
value: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const FeatureItem: FC<IFeatureItemProps> = ({
|
||||
icon,
|
||||
previewImgClassName,
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
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={onChange} defaultValue={value} />
|
||||
{
|
||||
previewImgClassName && (
|
||||
<div className={cn(s.preview, s[previewImgClassName])}>
|
||||
</div>)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FeatureItem)
|
||||
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 211 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,39 @@
|
||||
.preview {
|
||||
display: none;
|
||||
position: absolute;
|
||||
transform: translate(480px, -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(./preview-imgs/opening-statement.png);
|
||||
}
|
||||
|
||||
.suggestedQuestionsAfterAnswerPreview {
|
||||
background-image: url(./preview-imgs/suggested-questions-after-answer.svg);
|
||||
}
|
||||
|
||||
.moreLikeThisPreview {
|
||||
background-image: url(./preview-imgs/more-like-this.svg);
|
||||
}
|
||||
|
||||
.speechToTextPreview {
|
||||
background-image: url(./preview-imgs/speech-to-text.svg);
|
||||
}
|
||||
|
||||
.textToSpeechPreview {
|
||||
@apply shadow-lg rounded-lg;
|
||||
background-image: url(./preview-imgs/text-to-audio-preview-assistant@2x.png);
|
||||
}
|
||||
|
||||
.citationPreview {
|
||||
background-image: url(./preview-imgs/citation.svg);
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFeatures } from '../hooks'
|
||||
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 {
|
||||
MessageFast,
|
||||
MessageHeartCircle,
|
||||
} from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
|
||||
export type ChooseFeatureProps = {
|
||||
showTextToSpeechItem?: boolean
|
||||
showSpeechToTextItem?: boolean
|
||||
}
|
||||
|
||||
const ChooseFeature: FC<ChooseFeatureProps> = ({
|
||||
showTextToSpeechItem,
|
||||
showSpeechToTextItem,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const setShowFeaturesModal = useFeatures(s => s.setShowFeaturesModal)
|
||||
const openingStatement = useFeatures(s => s.openingStatement)
|
||||
const setOpeningStatement = useFeatures(s => s.setOpeningStatement)
|
||||
const suggestedQuestionsAfterAnswer = useFeatures(s => s.suggestedQuestionsAfterAnswer)
|
||||
const setSuggestedQuestionsAfterAnswer = useFeatures(s => s.setSuggestedQuestionsAfterAnswer)
|
||||
const textToSpeech = useFeatures(s => s.textToSpeech)
|
||||
const setTextToSpeech = useFeatures(s => s.setTextToSpeech)
|
||||
const speechToText = useFeatures(s => s.speechToText)
|
||||
const setSpeechToText = useFeatures(s => s.setSpeechToText)
|
||||
const citation = useFeatures(s => s.citation)
|
||||
const setCitation = useFeatures(s => s.setCitation)
|
||||
const moderation = useFeatures(s => s.moderation)
|
||||
const setModeration = useFeatures(s => s.setModeration)
|
||||
const annotation = useFeatures(s => s.annotation)
|
||||
const setAnnotation = useFeatures(s => s.setAnnotation)
|
||||
|
||||
const handleCancelModal = useCallback(() => {
|
||||
setShowFeaturesModal(false)
|
||||
}, [setShowFeaturesModal])
|
||||
|
||||
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={openingStatement.enabled}
|
||||
onChange={value => setOpeningStatement(produce(openingStatement, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
<FeatureItem
|
||||
icon={<SuggestedQuestionsAfterAnswerIcon />}
|
||||
previewImgClassName='suggestedQuestionsAfterAnswerPreview'
|
||||
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
|
||||
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
|
||||
value={suggestedQuestionsAfterAnswer.enabled}
|
||||
onChange={value => setSuggestedQuestionsAfterAnswer(produce(suggestedQuestionsAfterAnswer, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
{
|
||||
showTextToSpeechItem && (
|
||||
<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={textToSpeech.enabled}
|
||||
onChange={value => setTextToSpeech(produce(textToSpeech, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showSpeechToTextItem && (
|
||||
<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={speechToText.enabled}
|
||||
onChange={value => setSpeechToText(produce(speechToText, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<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={citation.enabled}
|
||||
onChange={value => setCitation(produce(citation, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
</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={moderation.enabled}
|
||||
onChange={value => setModeration(produce(moderation, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
<FeatureItem
|
||||
icon={<MessageFast className='w-4 h-4 text-[#444CE7]' />}
|
||||
title={t('appDebug.feature.annotation.title')}
|
||||
description={t('appDebug.feature.annotation.description')}
|
||||
value={annotation.enabled}
|
||||
onChange={value => setAnnotation(produce(annotation, (draft) => {
|
||||
draft.enabled = value
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
</FeatureGroup>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChooseFeature)
|
||||
30
web/app/components/base/features/feature-choose/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFeatures } from '../hooks'
|
||||
import FeatureModal from './feature-modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Plus02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const ChooseFeature = () => {
|
||||
const { t } = useTranslation()
|
||||
const showFeaturesModal = useFeatures(s => s.showFeaturesModal)
|
||||
const setShowFeaturesModal = useFeatures(s => s.setShowFeaturesModal)
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className='px-3 py-0 h-8 rounded-lg border border-primary-100 bg-primary-25 shadow-xs text-xs font-semibold text-primary-600'
|
||||
onClick={() => setShowFeaturesModal(true)}
|
||||
>
|
||||
<Plus02 className='mr-1 w-4 h-4' />
|
||||
{t('appDebug.operation.addFeature')}
|
||||
</Button>
|
||||
{
|
||||
showFeaturesModal && (
|
||||
<FeatureModal />
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChooseFeature)
|
||||
@@ -0,0 +1,135 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useHover } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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 TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
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(className, 'inline-block')}>
|
||||
<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
|
||||
? (
|
||||
<TooltipPlus
|
||||
popupContent={t('appDebug.feature.annotation.add') as string}
|
||||
>
|
||||
<div
|
||||
className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
|
||||
onClick={handleAdd}
|
||||
>
|
||||
<MessageFastPlus className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<TooltipPlus
|
||||
popupContent={t('appDebug.feature.annotation.edit') as string}
|
||||
>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Edit04 className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
|
||||
</div>
|
||||
<RemoveAnnotationConfirmModal
|
||||
isShow={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CacheCtrlBtn)
|
||||
@@ -0,0 +1,140 @@
|
||||
'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'
|
||||
|
||||
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(2)
|
||||
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]'
|
||||
wrapperClassName='!z-50'
|
||||
>
|
||||
<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 className='!text-sm' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={handleSave}
|
||||
className='flex items-center border-[0.5px] !text-sm'
|
||||
loading={isLoading}
|
||||
>
|
||||
<div></div>
|
||||
<div>{t(`appAnnotation.initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`)}</div>
|
||||
</Button >
|
||||
</div >
|
||||
</Modal >
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigParamModal)
|
||||
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import ConfigParamModal from './config-param-modal'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { HelpCircle, LinkExternal02, Settings04 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
import { updateAnnotationScore } from '@/service/annotation'
|
||||
|
||||
export type AnnotationProps = {
|
||||
onEmbeddingChange: (embeddingModel: EmbeddingModelConfig) => void
|
||||
onScoreChange: (score: number, embeddingModel?: EmbeddingModelConfig) => void
|
||||
}
|
||||
|
||||
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>
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div className='max-w-[200px] leading-[18px] text-[13px] font-medium text-gray-800'>{tooltip}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='w-3.5 h-3.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AnnotationReplyConfig: FC<AnnotationProps> = ({
|
||||
onEmbeddingChange,
|
||||
onScoreChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const matched = pathname.match(/\/app\/([^/]+)/)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const {
|
||||
annotationConfig,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
const [isShowEdit, setIsShowEdit] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
className="mt-4"
|
||||
headerIcon={
|
||||
<MessageFast className='w-4 h-4 text-[#444CE7]' />
|
||||
}
|
||||
title={t('appDebug.feature.annotation.title')}
|
||||
headerRight={
|
||||
<div className='flex items-center'>
|
||||
<div
|
||||
className='flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200'
|
||||
onClick={() => { setIsShowEdit(true) }}
|
||||
>
|
||||
<Settings04 className="w-[14px] h-[14px]" />
|
||||
<div className='text-xs font-medium'>
|
||||
|
||||
{t('common.operation.params')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='ml-1 flex items-center h-7 px-3 space-x-1 leading-[18px] text-xs font-medium text-gray-700 rounded-md cursor-pointer hover:bg-gray-200'
|
||||
onClick={() => {
|
||||
router.push(`/app/${appId}/annotations`)
|
||||
}}>
|
||||
<div>{t('appDebug.feature.annotation.cacheManagement')}</div>
|
||||
<LinkExternal02 className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
noBodySpacing
|
||||
/>
|
||||
{isShowEdit && (
|
||||
<ConfigParamModal
|
||||
appId={appId}
|
||||
isShow
|
||||
onHide={() => {
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
onSave={async (embeddingModel, score) => {
|
||||
let isEmbeddingModelChanged = false
|
||||
if (
|
||||
embeddingModel.embedding_model_name !== annotationConfig.embedding_model.embedding_model_name
|
||||
&& embeddingModel.embedding_provider_name !== annotationConfig.embedding_model.embedding_provider_name
|
||||
) {
|
||||
await onEmbeddingChange(embeddingModel)
|
||||
isEmbeddingModelChanged = true
|
||||
}
|
||||
|
||||
if (score !== annotationConfig.score_threshold) {
|
||||
await updateAnnotationScore(appId, annotationConfig.id, score)
|
||||
if (isEmbeddingModelChanged)
|
||||
onScoreChange(score, embeddingModel)
|
||||
|
||||
else
|
||||
onScoreChange(score)
|
||||
}
|
||||
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
annotationConfig={annotationConfig}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(AnnotationReplyConfig)
|
||||
@@ -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,25 @@
|
||||
'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)
|
||||
116
web/app/components/base/features/feature-panel/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFeatures } from '../hooks'
|
||||
import OpeningStatement from './opening-statement'
|
||||
import type { OpeningStatementProps } from './opening-statement'
|
||||
import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
|
||||
import TextToSpeech from './text-to-speech'
|
||||
import SpeechToText from './speech-to-text'
|
||||
import Citation from './citation'
|
||||
import Moderation from './moderation'
|
||||
import Annotation from './annotation/config-param'
|
||||
import type { AnnotationProps } from './annotation/config-param'
|
||||
|
||||
export type FeaturePanelProps = {
|
||||
openingStatementProps: OpeningStatementProps
|
||||
annotationProps: AnnotationProps
|
||||
}
|
||||
const FeaturePanel = ({
|
||||
openingStatementProps,
|
||||
annotationProps,
|
||||
}: FeaturePanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const openingStatement = useFeatures(s => s.openingStatement)
|
||||
const suggestedQuestionsAfterAnswer = useFeatures(s => s.suggestedQuestionsAfterAnswer)
|
||||
const textToSpeech = useFeatures(s => s.textToSpeech)
|
||||
const speechToText = useFeatures(s => s.speechToText)
|
||||
const citation = useFeatures(s => s.citation)
|
||||
const moderation = useFeatures(s => s.moderation)
|
||||
const annotation = useFeatures(s => s.annotation)
|
||||
|
||||
const showAdvanceFeature = useMemo(() => {
|
||||
return openingStatement.enabled || suggestedQuestionsAfterAnswer.enabled || textToSpeech.enabled || speechToText.enabled || citation.enabled
|
||||
}, [openingStatement, suggestedQuestionsAfterAnswer, textToSpeech, speechToText, citation])
|
||||
|
||||
const showToolFeature = useMemo(() => {
|
||||
return moderation.enabled || annotation.enabled
|
||||
}, [moderation, annotation])
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
{
|
||||
showAdvanceFeature && (
|
||||
<div>
|
||||
<div className='flex items-center'>
|
||||
<div className='shrink-0 text-xs font-semibold text-gray-500'>
|
||||
{t('appDebug.feature.groupChat.title')}
|
||||
</div>
|
||||
<div
|
||||
className='grow ml-3 h-[1px]'
|
||||
style={{ background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)' }}
|
||||
></div>
|
||||
</div>
|
||||
<div className='py-2 space-y-2'>
|
||||
{
|
||||
openingStatement.enabled && (
|
||||
<OpeningStatement {...openingStatementProps} />
|
||||
)
|
||||
}
|
||||
{
|
||||
suggestedQuestionsAfterAnswer.enabled && (
|
||||
<SuggestedQuestionsAfterAnswer />
|
||||
)
|
||||
}
|
||||
{
|
||||
textToSpeech.enabled && (
|
||||
<TextToSpeech />
|
||||
)
|
||||
}
|
||||
{
|
||||
speechToText.enabled && (
|
||||
<SpeechToText />
|
||||
)
|
||||
}
|
||||
{
|
||||
citation.enabled && (
|
||||
<Citation />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showToolFeature && (
|
||||
<div>
|
||||
<div className='flex items-center'>
|
||||
<div className='shrink-0 text-xs font-semibold text-gray-500'>
|
||||
{t('appDebug.feature.groupChat.title')}
|
||||
</div>
|
||||
<div
|
||||
className='grow ml-3 h-[1px]'
|
||||
style={{ background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)' }}
|
||||
></div>
|
||||
</div>
|
||||
<div className='py-2 space-y-2'>
|
||||
{
|
||||
moderation.enabled && (
|
||||
<Moderation />
|
||||
)
|
||||
}
|
||||
{
|
||||
annotation.enabled && (
|
||||
<Annotation {...annotationProps} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default memo(FeaturePanel)
|
||||
@@ -0,0 +1,80 @@
|
||||
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 type { ModerationConfig } from '@/models/debug'
|
||||
|
||||
type FormGenerationProps = {
|
||||
forms: CodeBasedExtensionForm[]
|
||||
value: ModerationConfig['config']
|
||||
onChange: (v: Record<string, string>) => void
|
||||
}
|
||||
const FormGeneration: FC<FormGenerationProps> = ({
|
||||
forms,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
const handleFormChange = (type: string, v: string) => {
|
||||
onChange({ ...value, [type]: v })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
forms.map((form, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='py-2'
|
||||
>
|
||||
<div className='flex items-center h-9 text-sm font-medium text-gray-900'>
|
||||
{locale === 'zh-Hans' ? form.label['zh-Hans'] : form.label['en-US']}
|
||||
</div>
|
||||
{
|
||||
form.type === 'text-input' && (
|
||||
<input
|
||||
value={value?.[form.variable] || ''}
|
||||
className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
|
||||
placeholder={form.placeholder}
|
||||
onChange={e => handleFormChange(form.variable, e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
form.type === 'paragraph' && (
|
||||
<div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
|
||||
<textarea
|
||||
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)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
form.type === 'select' && (
|
||||
<PortalSelect
|
||||
value={value?.[form.variable]}
|
||||
items={form.options.map((option) => {
|
||||
return {
|
||||
name: option.label[locale === 'zh-Hans' ? 'zh-Hans' : 'en-US'],
|
||||
value: option.value,
|
||||
}
|
||||
})}
|
||||
onSelect={item => handleFormChange(form.variable, item.value as string)}
|
||||
popupClassName='w-[576px] !z-[102]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FormGeneration)
|
||||
@@ -0,0 +1,81 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useFeatures } from '../../hooks'
|
||||
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'
|
||||
|
||||
const Moderation = () => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowModerationSettingModal } = useModalContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const moderation = useFeatures(s => s.moderation)
|
||||
const setModeration = useFeatures(s => s.setModeration)
|
||||
|
||||
const { data: codeBasedExtensionList } = useSWR(
|
||||
'/code-based-extension?module=moderation',
|
||||
fetchCodeBasedExtensionList,
|
||||
)
|
||||
|
||||
const handleOpenModerationSettingModal = () => {
|
||||
setShowModerationSettingModal({
|
||||
payload: moderation,
|
||||
onSaveCallback: setModeration,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
`}
|
||||
onClick={handleOpenModerationSettingModal}
|
||||
>
|
||||
<Settings01 className='mr-[5px] w-3.5 h-3.5' />
|
||||
{t('common.operation.settings')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Moderation)
|
||||
@@ -0,0 +1,73 @@
|
||||
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'
|
||||
|
||||
type ModerationContentProps = {
|
||||
title: string
|
||||
info?: string
|
||||
showPreset?: boolean
|
||||
config: ModerationContentConfig
|
||||
onConfigChange: (config: ModerationContentConfig) => void
|
||||
}
|
||||
const ModerationContent: FC<ModerationContentProps> = ({
|
||||
title,
|
||||
info,
|
||||
showPreset = true,
|
||||
config,
|
||||
onConfigChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleConfigChange = (field: string, value: boolean | string) => {
|
||||
if (field === 'preset_response' && typeof value === 'string')
|
||||
value = value.slice(0, 100)
|
||||
onConfigChange({ ...config, [field]: value })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-2'>
|
||||
<div className='rounded-lg bg-gray-50 border border-gray-200'>
|
||||
<div className='flex items-center justify-between px-3 h-10 rounded-lg'>
|
||||
<div className='shrink-0 text-sm font-medium text-gray-900'>{title}</div>
|
||||
<div className='grow flex items-center justify-end'>
|
||||
{
|
||||
info && (
|
||||
<div className='mr-2 text-xs text-gray-500 truncate' title={info}>{info}</div>
|
||||
)
|
||||
}
|
||||
<Switch
|
||||
size='l'
|
||||
defaultValue={config.enabled}
|
||||
onChange={v => handleConfigChange('enabled', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
config.enabled && showPreset && (
|
||||
<div className='px-3 pt-1 pb-3 bg-white rounded-lg'>
|
||||
<div className='flex items-center justify-between h-8 text-[13px] font-medium text-gray-700'>
|
||||
{t('appDebug.feature.moderation.modal.content.preset')}
|
||||
<span className='text-xs font-normal text-gray-500'>{t('appDebug.feature.moderation.modal.content.supportMarkdown')}</span>
|
||||
</div>
|
||||
<div className='relative px-3 py-2 h-20 rounded-lg bg-gray-100'>
|
||||
<textarea
|
||||
value={config.preset_response || ''}
|
||||
className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
|
||||
placeholder={t('appDebug.feature.moderation.modal.content.placeholder') || ''}
|
||||
onChange={e => handleConfigChange('preset_response', e.target.value)}
|
||||
/>
|
||||
<div className='absolute bottom-2 right-2 flex items-center px-1 h-5 rounded-md bg-gray-50 text-xs font-medium text-gray-300'>
|
||||
<span>{(config.preset_response || '').length}</span>/<span className='text-gray-500'>100</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModerationContent)
|
||||
@@ -0,0 +1,377 @@
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModerationContent from './moderation-content'
|
||||
import FormGeneration from './form-generation'
|
||||
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
|
||||
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
fetchCodeBasedExtensionList,
|
||||
fetchModelProviders,
|
||||
} from '@/service/common'
|
||||
import type { CodeBasedExtensionItem } from '@/models/common'
|
||||
import I18n from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
const systemTypes = ['openai_moderation', 'keywords', 'api']
|
||||
|
||||
type Provider = {
|
||||
key: string
|
||||
name: string
|
||||
form_schema?: CodeBasedExtensionItem['form_schema']
|
||||
}
|
||||
|
||||
type ModerationSettingModalProps = {
|
||||
data: ModerationConfig
|
||||
onCancel: () => void
|
||||
onSave: (moderationConfig: ModerationConfig) => void
|
||||
}
|
||||
|
||||
const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
data,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const { data: modelProviders, isLoading, mutate } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const handleOpenSettingsModal = () => {
|
||||
setShowAccountSettingModal({
|
||||
payload: 'provider',
|
||||
onCancelCallback: () => {
|
||||
mutate()
|
||||
},
|
||||
})
|
||||
}
|
||||
const { data: codeBasedExtensionList } = useSWR(
|
||||
'/code-based-extension?module=moderation',
|
||||
fetchCodeBasedExtensionList,
|
||||
)
|
||||
const openaiProvider = modelProviders?.data.find(item => item.provider === 'openai')
|
||||
const systemOpenaiProviderEnabled = openaiProvider?.system_configuration.enabled
|
||||
const systemOpenaiProviderQuota = systemOpenaiProviderEnabled ? openaiProvider?.system_configuration.quota_configurations.find(item => item.quota_type === openaiProvider.system_configuration.current_quota_type) : undefined
|
||||
const systemOpenaiProviderCanUse = systemOpenaiProviderQuota?.is_valid
|
||||
const customOpenaiProvidersCanUse = openaiProvider?.custom_configuration.status === CustomConfigurationStatusEnum.active
|
||||
const openaiProviderConfiged = customOpenaiProvidersCanUse || systemOpenaiProviderCanUse
|
||||
const providers: Provider[] = [
|
||||
{
|
||||
key: 'openai_moderation',
|
||||
name: t('appDebug.feature.moderation.modal.provider.openai'),
|
||||
},
|
||||
{
|
||||
key: 'keywords',
|
||||
name: t('appDebug.feature.moderation.modal.provider.keywords'),
|
||||
},
|
||||
{
|
||||
key: 'api',
|
||||
name: t('common.apiBasedExtension.selector.title'),
|
||||
},
|
||||
...(
|
||||
codeBasedExtensionList
|
||||
? codeBasedExtensionList.data.map((item) => {
|
||||
return {
|
||||
key: item.name,
|
||||
name: locale === 'zh-Hans' ? item.label['zh-Hans'] : item.label['en-US'],
|
||||
form_schema: item.form_schema,
|
||||
}
|
||||
})
|
||||
: []
|
||||
),
|
||||
]
|
||||
|
||||
const currentProvider = providers.find(provider => provider.key === localeData.type)
|
||||
|
||||
const handleDataTypeChange = (type: string) => {
|
||||
let config: undefined | Record<string, any>
|
||||
const currProvider = providers.find(provider => provider.key === type)
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currProvider?.form_schema) {
|
||||
config = currProvider?.form_schema.reduce((prev, next) => {
|
||||
prev[next.variable] = next.default
|
||||
return prev
|
||||
}, {} as Record<string, any>)
|
||||
}
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
type,
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataKeywordsChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
|
||||
const arr = value.split('\n').reduce((prev: string[], next: string) => {
|
||||
if (next !== '')
|
||||
prev.push(next.slice(0, 100))
|
||||
if (next === '' && prev[prev.length - 1] !== '')
|
||||
prev.push(next)
|
||||
|
||||
return prev
|
||||
}, [])
|
||||
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
config: {
|
||||
...localeData.config,
|
||||
keywords: arr.slice(0, 100).join('\n'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataContentChange = (contentType: string, contentConfig: ModerationContentConfig) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
config: {
|
||||
...localeData.config,
|
||||
[contentType]: contentConfig,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataApiBasedChange = (apiBasedExtensionId: string) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
config: {
|
||||
...localeData.config,
|
||||
api_based_extension_id: apiBasedExtensionId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleDataExtraChange = (extraValue: Record<string, string>) => {
|
||||
setLocaleData({
|
||||
...localeData,
|
||||
config: {
|
||||
...localeData.config,
|
||||
...extraValue,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const formatData = (originData: ModerationConfig) => {
|
||||
const { enabled, type, config } = originData
|
||||
const { inputs_config, outputs_config } = config!
|
||||
const params: Record<string, string | undefined> = {}
|
||||
|
||||
if (type === 'keywords')
|
||||
params.keywords = config?.keywords
|
||||
|
||||
if (type === 'api')
|
||||
params.api_based_extension_id = config?.api_based_extension_id
|
||||
|
||||
if (systemTypes.findIndex(t => t === type) < 0 && currentProvider?.form_schema) {
|
||||
currentProvider.form_schema.forEach((form) => {
|
||||
params[form.variable] = config?.[form.variable]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
enabled,
|
||||
config: {
|
||||
inputs_config: inputs_config || { enabled: false },
|
||||
outputs_config: outputs_config || { enabled: false },
|
||||
...params,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (localeData.type === 'openai_moderation' && !openaiProviderConfiged)
|
||||
return
|
||||
|
||||
if (!localeData.config?.inputs_config?.enabled && !localeData.config?.outputs_config?.enabled) {
|
||||
notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.condition') })
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'keywords' && !localeData.config.keywords) {
|
||||
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'keywords' : '关键词' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.type === 'api' && !localeData.config.api_based_extension_id) {
|
||||
notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? 'API Extension' : 'API 扩展' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (systemTypes.findIndex(t => t === localeData.type) < 0 && currentProvider?.form_schema) {
|
||||
for (let i = 0; i < currentProvider.form_schema.length; i++) {
|
||||
if (!localeData.config?.[currentProvider.form_schema[i].variable] && currentProvider.form_schema[i].required) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('appDebug.errorMessage.valueOfVarRequired', { key: locale !== LanguagesSupported[1] ? currentProvider.form_schema[i].label['en-US'] : currentProvider.form_schema[i].label['zh-Hans'] }),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localeData.config.inputs_config?.enabled && !localeData.config.inputs_config.preset_response && localeData.type !== 'api') {
|
||||
notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.errorMessage') })
|
||||
return
|
||||
}
|
||||
|
||||
if (localeData.config.outputs_config?.enabled && !localeData.config.outputs_config.preset_response && localeData.type !== 'api') {
|
||||
notify({ type: 'error', message: t('appDebug.feature.moderation.modal.content.errorMessage') })
|
||||
return
|
||||
}
|
||||
|
||||
onSave(formatData(localeData))
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => { }}
|
||||
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
|
||||
>
|
||||
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
|
||||
{t('appDebug.feature.moderation.modal.title')}
|
||||
</div>
|
||||
<div className='py-2'>
|
||||
<div className='leading-9 text-sm font-medium text-gray-900'>
|
||||
{t('appDebug.feature.moderation.modal.provider.title')}
|
||||
</div>
|
||||
<div className='grid gap-2.5 grid-cols-3'>
|
||||
{
|
||||
providers.map(provider => (
|
||||
<div
|
||||
key={provider.key}
|
||||
className={`
|
||||
flex items-center px-3 py-2 rounded-lg text-sm text-gray-900 cursor-pointer
|
||||
${localeData.type === provider.key ? 'bg-white border-[1.5px] border-primary-400 shadow-sm' : 'border border-gray-100 bg-gray-25'}
|
||||
${localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !openaiProviderConfiged && 'opacity-50'}
|
||||
`}
|
||||
onClick={() => handleDataTypeChange(provider.key)}
|
||||
>
|
||||
<div className={`
|
||||
mr-2 w-4 h-4 rounded-full border
|
||||
${localeData.type === provider.key ? 'border-[5px] border-primary-600' : 'border border-gray-300'}`} />
|
||||
{provider.name}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!isLoading && !openaiProviderConfiged && localeData.type === 'openai_moderation' && (
|
||||
<div className='flex items-center mt-2 px-3 py-2 bg-[#FFFAEB] rounded-lg border border-[#FEF0C7]'>
|
||||
<InfoCircle className='mr-1 w-4 h-4 text-[#F79009]' />
|
||||
<div className='flex items-center text-xs font-medium text-gray-700'>
|
||||
{t('appDebug.feature.moderation.modal.openaiNotConfig.before')}
|
||||
<span
|
||||
className='text-primary-600 cursor-pointer'
|
||||
onClick={handleOpenSettingsModal}
|
||||
>
|
||||
{t('common.settings.provider')}
|
||||
</span>
|
||||
{t('appDebug.feature.moderation.modal.openaiNotConfig.after')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
localeData.type === 'keywords' && (
|
||||
<div className='py-2'>
|
||||
<div className='mb-1 text-sm font-medium text-gray-900'>{t('appDebug.feature.moderation.modal.provider.keywords')}</div>
|
||||
<div className='mb-2 text-xs text-gray-500'>{t('appDebug.feature.moderation.modal.keywords.tip')}</div>
|
||||
<div className='relative px-3 py-2 h-[88px] bg-gray-100 rounded-lg'>
|
||||
<textarea
|
||||
value={localeData.config?.keywords || ''}
|
||||
onChange={handleDataKeywordsChange}
|
||||
className='block w-full h-full bg-transparent text-sm outline-none appearance-none resize-none'
|
||||
placeholder={t('appDebug.feature.moderation.modal.keywords.placeholder') || ''}
|
||||
/>
|
||||
<div className='absolute bottom-2 right-2 flex items-center px-1 h-5 rounded-md bg-gray-50 text-xs font-medium text-gray-300'>
|
||||
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>/<span className='text-gray-500'>100 {t('appDebug.feature.moderation.modal.keywords.line')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
localeData.type === 'api' && (
|
||||
<div className='py-2'>
|
||||
<div className='flex items-center justify-between h-9'>
|
||||
<div className='text-sm font-medium text-gray-900'>{t('common.apiBasedExtension.selector.title')}</div>
|
||||
<a
|
||||
href={t('common.apiBasedExtension.linkUrl') || '/'}
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
className='group flex items-center text-xs text-gray-500 hover:text-primary-600'
|
||||
>
|
||||
<BookOpen01 className='mr-1 w-3 h-3 text-gray-500 group-hover:text-primary-600' />
|
||||
{t('common.apiBasedExtension.link')}
|
||||
</a>
|
||||
</div>
|
||||
<ApiBasedExtensionSelector
|
||||
value={localeData.config?.api_based_extension_id || ''}
|
||||
onChange={handleDataApiBasedChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
systemTypes.findIndex(t => t === localeData.type) < 0
|
||||
&& currentProvider?.form_schema
|
||||
&& (
|
||||
<FormGeneration
|
||||
forms={currentProvider?.form_schema}
|
||||
value={localeData.config}
|
||||
onChange={handleDataExtraChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className='my-3 h-[1px] bg-gradient-to-r from-[#F3F4F6]'></div>
|
||||
<ModerationContent
|
||||
title={t('appDebug.feature.moderation.modal.content.input') || ''}
|
||||
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('inputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
|
||||
showPreset={!(localeData.type === 'api')}
|
||||
/>
|
||||
<ModerationContent
|
||||
title={t('appDebug.feature.moderation.modal.content.output') || ''}
|
||||
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
|
||||
onConfigChange={config => handleDataContentChange('outputs_config', config)}
|
||||
info={(localeData.type === 'api' && t('appDebug.feature.moderation.modal.content.fromApi')) || ''}
|
||||
showPreset={!(localeData.type === 'api')}
|
||||
/>
|
||||
<div className='mt-1 mb-8 text-xs font-medium text-gray-500'>{t('appDebug.feature.moderation.modal.content.condition')}</div>
|
||||
<div className='flex items-center justify-end'>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
className='mr-2 text-sm font-medium'
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
className='text-sm font-medium'
|
||||
onClick={handleSave}
|
||||
disabled={localeData.type === 'openai_moderation' && !openaiProviderConfiged}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ModerationSettingModal)
|
||||
@@ -0,0 +1,273 @@
|
||||
/* eslint-disable multiline-ternary */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { useFeatures } from '../../hooks'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import Button from '@/app/components/base/button'
|
||||
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
|
||||
import { getInputKeys } from '@/app/components/base/block-input'
|
||||
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
|
||||
import { Plus, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
|
||||
const MAX_QUESTION_NUM = 5
|
||||
|
||||
export type OpeningStatementProps = {
|
||||
readonly?: boolean
|
||||
promptVariables?: PromptVariable[]
|
||||
onAutoAddPromptVariable: (variable: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
// regex to match the {{}} and replace it with a span
|
||||
const regex = /\{\{([^}]+)\}\}/g
|
||||
|
||||
const OpeningStatement: FC<OpeningStatementProps> = ({
|
||||
readonly,
|
||||
promptVariables = [],
|
||||
onAutoAddPromptVariable,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const openingStatement = useFeatures(s => s.openingStatement)
|
||||
const setOpeningStatement = useFeatures(s => s.setOpeningStatement)
|
||||
const value = openingStatement.opening_statement || ''
|
||||
const suggestedQuestions = openingStatement.suggested_questions || []
|
||||
const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
|
||||
|
||||
const hasValue = !!(value || '').trim()
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const [isFocus, { setTrue: didSetFocus, setFalse: setBlur }] = useBoolean(false)
|
||||
|
||||
const setFocus = () => {
|
||||
didSetFocus()
|
||||
setTimeout(() => {
|
||||
const input = inputRef.current
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.setSelectionRange(input.value.length, input.value.length)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const [tempValue, setTempValue] = useState(value)
|
||||
useEffect(() => {
|
||||
setTempValue(value || '')
|
||||
}, [value])
|
||||
|
||||
const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(suggestedQuestions || [])
|
||||
const notEmptyQuestions = tempSuggestedQuestions.filter(question => !!question && question.trim())
|
||||
const coloredContent = (tempValue || '')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
const handleEdit = () => {
|
||||
if (readonly)
|
||||
return
|
||||
setFocus()
|
||||
}
|
||||
|
||||
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
|
||||
|
||||
const handleCancel = () => {
|
||||
setBlur()
|
||||
setTempValue(value)
|
||||
setTempSuggestedQuestions(suggestedQuestions)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const keys = getInputKeys(tempValue)
|
||||
const promptKeys = promptVariables.map(item => item.key)
|
||||
let notIncludeKeys: string[] = []
|
||||
|
||||
if (promptKeys.length === 0) {
|
||||
if (keys.length > 0)
|
||||
notIncludeKeys = keys
|
||||
}
|
||||
else {
|
||||
notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
|
||||
}
|
||||
|
||||
if (notIncludeKeys.length > 0) {
|
||||
setNotIncludeKeys(notIncludeKeys)
|
||||
showConfirmAddVar()
|
||||
return
|
||||
}
|
||||
setBlur()
|
||||
setOpeningStatement(produce(openingStatement, (draft) => {
|
||||
draft.opening_statement = tempValue
|
||||
draft.suggested_questions = tempSuggestedQuestions
|
||||
}))
|
||||
}
|
||||
|
||||
const cancelAutoAddVar = () => {
|
||||
setOpeningStatement(produce(openingStatement, (draft) => {
|
||||
draft.opening_statement = tempValue
|
||||
}))
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const autoAddVar = () => {
|
||||
setOpeningStatement(produce(openingStatement, (draft) => {
|
||||
draft.opening_statement = tempValue
|
||||
}))
|
||||
onAutoAddPromptVariable([...notIncludeKeys.map(key => getNewVar(key, 'string'))])
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const headerRight = !readonly ? (
|
||||
isFocus ? (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div className='px-3 leading-[18px] text-xs font-medium text-gray-700 cursor-pointer' onClick={handleCancel}>{t('common.operation.cancel')}</div>
|
||||
<Button className='!h-8 !px-3 text-xs' onClick={handleConfirm} type="primary">{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<OperationBtn type='edit' actionName={hasValue ? '' : t('appDebug.openingStatement.writeOpner') as string} onClick={handleEdit} />
|
||||
)
|
||||
) : null
|
||||
|
||||
const renderQuestions = () => {
|
||||
return isFocus ? (
|
||||
<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}>
|
||||
<div className='handle flex items-center justify-center w-4 h-4 cursor-grab'>
|
||||
<svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="#98A2B3" />
|
||||
</svg>
|
||||
</div>
|
||||
<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))
|
||||
}}
|
||||
>
|
||||
<Trash03 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'>
|
||||
<Plus className='w-4 h-4'></Plus>
|
||||
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className='mt-1.5 flex flex-wrap'>
|
||||
{notEmptyQuestions.map((question, index) => {
|
||||
return (
|
||||
<div key={index} className='mt-1 mr-1 max-w-full truncate last:mr-0 shrink-0 leading-8 items-center px-2.5 rounded-lg border border-gray-200 shadow-xs bg-white text-[13px] font-normal text-gray-900 cursor-pointer'>
|
||||
{question}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className={cn(isShowConfirmAddVar && 'h-[220px]', 'relative !bg-gray-25')}
|
||||
title={t('appDebug.openingStatement.title')}
|
||||
headerIcon={
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.33353 1.33301C4.83572 1.33301 2.00019 4.16854 2.00019 7.66634C2.00019 8.37301 2.11619 9.05395 2.3307 9.69036C2.36843 9.80229 2.39063 9.86853 2.40507 9.91738L2.40979 9.93383L2.40729 9.93903C2.39015 9.97437 2.36469 10.0218 2.31705 10.11L1.2158 12.1484C1.14755 12.2746 1.07633 12.4064 1.02735 12.5209C0.978668 12.6348 0.899813 12.8437 0.938613 13.0914C0.984094 13.3817 1.15495 13.6373 1.40581 13.7903C1.61981 13.9208 1.843 13.9279 1.96683 13.9264C2.09141 13.925 2.24036 13.9095 2.38314 13.8947L5.81978 13.5395C5.87482 13.5338 5.9036 13.5309 5.92468 13.5292L5.92739 13.529L5.93564 13.532C5.96154 13.5413 5.99666 13.5548 6.0573 13.5781C6.76459 13.8506 7.53244 13.9997 8.33353 13.9997C11.8313 13.9997 14.6669 11.1641 14.6669 7.66634C14.6669 4.16854 11.8313 1.33301 8.33353 1.33301ZM5.9799 5.72116C6.73142 5.08698 7.73164 5.27327 8.33144 5.96584C8.93125 5.27327 9.91854 5.09365 10.683 5.72116C11.4474 6.34867 11.5403 7.41567 10.9501 8.16572C10.5845 8.6304 9.6668 9.47911 9.02142 10.0576C8.78435 10.2702 8.66582 10.3764 8.52357 10.4192C8.40154 10.456 8.26134 10.456 8.13931 10.4192C7.99706 10.3764 7.87853 10.2702 7.64147 10.0576C6.99609 9.47911 6.07839 8.6304 5.71276 8.16572C5.12259 7.41567 5.22839 6.35534 5.9799 5.72116Z" fill="#E74694" />
|
||||
</svg>
|
||||
}
|
||||
headerRight={headerRight}
|
||||
hasHeaderBottomBorder={!hasValue}
|
||||
isFocus={isFocus}
|
||||
>
|
||||
<div className='text-gray-700 text-sm'>
|
||||
{(hasValue || (!hasValue && isFocus)) ? (
|
||||
<>
|
||||
{isFocus
|
||||
? (
|
||||
<div>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={tempValue}
|
||||
rows={3}
|
||||
onChange={e => setTempValue(e.target.value)}
|
||||
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
|
||||
placeholder={t('appDebug.openingStatement.placeholder') as string}
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: coloredContent,
|
||||
}}></div>
|
||||
)}
|
||||
{renderQuestions()}
|
||||
</>) : (
|
||||
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
|
||||
)}
|
||||
|
||||
{isShowConfirmAddVar && (
|
||||
<ConfirmAddVar
|
||||
varNameArr={notIncludeKeys}
|
||||
onConfrim={autoAddVar}
|
||||
onCancel={cancelAutoAddVar}
|
||||
onHide={hideConfirmAddVar}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
export default React.memo(OpeningStatement)
|
||||
@@ -0,0 +1,38 @@
|
||||
import ReactSlider from 'react-slider'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
type ISliderProps = {
|
||||
className?: string
|
||||
value: number
|
||||
max?: number
|
||||
min?: number
|
||||
step?: number
|
||||
disabled?: boolean
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
|
||||
return <ReactSlider
|
||||
disabled={disabled}
|
||||
value={isNaN(value) ? 0 : value}
|
||||
min={min || 0}
|
||||
max={max || 100}
|
||||
step={step || 1}
|
||||
className={cn(className, s.slider)}
|
||||
thumbClassName={cn(s['slider-thumb'], 'top-[-7px] w-2 h-[18px] bg-white border !border-black/8 rounded-[36px] shadow-md cursor-pointer')}
|
||||
trackClassName={s['slider-track']}
|
||||
onChange={onChange}
|
||||
renderThumb={(props, state) => (
|
||||
<div {...props}>
|
||||
<div className='relative w-full h-full'>
|
||||
<div className='absolute top-[-16px] left-[50%] translate-x-[-50%] leading-[18px] text-xs font-medium text-gray-900'>
|
||||
{(state.valueNow / 100).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
export default Slider
|
||||
@@ -0,0 +1,20 @@
|
||||
.slider {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slider.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.slider-thumb:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
background-color: #528BFF;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.slider-track-1 {
|
||||
background-color: #E5E7EB;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const ScoreSlider: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='h-[1px] mt-[14px]'>
|
||||
<Slider
|
||||
max={100}
|
||||
min={80}
|
||||
step={1}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-[10px] flex justify-between items-center leading-4 text-xs font-normal '>
|
||||
<div className='flex space-x-1 text-[#00A286]'>
|
||||
<div>0.8</div>
|
||||
<div>·</div>
|
||||
<div>{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}</div>
|
||||
</div>
|
||||
<div className='flex space-x-1 text-[#0057D8]'>
|
||||
<div>1.0</div>
|
||||
<div>·</div>
|
||||
<div>{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ScoreSlider)
|
||||
@@ -0,0 +1,25 @@
|
||||
'use client'
|
||||
import React, { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
|
||||
const SpeechToTextConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
<div>{t('appDebug.feature.speechToText.title')}</div>
|
||||
</div>
|
||||
}
|
||||
headerIcon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
|
||||
headerRight={
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.feature.speechToText.resDes')}</div>
|
||||
}
|
||||
noBodySpacing
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(SpeechToTextConfig)
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
const SuggestedQuestionsAfterAnswer: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
<div>{t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}</div>
|
||||
<Tooltip htmlContent={<div className='w-[180px]'>
|
||||
{t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
|
||||
</div>} selector='suggestion-question-tooltip'>
|
||||
<HelpCircle className='w-[14px] h-[14px] text-gray-400' />
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
headerIcon={<SuggestedQuestionsAfterAnswerIcon />}
|
||||
headerRight={
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.resDes')}</div>
|
||||
}
|
||||
noBodySpacing
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(SuggestedQuestionsAfterAnswer)
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
import useSWR from 'swr'
|
||||
import React, { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useFeatures } from '../../hooks'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
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'
|
||||
|
||||
const TextToSpeech: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const textToSpeech = useFeatures(s => s.textToSpeech)
|
||||
|
||||
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 (
|
||||
<Panel
|
||||
title={
|
||||
<div className='flex items-center'>
|
||||
<div>{t('appDebug.feature.textToSpeech.title')}</div>
|
||||
</div>
|
||||
}
|
||||
headerIcon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
|
||||
headerRight={
|
||||
<div className='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}
|
||||
isAudition={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
noBodySpacing
|
||||
isShowTextToSpeech={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(TextToSpeech)
|
||||
12
web/app/components/base/features/hooks.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useContext } from 'react'
|
||||
import { useStore } from 'zustand'
|
||||
import { FeaturesContext } from './context'
|
||||
import type { FeatureStoreState } from './store'
|
||||
|
||||
export function useFeatures<T>(selector: (state: FeatureStoreState) => T): T {
|
||||
const store = useContext(FeaturesContext)
|
||||
if (!store)
|
||||
throw new Error('Missing FeaturesContext.Provider in the tree')
|
||||
|
||||
return useStore(store, selector)
|
||||
}
|
||||
3
web/app/components/base/features/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as FeaturesPanel } from './feature-panel'
|
||||
export { default as FeaturesChoose } from './feature-choose'
|
||||
export { FeaturesProvider } from './context'
|
||||
79
web/app/components/base/features/store.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createStore } from 'zustand'
|
||||
import type {
|
||||
AnnotationReply,
|
||||
OpeningStatement,
|
||||
RetrieverResource,
|
||||
SensitiveWordAvoidance,
|
||||
SpeechToText,
|
||||
SuggestedQuestionsAfterAnswer,
|
||||
TextToSpeech,
|
||||
} from './types'
|
||||
|
||||
export type FeaturesModal = {
|
||||
showFeaturesModal: boolean
|
||||
setShowFeaturesModal: (showFeaturesModal: boolean) => void
|
||||
}
|
||||
|
||||
export type FeaturesState = {
|
||||
openingStatement: OpeningStatement
|
||||
suggestedQuestionsAfterAnswer: SuggestedQuestionsAfterAnswer
|
||||
textToSpeech: TextToSpeech
|
||||
speechToText: SpeechToText
|
||||
citation: RetrieverResource
|
||||
moderation: SensitiveWordAvoidance
|
||||
annotation: AnnotationReply
|
||||
}
|
||||
|
||||
export type FeaturesAction = {
|
||||
setOpeningStatement: (openingStatement: OpeningStatement) => void
|
||||
setSuggestedQuestionsAfterAnswer: (suggestedQuestionsAfterAnswer: SuggestedQuestionsAfterAnswer) => void
|
||||
setTextToSpeech: (textToSpeech: TextToSpeech) => void
|
||||
setSpeechToText: (speechToText: SpeechToText) => void
|
||||
setCitation: (citation: RetrieverResource) => void
|
||||
setModeration: (moderation: SensitiveWordAvoidance) => void
|
||||
setAnnotation: (annotation: AnnotationReply) => void
|
||||
}
|
||||
|
||||
export type FeatureStoreState = FeaturesState & FeaturesAction & FeaturesModal
|
||||
|
||||
export type FeaturesStore = ReturnType<typeof createFeaturesStore>
|
||||
|
||||
export const createFeaturesStore = (initProps?: Partial<FeaturesState>) => {
|
||||
const DEFAULT_PROPS: FeaturesState = {
|
||||
openingStatement: {
|
||||
enabled: false,
|
||||
},
|
||||
suggestedQuestionsAfterAnswer: {
|
||||
enabled: false,
|
||||
},
|
||||
textToSpeech: {
|
||||
enabled: false,
|
||||
},
|
||||
speechToText: {
|
||||
enabled: false,
|
||||
},
|
||||
citation: {
|
||||
enabled: false,
|
||||
},
|
||||
moderation: {
|
||||
enabled: false,
|
||||
},
|
||||
annotation: {
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
return createStore<FeatureStoreState>()(set => ({
|
||||
...DEFAULT_PROPS,
|
||||
...initProps,
|
||||
setOpeningStatement: openingStatement => set(() => ({ openingStatement })),
|
||||
setSuggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswer => set(() => ({ suggestedQuestionsAfterAnswer })),
|
||||
setSpeechToText: speechToText => set(() => ({ speechToText })),
|
||||
setTextToSpeech: textToSpeech => set(() => ({ textToSpeech })),
|
||||
setCitation: citation => set(() => ({ citation })),
|
||||
setModeration: moderation => set(() => ({ moderation })),
|
||||
setAnnotation: annotation => set(() => ({ annotation })),
|
||||
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: showFeaturesModal => set(() => ({ showFeaturesModal })),
|
||||
}))
|
||||
}
|
||||
33
web/app/components/base/features/types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type EnabledOrDisabled = {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type OpeningStatement = EnabledOrDisabled & {
|
||||
opening_statement?: string
|
||||
suggested_questions?: string[]
|
||||
}
|
||||
|
||||
export type SuggestedQuestionsAfterAnswer = EnabledOrDisabled
|
||||
|
||||
export type TextToSpeech = EnabledOrDisabled & {
|
||||
language?: string
|
||||
voice?: string
|
||||
}
|
||||
|
||||
export type SpeechToText = EnabledOrDisabled
|
||||
|
||||
export type RetrieverResource = EnabledOrDisabled
|
||||
|
||||
export type SensitiveWordAvoidance = EnabledOrDisabled & {
|
||||
type?: string
|
||||
config?: any
|
||||
}
|
||||
|
||||
export type AnnotationReply = EnabledOrDisabled & {
|
||||
id?: string
|
||||
score_threshold?: number
|
||||
embedding_model?: {
|
||||
embedding_model_name: string
|
||||
embedding_provider_name: string
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,44 @@
|
||||
import { memo } from 'react'
|
||||
import { useStore } from './store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
Plus02,
|
||||
XClose,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
FeaturesChoose,
|
||||
FeaturesPanel,
|
||||
FeaturesProvider,
|
||||
} from '@/app/components/base/features'
|
||||
|
||||
const Features = () => {
|
||||
const showFeatures = useStore(state => state.showFeatures)
|
||||
const setShowFeatures = useStore(state => state.setShowFeatures)
|
||||
|
||||
if (!showFeatures)
|
||||
return null
|
||||
const setShowFeaturesPanel = useStore(state => state.setShowFeaturesPanel)
|
||||
|
||||
return (
|
||||
<div className='absolute top-2 left-2 bottom-2 w-[600px] rounded-2xl border-[0.5px] border-gray-200 bg-white shadow-xl z-10'>
|
||||
<div className='flex items-center justify-between px-4 pt-3'>
|
||||
Features
|
||||
<div className='flex items-center'>
|
||||
<Button className='px-3 py-0 h-8 rounded-lg border border-primary-100 bg-primary-25 shadow-xs text-xs font-semibold text-primary-600'>
|
||||
<Plus02 className='mr-1 w-4 h-4' />
|
||||
Add Features
|
||||
</Button>
|
||||
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => setShowFeatures(false)}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
<FeaturesProvider>
|
||||
<div className='absolute top-2 left-2 bottom-2 w-[600px] rounded-2xl border-[0.5px] border-gray-200 bg-white shadow-xl z-10'>
|
||||
<div className='flex items-center justify-between px-4 pt-3'>
|
||||
Features
|
||||
<div className='flex items-center'>
|
||||
<FeaturesChoose />
|
||||
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => setShowFeaturesPanel(false)}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<FeaturesPanel
|
||||
openingStatementProps={{
|
||||
onAutoAddPromptVariable: () => {},
|
||||
}}
|
||||
annotationProps={{
|
||||
onEmbeddingChange: () => {},
|
||||
onScoreChange: () => {},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FeaturesProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arr
|
||||
|
||||
const Header: FC = () => {
|
||||
const mode = useStore(state => state.mode)
|
||||
const setShowFeatures = useStore(state => state.setShowFeatures)
|
||||
const setShowFeaturesPanel = useStore(state => state.setShowFeaturesPanel)
|
||||
const runStaus = useStore(state => state.runStaus)
|
||||
const setRunStaus = useStore(state => state.setRunStaus)
|
||||
|
||||
const handleShowFeatures = useCallback(() => {
|
||||
setShowFeatures(true)
|
||||
}, [setShowFeatures])
|
||||
setShowFeaturesPanel(true)
|
||||
}, [setShowFeaturesPanel])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -22,6 +22,7 @@ import CustomEdge from './custom-edge'
|
||||
import CustomConnectionLine from './custom-connection-line'
|
||||
import Panel from './panel'
|
||||
import Features from './features'
|
||||
import { useStore } from './store'
|
||||
|
||||
const nodeTypes = {
|
||||
custom: CustomNode,
|
||||
@@ -38,6 +39,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
}) => {
|
||||
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
|
||||
const [nodes] = useNodesState(initialNodes)
|
||||
const [edges, _, onEdgesChange] = useEdgesState(initialEdges)
|
||||
const nodesInitialized = useNodesInitialized()
|
||||
@@ -64,7 +66,9 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
<Header />
|
||||
<Panel />
|
||||
<ZoomInOut />
|
||||
<Features />
|
||||
{
|
||||
showFeaturesPanel && <Features />
|
||||
}
|
||||
<ReactFlow
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
|
||||
@@ -3,13 +3,15 @@ import { create } from 'zustand'
|
||||
type State = {
|
||||
mode: string
|
||||
showRunHistory: boolean
|
||||
showFeatures: boolean
|
||||
showFeaturesPanel: boolean
|
||||
showFeaturesModal: boolean
|
||||
runStaus: string
|
||||
}
|
||||
|
||||
type Action = {
|
||||
setShowRunHistory: (showRunHistory: boolean) => void
|
||||
setShowFeatures: (showFeatures: boolean) => void
|
||||
setShowFeaturesPanel: (showFeaturesPanel: boolean) => void
|
||||
setShowFeaturesModal: (showFeaturesModal: boolean) => void
|
||||
setRunStaus: (runStaus: string) => void
|
||||
}
|
||||
|
||||
@@ -17,8 +19,10 @@ export const useStore = create<State & Action>(set => ({
|
||||
mode: 'workflow',
|
||||
showRunHistory: false,
|
||||
setShowRunHistory: showRunHistory => set(() => ({ showRunHistory })),
|
||||
showFeatures: false,
|
||||
setShowFeatures: showFeatures => set(() => ({ showFeatures })),
|
||||
showFeaturesPanel: false,
|
||||
setShowFeaturesPanel: showFeaturesPanel => set(() => ({ showFeaturesPanel })),
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: showFeaturesModal => set(() => ({ showFeaturesModal })),
|
||||
runStaus: '',
|
||||
setRunStaus: runStaus => set(() => ({ runStaus })),
|
||||
}))
|
||||
|
||||