fix: multiple model configuration clear conversation by rerender (#2286)

This commit is contained in:
zxhlyh
2024-01-30 16:06:01 +08:00
committed by GitHub
parent 6f7fd6613a
commit 68406b9906
22 changed files with 816 additions and 712 deletions

View File

@@ -1,6 +1,7 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
} from 'react'
import type { ModelAndParameter } from '../types'
@@ -9,16 +10,13 @@ import {
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
} from '../types'
import {
AgentStrategy,
ModelModeType,
} from '@/types/app'
useConfigFromDebugContext,
useFormattingChangedSubscription,
} from '../hooks'
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type {
ChatConfig,
OnSend,
} from '@/app/components/base/chat/types'
import type { OnSend } from '@/app/components/base/chat/types'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
import {
@@ -26,7 +24,6 @@ import {
fetchSuggestedQuestions,
stopChatMessageResponding,
} from '@/service/debug'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@@ -39,66 +36,14 @@ const ChatItem: FC<ChatItemProps> = ({
}) => {
const { userProfile } = useAppContext()
const {
isAdvancedMode,
modelConfig,
appId,
inputs,
promptMode,
speechToTextConfig,
introduction,
suggestedQuestions: openingSuggestedQuestions,
suggestedQuestionsAfterAnswerConfig,
citationConfig,
moderationConfig,
chatPromptConfig,
completionPromptConfig,
dataSets,
datasetConfigs,
visionConfig,
annotationConfig,
collectionList,
textToSpeechConfig,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const postDatasets = dataSets.map(({ id }) => ({
dataset: {
enabled: true,
id,
},
}))
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const config: ChatConfig = {
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: isAdvancedMode ? chatPromptConfig : {},
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
opening_statement: introduction,
more_like_this: {
enabled: false,
},
suggested_questions: openingSuggestedQuestions,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
text_to_speech: textToSpeechConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
agent_mode: {
...modelConfig.agentConfig,
strategy: (modelAndParameter.provider === 'openai' && modelConfig.mode === ModelModeType.chat) ? AgentStrategy.functionCall : AgentStrategy.react,
},
dataset_configs: {
...datasetConfigs,
datasets: {
datasets: [...postDatasets],
} as any,
},
file_upload: {
image: visionConfig,
},
annotation_reply: annotationConfig,
}
const config = useConfigFromDebugContext()
const {
chatList,
isResponsing,
@@ -114,8 +59,9 @@ const ChatItem: FC<ChatItemProps> = ({
[],
taskId => stopChatMessageResponding(appId, taskId),
)
useFormattingChangedSubscription(chatList)
const doSend: OnSend = (message, files) => {
const doSend: OnSend = useCallback((message, files) => {
const currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider)
const currentModel = currentProvider?.models.find(model => model.model === modelAndParameter.model)
const supportVision = currentModel?.features?.includes(ModelFeatureEnum.vision)
@@ -147,7 +93,7 @@ const ChatItem: FC<ChatItemProps> = ({
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
},
)
}
}, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled])
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {
@@ -174,8 +120,9 @@ const ChatItem: FC<ChatItemProps> = ({
chatList={chatList}
isResponsing={isResponsing}
noChatInput
noStopResponding
chatContainerclassName='p-4'
chatFooterClassName='!-bottom-4'
chatFooterClassName='p-4 pb-0'
suggestedQuestions={suggestedQuestions}
onSend={doSend}
showPromptLog

View File

@@ -7,6 +7,7 @@ export type DebugWithMultipleModelContextType = {
multipleModelConfigs: ModelAndParameter[]
onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void
onDebugWithMultipleModelChange: (singleModelConfig: ModelAndParameter) => void
checkCanSend?: () => boolean
}
const DebugWithMultipleModelContext = createContext<DebugWithMultipleModelContextType>({
multipleModelConfigs: [],
@@ -24,12 +25,14 @@ export const DebugWithMultipleModelContextProvider = ({
onMultipleModelConfigsChange,
multipleModelConfigs,
onDebugWithMultipleModelChange,
checkCanSend,
}: DebugWithMultipleModelContextProviderProps) => {
return (
<DebugWithMultipleModelContext.Provider value={{
onMultipleModelConfigsChange,
multipleModelConfigs,
onDebugWithMultipleModelChange,
checkCanSend,
}}>
{children}
</DebugWithMultipleModelContext.Provider>

View File

@@ -1,4 +1,4 @@
import type { FC } from 'react'
import type { CSSProperties, FC } from 'react'
import { useTranslation } from 'react-i18next'
import { memo } from 'react'
import type { ModelAndParameter } from '../types'
@@ -15,10 +15,12 @@ import { ModelStatusEnum } from '@/app/components/header/account-setting/model-p
type DebugItemProps = {
modelAndParameter: ModelAndParameter
className?: string
style?: CSSProperties
}
const DebugItem: FC<DebugItemProps> = ({
modelAndParameter,
className,
style,
}) => {
const { t } = useTranslation()
const { mode } = useDebugConfigurationContext()
@@ -61,7 +63,10 @@ const DebugItem: FC<DebugItemProps> = ({
}
return (
<div className={`flex flex-col min-w-[320px] rounded-xl bg-white border-[0.5px] border-black/5 ${className}`}>
<div
className={`flex flex-col min-w-[320px] rounded-xl bg-white border-[0.5px] border-black/5 ${className}`}
style={style}
>
<div className='shrink-0 flex items-center justify-between h-10 px-3 border-b-[0.5px] border-b-black/5'>
<div className='flex items-center justify-center w-6 h-5 font-medium italic text-gray-500'>
#{index + 1}

View File

@@ -2,6 +2,7 @@ import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
} from 'react'
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
import DebugItem from './debug-item'
@@ -21,10 +22,16 @@ const DebugWithMultipleModel = () => {
speechToTextConfig,
visionConfig,
} = useDebugConfigurationContext()
const { multipleModelConfigs } = useDebugWithMultipleModelContext()
const {
multipleModelConfigs,
checkCanSend,
} = useDebugWithMultipleModelContext()
const { eventEmitter } = useEventEmitterContextContext()
const handleSend = useCallback((message: string, files?: VisionFile[]) => {
if (checkCanSend && !checkCanSend())
return
eventEmitter?.emit({
type: APP_CHAT_WITH_MULTIPLE_MODEL,
payload: {
@@ -32,72 +39,90 @@ const DebugWithMultipleModel = () => {
files,
},
} as any)
}, [eventEmitter])
}, [eventEmitter, checkCanSend])
const twoLine = multipleModelConfigs.length === 2
const threeLine = multipleModelConfigs.length === 3
const fourLine = multipleModelConfigs.length === 4
const size = useMemo(() => {
let width = ''
let height = ''
if (twoLine) {
width = 'calc(50% - 4px - 24px)'
height = '100%'
}
if (threeLine) {
width = 'calc(33.3% - 5.33px - 16px)'
height = '100%'
}
if (fourLine) {
width = 'calc(50% - 4px - 24px)'
height = 'calc(50% - 4px)'
}
return {
width,
height,
}
}, [twoLine, threeLine, fourLine])
const position = useCallback((idx: number) => {
let translateX = '0'
let translateY = '0'
if (twoLine && idx === 1)
translateX = 'calc(100% + 8px)'
if (threeLine && idx === 1)
translateX = 'calc(100% + 8px)'
if (threeLine && idx === 2)
translateX = 'calc(200% + 16px)'
if (fourLine && idx === 1)
translateX = 'calc(100% + 8px)'
if (fourLine && idx === 2)
translateY = 'calc(100% + 8px)'
if (fourLine && idx === 3) {
translateX = 'calc(100% + 8px)'
translateY = 'calc(100% + 8px)'
}
return {
translateX,
translateY,
}
}, [twoLine, threeLine, fourLine])
return (
<div className='flex flex-col h-full'>
<div
className={`
mb-3 overflow-auto
${(twoLine || threeLine) && 'flex gap-2'}
grow mb-3 relative px-6 overflow-auto
`}
style={{ height: mode === 'chat' ? 'calc(100% - 60px)' : '100%' }}
>
{
(twoLine || threeLine) && multipleModelConfigs.map(modelConfig => (
multipleModelConfigs.map((modelConfig, index) => (
<DebugItem
key={modelConfig.id}
modelAndParameter={modelConfig}
className={`
h-full min-h-[200px]
${twoLine && 'w-1/2'}
${threeLine && 'w-1/3'}
absolute left-6 top-0 min-h-[200px]
${twoLine && index === 0 && 'mr-2'}
${threeLine && (index === 0 || index === 1) && 'mr-2'}
${fourLine && (index === 0 || index === 2) && 'mr-2'}
${fourLine && (index === 0 || index === 1) && 'mb-2'}
`}
style={{
width: size.width,
height: size.height,
transform: `translateX(${position(index).translateX}) translateY(${position(index).translateY})`,
}}
/>
))
}
{
fourLine && (
<>
<div
className='flex space-x-2 mb-2 min-h-[200px]'
style={{ height: 'calc(50% - 4px)' }}
>
{
multipleModelConfigs.slice(0, 2).map(modelConfig => (
<DebugItem
key={modelConfig.id}
modelAndParameter={modelConfig}
className='w-1/2 h-full'
/>
))
}
</div>
<div
className='flex space-x-2 min-h-[200px]'
style={{ height: 'calc(50% - 4px)' }}
>
{
multipleModelConfigs.slice(2, 4).map(modelConfig => (
<DebugItem
key={modelConfig.id}
modelAndParameter={modelConfig}
className='w-1/2 h-full'
/>
))
}
</div>
</>
)
}
</div>
{
mode === 'chat' && (
<div className='shrink-0'>
<div className='shrink-0 pb-4 px-6'>
<ChatInput
onSend={handleSend}
speechToTextConfig={speechToTextConfig}
@@ -116,12 +141,14 @@ const DebugWithMultipleModelWrapper: FC<DebugWithMultipleModelContextType> = ({
onMultipleModelConfigsChange,
multipleModelConfigs,
onDebugWithMultipleModelChange,
checkCanSend,
}) => {
return (
<DebugWithMultipleModelContextProvider
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
multipleModelConfigs={multipleModelConfigs}
onDebugWithMultipleModelChange={onDebugWithMultipleModelChange}
checkCanSend={checkCanSend}
>
<DebugWithMultipleModelMemoed />
</DebugWithMultipleModelContextProvider>

View File

@@ -0,0 +1,143 @@
import {
forwardRef,
memo,
useCallback,
useImperativeHandle,
useMemo,
} from 'react'
import {
useConfigFromDebugContext,
useFormattingChangedSubscription,
} from '../hooks'
import Chat from '@/app/components/base/chat/chat'
import { useChat } from '@/app/components/base/chat/chat/hooks'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import type { OnSend } from '@/app/components/base/chat/types'
import { useProviderContext } from '@/context/provider-context'
import {
fetchConvesationMessages,
fetchSuggestedQuestions,
stopChatMessageResponding,
} from '@/service/debug'
import Avatar from '@/app/components/base/avatar'
import { useAppContext } from '@/context/app-context'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
type DebugWithSingleModelProps = {
checkCanSend?: () => boolean
}
export type DebugWithSingleModelRefType = {
handleRestart: () => void
}
const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSingleModelProps>(({
checkCanSend,
}, ref) => {
const { userProfile } = useAppContext()
const {
modelConfig,
appId,
inputs,
visionConfig,
collectionList,
completionParams,
} = useDebugConfigurationContext()
const { textGenerationModelList } = useProviderContext()
const config = useConfigFromDebugContext()
const {
chatList,
isResponsing,
handleSend,
suggestedQuestions,
handleStop,
handleRestart,
handleAnnotationAdded,
handleAnnotationEdited,
handleAnnotationRemoved,
} = useChat(
{
...config,
supportAnnotation: true,
appId,
},
{
inputs,
promptVariables: modelConfig.configs.prompt_variables,
},
[],
taskId => stopChatMessageResponding(appId, taskId),
)
useFormattingChangedSubscription(chatList)
const doSend: OnSend = useCallback((message, files) => {
if (checkCanSend && !checkCanSend())
return
const currentProvider = textGenerationModelList.find(item => item.provider === modelConfig.provider)
const currentModel = currentProvider?.models.find(model => model.model === modelConfig.model_id)
const supportVision = currentModel?.features?.includes(ModelFeatureEnum.vision)
const configData = {
...config,
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams,
},
}
const data: any = {
query: message,
inputs,
model_config: configData,
}
if (visionConfig.enabled && files?.length && supportVision)
data.files = files
handleSend(
`apps/${appId}/chat-messages`,
data,
{
onGetConvesationMessages: (conversationId, getAbortController) => fetchConvesationMessages(appId, conversationId, getAbortController),
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
},
)
}, [appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
const allToolIcons = useMemo(() => {
const icons: Record<string, any> = {}
modelConfig.agentConfig.tools?.forEach((item: any) => {
icons[item.tool_name] = collectionList.find((collection: any) => collection.id === item.provider_id)?.icon
})
return icons
}, [collectionList, modelConfig.agentConfig.tools])
useImperativeHandle(ref, () => {
return {
handleRestart,
}
}, [handleRestart])
return (
<Chat
config={config}
chatList={chatList}
isResponsing={isResponsing}
chatContainerclassName='p-6'
chatFooterClassName='px-6 pt-10 pb-4'
suggestedQuestions={suggestedQuestions}
onSend={doSend}
onStopResponding={handleStop}
showPromptLog
questionIcon={<Avatar name={userProfile.name} size={40} />}
allToolIcons={allToolIcons}
onAnnotationEdited={handleAnnotationEdited}
onAnnotationAdded={handleAnnotationAdded}
onAnnotationRemoved={handleAnnotationRemoved}
/>
)
})
DebugWithSingleModel.displayName = 'DebugWithSingleModel'
export default memo(DebugWithSingleModel)

View File

@@ -7,6 +7,17 @@ import type {
DebugWithSingleOrMultipleModelConfigs,
ModelAndParameter,
} from './types'
import { ORCHESTRATE_CHANGED } from './types'
import type {
ChatConfig,
ChatItem,
} from '@/app/components/base/chat/types'
import {
AgentStrategy,
} from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useEventEmitterContextContext } from '@/context/event-emitter'
export const useDebugWithSingleOrMultipleModel = (appId: string) => {
const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem('app-debug-with-single-or-multiple-models')
@@ -52,3 +63,95 @@ export const useDebugWithSingleOrMultipleModel = (appId: string) => {
handleMultipleModelConfigsChange,
}
}
export const useConfigFromDebugContext = () => {
const {
isAdvancedMode,
modelConfig,
appId,
promptMode,
speechToTextConfig,
introduction,
suggestedQuestions: openingSuggestedQuestions,
suggestedQuestionsAfterAnswerConfig,
citationConfig,
moderationConfig,
chatPromptConfig,
completionPromptConfig,
dataSets,
datasetConfigs,
visionConfig,
annotationConfig,
textToSpeechConfig,
isFunctionCall,
} = useDebugConfigurationContext()
const postDatasets = dataSets.map(({ id }) => ({
dataset: {
enabled: true,
id,
},
}))
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const config: ChatConfig = {
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: isAdvancedMode ? chatPromptConfig : {},
completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
opening_statement: introduction,
more_like_this: {
enabled: false,
},
suggested_questions: openingSuggestedQuestions,
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
text_to_speech: textToSpeechConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
agent_mode: {
...modelConfig.agentConfig,
strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
},
dataset_configs: {
...datasetConfigs,
datasets: {
datasets: [...postDatasets],
} as any,
},
file_upload: {
image: visionConfig,
},
annotation_reply: annotationConfig,
supportAnnotation: true,
appId,
}
return config
}
export const useFormattingChangedDispatcher = () => {
const { eventEmitter } = useEventEmitterContextContext()
const dispatcher = useCallback(() => {
eventEmitter?.emit({
type: ORCHESTRATE_CHANGED,
} as any)
}, [eventEmitter])
return dispatcher
}
export const useFormattingChangedSubscription = (chatList: ChatItem[]) => {
const {
formattingChanged,
setFormattingChanged,
} = useDebugConfigurationContext()
const { eventEmitter } = useEventEmitterContextContext()
eventEmitter?.useSubscription((v: any) => {
if (v.type === ORCHESTRATE_CHANGED) {
if (chatList.some(item => item.isAnswer) && !formattingChanged)
setFormattingChanged(true)
}
})
}

View File

@@ -2,29 +2,27 @@
import type { FC } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import React, { useEffect, useRef, useState } from 'react'
import cn from 'classnames'
import produce, { setAutoFreeze } from 'immer'
import { useBoolean, useGetState } from 'ahooks'
import React, { useCallback, useEffect, useState } from 'react'
import { setAutoFreeze } from 'immer'
import { useBoolean } from 'ahooks'
import { useContext } from 'use-context-selector'
import dayjs from 'dayjs'
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
import FormattingChanged from '../base/warning-mask/formatting-changed'
import GroupName from '../base/group-name'
import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
import DebugWithMultipleModel from './debug-with-multiple-model'
import DebugWithSingleModel from './debug-with-single-model'
import type { DebugWithSingleModelRefType } from './debug-with-single-model'
import type { ModelAndParameter } from './types'
import {
APP_CHAT_WITH_MULTIPLE_MODEL,
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
} from './types'
import { AgentStrategy, AppType, ModelModeType, TransferMethod } from '@/types/app'
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
import type { IChatItem } from '@/app/components/app/chat/type'
import Chat from '@/app/components/app/chat'
import { AppType, ModelModeType, TransferMethod } from '@/types/app'
import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
import ConfigContext from '@/context/debug-configuration'
import { ToastContext } from '@/app/components/base/toast'
import { fetchConvesationMessages, fetchSuggestedQuestions, sendChatMessage, sendCompletionMessage, stopChatMessageResponding } from '@/service/debug'
import { sendCompletionMessage } from '@/service/debug'
import Button from '@/app/components/base/button'
import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
@@ -32,7 +30,6 @@ import TextGeneration from '@/app/components/app/text-generate/item'
import { IS_CE_EDITION } from '@/config'
import type { Inputs } from '@/models/debug'
import { fetchFileUploadConfig } from '@/service/common'
import type { Annotation as AnnotationType } from '@/models/log'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@@ -63,8 +60,6 @@ const Debug: FC<IDebug> = ({
const {
appId,
mode,
isFunctionCall,
collectionList,
modelModeType,
hasSetBlockStatus,
isAdvancedMode,
@@ -72,7 +67,6 @@ const Debug: FC<IDebug> = ({
chatPromptConfig,
completionPromptConfig,
introduction,
suggestedQuestions,
suggestedQuestionsAfterAnswerConfig,
speechToTextConfig,
textToSpeechConfig,
@@ -81,79 +75,36 @@ const Debug: FC<IDebug> = ({
moreLikeThisConfig,
formattingChanged,
setFormattingChanged,
conversationId,
setConversationId,
controlClearChatMessage,
dataSets,
modelConfig,
completionParams,
hasSetContextVar,
datasetConfigs,
visionConfig,
annotationConfig,
setVisionConfig,
} = useContext(ConfigContext)
const { eventEmitter } = useEventEmitterContextContext()
const { data: speech2textDefaultModel } = useDefaultModel(4)
const { data: text2speechDefaultModel } = useDefaultModel(5)
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
const chatListDomRef = useRef<HTMLDivElement>(null)
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
// onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576
useEffect(() => {
setAutoFreeze(false)
return () => {
setAutoFreeze(true)
}
}, [])
useEffect(() => {
// scroll to bottom
if (chatListDomRef.current)
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
}, [chatList])
const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs)
useEffect(() => {
if (introduction && !chatList.some(item => !item.isAnswer)) {
setChatList([{
id: `${Date.now()}`,
content: getIntroduction(),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions,
}])
}
}, [introduction, suggestedQuestions, modelConfig.configs.prompt_variables, inputs])
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
const [isShowCannotQueryDataset, setShowCannotQueryDataset] = useState(false)
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
const [messageTaskId, setMessageTaskId] = useState('')
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
useEffect(() => {
if (formattingChanged && chatList.some(item => !item.isAnswer))
if (formattingChanged)
setIsShowFormattingChangeConfirm(true)
setFormattingChanged(false)
}, [formattingChanged])
const debugWithSingleModelRef = React.useRef<DebugWithSingleModelRefType | null>(null)
const handleClearConversation = () => {
setConversationId(null)
abortController?.abort()
setResponsingFalse()
setChatList(introduction
? [{
id: `${Date.now()}`,
content: getIntroduction(),
isAnswer: true,
isOpeningStatement: true,
suggestedQuestions,
}]
: [])
setIsShowSuggestion(false)
debugWithSingleModelRef.current?.handleRestart()
}
const clearConversation = async () => {
if (debugWithMultipleModel) {
@@ -169,18 +120,21 @@ const Debug: FC<IDebug> = ({
const handleConfirm = () => {
clearConversation()
setIsShowFormattingChangeConfirm(false)
setFormattingChanged(false)
}
const handleCancel = () => {
setIsShowFormattingChangeConfirm(false)
setFormattingChanged(false)
}
const { notify } = useContext(ToastContext)
const logError = (message: string) => {
const logError = useCallback((message: string) => {
notify({ type: 'error', message })
}
}, [notify])
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
const checkCanSend = () => {
const checkCanSend = useCallback(() => {
if (isAdvancedMode && mode === AppType.chat) {
if (modelModeType === ModelModeType.completion) {
if (!hasSetBlockStatus.history) {
@@ -214,319 +168,28 @@ const Debug: FC<IDebug> = ({
return false
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
return false
}
return !hasEmptyInput
}
const doShowSuggestion = isShowSuggestion && !isResponsing
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
const [userQuery, setUserQuery] = useState('')
const onSend = async (message: string, files?: VisionFile[]) => {
if (isResponsing) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
return false
}
if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
return false
}
const postDatasets = dataSets.map(({ id }) => ({
dataset: {
enabled: true,
id,
},
}))
const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
const updateCurrentQA = ({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
}: {
responseItem: IChatItem
questionId: string
placeholderAnswerId: string
questionItem: IChatItem
}) => {
// closesure new list is outdated.
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
setChatList(newListWithAnswer)
}
const postModelConfig: BackendModelConfig = {
text_to_speech: {
enabled: false,
},
pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
prompt_type: promptMode,
chat_prompt_config: {},
completion_prompt_config: {},
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
dataset_query_variable: contextVar || '',
opening_statement: introduction,
more_like_this: {
enabled: false,
},
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
speech_to_text: speechToTextConfig,
retriever_resource: citationConfig,
sensitive_word_avoidance: moderationConfig,
agent_mode: {
...modelConfig.agentConfig,
strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
},
model: {
provider: modelConfig.provider,
name: modelConfig.model_id,
mode: modelConfig.mode,
completion_params: completionParams as any,
},
dataset_configs: {
...datasetConfigs,
datasets: {
datasets: [...postDatasets],
} as any,
},
file_upload: {
image: visionConfig,
},
annotation_reply: annotationConfig,
}
if (isAdvancedMode) {
postModelConfig.chat_prompt_config = chatPromptConfig
postModelConfig.completion_prompt_config = completionPromptConfig
}
const data: Record<string, any> = {
conversation_id: conversationId,
inputs,
query: message,
model_config: postModelConfig,
}
if (visionConfig.enabled && files && files?.length > 0) {
data.files = files.map((item) => {
if (item.transfer_method === TransferMethod.local_file) {
return {
...item,
url: '',
}
}
return item
})
}
// qustion
const questionId = `question-${Date.now()}`
const questionItem = {
id: questionId,
content: message,
isAnswer: false,
message_files: files,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
const placeholderAnswerItem = {
id: placeholderAnswerId,
content: '',
isAnswer: true,
}
const newList = [...getChatList(), questionItem, placeholderAnswerItem]
setChatList(newList)
let isAgentMode = false
// answer
const responseItem: IChatItem = {
id: `${Date.now()}`,
content: '',
agent_thoughts: [],
message_files: [],
isAnswer: true,
}
let hasSetResponseId = false
let _newConversationId: null | string = null
setHasStopResponded(false)
setResponsingTrue()
setIsShowSuggestion(false)
sendChatMessage(appId, data, {
getAbortController: (abortController) => {
setAbortController(abortController)
},
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
// console.log('onData', message)
if (!isAgentMode) {
responseItem.content = responseItem.content + message
}
else {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
if (lastThought)
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
}
if (messageId && !hasSetResponseId) {
responseItem.id = messageId
hasSetResponseId = true
}
if (isFirstMessage && newConversationId) {
setConversationId(newConversationId)
_newConversationId = newConversationId
}
setMessageTaskId(taskId)
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
async onCompleted(hasError?: boolean) {
setResponsingFalse()
if (hasError)
return
if (_newConversationId) {
const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string)
const newResponseItem = data.find((item: any) => item.id === responseItem.id)
if (!newResponseItem)
return
setChatList(produce(getChatList(), (draft) => {
const index = draft.findIndex(item => item.id === responseItem.id)
if (index !== -1) {
const requestion = draft[index - 1]
draft[index - 1] = {
...requestion,
log: newResponseItem.message,
}
draft[index] = {
...draft[index],
more: {
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
latency: newResponseItem.provider_response_latency.toFixed(2),
},
}
}
}))
}
if (suggestedQuestionsAfterAnswerConfig.enabled && !getHasStopResponded()) {
const { data }: any = await fetchSuggestedQuestions(appId, responseItem.id)
setSuggestQuestions(data)
setIsShowSuggestion(true)
}
},
onFile(file) {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
if (lastThought)
responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
onThought(thought) {
isAgentMode = true
const response = responseItem as any
if (thought.message_id && !hasSetResponseId)
response.id = thought.message_id
if (response.agent_thoughts.length === 0) {
response.agent_thoughts.push(thought)
}
else {
const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
// thought changed but still the same thought, so update.
if (lastThought.id === thought.id) {
thought.thought = lastThought.thought
thought.message_files = lastThought.message_files
responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
}
else {
responseItem.agent_thoughts!.push(thought)
}
}
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
onMessageEnd: (messageEnd) => {
if (messageEnd.metadata?.annotation_reply) {
responseItem.id = messageEnd.id
responseItem.annotation = ({
id: messageEnd.metadata.annotation_reply.id,
authorName: messageEnd.metadata.annotation_reply.account.name,
} as AnnotationType)
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({
...responseItem,
})
})
setChatList(newListWithAnswer)
return
}
responseItem.citation = messageEnd.metadata?.retriever_resources || []
const newListWithAnswer = produce(
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
(draft) => {
if (!draft.find(item => item.id === questionId))
draft.push({ ...questionItem })
draft.push({ ...responseItem })
})
setChatList(newListWithAnswer)
},
onMessageReplace: (messageReplace) => {
responseItem.content = messageReplace.answer
},
onError() {
setResponsingFalse()
// role back placeholder answer
setChatList(produce(getChatList(), (draft) => {
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
}))
},
})
return true
}
useEffect(() => {
if (controlClearChatMessage)
setChatList([])
}, [controlClearChatMessage])
}, [
completionFiles,
hasSetBlockStatus.history,
hasSetBlockStatus.query,
inputs,
isAdvancedMode,
mode,
modelConfig.configs.prompt_variables,
t,
logError,
notify,
modelModeType,
])
const [completionRes, setCompletionRes] = useState('')
const [messageId, setMessageId] = useState<string | null>(null)
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
const sendTextCompletion = async () => {
if (isResponsing) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
@@ -685,13 +348,13 @@ const Debug: FC<IDebug> = ({
setVisionConfig({
...visionConfig,
enabled: true,
})
}, true)
}
else {
setVisionConfig({
...visionConfig,
enabled: false,
})
}, true)
}
}
}
@@ -699,17 +362,10 @@ const Debug: FC<IDebug> = ({
useEffect(() => {
handleVisionConfigInMultipleModel()
}, [multipleModelConfigs, mode])
const allToolIcons = (() => {
const icons: Record<string, any> = {}
modelConfig.agentConfig.tools?.forEach((item: any) => {
icons[item.tool_name] = collectionList.find((collection: any) => collection.id === item.provider_id)?.icon
})
return icons
})()
return (
<>
<div className="shrink-0">
<div className="shrink-0 pt-4 px-6">
<div className='flex items-center justify-between mb-2'>
<div className='h2 '>{t('appDebug.inputs.title')}</div>
<div className='flex items-center'>
@@ -761,6 +417,7 @@ const Debug: FC<IDebug> = ({
multipleModelConfigs={multipleModelConfigs}
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
onDebugWithMultipleModelChange={handleChangeToSingleModel}
checkCanSend={checkCanSend}
/>
</div>
)
@@ -770,47 +427,16 @@ const Debug: FC<IDebug> = ({
<div className="flex flex-col grow">
{/* Chat */}
{mode === AppType.chat && (
<div className="mt-[34px] h-full flex flex-col">
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative mt-1.5 grow h-[200px] overflow-hidden')}>
<div className="h-full overflow-y-auto overflow-x-hidden" ref={chatListDomRef}>
<Chat
chatList={chatList}
query={userQuery}
onQueryChange={setUserQuery}
onSend={onSend}
checkCanSend={checkCanSend}
feedbackDisabled
useCurrentUserAvatar
isResponsing={isResponsing}
canStopResponsing={!!messageTaskId}
abortResponsing={async () => {
await stopChatMessageResponding(appId, messageTaskId)
setHasStopResponded(true)
setResponsingFalse()
}}
isShowSuggestion={doShowSuggestion}
suggestionList={suggestQuestions}
isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
isShowCitation={citationConfig.enabled}
isShowCitationHitInfo
isShowPromptLog
visionConfig={{
...visionConfig,
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
}}
supportAnnotation
appId={appId}
onChatListChange={setChatList}
allToolIcons={allToolIcons}
/>
</div>
</div>
<div className='grow h-0 overflow-hidden'>
<DebugWithSingleModel
ref={debugWithSingleModelRef}
checkCanSend={checkCanSend}
/>
</div>
)}
{/* Text Generation */}
{mode === AppType.completion && (
<div className="mt-6">
<div className="mt-6 px-6 pb-4">
<GroupName name={t('appDebug.result')} />
{(completionRes || isResponsing) && (
<TextGeneration
@@ -830,12 +456,6 @@ const Debug: FC<IDebug> = ({
)}
</div>
)}
{isShowFormattingChangeConfirm && (
<FormattingChanged
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
{isShowCannotQueryDataset && (
<CannotQueryDataset
onConfirm={() => setShowCannotQueryDataset(false)}
@@ -844,6 +464,12 @@ const Debug: FC<IDebug> = ({
</div>
)
}
{isShowFormattingChangeConfirm && (
<FormattingChanged
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
)}
{!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
</>
)

View File

@@ -16,3 +16,4 @@ export type DebugWithSingleOrMultipleModelConfigs = {
export const APP_CHAT_WITH_MULTIPLE_MODEL = 'APP_CHAT_WITH_MULTIPLE_MODEL'
export const APP_CHAT_WITH_MULTIPLE_MODEL_RESTART = 'APP_CHAT_WITH_MULTIPLE_MODEL_RESTART'
export const APP_SIDEBAR_SHOULD_COLLAPSE = 'APP_SIDEBAR_SHOULD_COLLAPSE'
export const ORCHESTRATE_CHANGED = 'ORCHESTRATE_CHANGED'