mirror of https://github.com/langgenius/dify.git
conversation opener
This commit is contained in:
parent
fe94c876fb
commit
565a835947
|
|
@ -25,10 +25,10 @@ import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
|
|||
import type { Model } from '@/types/app'
|
||||
import { AppType } from '@/types/app'
|
||||
import ConfigVar from '@/app/components/app/configuration/config-var'
|
||||
import OpeningStatement from '@/app/components/app/configuration/features/chat-group/opening-statement'
|
||||
import GroupName from '@/app/components/app/configuration/base/group-name'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
|
||||
|
||||
// type
|
||||
import type { AutomaticRes } from '@/service/debug'
|
||||
|
|
@ -262,11 +262,19 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
|||
{(mode !== AppType.completion && res?.opening_statement) && (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.groupChat.title')} />
|
||||
{/* ##TODO## use new style of opening */}
|
||||
<OpeningStatement
|
||||
value={res?.opening_statement || ''}
|
||||
readonly
|
||||
/>
|
||||
<div
|
||||
className='mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'
|
||||
>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
<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>
|
||||
<div className='grow flex items-center text-text-secondary system-sm-semibold'>
|
||||
{t('appDebug.feature.conversationOpener.title')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-8 text-text-tertiary system-xs-regular'>{res.opening_statement}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import React, { useEffect } from 'react'
|
||||
|
||||
function useFeature({
|
||||
introduction,
|
||||
setIntroduction,
|
||||
moreLikeThis,
|
||||
setMoreLikeThis,
|
||||
suggestedQuestionsAfterAnswer,
|
||||
setSuggestedQuestionsAfterAnswer,
|
||||
speechToText,
|
||||
setSpeechToText,
|
||||
textToSpeech,
|
||||
setTextToSpeech,
|
||||
citation,
|
||||
setCitation,
|
||||
annotation,
|
||||
setAnnotation,
|
||||
moderation,
|
||||
setModeration,
|
||||
}: {
|
||||
introduction: string
|
||||
setIntroduction: (introduction: string) => void
|
||||
moreLikeThis: boolean
|
||||
setMoreLikeThis: (moreLikeThis: boolean) => void
|
||||
suggestedQuestionsAfterAnswer: boolean
|
||||
setSuggestedQuestionsAfterAnswer: (suggestedQuestionsAfterAnswer: boolean) => void
|
||||
speechToText: boolean
|
||||
setSpeechToText: (speechToText: boolean) => void
|
||||
textToSpeech: boolean
|
||||
setTextToSpeech: (textToSpeech: boolean) => void
|
||||
citation: boolean
|
||||
setCitation: (citation: boolean) => void
|
||||
annotation: boolean
|
||||
setAnnotation: (annotation: boolean) => void
|
||||
moderation: boolean
|
||||
setModeration: (moderation: boolean) => void
|
||||
}) {
|
||||
const [tempshowOpeningStatement, setTempShowOpeningStatement] = React.useState(!!introduction)
|
||||
useEffect(() => {
|
||||
// wait to api data back
|
||||
if (introduction)
|
||||
setTempShowOpeningStatement(true)
|
||||
}, [introduction])
|
||||
|
||||
// const [tempMoreLikeThis, setTempMoreLikeThis] = React.useState(moreLikeThis)
|
||||
// useEffect(() => {
|
||||
// setTempMoreLikeThis(moreLikeThis)
|
||||
// }, [moreLikeThis])
|
||||
|
||||
const featureConfig = {
|
||||
openingStatement: tempshowOpeningStatement,
|
||||
moreLikeThis,
|
||||
suggestedQuestionsAfterAnswer,
|
||||
speechToText,
|
||||
textToSpeech,
|
||||
citation,
|
||||
annotation,
|
||||
moderation,
|
||||
}
|
||||
const handleFeatureChange = (key: string, value: boolean) => {
|
||||
switch (key) {
|
||||
case 'openingStatement':
|
||||
if (!value)
|
||||
setIntroduction('')
|
||||
|
||||
setTempShowOpeningStatement(value)
|
||||
break
|
||||
case 'moreLikeThis':
|
||||
setMoreLikeThis(value)
|
||||
break
|
||||
case 'suggestedQuestionsAfterAnswer':
|
||||
setSuggestedQuestionsAfterAnswer(value)
|
||||
break
|
||||
case 'speechToText':
|
||||
setSpeechToText(value)
|
||||
break
|
||||
case 'textToSpeech':
|
||||
setTextToSpeech(value)
|
||||
break
|
||||
case 'citation':
|
||||
setCitation(value)
|
||||
break
|
||||
case 'annotation':
|
||||
setAnnotation(value)
|
||||
break
|
||||
case 'moderation':
|
||||
setModeration(value)
|
||||
}
|
||||
}
|
||||
return {
|
||||
featureConfig,
|
||||
handleFeatureChange,
|
||||
}
|
||||
}
|
||||
|
||||
export default useFeature
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
/* eslint-disable multiline-ternary */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import cn from '@/utils/classnames'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
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 Toast from '@/app/components/base/toast'
|
||||
|
||||
const MAX_QUESTION_NUM = 5
|
||||
|
||||
export type IOpeningStatementProps = {
|
||||
value: string
|
||||
readonly?: boolean
|
||||
onChange?: (value: string) => void
|
||||
suggestedQuestions?: string[]
|
||||
onSuggestedQuestionsChange?: (value: string[]) => void
|
||||
}
|
||||
|
||||
// regex to match the {{}} and replace it with a span
|
||||
const regex = /\{\{([^}]+)\}\}/g
|
||||
|
||||
const OpeningStatement: FC<IOpeningStatementProps> = ({
|
||||
value = '',
|
||||
readonly,
|
||||
onChange,
|
||||
suggestedQuestions = [],
|
||||
onSuggestedQuestionsChange = () => { },
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
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 = () => {
|
||||
if (!(tempValue || '').trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('common.errorMsg.fieldRequired', {
|
||||
field: t('appDebug.openingStatement.title'),
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
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()
|
||||
onChange?.(tempValue)
|
||||
onSuggestedQuestionsChange(tempSuggestedQuestions)
|
||||
}
|
||||
|
||||
const cancelAutoAddVar = () => {
|
||||
onChange?.(tempValue)
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const autoAddVar = () => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...notIncludeKeys.map(key => getNewVar(key, 'string'))]
|
||||
})
|
||||
onChange?.(tempValue)
|
||||
setModelConfig(newModelConfig)
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const headerRight = !readonly ? (
|
||||
isFocus ? (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant="primary"
|
||||
size='small'
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<OperationBtn type='edit' actionName={hasValue ? '' : t('appDebug.openingStatement.writeOpener') 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))
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}</ReactSortable>
|
||||
{tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
|
||||
<div
|
||||
onClick={() => { setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
|
||||
className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100 hover:bg-gray-200'>
|
||||
<RiAddLine className='w-4 h-4' />
|
||||
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<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 mt-4 !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)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import GroupName from '../../base/group-name'
|
||||
import TextToSpeech from '../chat-group/text-to-speech'
|
||||
import MoreLikeThis from './more-like-this'
|
||||
|
||||
/*
|
||||
* Include
|
||||
* 1. More like this
|
||||
*/
|
||||
|
||||
type ExperienceGroupProps = {
|
||||
isShowTextToSpeech: boolean
|
||||
isShowMoreLike: boolean
|
||||
}
|
||||
|
||||
const ExperienceEnchanceGroup: FC<ExperienceGroupProps> = ({
|
||||
isShowTextToSpeech,
|
||||
isShowMoreLike,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.groupExperience.title')}/>
|
||||
<div className='space-y-3'>
|
||||
{
|
||||
isShowMoreLike && (
|
||||
<MoreLikeThis/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isShowTextToSpeech && (
|
||||
<TextToSpeech/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ExperienceEnchanceGroup)
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
|
||||
const GENERATE_NUM = 1
|
||||
|
||||
const warningIcon = (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.40616 0.834307C6.14751 0.719294 5.85222 0.719294 5.59356 0.834307C5.3938 0.923133 5.26403 1.07959 5.17373 1.20708C5.08495 1.33242 4.9899 1.49664 4.88536 1.67723L0.751783 8.81705C0.646828 8.9983 0.551451 9.16302 0.486781 9.3028C0.421056 9.44487 0.349754 9.63584 0.372478 9.85381C0.401884 10.1359 0.549654 10.3922 0.779012 10.5589C0.956259 10.6878 1.15726 10.7218 1.31314 10.7361C1.46651 10.7501 1.65684 10.7501 1.86628 10.7501H10.1334C10.3429 10.7501 10.5332 10.7501 10.6866 10.7361C10.8425 10.7218 11.0435 10.6878 11.2207 10.5589C11.4501 10.3922 11.5978 10.1359 11.6272 9.85381C11.65 9.63584 11.5787 9.44487 11.5129 9.3028C11.4483 9.16303 11.3529 8.99833 11.248 8.81709L7.11436 1.67722C7.00983 1.49663 6.91477 1.33242 6.82599 1.20708C6.73569 1.07959 6.60593 0.923133 6.40616 0.834307ZM6.49988 4.50012C6.49988 4.22398 6.27602 4.00012 5.99988 4.00012C5.72374 4.00012 5.49988 4.22398 5.49988 4.50012V6.50012C5.49988 6.77626 5.72374 7.00012 5.99988 7.00012C6.27602 7.00012 6.49988 6.77626 6.49988 6.50012V4.50012ZM5.99988 8.00012C5.72374 8.00012 5.49988 8.22398 5.49988 8.50012C5.49988 8.77626 5.72374 9.00012 5.99988 9.00012H6.00488C6.28102 9.00012 6.50488 8.77626 6.50488 8.50012C6.50488 8.22398 6.28102 8.00012 6.00488 8.00012H5.99988Z" fill="#F79009" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
const MoreLikeThis: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isHideTip, setIsHideTip] = useLocalStorageState('isHideMoreLikeThisTip', {
|
||||
defaultValue: false,
|
||||
})
|
||||
|
||||
const headerRight = (
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.feature.moreLikeThis.generateNumTip')} {GENERATE_NUM}</div>
|
||||
)
|
||||
return (
|
||||
<Panel
|
||||
className='mt-4'
|
||||
title={t('appDebug.feature.moreLikeThis.title')}
|
||||
headerIcon={<MoreLikeThisIcon />}
|
||||
headerRight={headerRight}
|
||||
noBodySpacing
|
||||
>
|
||||
{!isHideTip && (
|
||||
<div className='flex justify-between items-center h-9 px-3 rounded-b-xl bg-[#FFFAEB] text-xs text-gray-700'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div>{warningIcon}</div>
|
||||
<div>{t('appDebug.feature.moreLikeThis.tip')}</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-center w-4 h-4 cursor-pointer' onClick={() => setIsHideTip(true)}>
|
||||
<XMarkIcon className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
export default React.memo(MoreLikeThis)
|
||||
|
|
@ -466,6 +466,12 @@ const Configuration: FC = () => {
|
|||
if (flag)
|
||||
formattingChangedDispatcher()
|
||||
}, [formattingChangedDispatcher, setShowAppConfigureFeaturesModal])
|
||||
const handleAddPromptVariable = useCallback((variable: PromptVariable[]) => {
|
||||
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.configs.prompt_variables = variable
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
}, [modelConfig])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
@ -994,6 +1000,8 @@ const Configuration: FC = () => {
|
|||
disabled={false}
|
||||
onChange={handleFeaturesChange}
|
||||
onClose={() => setShowAppConfigureFeaturesModal(false)}
|
||||
promptVariables={modelConfig.configs.prompt_variables}
|
||||
onAutoAddPromptVariable={handleAddPromptVariable}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import GroupName from '../base/group-name'
|
||||
import Moderation from './moderation'
|
||||
import Annotation from './annotation/config-param'
|
||||
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
|
||||
export type ToolboxProps = {
|
||||
showModerationSettings: boolean
|
||||
showAnnotation: boolean
|
||||
onEmbeddingChange: (embeddingModel: EmbeddingModelConfig) => void
|
||||
onScoreChange: (score: number, embeddingModel?: EmbeddingModelConfig) => void
|
||||
}
|
||||
|
||||
const Toolbox: FC<ToolboxProps> = ({
|
||||
showModerationSettings,
|
||||
showAnnotation,
|
||||
onEmbeddingChange,
|
||||
onScoreChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.toolbox.title')} />
|
||||
{
|
||||
showModerationSettings && (
|
||||
<Moderation />
|
||||
)
|
||||
}
|
||||
{
|
||||
(showAnnotation || true) && (
|
||||
<Annotation
|
||||
onEmbeddingChange={onEmbeddingChange}
|
||||
onScoreChange={onScoreChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Toolbox)
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import { useContext } from 'use-context-selector'
|
||||
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 ConfigContext from '@/context/debug-configuration'
|
||||
import { fetchCodeBasedExtensionList } from '@/service/common'
|
||||
import I18n from '@/context/i18n'
|
||||
const Moderation = () => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowModerationSettingModal } = useModalContext()
|
||||
const { locale } = useContext(I18n)
|
||||
const {
|
||||
moderationConfig,
|
||||
setModerationConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const { data: codeBasedExtensionList } = useSWR(
|
||||
'/code-based-extension?module=moderation',
|
||||
fetchCodeBasedExtensionList,
|
||||
)
|
||||
|
||||
const handleOpenModerationSettingModal = () => {
|
||||
setShowModerationSettingModal({
|
||||
payload: moderationConfig,
|
||||
onSaveCallback: setModerationConfig,
|
||||
})
|
||||
}
|
||||
|
||||
const renderInfo = () => {
|
||||
let prefix = ''
|
||||
let suffix = ''
|
||||
if (moderationConfig.type === 'openai_moderation')
|
||||
prefix = t('appDebug.feature.moderation.modal.provider.openai')
|
||||
else if (moderationConfig.type === 'keywords')
|
||||
prefix = t('appDebug.feature.moderation.modal.provider.keywords')
|
||||
else if (moderationConfig.type === 'api')
|
||||
prefix = t('common.apiBasedExtension.selector.title')
|
||||
else
|
||||
prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[locale] || ''
|
||||
|
||||
if (moderationConfig.config?.inputs_config?.enabled && moderationConfig.config?.outputs_config?.enabled)
|
||||
suffix = t('appDebug.feature.moderation.allEnabled')
|
||||
else if (moderationConfig.config?.inputs_config?.enabled)
|
||||
suffix = t('appDebug.feature.moderation.inputEnabled')
|
||||
else if (moderationConfig.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 Moderation
|
||||
|
|
@ -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,115 +0,0 @@
|
|||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OnFeaturesChange } from '../types'
|
||||
import { useFeatures } from '../hooks'
|
||||
import FileUpload from './file-upload'
|
||||
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'
|
||||
|
||||
export type FeaturePanelProps = {
|
||||
onChange?: OnFeaturesChange
|
||||
openingStatementProps: OpeningStatementProps
|
||||
disabled?: boolean
|
||||
}
|
||||
const FeaturePanel = ({
|
||||
onChange,
|
||||
openingStatementProps,
|
||||
disabled,
|
||||
}: FeaturePanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
const features = useFeatures(s => s.features)
|
||||
|
||||
const showAdvanceFeature = useMemo(() => {
|
||||
return features.opening?.enabled || features.suggested?.enabled || features.speech2text?.enabled || features.text2speech?.enabled || features.citation?.enabled
|
||||
}, [features])
|
||||
|
||||
const showToolFeature = useMemo(() => {
|
||||
return features.moderation?.enabled
|
||||
}, [features])
|
||||
|
||||
return (
|
||||
<div className='space-y-3'>
|
||||
<FileUpload
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{
|
||||
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'>
|
||||
{
|
||||
features.opening?.enabled && (
|
||||
<OpeningStatement
|
||||
{...openingStatementProps}
|
||||
onChange={onChange}
|
||||
readonly={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
features.suggested?.enabled && (
|
||||
<SuggestedQuestionsAfterAnswer />
|
||||
)
|
||||
}
|
||||
{
|
||||
features.text2speech?.enabled && (
|
||||
<TextToSpeech onChange={onChange} disabled={disabled} />
|
||||
)
|
||||
}
|
||||
{
|
||||
features.speech2text?.enabled && (
|
||||
<SpeechToText />
|
||||
)
|
||||
}
|
||||
{
|
||||
features.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'>
|
||||
{
|
||||
features.moderation?.enabled && (
|
||||
<Moderation onChange={onChange} disabled={disabled} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default memo(FeaturePanel)
|
||||
|
|
@ -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,321 +0,0 @@
|
|||
/* eslint-disable multiline-ternary */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiDeleteBinLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import {
|
||||
useFeatures,
|
||||
useFeaturesStore,
|
||||
} from '../../hooks'
|
||||
import type { OnFeaturesChange } from '../../types'
|
||||
import cn from '@/utils/classnames'
|
||||
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 type { PromptVariable } from '@/models/debug'
|
||||
|
||||
const MAX_QUESTION_NUM = 5
|
||||
|
||||
export type OpeningStatementProps = {
|
||||
onChange?: OnFeaturesChange
|
||||
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> = ({
|
||||
onChange,
|
||||
readonly,
|
||||
promptVariables = [],
|
||||
onAutoAddPromptVariable,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const featureStore = useFeaturesStore()
|
||||
const openingStatement = useFeatures(s => s.features.opening)
|
||||
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()
|
||||
const { getState } = featureStore!
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = getState()
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (draft.opening) {
|
||||
draft.opening.opening_statement = tempValue
|
||||
draft.opening.suggested_questions = tempSuggestedQuestions
|
||||
}
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
}
|
||||
|
||||
const cancelAutoAddVar = () => {
|
||||
const { getState } = featureStore!
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = getState()
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (draft.opening)
|
||||
draft.opening.opening_statement = tempValue
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const autoAddVar = () => {
|
||||
const { getState } = featureStore!
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = getState()
|
||||
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
if (draft.opening)
|
||||
draft.opening.opening_statement = tempValue
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
if (onChange)
|
||||
onChange(newFeatures)
|
||||
onAutoAddPromptVariable([...notIncludeKeys.map(key => getNewVar(key, 'string'))])
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const headerRight = !readonly ? (
|
||||
isFocus ? (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='small'
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button size='small' onClick={handleConfirm} variant="primary">{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
) : (
|
||||
<OperationBtn type='edit' actionName={hasValue ? '' : t('appDebug.openingStatement.writeOpener') 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))
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}</ReactSortable>
|
||||
{tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
|
||||
<div
|
||||
onClick={() => { setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
|
||||
className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100 hover:bg-gray-200'>
|
||||
<RiAddLine className='w-4 h-4' />
|
||||
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<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)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import ReactSlider from 'react-slider'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
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
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
'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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -6,18 +6,23 @@ import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
|
|||
import FeatureCard from '@/app/components/base/features/new-feature-panel/feature-card'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import type { OnFeaturesChange, OpeningStatement } from '@/app/components/base/features/types'
|
||||
import 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'
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
onChange?: OnFeaturesChange
|
||||
promptVariables?: PromptVariable[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
const ConversationOpener = ({
|
||||
disabled,
|
||||
onChange,
|
||||
promptVariables,
|
||||
onAutoAddPromptVariable,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowOpeningModal } = useModalContext()
|
||||
|
|
@ -32,7 +37,11 @@ const ConversationOpener = ({
|
|||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
setShowOpeningModal({
|
||||
payload: opening as OpeningStatement,
|
||||
payload: {
|
||||
...opening,
|
||||
promptVariables,
|
||||
onAutoAddPromptVariable,
|
||||
},
|
||||
onSaveCallback: (newOpening) => {
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.opening = newOpening
|
||||
|
|
@ -46,7 +55,7 @@ const ConversationOpener = ({
|
|||
onChange()
|
||||
},
|
||||
})
|
||||
}, [disabled, featuresStore, onChange, opening, setShowOpeningModal])
|
||||
}, [disabled, featuresStore, onAutoAddPromptVariable, onChange, opening, promptVariables, setShowOpeningModal])
|
||||
|
||||
const handleChange = useCallback((type: FeatureEnum, enabled: boolean) => {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
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 { getNewVar } from '@/utils/var'
|
||||
|
||||
type OpeningSettingModalProps = {
|
||||
data: OpeningStatement
|
||||
onSave: (newState: OpeningStatement) => void
|
||||
onCancel: () => void
|
||||
promptVariables?: PromptVariable[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
const MAX_QUESTION_NUM = 5
|
||||
|
|
@ -19,6 +26,8 @@ const OpeningSettingModal = ({
|
|||
data,
|
||||
onSave,
|
||||
onCancel,
|
||||
promptVariables = [],
|
||||
onAutoAddPromptVariable,
|
||||
}: OpeningSettingModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempValue, setTempValue] = useState(data?.opening_statement || '')
|
||||
|
|
@ -26,8 +35,29 @@ const OpeningSettingModal = ({
|
|||
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 = () => {
|
||||
const handleSave = useCallback((ignoreVariablesCheck?: boolean) => {
|
||||
if (!ignoreVariablesCheck) {
|
||||
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
|
||||
}
|
||||
}
|
||||
const newOpening = produce(data, (draft) => {
|
||||
if (draft) {
|
||||
draft.opening_statement = tempValue
|
||||
|
|
@ -35,7 +65,21 @@ const OpeningSettingModal = ({
|
|||
}
|
||||
})
|
||||
onSave(newOpening)
|
||||
}
|
||||
}, [data, onSave, promptVariables, showConfirmAddVar, tempSuggestedQuestions, tempValue])
|
||||
|
||||
const cancelAutoAddVar = useCallback(() => {
|
||||
hideConfirmAddVar()
|
||||
handleSave(true)
|
||||
}, [handleSave, hideConfirmAddVar])
|
||||
|
||||
const autoAddVar = useCallback(() => {
|
||||
onAutoAddPromptVariable?.([
|
||||
...promptVariables,
|
||||
...notIncludeKeys.map(key => getNewVar(key, 'string')),
|
||||
])
|
||||
hideConfirmAddVar()
|
||||
handleSave(true)
|
||||
}, [handleSave, hideConfirmAddVar, notIncludeKeys, onAutoAddPromptVariable, promptVariables])
|
||||
|
||||
const renderQuestions = () => {
|
||||
return (
|
||||
|
|
@ -137,11 +181,19 @@ const OpeningSettingModal = ({
|
|||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
onClick={() => handleSave()}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
{isShowConfirmAddVar && (
|
||||
<ConfirmAddVar
|
||||
varNameArr={notIncludeKeys}
|
||||
onConfrim={autoAddVar}
|
||||
onCancel={cancelAutoAddVar}
|
||||
onHide={hideConfirmAddVar}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ const FeatureBar = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* annotation reply ##TODO## */}
|
||||
{/* ##TODO## annotation_reply */}
|
||||
</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)}>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import TextToSpeech from '@/app/components/base/features/new-feature-panel/text-
|
|||
import FileUpload from '@/app/components/base/features/new-feature-panel/file-upload'
|
||||
import FollowUp from '@/app/components/base/features/new-feature-panel/follow-up'
|
||||
import Citation from '@/app/components/base/features/new-feature-panel/citation'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
|
|
@ -34,6 +35,8 @@ type Props = {
|
|||
onChange?: OnFeaturesChange
|
||||
onClose: () => void
|
||||
inWorkflow?: boolean
|
||||
promptVariables?: PromptVariable[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
const NewFeaturePanel = ({
|
||||
|
|
@ -44,6 +47,8 @@ const NewFeaturePanel = ({
|
|||
onChange,
|
||||
onClose,
|
||||
inWorkflow = true,
|
||||
promptVariables,
|
||||
onAutoAddPromptVariable,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
|
@ -93,7 +98,12 @@ const NewFeaturePanel = ({
|
|||
<MoreLikeThis disabled={disabled} onChange={onChange} />
|
||||
)}
|
||||
{isChatMode && (
|
||||
<ConversationOpener disabled={disabled} onChange={onChange} />
|
||||
<ConversationOpener
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
promptVariables={promptVariables}
|
||||
onAutoAddPromptVariable={onAutoAddPromptVariable}
|
||||
/>
|
||||
)}
|
||||
<Moderation disabled={disabled} onChange={onChange} />
|
||||
{isChatMode && speech2textDefaultModel && (
|
||||
|
|
@ -109,7 +119,7 @@ const NewFeaturePanel = ({
|
|||
{isChatMode && (
|
||||
<Citation disabled={disabled} onChange={onChange} />
|
||||
)}
|
||||
{/* annotation reply ##TODO## */}
|
||||
{/* ##TODO## annotation_reply */}
|
||||
{showAnnotation && (
|
||||
<div className='group mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'>
|
||||
<div className='mb-2 flex items-center gap-2'>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type {
|
|||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
import Pricing from '@/app/components/billing/pricing'
|
||||
import type { ModerationConfig } from '@/models/debug'
|
||||
import type { ModerationConfig, PromptVariable } from '@/models/debug'
|
||||
import type {
|
||||
ApiBasedExtension,
|
||||
ExternalDataTool,
|
||||
|
|
@ -56,7 +56,10 @@ export type ModalContextState = {
|
|||
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
|
||||
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
|
||||
setShowModelLoadBalancingEntryModal: Dispatch<SetStateAction<ModalState<LoadBalancingEntryModalType> | null>>
|
||||
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement> | null>>
|
||||
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement & {
|
||||
promptVariables?: PromptVariable[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}> | null>>
|
||||
}
|
||||
const ModalContext = createContext<ModalContextState>({
|
||||
setShowAccountSettingModal: () => { },
|
||||
|
|
@ -92,7 +95,10 @@ export const ModalContextProvider = ({
|
|||
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
|
||||
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
|
||||
const [showModelLoadBalancingEntryModal, setShowModelLoadBalancingEntryModal] = useState<ModalState<LoadBalancingEntryModalType> | null>(null)
|
||||
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement> | null>(null)
|
||||
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement & {
|
||||
promptVariables?: PromptVariable[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}> | null>(null)
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const [showPricingModal, setShowPricingModal] = useState(searchParams.get('show-pricing') === '1')
|
||||
|
|
@ -285,7 +291,10 @@ export const ModalContextProvider = ({
|
|||
<OpeningSettingModal
|
||||
data={showOpeningModal.payload}
|
||||
onSave={handleSaveOpeningModal}
|
||||
onCancel={handleCancelOpeningModal} />
|
||||
onCancel={handleCancelOpeningModal}
|
||||
promptVariables={showOpeningModal.payload.promptVariables}
|
||||
onAutoAddPromptVariable={showOpeningModal.payload.onAutoAddPromptVariable}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ModalContext.Provider>
|
||||
|
|
|
|||
Loading…
Reference in New Issue