| @@ -132,6 +132,7 @@ const EditAnnotationModal: FC<Props> = ({ | |||
| onRemove={() => { | |||
| onRemove() | |||
| setShowModal(false) | |||
| onHide() | |||
| }} | |||
| text={t('appDebug.feature.annotation.removeConfirm') as string} | |||
| /> | |||
| @@ -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<AgentToolWithMoreInfo>(null) | |||
| const [selectedProviderId, setSelectedProviderId] = useState<string | undefined>(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() | |||
| }}> | |||
| <Trash03 className='w-4 h-4 text-gray-500' /> | |||
| </div> | |||
| @@ -167,6 +171,7 @@ const AgentTools: FC = () => { | |||
| draft.agentConfig.tools.splice(index, 1) | |||
| }) | |||
| setModelConfig(newModelConfig) | |||
| formattingChangedDispatcher() | |||
| }}> | |||
| <Trash03 className='w-4 h-4 text-gray-500' /> | |||
| </div> | |||
| @@ -183,6 +188,7 @@ const AgentTools: FC = () => { | |||
| (draft.agentConfig.tools[index] as any).enabled = enabled | |||
| }) | |||
| setModelConfig(newModelConfig) | |||
| formattingChangedDispatcher() | |||
| }} /> | |||
| </div> | |||
| </div> | |||
| @@ -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) => { | |||
| @@ -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 | |||
| @@ -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 | |||
| @@ -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> | |||
| @@ -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} | |||
| @@ -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> | |||
| @@ -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) | |||
| @@ -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) | |||
| } | |||
| }) | |||
| } | |||
| @@ -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} />)} | |||
| </> | |||
| ) | |||
| @@ -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' | |||
| @@ -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<ModerationConfig>({ | |||
| @@ -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 = () => { | |||
| ) | |||
| } | |||
| </div> | |||
| <div className='flex flex-col grow h-0 px-6 py-4 rounded-tl-2xl border-t border-l bg-gray-50 '> | |||
| <div className='flex flex-col grow h-0 rounded-tl-2xl border-t border-l bg-gray-50 '> | |||
| <Debug | |||
| hasSetAPIKEY={hasSettedApiKey} | |||
| onSetting={() => setShowAccountSettingModal({ payload: 'provider' })} | |||
| @@ -97,18 +97,21 @@ const CacheCtrlBtn: FC<Props> = ({ | |||
| </div> | |||
| </div> | |||
| ) | |||
| : ( | |||
| <TooltipPlus | |||
| popupContent={t('appDebug.feature.annotation.add') as string} | |||
| > | |||
| <div | |||
| className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer' | |||
| onClick={handleAdd} | |||
| : answer | |||
| ? ( | |||
| <TooltipPlus | |||
| popupContent={t('appDebug.feature.annotation.add') as string} | |||
| > | |||
| <MessageFastPlus className='w-4 h-4' /> | |||
| </div> | |||
| </TooltipPlus> | |||
| )} | |||
| <div | |||
| className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer' | |||
| onClick={handleAdd} | |||
| > | |||
| <MessageFastPlus className='w-4 h-4' /> | |||
| </div> | |||
| </TooltipPlus> | |||
| ) | |||
| : null | |||
| } | |||
| <TooltipPlus | |||
| popupContent={t('appDebug.feature.annotation.edit') as string} | |||
| > | |||
| @@ -14,7 +14,10 @@ type AgentContentProps = { | |||
| const AgentContent: FC<AgentContentProps> = ({ | |||
| item, | |||
| }) => { | |||
| const { allToolIcons } = useChatContext() | |||
| const { | |||
| allToolIcons, | |||
| isResponsing, | |||
| } = useChatContext() | |||
| const { | |||
| annotation, | |||
| agent_thoughts, | |||
| @@ -42,7 +45,7 @@ const AgentContent: FC<AgentContentProps> = ({ | |||
| <Thought | |||
| thought={thought} | |||
| allToolIcons={allToolIcons || {}} | |||
| isFinished={!!thought.observation} | |||
| isFinished={!!thought.observation || !isResponsing} | |||
| /> | |||
| )} | |||
| @@ -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<AnswerProps> = ({ | |||
| item, | |||
| question, | |||
| index, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const { | |||
| @@ -56,7 +60,15 @@ const Answer: FC<AnswerProps> = ({ | |||
| <div className='relative pr-10'> | |||
| <AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' /> | |||
| <div className='group relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900'> | |||
| <Operation item={item} /> | |||
| { | |||
| !responsing && ( | |||
| <Operation | |||
| item={item} | |||
| question={question} | |||
| index={index} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| responsing && !content && !hasAgentThoughts && ( | |||
| <div className='flex items-center justify-center w-6 h-5'> | |||
| @@ -75,7 +87,7 @@ const Answer: FC<AnswerProps> = ({ | |||
| ) | |||
| } | |||
| { | |||
| annotation?.id && !annotation?.logAnnotation && ( | |||
| annotation?.id && annotation.authorName && ( | |||
| <EditTitle | |||
| className='mt-1' | |||
| title={t('appAnnotation.editBy', { author: annotation.authorName })} | |||
| @@ -1,24 +1,39 @@ | |||
| import type { FC } from 'react' | |||
| import { useState } from 'react' | |||
| import type { ChatItem } from '../../types' | |||
| import { useCurrentAnswerIsResponsing } from '../hooks' | |||
| import { useChatContext } from '../context' | |||
| import CopyBtn from '@/app/components/app/chat/copy-btn' | |||
| import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' | |||
| import AudioBtn from '@/app/components/base/audio-btn' | |||
| import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn' | |||
| import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal' | |||
| type OperationProps = { | |||
| item: ChatItem | |||
| question: string | |||
| index: number | |||
| } | |||
| const Operation: FC<OperationProps> = ({ | |||
| 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 ( | |||
| <div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'> | |||
| @@ -36,6 +51,34 @@ const Operation: FC<OperationProps> = ({ | |||
| className='hidden group-hover:block' | |||
| /> | |||
| )} | |||
| {(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && ( | |||
| <AnnotationCtrlBtn | |||
| appId={config?.appId || ''} | |||
| messageId={id} | |||
| annotationId={annotation?.id || ''} | |||
| className='hidden group-hover:block ml-1 shrink-0' | |||
| cached={hasAnnotation} | |||
| query={question} | |||
| answer={content} | |||
| onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)} | |||
| onEdit={() => setIsShowReplyModal(true)} | |||
| onRemoved={() => onAnnotationRemoved?.(index)} | |||
| /> | |||
| )} | |||
| <EditReplyModal | |||
| isShow={isShowReplyModal} | |||
| onHide={() => 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 && ( | |||
| <div | |||
| @@ -2,23 +2,20 @@ | |||
| import type { ReactNode } from 'react' | |||
| import { createContext, useContext } from 'use-context-selector' | |||
| import type { | |||
| ChatConfig, | |||
| ChatItem, | |||
| OnSend, | |||
| } from '../types' | |||
| import type { Emoji } from '@/app/components/tools/types' | |||
| import type { ChatProps } from './index' | |||
| export type ChatContextValue = { | |||
| config?: ChatConfig | |||
| isResponsing?: boolean | |||
| chatList: ChatItem[] | |||
| showPromptLog?: boolean | |||
| questionIcon?: ReactNode | |||
| answerIcon?: ReactNode | |||
| allToolIcons?: Record<string, string | Emoji> | |||
| onSend?: OnSend | |||
| } | |||
| export type ChatContextValue = Pick<ChatProps, 'config' | |||
| | 'isResponsing' | |||
| | 'chatList' | |||
| | 'showPromptLog' | |||
| | 'questionIcon' | |||
| | 'answerIcon' | |||
| | 'allToolIcons' | |||
| | 'onSend' | |||
| | 'onAnnotationEdited' | |||
| | 'onAnnotationAdded' | |||
| | 'onAnnotationRemoved' | |||
| > | |||
| const ChatContext = createContext<ChatContextValue>({ | |||
| chatList: [], | |||
| @@ -38,6 +35,9 @@ export const ChatContextProvider = ({ | |||
| answerIcon, | |||
| allToolIcons, | |||
| onSend, | |||
| onAnnotationEdited, | |||
| onAnnotationAdded, | |||
| onAnnotationRemoved, | |||
| }: ChatContextProviderProps) => { | |||
| return ( | |||
| <ChatContext.Provider value={{ | |||
| @@ -49,6 +49,9 @@ export const ChatContextProvider = ({ | |||
| answerIcon, | |||
| allToolIcons, | |||
| onSend, | |||
| onAnnotationEdited, | |||
| onAnnotationAdded, | |||
| onAnnotationRemoved, | |||
| }}> | |||
| {children} | |||
| </ChatContext.Provider> | |||
| @@ -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<any> | |||
| onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any> | |||
| } | |||
| 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<ChatItem[]>(prevChatList || []) | |||
| const [taskId, setTaskId] = useState('') | |||
| const isResponsingRef = useRef(false) | |||
| const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || []) | |||
| const chatListRef = useRef<ChatItem[]>(prevChatList || []) | |||
| const taskIdRef = useRef('') | |||
| const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) | |||
| const [abortController, setAbortController] = useState<AbortController | null>(null) | |||
| const [conversationMessagesAbortController, setConversationMessagesAbortController] = useState<AbortController | null>(null) | |||
| const [suggestedQuestionsAbortController, setSuggestedQuestionsAbortController] = useState<AbortController | null>(null) | |||
| const getIntroduction = (str: string) => { | |||
| const abortControllerRef = useRef<AbortController | null>(null) | |||
| const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null) | |||
| const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null) | |||
| const checkPromptVariables = useCheckPromptVariables() | |||
| 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]) | |||
| const handleStop = () => { | |||
| if (stopChat && taskId) | |||
| stopChat(taskId) | |||
| if (abortController) | |||
| abortController.abort() | |||
| if (conversationMessagesAbortController) | |||
| conversationMessagesAbortController.abort() | |||
| if (suggestedQuestionsAbortController) | |||
| suggestedQuestionsAbortController.abort() | |||
| } | |||
| }, [ | |||
| config.opening_statement, | |||
| config.suggested_questions, | |||
| getIntroduction, | |||
| chatList, | |||
| handleUpdateChatList, | |||
| ]) | |||
| 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, | |||
| } | |||
| } | |||
| @@ -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<string, string | Emoji> | |||
| 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<ChatProps> = ({ | |||
| config, | |||
| onSend, | |||
| chatList, | |||
| isResponsing, | |||
| noStopResponding, | |||
| onStopResponding, | |||
| noChatInput, | |||
| chatContainerclassName, | |||
| chatFooterClassName, | |||
| @@ -46,16 +57,46 @@ const Chat: FC<ChatProps> = ({ | |||
| questionIcon, | |||
| answerIcon, | |||
| allToolIcons, | |||
| onAnnotationAdded, | |||
| onAnnotationEdited, | |||
| onAnnotationRemoved, | |||
| }) => { | |||
| const ref = useRef<HTMLDivElement>(null) | |||
| const { t } = useTranslation() | |||
| const chatContainerRef = useRef<HTMLDivElement>(null) | |||
| const chatFooterRef = useRef<HTMLDivElement>(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 ( | |||
| <ChatContextProvider | |||
| @@ -67,19 +108,24 @@ const Chat: FC<ChatProps> = ({ | |||
| answerIcon={answerIcon} | |||
| allToolIcons={allToolIcons} | |||
| onSend={onSend} | |||
| onAnnotationAdded={onAnnotationAdded} | |||
| onAnnotationEdited={onAnnotationEdited} | |||
| onAnnotationRemoved={onAnnotationRemoved} | |||
| > | |||
| <div className='relative h-full'> | |||
| <div | |||
| ref={ref} | |||
| ref={chatContainerRef} | |||
| className={`relative h-full overflow-y-auto ${chatContainerclassName}`} | |||
| > | |||
| { | |||
| chatList.map((item) => { | |||
| chatList.map((item, index) => { | |||
| if (item.isAnswer) { | |||
| return ( | |||
| <Answer | |||
| key={item.id} | |||
| item={item} | |||
| question={chatList[index - 1]?.content} | |||
| index={index} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -91,35 +137,41 @@ const Chat: FC<ChatProps> = ({ | |||
| ) | |||
| }) | |||
| } | |||
| </div> | |||
| <div | |||
| className={`absolute bottom-0 ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`} | |||
| ref={chatFooterRef} | |||
| style={{ | |||
| background: 'linear-gradient(0deg, #F9FAFB 40%, rgba(255, 255, 255, 0.00) 100%)', | |||
| }} | |||
| > | |||
| { | |||
| (hasTryToAsk || !noChatInput) && ( | |||
| <div | |||
| className={`sticky bottom-0 w-full backdrop-blur-[20px] ${chatFooterClassName}`} | |||
| ref={chatFooterRef} | |||
| style={{ | |||
| background: 'linear-gradient(0deg, #FFF 0%, rgba(255, 255, 255, 0.40) 100%)', | |||
| }} | |||
| > | |||
| { | |||
| hasTryToAsk && ( | |||
| <TryToAsk | |||
| suggestedQuestions={suggestedQuestions} | |||
| onSend={onSend} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !noChatInput && ( | |||
| <ChatInput | |||
| visionConfig={config?.file_upload?.image} | |||
| speechToTextConfig={config.speech_to_text} | |||
| onSend={onSend} | |||
| /> | |||
| ) | |||
| } | |||
| !noStopResponding && isResponsing && ( | |||
| <div className='flex justify-center mb-2'> | |||
| <Button className='py-0 px-3 h-7 bg-white shadow-xs' onClick={onStopResponding}> | |||
| <StopCircle className='mr-[5px] w-3.5 h-3.5 text-gray-500' /> | |||
| <span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span> | |||
| </Button> | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| hasTryToAsk && ( | |||
| <TryToAsk | |||
| suggestedQuestions={suggestedQuestions} | |||
| onSend={onSend} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| !noChatInput && ( | |||
| <ChatInput | |||
| visionConfig={config?.file_upload?.image} | |||
| speechToTextConfig={config?.speech_to_text} | |||
| onSend={onSend} | |||
| /> | |||
| ) | |||
| } | |||
| </div> | |||
| </div> | |||
| </ChatContextProvider> | |||
| @@ -41,7 +41,10 @@ export type EnableType = { | |||
| enabled: boolean | |||
| } | |||
| export type ChatConfig = Omit<ModelConfig, 'model'> | |||
| export type ChatConfig = Omit<ModelConfig, 'model'> & { | |||
| supportAnnotation?: boolean | |||
| appId?: string | |||
| } | |||
| export type ChatItem = IChatItem | |||
| @@ -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<IDebugConfiguration>({ | |||