diff --git a/web/app/components/app/annotation/edit-annotation-modal/index.tsx b/web/app/components/app/annotation/edit-annotation-modal/index.tsx index bb4599d718..5edf07240d 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/index.tsx @@ -132,6 +132,7 @@ const EditAnnotationModal: FC = ({ onRemove={() => { onRemove() setShowModal(false) + onHide() }} text={t('appDebug.feature.annotation.removeConfirm') as string} /> diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index dc6397832d..1d66f791e7 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import cn from 'classnames' import { useContext } from 'use-context-selector' import produce from 'immer' +import { useFormattingChangedDispatcher } from '../../../debug/hooks' import ChooseTool from './choose-tool' import SettingBuiltInTool from './setting-built-in-tool' import Panel from '@/app/components/app/configuration/base/feature-panel' @@ -27,6 +28,7 @@ const AgentTools: FC = () => { const { t } = useTranslation() const [isShowChooseTool, setIsShowChooseTool] = useState(false) const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext) + const formattingChangedDispatcher = useFormattingChangedDispatcher() const [currentTool, setCurrentTool] = useState(null) const [selectedProviderId, setSelectedProviderId] = useState(undefined) @@ -49,6 +51,7 @@ const AgentTools: FC = () => { }) setModelConfig(newModelConfig) setIsShowSettingTool(false) + formattingChangedDispatcher() } return ( @@ -141,6 +144,7 @@ const AgentTools: FC = () => { draft.agentConfig.tools.splice(index, 1) }) setModelConfig(newModelConfig) + formattingChangedDispatcher() }}> @@ -167,6 +171,7 @@ const AgentTools: FC = () => { draft.agentConfig.tools.splice(index, 1) }) setModelConfig(newModelConfig) + formattingChangedDispatcher() }}> @@ -183,6 +188,7 @@ const AgentTools: FC = () => { (draft.agentConfig.tools[index] as any).enabled = enabled }) setModelConfig(newModelConfig) + formattingChangedDispatcher() }} /> diff --git a/web/app/components/app/configuration/config/index.tsx b/web/app/components/app/configuration/config/index.tsx index 7cd6312042..3d1da56d83 100644 --- a/web/app/components/app/configuration/config/index.tsx +++ b/web/app/components/app/configuration/config/index.tsx @@ -4,6 +4,7 @@ import React, { useRef } from 'react' import { useContext } from 'use-context-selector' import produce from 'immer' import { useBoolean, useScroll } from 'ahooks' +import { useFormattingChangedDispatcher } from '../debug/hooks' import DatasetConfig from '../dataset-config' import ChatGroup from '../features/chat-group' import ExperienceEnchanceGroup from '../features/experience-enchance-group' @@ -44,7 +45,6 @@ const Config: FC = () => { modelConfig, setModelConfig, setPrevPromptConfig, - setFormattingChanged, moreLikeThisConfig, setMoreLikeThisConfig, suggestedQuestionsAfterAnswerConfig, @@ -64,6 +64,7 @@ const Config: FC = () => { const { data: speech2textDefaultModel } = useDefaultModel(4) const { data: text2speechDefaultModel } = useDefaultModel(5) const { setShowModerationSettingModal } = useModalContext() + const formattingChangedDispatcher = useFormattingChangedDispatcher() const promptTemplate = modelConfig.configs.prompt_template const promptVariables = modelConfig.configs.prompt_variables @@ -73,9 +74,8 @@ const Config: FC = () => { draft.configs.prompt_template = newTemplate draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newVariables] }) - if (modelConfig.configs.prompt_template !== newTemplate) - setFormattingChanged(true) + formattingChangedDispatcher() setPrevPromptConfig(modelConfig.configs) setModelConfig(newModelConfig) @@ -107,6 +107,7 @@ const Config: FC = () => { setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => { draft.enabled = value })) + formattingChangedDispatcher() }, speechToText: speechToTextConfig.enabled, setSpeechToText: (value) => { @@ -125,6 +126,7 @@ const Config: FC = () => { setCitationConfig(produce(citationConfig, (draft: CitationConfig) => { draft.enabled = value })) + formattingChangedDispatcher() }, annotation: annotationConfig.enabled, setAnnotation: async (value) => { diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx index c2039c98a4..ea25665fd3 100644 --- a/web/app/components/app/configuration/dataset-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/index.tsx @@ -4,6 +4,7 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import produce from 'immer' +import { useFormattingChangedDispatcher } from '../debug/hooks' import FeaturePanel from '../base/feature-panel' import OperationBtn from '../base/operation-btn' import CardItem from './card-item/item' @@ -26,25 +27,25 @@ const DatasetConfig: FC = () => { mode, dataSets: dataSet, setDataSets: setDataSet, - setFormattingChanged, modelConfig, setModelConfig, showSelectDataSet, isAgent, } = useContext(ConfigContext) + const formattingChangedDispatcher = useFormattingChangedDispatcher() const hasData = dataSet.length > 0 const onRemove = (id: string) => { setDataSet(dataSet.filter(item => item.id !== id)) - setFormattingChanged(true) + formattingChangedDispatcher() } const handleSave = (newDataset: DataSet) => { const index = dataSet.findIndex(item => item.id === newDataset.id) setDataSet([...dataSet.slice(0, index), newDataset, ...dataSet.slice(index + 1)]) - setFormattingChanged(true) + formattingChangedDispatcher() } const promptVariables = modelConfig.configs.prompt_variables diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx index ccf27c530a..2178527e4b 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx @@ -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 = ({ }) => { 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 = ({ [], 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 = ({ 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 = ({ chatList={chatList} isResponsing={isResponsing} noChatInput + noStopResponding chatContainerclassName='p-4' - chatFooterClassName='!-bottom-4' + chatFooterClassName='p-4 pb-0' suggestedQuestions={suggestedQuestions} onSend={doSend} showPromptLog diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx index 5b1fb04a1a..d95faf7ae9 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx @@ -7,6 +7,7 @@ export type DebugWithMultipleModelContextType = { multipleModelConfigs: ModelAndParameter[] onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void onDebugWithMultipleModelChange: (singleModelConfig: ModelAndParameter) => void + checkCanSend?: () => boolean } const DebugWithMultipleModelContext = createContext({ multipleModelConfigs: [], @@ -24,12 +25,14 @@ export const DebugWithMultipleModelContextProvider = ({ onMultipleModelConfigsChange, multipleModelConfigs, onDebugWithMultipleModelChange, + checkCanSend, }: DebugWithMultipleModelContextProviderProps) => { return ( {children} diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx index 30185bce9a..43a782f610 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx @@ -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 = ({ modelAndParameter, className, + style, }) => { const { t } = useTranslation() const { mode } = useDebugConfigurationContext() @@ -61,7 +63,10 @@ const DebugItem: FC = ({ } return ( -
+
#{index + 1} diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx index d7eef769e7..27348ecd55 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx @@ -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 (
{ - (twoLine || threeLine) && multipleModelConfigs.map(modelConfig => ( + multipleModelConfigs.map((modelConfig, index) => ( )) } - { - fourLine && ( - <> -
- { - multipleModelConfigs.slice(0, 2).map(modelConfig => ( - - )) - } -
-
- { - multipleModelConfigs.slice(2, 4).map(modelConfig => ( - - )) - } -
- - ) - }
{ mode === 'chat' && ( -
+
= ({ onMultipleModelConfigsChange, multipleModelConfigs, onDebugWithMultipleModelChange, + checkCanSend, }) => { return ( diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx new file mode 100644 index 0000000000..c235617eeb --- /dev/null +++ b/web/app/components/app/configuration/debug/debug-with-single-model/index.tsx @@ -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(({ + 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 = {} + 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 ( + } + allToolIcons={allToolIcons} + onAnnotationEdited={handleAnnotationEdited} + onAnnotationAdded={handleAnnotationAdded} + onAnnotationRemoved={handleAnnotationRemoved} + /> + ) +}) + +DebugWithSingleModel.displayName = 'DebugWithSingleModel' + +export default memo(DebugWithSingleModel) diff --git a/web/app/components/app/configuration/debug/hooks.tsx b/web/app/components/app/configuration/debug/hooks.tsx index c708a26f06..a77403db18 100644 --- a/web/app/components/app/configuration/debug/hooks.tsx +++ b/web/app/components/app/configuration/debug/hooks.tsx @@ -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) + } + }) +} diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index 6d4401e417..04be614349 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -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 = ({ const { appId, mode, - isFunctionCall, - collectionList, modelModeType, hasSetBlockStatus, isAdvancedMode, @@ -72,7 +67,6 @@ const Debug: FC = ({ chatPromptConfig, completionPromptConfig, introduction, - suggestedQuestions, suggestedQuestionsAfterAnswerConfig, speechToTextConfig, textToSpeechConfig, @@ -81,79 +75,36 @@ const Debug: FC = ({ 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([]) - const chatListDomRef = useRef(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(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(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 = ({ 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([]) - const checkCanSend = () => { + const checkCanSend = useCallback(() => { if (isAdvancedMode && mode === AppType.chat) { if (modelModeType === ModelModeType.completion) { if (!hasSetBlockStatus.history) { @@ -214,319 +168,28 @@ const Debug: FC = ({ 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([]) - 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 = { - 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(null) - const [completionFiles, setCompletionFiles] = useState([]) const sendTextCompletion = async () => { if (isResponsing) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) @@ -685,13 +348,13 @@ const Debug: FC = ({ setVisionConfig({ ...visionConfig, enabled: true, - }) + }, true) } else { setVisionConfig({ ...visionConfig, enabled: false, - }) + }, true) } } } @@ -699,17 +362,10 @@ const Debug: FC = ({ useEffect(() => { handleVisionConfigInMultipleModel() }, [multipleModelConfigs, mode]) - const allToolIcons = (() => { - const icons: Record = {} - modelConfig.agentConfig.tools?.forEach((item: any) => { - icons[item.tool_name] = collectionList.find((collection: any) => collection.id === item.provider_id)?.icon - }) - return icons - })() return ( <> -
+
{t('appDebug.inputs.title')}
@@ -761,6 +417,7 @@ const Debug: FC = ({ multipleModelConfigs={multipleModelConfigs} onMultipleModelConfigsChange={onMultipleModelConfigsChange} onDebugWithMultipleModelChange={handleChangeToSingleModel} + checkCanSend={checkCanSend} />
) @@ -770,47 +427,16 @@ const Debug: FC = ({
{/* Chat */} {mode === AppType.chat && ( -
-
-
- { - 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} - /> -
-
+
+
)} {/* Text Generation */} {mode === AppType.completion && ( -
+
{(completionRes || isResponsing) && ( = ({ )}
)} - {isShowFormattingChangeConfirm && ( - - )} {isShowCannotQueryDataset && ( setShowCannotQueryDataset(false)} @@ -844,6 +464,12 @@ const Debug: FC = ({
) } + {isShowFormattingChangeConfirm && ( + + )} {!hasSetAPIKEY && ()} ) diff --git a/web/app/components/app/configuration/debug/types.ts b/web/app/components/app/configuration/debug/types.ts index dd7d2ec712..f5cb418b37 100644 --- a/web/app/components/app/configuration/debug/types.ts +++ b/web/app/components/app/configuration/debug/types.ts @@ -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' diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index 08a3ea3585..3ecd300b32 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -13,7 +13,10 @@ import Button from '../../base/button' import Loading from '../../base/loading' import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config' import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal' -import { useDebugWithSingleOrMultipleModel } from './debug/hooks' +import { + useDebugWithSingleOrMultipleModel, + useFormattingChangedDispatcher, +} from './debug/hooks' import type { ModelAndParameter } from './debug/types' import { APP_SIDEBAR_SHOULD_COLLAPSE } from './debug/types' import PublishWithMultipleModel from './debug/debug-with-multiple-model/publish-with-multiple-model' @@ -45,7 +48,6 @@ import { AgentStrategy, AppType, ModelModeType, RETRIEVE_TYPE, Resolution, Trans import { PromptMode } from '@/models/debug' import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG, supportFunctionCallModels } from '@/config' import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset' -import I18n from '@/context/i18n' import { useModalContext } from '@/context/modal-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Drawer from '@/app/components/base/drawer' @@ -111,10 +113,11 @@ const Configuration: FC = () => { embedding_model_name: '', }, }) + const formattingChangedDispatcher = useFormattingChangedDispatcher() const setAnnotationConfig = (config: AnnotationReplyConfig, notSetFormatChanged?: boolean) => { doSetAnnotationConfig(config) if (!notSetFormatChanged) - setFormattingChanged(true) + formattingChangedDispatcher() } const [moderationConfig, setModerationConfig] = useState({ @@ -203,7 +206,7 @@ const Configuration: FC = () => { return } - setFormattingChanged(true) + formattingChangedDispatcher() if (data.find(item => !item.name)) { // has not loaded selected dataset const newSelected = produce(data, (draft: any) => { data.forEach((item, index) => { @@ -299,7 +302,7 @@ const Configuration: FC = () => { transfer_methods: config.transfer_methods || [TransferMethod.local_file], }) if (!notNoticeFormattingChanged) - setFormattingChanged(true) + formattingChangedDispatcher() } const { @@ -634,7 +637,6 @@ const Configuration: FC = () => { } const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false) - const { locale } = useContext(I18n) const { eventEmitter } = useEventEmitterContextContext() const { @@ -820,7 +822,7 @@ const Configuration: FC = () => { ) }
-
+
setShowAccountSettingModal({ payload: 'provider' })} diff --git a/web/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn/index.tsx b/web/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn/index.tsx index 3f92791749..1dcae64416 100644 --- a/web/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn/index.tsx +++ b/web/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn/index.tsx @@ -97,18 +97,21 @@ const CacheCtrlBtn: FC = ({
) - : ( - -
- -
-
- )} +
+ +
+ + ) + : null + } diff --git a/web/app/components/base/chat/chat/answer/agent-content.tsx b/web/app/components/base/chat/chat/answer/agent-content.tsx index e911934ee7..99f5f526d1 100644 --- a/web/app/components/base/chat/chat/answer/agent-content.tsx +++ b/web/app/components/base/chat/chat/answer/agent-content.tsx @@ -14,7 +14,10 @@ type AgentContentProps = { const AgentContent: FC = ({ item, }) => { - const { allToolIcons } = useChatContext() + const { + allToolIcons, + isResponsing, + } = useChatContext() const { annotation, agent_thoughts, @@ -42,7 +45,7 @@ const AgentContent: FC = ({ )} diff --git a/web/app/components/base/chat/chat/answer/index.tsx b/web/app/components/base/chat/chat/answer/index.tsx index c069583bcd..019a7ccbad 100644 --- a/web/app/components/base/chat/chat/answer/index.tsx +++ b/web/app/components/base/chat/chat/answer/index.tsx @@ -15,9 +15,13 @@ import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal type AnswerProps = { item: ChatItem + question: string + index: number } const Answer: FC = ({ item, + question, + index, }) => { const { t } = useTranslation() const { @@ -56,7 +60,15 @@ const Answer: FC = ({
- + { + !responsing && ( + + ) + } { responsing && !content && !hasAgentThoughts && (
@@ -75,7 +87,7 @@ const Answer: FC = ({ ) } { - annotation?.id && !annotation?.logAnnotation && ( + annotation?.id && annotation.authorName && ( = ({ item, + question, + index, }) => { - const { config } = useChatContext() + const { + config, + onAnnotationAdded, + onAnnotationEdited, + onAnnotationRemoved, + } = useChatContext() + const [isShowReplyModal, setIsShowReplyModal] = useState(false) const responsing = useCurrentAnswerIsResponsing(item.id) const { + id, isOpeningStatement, content, annotation, } = item + const hasAnnotation = !!annotation?.id return (
@@ -36,6 +51,34 @@ const Operation: FC = ({ className='hidden group-hover:block' /> )} + {(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && ( + onAnnotationAdded?.(id, authorName, question, content, index)} + onEdit={() => setIsShowReplyModal(true)} + onRemoved={() => onAnnotationRemoved?.(index)} + /> + )} + + setIsShowReplyModal(false)} + query={question} + answer={content} + onEdited={(editedQuery, editedAnswer) => onAnnotationEdited?.(editedQuery, editedAnswer, index)} + onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded?.(annotationId, authorName, editedQuery, editedAnswer, index)} + appId={config?.appId || ''} + messageId={id} + annotationId={annotation?.id || ''} + createdAt={annotation?.created_at} + onRemove={() => onAnnotationRemoved?.(index)} + /> { annotation?.id && (
- onSend?: OnSend -} +export type ChatContextValue = Pick const ChatContext = createContext({ chatList: [], @@ -38,6 +35,9 @@ export const ChatContextProvider = ({ answerIcon, allToolIcons, onSend, + onAnnotationEdited, + onAnnotationAdded, + onAnnotationRemoved, }: ChatContextProviderProps) => { return ( {children} diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 3261b779de..ce77211310 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -1,11 +1,11 @@ import { + useCallback, useEffect, useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' import { produce } from 'immer' -import { useGetState } from 'ahooks' import dayjs from 'dayjs' import type { ChatConfig, @@ -19,12 +19,53 @@ import { TransferMethod } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' import { ssePost } from '@/service/base' import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' +import type { Annotation } from '@/models/log' type GetAbortController = (abortController: AbortController) => void type SendCallback = { onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise } + +export const useCheckPromptVariables = () => { + const { t } = useTranslation() + const { notify } = useToastContext() + + const checkPromptVariables = useCallback((promptVariablesConfig: { + inputs: Inputs + promptVariables: PromptVariable[] + }) => { + const { + promptVariables, + inputs, + } = promptVariablesConfig + let hasEmptyInput = '' + const requiredVars = promptVariables.filter(({ key, name, required, type }) => { + if (type === 'api') + return false + const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) + return res + }) + + if (requiredVars?.length) { + requiredVars.forEach(({ key, name }) => { + if (hasEmptyInput) + return + + if (!inputs[key]) + hasEmptyInput = name + }) + } + + if (hasEmptyInput) { + notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) + return false + } + }, [notify, t]) + + return checkPromptVariables +} + export const useChat = ( config: ChatConfig, promptVariablesConfig?: { @@ -39,19 +80,31 @@ export const useChat = ( const connversationId = useRef('') const hasStopResponded = useRef(false) const [isResponsing, setIsResponsing] = useState(false) - const [chatList, setChatList, getChatList] = useGetState(prevChatList || []) - const [taskId, setTaskId] = useState('') + const isResponsingRef = useRef(false) + const [chatList, setChatList] = useState(prevChatList || []) + const chatListRef = useRef(prevChatList || []) + const taskIdRef = useRef('') const [suggestedQuestions, setSuggestQuestions] = useState([]) - const [abortController, setAbortController] = useState(null) - const [conversationMessagesAbortController, setConversationMessagesAbortController] = useState(null) - const [suggestedQuestionsAbortController, setSuggestedQuestionsAbortController] = useState(null) + const abortControllerRef = useRef(null) + const conversationMessagesAbortControllerRef = useRef(null) + const suggestedQuestionsAbortControllerRef = useRef(null) + const checkPromptVariables = useCheckPromptVariables() - const getIntroduction = (str: string) => { + const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => { + setChatList(newChatList) + chatListRef.current = newChatList + }, []) + const handleResponsing = useCallback((isResponsing: boolean) => { + setIsResponsing(isResponsing) + isResponsingRef.current = isResponsing + }, []) + + const getIntroduction = useCallback((str: string) => { return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {}) - } + }, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables]) useEffect(() => { - if (config.opening_statement && !chatList.some(item => !item.isAnswer)) { - setChatList([{ + if (config.opening_statement && !chatList.length) { + handleUpdateChatList([{ id: `${Date.now()}`, content: getIntroduction(config.opening_statement), isAnswer: true, @@ -59,25 +112,31 @@ export const useChat = ( suggestedQuestions: config.suggested_questions, }]) } - }, [config.opening_statement, config.suggested_questions, promptVariablesConfig?.inputs]) + }, [ + config.opening_statement, + config.suggested_questions, + getIntroduction, + chatList, + handleUpdateChatList, + ]) - const handleStop = () => { - if (stopChat && taskId) - stopChat(taskId) - if (abortController) - abortController.abort() - if (conversationMessagesAbortController) - conversationMessagesAbortController.abort() - if (suggestedQuestionsAbortController) - suggestedQuestionsAbortController.abort() - } - - const handleRestart = () => { - handleStop() + const handleStop = useCallback(() => { hasStopResponded.current = true + handleResponsing(false) + if (stopChat && taskIdRef.current) + stopChat(taskIdRef.current) + if (abortControllerRef.current) + abortControllerRef.current.abort() + if (conversationMessagesAbortControllerRef.current) + conversationMessagesAbortControllerRef.current.abort() + if (suggestedQuestionsAbortControllerRef.current) + suggestedQuestionsAbortControllerRef.current.abort() + }, [stopChat, handleResponsing]) + + const handleRestart = useCallback(() => { + handleStop() connversationId.current = '' - setIsResponsing(false) - setChatList(config.opening_statement + const newChatList = config.opening_statement ? [{ id: `${Date.now()}`, content: config.opening_statement, @@ -85,10 +144,38 @@ export const useChat = ( isOpeningStatement: true, suggestedQuestions: config.suggested_questions, }] - : []) + : [] + handleUpdateChatList(newChatList) setSuggestQuestions([]) - } - const handleSend = async ( + }, [ + config, + handleStop, + handleUpdateChatList, + ]) + + const updateCurrentQA = useCallback(({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }: { + responseItem: ChatItem + questionId: string + placeholderAnswerId: string + questionItem: ChatItem + }) => { + const newListWithAnswer = produce( + chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + (draft) => { + if (!draft.find(item => item.id === questionId)) + draft.push({ ...questionItem }) + + draft.push({ ...responseItem }) + }) + handleUpdateChatList(newListWithAnswer) + }, [handleUpdateChatList]) + + const handleSend = useCallback(async ( url: string, data: any, { @@ -97,62 +184,13 @@ export const useChat = ( }: SendCallback, ) => { setSuggestQuestions([]) - if (isResponsing) { + if (isResponsingRef.current) { notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) return false } - if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables) { - const { - promptVariables, - inputs, - } = promptVariablesConfig - let hasEmptyInput = '' - const requiredVars = promptVariables.filter(({ key, name, required, type }) => { - if (type === 'api') - return false - const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) - return res - }) - - if (requiredVars?.length) { - requiredVars.forEach(({ key, name }) => { - if (hasEmptyInput) - return - - if (!inputs[key]) - hasEmptyInput = name - }) - } - - if (hasEmptyInput) { - notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) }) - return false - } - } - - const updateCurrentQA = ({ - responseItem, - questionId, - placeholderAnswerId, - questionItem, - }: { - responseItem: ChatItem - questionId: string - placeholderAnswerId: string - questionItem: ChatItem - }) => { - // 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) - } + if (promptVariablesConfig?.inputs && promptVariablesConfig?.promptVariables) + checkPromptVariables(promptVariablesConfig) const questionId = `question-${Date.now()}` const questionItem = { @@ -169,8 +207,8 @@ export const useChat = ( isAnswer: true, } - const newList = [...getChatList(), questionItem, placeholderAnswerItem] - setChatList(newList) + const newList = [...chatListRef.current, questionItem, placeholderAnswerItem] + handleUpdateChatList(newList) // answer const responseItem: ChatItem = { @@ -181,7 +219,7 @@ export const useChat = ( isAnswer: true, } - setIsResponsing(true) + handleResponsing(true) hasStopResponded.current = false const bodyParams = { @@ -211,7 +249,7 @@ export const useChat = ( }, { getAbortController: (abortController) => { - setAbortController(abortController) + abortControllerRef.current = abortController }, onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { if (!isAgentMode) { @@ -231,7 +269,7 @@ export const useChat = ( if (isFirstMessage && newConversationId) connversationId.current = newConversationId - setTaskId(taskId) + taskIdRef.current = taskId if (messageId) responseItem.id = messageId @@ -243,21 +281,21 @@ export const useChat = ( }) }, async onCompleted(hasError?: boolean) { - setIsResponsing(false) + handleResponsing(false) if (hasError) return - if (connversationId.current) { + if (connversationId.current && !hasStopResponded.current) { const { data }: any = await onGetConvesationMessages( connversationId.current, - newAbortController => setConversationMessagesAbortController(newAbortController), + newAbortController => conversationMessagesAbortControllerRef.current = newAbortController, ) const newResponseItem = data.find((item: any) => item.id === responseItem.id) if (!newResponseItem) return - setChatList(produce(getChatList(), (draft) => { + const newChatList = produce(chatListRef.current, (draft) => { const index = draft.findIndex(item => item.id === responseItem.id) if (index !== -1) { const requestion = draft[index - 1] @@ -274,12 +312,13 @@ export const useChat = ( }, } } - })) + }) + handleUpdateChatList(newChatList) } if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) { const { data }: any = await onGetSuggestedQuestions( responseItem.id, - newAbortController => setSuggestedQuestionsAbortController(newAbortController), + newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController, ) setSuggestQuestions(data) } @@ -330,8 +369,9 @@ export const useChat = ( id: messageEnd.metadata.annotation_reply.id, authorName: messageEnd.metadata.annotation_reply.account.name, }) + const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId) const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + baseState, (draft) => { if (!draft.find(item => item.id === questionId)) draft.push({ ...questionItem }) @@ -340,38 +380,113 @@ export const useChat = ( ...responseItem, }) }) - setChatList(newListWithAnswer) + handleUpdateChatList(newListWithAnswer) return } responseItem.citation = messageEnd.metadata?.retriever_resources || [] const newListWithAnswer = produce( - getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + chatListRef.current.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) + handleUpdateChatList(newListWithAnswer) }, onMessageReplace: (messageReplace) => { responseItem.content = messageReplace.answer }, onError() { - setIsResponsing(false) - // role back placeholder answer - setChatList(produce(getChatList(), (draft) => { + handleResponsing(false) + const newChatList = produce(chatListRef.current, (draft) => { draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) - })) + }) + handleUpdateChatList(newChatList) }, }) return true - } + }, [ + checkPromptVariables, + config.suggested_questions_after_answer, + updateCurrentQA, + t, + notify, + promptVariablesConfig, + handleUpdateChatList, + handleResponsing, + ]) + + const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { + setChatList(chatListRef.current.map((item, i) => { + if (i === index - 1) { + return { + ...item, + content: query, + } + } + if (i === index) { + return { + ...item, + content: answer, + annotation: { + ...item.annotation, + logAnnotation: undefined, + } as any, + } + } + return item + })) + }, []) + const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { + setChatList(chatListRef.current.map((item, i) => { + if (i === index - 1) { + return { + ...item, + content: query, + } + } + if (i === index) { + const answerItem = { + ...item, + content: item.content, + annotation: { + id: annotationId, + authorName, + logAnnotation: { + content: answer, + account: { + id: '', + name: authorName, + email: '', + }, + }, + } as Annotation, + } + return answerItem + } + return item + })) + }, []) + const handleAnnotationRemoved = useCallback((index: number) => { + setChatList(chatListRef.current.map((item, i) => { + if (i === index) { + return { + ...item, + content: item.content, + annotation: { + ...(item.annotation || {}), + id: '', + } as Annotation, + } + } + return item + })) + }, []) return { chatList, - getChatList, setChatList, conversationId: connversationId.current, isResponsing, @@ -380,6 +495,9 @@ export const useChat = ( suggestedQuestions, handleRestart, handleStop, + handleAnnotationEdited, + handleAnnotationAdded, + handleAnnotationRemoved, } } diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index ca437f3642..16dff8ab5a 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -4,8 +4,10 @@ import type { } from 'react' import { memo, + useEffect, useRef, } from 'react' +import { useTranslation } from 'react-i18next' import { useThrottleEffect } from 'ahooks' import type { ChatConfig, @@ -18,13 +20,17 @@ import ChatInput from './chat-input' import TryToAsk from './try-to-ask' import { ChatContextProvider } from './context' import type { Emoji } from '@/app/components/tools/types' +import Button from '@/app/components/base/button' +import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices' export type ChatProps = { - config: ChatConfig - onSend?: OnSend chatList: ChatItem[] - isResponsing: boolean + config?: ChatConfig + isResponsing?: boolean + noStopResponding?: boolean + onStopResponding?: () => void noChatInput?: boolean + onSend?: OnSend chatContainerclassName?: string chatFooterClassName?: string suggestedQuestions?: string[] @@ -32,12 +38,17 @@ export type ChatProps = { questionIcon?: ReactNode answerIcon?: ReactNode allToolIcons?: Record + onAnnotationEdited?: (question: string, answer: string, index: number) => void + onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void + onAnnotationRemoved?: (index: number) => void } const Chat: FC = ({ config, onSend, chatList, isResponsing, + noStopResponding, + onStopResponding, noChatInput, chatContainerclassName, chatFooterClassName, @@ -46,16 +57,46 @@ const Chat: FC = ({ questionIcon, answerIcon, allToolIcons, + onAnnotationAdded, + onAnnotationEdited, + onAnnotationRemoved, }) => { - const ref = useRef(null) + const { t } = useTranslation() + const chatContainerRef = useRef(null) const chatFooterRef = useRef(null) + const handleScrolltoBottom = () => { + if (chatContainerRef.current) + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight + } + useThrottleEffect(() => { - if (ref.current) - ref.current.scrollTop = ref.current.scrollHeight + handleScrolltoBottom() + + if (chatContainerRef.current && chatFooterRef.current) + chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px` }, [chatList], { wait: 500 }) - const hasTryToAsk = config.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend + useEffect(() => { + if (chatFooterRef.current && chatContainerRef.current) { + const resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { blockSize } = entry.borderBoxSize[0] + + chatContainerRef.current!.style.paddingBottom = `${blockSize}px` + handleScrolltoBottom() + } + }) + + resizeObserver.observe(chatFooterRef.current) + + return () => { + resizeObserver.disconnect() + } + } + }, [chatFooterRef, chatContainerRef]) + + const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend return ( = ({ answerIcon={answerIcon} allToolIcons={allToolIcons} onSend={onSend} + onAnnotationAdded={onAnnotationAdded} + onAnnotationEdited={onAnnotationEdited} + onAnnotationRemoved={onAnnotationRemoved} >
{ - chatList.map((item) => { + chatList.map((item, index) => { if (item.isAnswer) { return ( ) } @@ -91,35 +137,41 @@ const Chat: FC = ({ ) }) } +
+
{ - (hasTryToAsk || !noChatInput) && ( -
- { - hasTryToAsk && ( - - ) - } - { - !noChatInput && ( - - ) - } + !noStopResponding && isResponsing && ( +
+
) } + { + hasTryToAsk && ( + + ) + } + { + !noChatInput && ( + + ) + }
diff --git a/web/app/components/base/chat/types.ts b/web/app/components/base/chat/types.ts index ed13a899de..226b14e2ae 100644 --- a/web/app/components/base/chat/types.ts +++ b/web/app/components/base/chat/types.ts @@ -41,7 +41,10 @@ export type EnableType = { enabled: boolean } -export type ChatConfig = Omit +export type ChatConfig = Omit & { + supportAnnotation?: boolean + appId?: string +} export type ChatItem = IChatItem diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index 574d8fe3e0..92bec00121 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -96,7 +96,7 @@ type IDebugConfiguration = { hasSetContextVar: boolean isShowVisionConfig: boolean visionConfig: VisionSettings - setVisionConfig: (visionConfig: VisionSettings) => void + setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void } const DebugConfigurationContext = createContext({