| onRemove={() => { | onRemove={() => { | ||||
| onRemove() | onRemove() | ||||
| setShowModal(false) | setShowModal(false) | ||||
| onHide() | |||||
| }} | }} | ||||
| text={t('appDebug.feature.annotation.removeConfirm') as string} | text={t('appDebug.feature.annotation.removeConfirm') as string} | ||||
| /> | /> |
| import cn from 'classnames' | import cn from 'classnames' | ||||
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import produce from 'immer' | import produce from 'immer' | ||||
| import { useFormattingChangedDispatcher } from '../../../debug/hooks' | |||||
| import ChooseTool from './choose-tool' | import ChooseTool from './choose-tool' | ||||
| import SettingBuiltInTool from './setting-built-in-tool' | import SettingBuiltInTool from './setting-built-in-tool' | ||||
| import Panel from '@/app/components/app/configuration/base/feature-panel' | import Panel from '@/app/components/app/configuration/base/feature-panel' | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [isShowChooseTool, setIsShowChooseTool] = useState(false) | const [isShowChooseTool, setIsShowChooseTool] = useState(false) | ||||
| const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext) | const { modelConfig, setModelConfig, collectionList } = useContext(ConfigContext) | ||||
| const formattingChangedDispatcher = useFormattingChangedDispatcher() | |||||
| const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null) | const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null) | ||||
| const [selectedProviderId, setSelectedProviderId] = useState<string | undefined>(undefined) | const [selectedProviderId, setSelectedProviderId] = useState<string | undefined>(undefined) | ||||
| }) | }) | ||||
| setModelConfig(newModelConfig) | setModelConfig(newModelConfig) | ||||
| setIsShowSettingTool(false) | setIsShowSettingTool(false) | ||||
| formattingChangedDispatcher() | |||||
| } | } | ||||
| return ( | return ( | ||||
| draft.agentConfig.tools.splice(index, 1) | draft.agentConfig.tools.splice(index, 1) | ||||
| }) | }) | ||||
| setModelConfig(newModelConfig) | setModelConfig(newModelConfig) | ||||
| formattingChangedDispatcher() | |||||
| }}> | }}> | ||||
| <Trash03 className='w-4 h-4 text-gray-500' /> | <Trash03 className='w-4 h-4 text-gray-500' /> | ||||
| </div> | </div> | ||||
| draft.agentConfig.tools.splice(index, 1) | draft.agentConfig.tools.splice(index, 1) | ||||
| }) | }) | ||||
| setModelConfig(newModelConfig) | setModelConfig(newModelConfig) | ||||
| formattingChangedDispatcher() | |||||
| }}> | }}> | ||||
| <Trash03 className='w-4 h-4 text-gray-500' /> | <Trash03 className='w-4 h-4 text-gray-500' /> | ||||
| </div> | </div> | ||||
| (draft.agentConfig.tools[index] as any).enabled = enabled | (draft.agentConfig.tools[index] as any).enabled = enabled | ||||
| }) | }) | ||||
| setModelConfig(newModelConfig) | setModelConfig(newModelConfig) | ||||
| formattingChangedDispatcher() | |||||
| }} /> | }} /> | ||||
| </div> | </div> | ||||
| </div> | </div> |
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import produce from 'immer' | import produce from 'immer' | ||||
| import { useBoolean, useScroll } from 'ahooks' | import { useBoolean, useScroll } from 'ahooks' | ||||
| import { useFormattingChangedDispatcher } from '../debug/hooks' | |||||
| import DatasetConfig from '../dataset-config' | import DatasetConfig from '../dataset-config' | ||||
| import ChatGroup from '../features/chat-group' | import ChatGroup from '../features/chat-group' | ||||
| import ExperienceEnchanceGroup from '../features/experience-enchance-group' | import ExperienceEnchanceGroup from '../features/experience-enchance-group' | ||||
| modelConfig, | modelConfig, | ||||
| setModelConfig, | setModelConfig, | ||||
| setPrevPromptConfig, | setPrevPromptConfig, | ||||
| setFormattingChanged, | |||||
| moreLikeThisConfig, | moreLikeThisConfig, | ||||
| setMoreLikeThisConfig, | setMoreLikeThisConfig, | ||||
| suggestedQuestionsAfterAnswerConfig, | suggestedQuestionsAfterAnswerConfig, | ||||
| const { data: speech2textDefaultModel } = useDefaultModel(4) | const { data: speech2textDefaultModel } = useDefaultModel(4) | ||||
| const { data: text2speechDefaultModel } = useDefaultModel(5) | const { data: text2speechDefaultModel } = useDefaultModel(5) | ||||
| const { setShowModerationSettingModal } = useModalContext() | const { setShowModerationSettingModal } = useModalContext() | ||||
| const formattingChangedDispatcher = useFormattingChangedDispatcher() | |||||
| const promptTemplate = modelConfig.configs.prompt_template | const promptTemplate = modelConfig.configs.prompt_template | ||||
| const promptVariables = modelConfig.configs.prompt_variables | const promptVariables = modelConfig.configs.prompt_variables | ||||
| draft.configs.prompt_template = newTemplate | draft.configs.prompt_template = newTemplate | ||||
| draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newVariables] | draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newVariables] | ||||
| }) | }) | ||||
| if (modelConfig.configs.prompt_template !== newTemplate) | if (modelConfig.configs.prompt_template !== newTemplate) | ||||
| setFormattingChanged(true) | |||||
| formattingChangedDispatcher() | |||||
| setPrevPromptConfig(modelConfig.configs) | setPrevPromptConfig(modelConfig.configs) | ||||
| setModelConfig(newModelConfig) | setModelConfig(newModelConfig) | ||||
| setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => { | setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => { | ||||
| draft.enabled = value | draft.enabled = value | ||||
| })) | })) | ||||
| formattingChangedDispatcher() | |||||
| }, | }, | ||||
| speechToText: speechToTextConfig.enabled, | speechToText: speechToTextConfig.enabled, | ||||
| setSpeechToText: (value) => { | setSpeechToText: (value) => { | ||||
| setCitationConfig(produce(citationConfig, (draft: CitationConfig) => { | setCitationConfig(produce(citationConfig, (draft: CitationConfig) => { | ||||
| draft.enabled = value | draft.enabled = value | ||||
| })) | })) | ||||
| formattingChangedDispatcher() | |||||
| }, | }, | ||||
| annotation: annotationConfig.enabled, | annotation: annotationConfig.enabled, | ||||
| setAnnotation: async (value) => { | setAnnotation: async (value) => { |
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import produce from 'immer' | import produce from 'immer' | ||||
| import { useFormattingChangedDispatcher } from '../debug/hooks' | |||||
| import FeaturePanel from '../base/feature-panel' | import FeaturePanel from '../base/feature-panel' | ||||
| import OperationBtn from '../base/operation-btn' | import OperationBtn from '../base/operation-btn' | ||||
| import CardItem from './card-item/item' | import CardItem from './card-item/item' | ||||
| mode, | mode, | ||||
| dataSets: dataSet, | dataSets: dataSet, | ||||
| setDataSets: setDataSet, | setDataSets: setDataSet, | ||||
| setFormattingChanged, | |||||
| modelConfig, | modelConfig, | ||||
| setModelConfig, | setModelConfig, | ||||
| showSelectDataSet, | showSelectDataSet, | ||||
| isAgent, | isAgent, | ||||
| } = useContext(ConfigContext) | } = useContext(ConfigContext) | ||||
| const formattingChangedDispatcher = useFormattingChangedDispatcher() | |||||
| const hasData = dataSet.length > 0 | const hasData = dataSet.length > 0 | ||||
| const onRemove = (id: string) => { | const onRemove = (id: string) => { | ||||
| setDataSet(dataSet.filter(item => item.id !== id)) | setDataSet(dataSet.filter(item => item.id !== id)) | ||||
| setFormattingChanged(true) | |||||
| formattingChangedDispatcher() | |||||
| } | } | ||||
| const handleSave = (newDataset: DataSet) => { | const handleSave = (newDataset: DataSet) => { | ||||
| const index = dataSet.findIndex(item => item.id === newDataset.id) | const index = dataSet.findIndex(item => item.id === newDataset.id) | ||||
| setDataSet([...dataSet.slice(0, index), newDataset, ...dataSet.slice(index + 1)]) | setDataSet([...dataSet.slice(0, index), newDataset, ...dataSet.slice(index + 1)]) | ||||
| setFormattingChanged(true) | |||||
| formattingChangedDispatcher() | |||||
| } | } | ||||
| const promptVariables = modelConfig.configs.prompt_variables | const promptVariables = modelConfig.configs.prompt_variables |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { | import { | ||||
| memo, | memo, | ||||
| useCallback, | |||||
| useMemo, | useMemo, | ||||
| } from 'react' | } from 'react' | ||||
| import type { ModelAndParameter } from '../types' | import type { ModelAndParameter } from '../types' | ||||
| APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, | APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, | ||||
| } from '../types' | } from '../types' | ||||
| import { | import { | ||||
| AgentStrategy, | |||||
| ModelModeType, | |||||
| } from '@/types/app' | |||||
| useConfigFromDebugContext, | |||||
| useFormattingChangedSubscription, | |||||
| } from '../hooks' | |||||
| import Chat from '@/app/components/base/chat/chat' | import Chat from '@/app/components/base/chat/chat' | ||||
| import { useChat } from '@/app/components/base/chat/chat/hooks' | import { useChat } from '@/app/components/base/chat/chat/hooks' | ||||
| import { useDebugConfigurationContext } from '@/context/debug-configuration' | 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 { useEventEmitterContextContext } from '@/context/event-emitter' | ||||
| import { useProviderContext } from '@/context/provider-context' | import { useProviderContext } from '@/context/provider-context' | ||||
| import { | import { | ||||
| fetchSuggestedQuestions, | fetchSuggestedQuestions, | ||||
| stopChatMessageResponding, | stopChatMessageResponding, | ||||
| } from '@/service/debug' | } from '@/service/debug' | ||||
| import { promptVariablesToUserInputsForm } from '@/utils/model-config' | |||||
| import Avatar from '@/app/components/base/avatar' | import Avatar from '@/app/components/base/avatar' | ||||
| import { useAppContext } from '@/context/app-context' | import { useAppContext } from '@/context/app-context' | ||||
| import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | ||||
| }) => { | }) => { | ||||
| const { userProfile } = useAppContext() | const { userProfile } = useAppContext() | ||||
| const { | const { | ||||
| isAdvancedMode, | |||||
| modelConfig, | modelConfig, | ||||
| appId, | appId, | ||||
| inputs, | inputs, | ||||
| promptMode, | |||||
| speechToTextConfig, | |||||
| introduction, | |||||
| suggestedQuestions: openingSuggestedQuestions, | |||||
| suggestedQuestionsAfterAnswerConfig, | |||||
| citationConfig, | |||||
| moderationConfig, | |||||
| chatPromptConfig, | |||||
| completionPromptConfig, | |||||
| dataSets, | |||||
| datasetConfigs, | |||||
| visionConfig, | visionConfig, | ||||
| annotationConfig, | |||||
| collectionList, | collectionList, | ||||
| textToSpeechConfig, | |||||
| } = useDebugConfigurationContext() | } = useDebugConfigurationContext() | ||||
| const { textGenerationModelList } = useProviderContext() | 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 { | const { | ||||
| chatList, | chatList, | ||||
| isResponsing, | isResponsing, | ||||
| [], | [], | ||||
| taskId => stopChatMessageResponding(appId, taskId), | 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 currentProvider = textGenerationModelList.find(item => item.provider === modelAndParameter.provider) | ||||
| const currentModel = currentProvider?.models.find(model => model.model === modelAndParameter.model) | const currentModel = currentProvider?.models.find(model => model.model === modelAndParameter.model) | ||||
| const supportVision = currentModel?.features?.includes(ModelFeatureEnum.vision) | const supportVision = currentModel?.features?.includes(ModelFeatureEnum.vision) | ||||
| onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), | onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController), | ||||
| }, | }, | ||||
| ) | ) | ||||
| } | |||||
| }, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled]) | |||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| eventEmitter?.useSubscription((v: any) => { | eventEmitter?.useSubscription((v: any) => { | ||||
| chatList={chatList} | chatList={chatList} | ||||
| isResponsing={isResponsing} | isResponsing={isResponsing} | ||||
| noChatInput | noChatInput | ||||
| noStopResponding | |||||
| chatContainerclassName='p-4' | chatContainerclassName='p-4' | ||||
| chatFooterClassName='!-bottom-4' | |||||
| chatFooterClassName='p-4 pb-0' | |||||
| suggestedQuestions={suggestedQuestions} | suggestedQuestions={suggestedQuestions} | ||||
| onSend={doSend} | onSend={doSend} | ||||
| showPromptLog | showPromptLog |
| multipleModelConfigs: ModelAndParameter[] | multipleModelConfigs: ModelAndParameter[] | ||||
| onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void | onMultipleModelConfigsChange: (multiple: boolean, modelConfigs: ModelAndParameter[]) => void | ||||
| onDebugWithMultipleModelChange: (singleModelConfig: ModelAndParameter) => void | onDebugWithMultipleModelChange: (singleModelConfig: ModelAndParameter) => void | ||||
| checkCanSend?: () => boolean | |||||
| } | } | ||||
| const DebugWithMultipleModelContext = createContext<DebugWithMultipleModelContextType>({ | const DebugWithMultipleModelContext = createContext<DebugWithMultipleModelContextType>({ | ||||
| multipleModelConfigs: [], | multipleModelConfigs: [], | ||||
| onMultipleModelConfigsChange, | onMultipleModelConfigsChange, | ||||
| multipleModelConfigs, | multipleModelConfigs, | ||||
| onDebugWithMultipleModelChange, | onDebugWithMultipleModelChange, | ||||
| checkCanSend, | |||||
| }: DebugWithMultipleModelContextProviderProps) => { | }: DebugWithMultipleModelContextProviderProps) => { | ||||
| return ( | return ( | ||||
| <DebugWithMultipleModelContext.Provider value={{ | <DebugWithMultipleModelContext.Provider value={{ | ||||
| onMultipleModelConfigsChange, | onMultipleModelConfigsChange, | ||||
| multipleModelConfigs, | multipleModelConfigs, | ||||
| onDebugWithMultipleModelChange, | onDebugWithMultipleModelChange, | ||||
| checkCanSend, | |||||
| }}> | }}> | ||||
| {children} | {children} | ||||
| </DebugWithMultipleModelContext.Provider> | </DebugWithMultipleModelContext.Provider> |
| import type { FC } from 'react' | |||||
| import type { CSSProperties, FC } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { memo } from 'react' | import { memo } from 'react' | ||||
| import type { ModelAndParameter } from '../types' | import type { ModelAndParameter } from '../types' | ||||
| type DebugItemProps = { | type DebugItemProps = { | ||||
| modelAndParameter: ModelAndParameter | modelAndParameter: ModelAndParameter | ||||
| className?: string | className?: string | ||||
| style?: CSSProperties | |||||
| } | } | ||||
| const DebugItem: FC<DebugItemProps> = ({ | const DebugItem: FC<DebugItemProps> = ({ | ||||
| modelAndParameter, | modelAndParameter, | ||||
| className, | className, | ||||
| style, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { mode } = useDebugConfigurationContext() | const { mode } = useDebugConfigurationContext() | ||||
| } | } | ||||
| return ( | 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='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'> | <div className='flex items-center justify-center w-6 h-5 font-medium italic text-gray-500'> | ||||
| #{index + 1} | #{index + 1} |
| import { | import { | ||||
| memo, | memo, | ||||
| useCallback, | useCallback, | ||||
| useMemo, | |||||
| } from 'react' | } from 'react' | ||||
| import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types' | import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types' | ||||
| import DebugItem from './debug-item' | import DebugItem from './debug-item' | ||||
| speechToTextConfig, | speechToTextConfig, | ||||
| visionConfig, | visionConfig, | ||||
| } = useDebugConfigurationContext() | } = useDebugConfigurationContext() | ||||
| const { multipleModelConfigs } = useDebugWithMultipleModelContext() | |||||
| const { | |||||
| multipleModelConfigs, | |||||
| checkCanSend, | |||||
| } = useDebugWithMultipleModelContext() | |||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| const handleSend = useCallback((message: string, files?: VisionFile[]) => { | const handleSend = useCallback((message: string, files?: VisionFile[]) => { | ||||
| if (checkCanSend && !checkCanSend()) | |||||
| return | |||||
| eventEmitter?.emit({ | eventEmitter?.emit({ | ||||
| type: APP_CHAT_WITH_MULTIPLE_MODEL, | type: APP_CHAT_WITH_MULTIPLE_MODEL, | ||||
| payload: { | payload: { | ||||
| files, | files, | ||||
| }, | }, | ||||
| } as any) | } as any) | ||||
| }, [eventEmitter]) | |||||
| }, [eventEmitter, checkCanSend]) | |||||
| const twoLine = multipleModelConfigs.length === 2 | const twoLine = multipleModelConfigs.length === 2 | ||||
| const threeLine = multipleModelConfigs.length === 3 | const threeLine = multipleModelConfigs.length === 3 | ||||
| const fourLine = multipleModelConfigs.length === 4 | 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 ( | return ( | ||||
| <div className='flex flex-col h-full'> | <div className='flex flex-col h-full'> | ||||
| <div | <div | ||||
| className={` | 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%' }} | style={{ height: mode === 'chat' ? 'calc(100% - 60px)' : '100%' }} | ||||
| > | > | ||||
| { | { | ||||
| (twoLine || threeLine) && multipleModelConfigs.map(modelConfig => ( | |||||
| multipleModelConfigs.map((modelConfig, index) => ( | |||||
| <DebugItem | <DebugItem | ||||
| key={modelConfig.id} | key={modelConfig.id} | ||||
| modelAndParameter={modelConfig} | modelAndParameter={modelConfig} | ||||
| className={` | 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> | </div> | ||||
| { | { | ||||
| mode === 'chat' && ( | mode === 'chat' && ( | ||||
| <div className='shrink-0'> | |||||
| <div className='shrink-0 pb-4 px-6'> | |||||
| <ChatInput | <ChatInput | ||||
| onSend={handleSend} | onSend={handleSend} | ||||
| speechToTextConfig={speechToTextConfig} | speechToTextConfig={speechToTextConfig} | ||||
| onMultipleModelConfigsChange, | onMultipleModelConfigsChange, | ||||
| multipleModelConfigs, | multipleModelConfigs, | ||||
| onDebugWithMultipleModelChange, | onDebugWithMultipleModelChange, | ||||
| checkCanSend, | |||||
| }) => { | }) => { | ||||
| return ( | return ( | ||||
| <DebugWithMultipleModelContextProvider | <DebugWithMultipleModelContextProvider | ||||
| onMultipleModelConfigsChange={onMultipleModelConfigsChange} | onMultipleModelConfigsChange={onMultipleModelConfigsChange} | ||||
| multipleModelConfigs={multipleModelConfigs} | multipleModelConfigs={multipleModelConfigs} | ||||
| onDebugWithMultipleModelChange={onDebugWithMultipleModelChange} | onDebugWithMultipleModelChange={onDebugWithMultipleModelChange} | ||||
| checkCanSend={checkCanSend} | |||||
| > | > | ||||
| <DebugWithMultipleModelMemoed /> | <DebugWithMultipleModelMemoed /> | ||||
| </DebugWithMultipleModelContextProvider> | </DebugWithMultipleModelContextProvider> |
| 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) |
| DebugWithSingleOrMultipleModelConfigs, | DebugWithSingleOrMultipleModelConfigs, | ||||
| ModelAndParameter, | ModelAndParameter, | ||||
| } from './types' | } 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) => { | export const useDebugWithSingleOrMultipleModel = (appId: string) => { | ||||
| const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem('app-debug-with-single-or-multiple-models') | const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem('app-debug-with-single-or-multiple-models') | ||||
| handleMultipleModelConfigsChange, | 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) | |||||
| } | |||||
| }) | |||||
| } |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import useSWR from 'swr' | import useSWR from 'swr' | ||||
| import { useTranslation } from 'react-i18next' | 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 { useContext } from 'use-context-selector' | ||||
| import dayjs from 'dayjs' | |||||
| import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api' | import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api' | ||||
| import FormattingChanged from '../base/warning-mask/formatting-changed' | import FormattingChanged from '../base/warning-mask/formatting-changed' | ||||
| import GroupName from '../base/group-name' | import GroupName from '../base/group-name' | ||||
| import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset' | import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset' | ||||
| import DebugWithMultipleModel from './debug-with-multiple-model' | 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 type { ModelAndParameter } from './types' | ||||
| import { | import { | ||||
| APP_CHAT_WITH_MULTIPLE_MODEL, | APP_CHAT_WITH_MULTIPLE_MODEL, | ||||
| APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, | APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, | ||||
| } from './types' | } 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 ConfigContext from '@/context/debug-configuration' | ||||
| import { ToastContext } from '@/app/components/base/toast' | 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 Button from '@/app/components/base/button' | ||||
| import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app' | import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app' | ||||
| import { promptVariablesToUserInputsForm } from '@/utils/model-config' | import { promptVariablesToUserInputsForm } from '@/utils/model-config' | ||||
| import { IS_CE_EDITION } from '@/config' | import { IS_CE_EDITION } from '@/config' | ||||
| import type { Inputs } from '@/models/debug' | import type { Inputs } from '@/models/debug' | ||||
| import { fetchFileUploadConfig } from '@/service/common' | 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 { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' | ||||
| import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' | 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' | import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' | ||||
| const { | const { | ||||
| appId, | appId, | ||||
| mode, | mode, | ||||
| isFunctionCall, | |||||
| collectionList, | |||||
| modelModeType, | modelModeType, | ||||
| hasSetBlockStatus, | hasSetBlockStatus, | ||||
| isAdvancedMode, | isAdvancedMode, | ||||
| chatPromptConfig, | chatPromptConfig, | ||||
| completionPromptConfig, | completionPromptConfig, | ||||
| introduction, | introduction, | ||||
| suggestedQuestions, | |||||
| suggestedQuestionsAfterAnswerConfig, | suggestedQuestionsAfterAnswerConfig, | ||||
| speechToTextConfig, | speechToTextConfig, | ||||
| textToSpeechConfig, | textToSpeechConfig, | ||||
| moreLikeThisConfig, | moreLikeThisConfig, | ||||
| formattingChanged, | formattingChanged, | ||||
| setFormattingChanged, | setFormattingChanged, | ||||
| conversationId, | |||||
| setConversationId, | |||||
| controlClearChatMessage, | |||||
| dataSets, | dataSets, | ||||
| modelConfig, | modelConfig, | ||||
| completionParams, | completionParams, | ||||
| hasSetContextVar, | hasSetContextVar, | ||||
| datasetConfigs, | datasetConfigs, | ||||
| visionConfig, | visionConfig, | ||||
| annotationConfig, | |||||
| setVisionConfig, | setVisionConfig, | ||||
| } = useContext(ConfigContext) | } = useContext(ConfigContext) | ||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| const { data: speech2textDefaultModel } = useDefaultModel(4) | |||||
| const { data: text2speechDefaultModel } = useDefaultModel(5) | const { data: text2speechDefaultModel } = useDefaultModel(5) | ||||
| const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) | |||||
| const chatListDomRef = useRef<HTMLDivElement>(null) | |||||
| const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) | const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) | ||||
| // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| setAutoFreeze(false) | setAutoFreeze(false) | ||||
| return () => { | return () => { | ||||
| setAutoFreeze(true) | 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 [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) | ||||
| const [abortController, setAbortController] = useState<AbortController | null>(null) | |||||
| const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false) | const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false) | ||||
| const [isShowCannotQueryDataset, setShowCannotQueryDataset] = useState(false) | const [isShowCannotQueryDataset, setShowCannotQueryDataset] = useState(false) | ||||
| const [isShowSuggestion, setIsShowSuggestion] = useState(false) | |||||
| const [messageTaskId, setMessageTaskId] = useState('') | |||||
| const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (formattingChanged && chatList.some(item => !item.isAnswer)) | |||||
| if (formattingChanged) | |||||
| setIsShowFormattingChangeConfirm(true) | setIsShowFormattingChangeConfirm(true) | ||||
| setFormattingChanged(false) | |||||
| }, [formattingChanged]) | }, [formattingChanged]) | ||||
| const debugWithSingleModelRef = React.useRef<DebugWithSingleModelRefType | null>(null) | |||||
| const handleClearConversation = () => { | 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 () => { | const clearConversation = async () => { | ||||
| if (debugWithMultipleModel) { | if (debugWithMultipleModel) { | ||||
| const handleConfirm = () => { | const handleConfirm = () => { | ||||
| clearConversation() | clearConversation() | ||||
| setIsShowFormattingChangeConfirm(false) | setIsShowFormattingChangeConfirm(false) | ||||
| setFormattingChanged(false) | |||||
| } | } | ||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| setIsShowFormattingChangeConfirm(false) | setIsShowFormattingChangeConfirm(false) | ||||
| setFormattingChanged(false) | |||||
| } | } | ||||
| const { notify } = useContext(ToastContext) | const { notify } = useContext(ToastContext) | ||||
| const logError = (message: string) => { | |||||
| const logError = useCallback((message: string) => { | |||||
| notify({ type: 'error', message }) | notify({ type: 'error', message }) | ||||
| } | |||||
| }, [notify]) | |||||
| const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([]) | |||||
| const checkCanSend = () => { | |||||
| const checkCanSend = useCallback(() => { | |||||
| if (isAdvancedMode && mode === AppType.chat) { | if (isAdvancedMode && mode === AppType.chat) { | ||||
| if (modelModeType === ModelModeType.completion) { | if (modelModeType === ModelModeType.completion) { | ||||
| if (!hasSetBlockStatus.history) { | if (!hasSetBlockStatus.history) { | ||||
| return false | 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)) { | if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) { | ||||
| notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) | notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') }) | ||||
| return false | return false | ||||
| } | } | ||||
| return !hasEmptyInput | 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 [completionRes, setCompletionRes] = useState('') | ||||
| const [messageId, setMessageId] = useState<string | null>(null) | const [messageId, setMessageId] = useState<string | null>(null) | ||||
| const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([]) | |||||
| const sendTextCompletion = async () => { | const sendTextCompletion = async () => { | ||||
| if (isResponsing) { | if (isResponsing) { | ||||
| notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) | notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) | ||||
| setVisionConfig({ | setVisionConfig({ | ||||
| ...visionConfig, | ...visionConfig, | ||||
| enabled: true, | enabled: true, | ||||
| }) | |||||
| }, true) | |||||
| } | } | ||||
| else { | else { | ||||
| setVisionConfig({ | setVisionConfig({ | ||||
| ...visionConfig, | ...visionConfig, | ||||
| enabled: false, | enabled: false, | ||||
| }) | |||||
| }, true) | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| useEffect(() => { | useEffect(() => { | ||||
| handleVisionConfigInMultipleModel() | handleVisionConfigInMultipleModel() | ||||
| }, [multipleModelConfigs, mode]) | }, [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 ( | return ( | ||||
| <> | <> | ||||
| <div className="shrink-0"> | |||||
| <div className="shrink-0 pt-4 px-6"> | |||||
| <div className='flex items-center justify-between mb-2'> | <div className='flex items-center justify-between mb-2'> | ||||
| <div className='h2 '>{t('appDebug.inputs.title')}</div> | <div className='h2 '>{t('appDebug.inputs.title')}</div> | ||||
| <div className='flex items-center'> | <div className='flex items-center'> | ||||
| multipleModelConfigs={multipleModelConfigs} | multipleModelConfigs={multipleModelConfigs} | ||||
| onMultipleModelConfigsChange={onMultipleModelConfigsChange} | onMultipleModelConfigsChange={onMultipleModelConfigsChange} | ||||
| onDebugWithMultipleModelChange={handleChangeToSingleModel} | onDebugWithMultipleModelChange={handleChangeToSingleModel} | ||||
| checkCanSend={checkCanSend} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| <div className="flex flex-col grow"> | <div className="flex flex-col grow"> | ||||
| {/* Chat */} | {/* Chat */} | ||||
| {mode === AppType.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> | </div> | ||||
| )} | )} | ||||
| {/* Text Generation */} | {/* Text Generation */} | ||||
| {mode === AppType.completion && ( | {mode === AppType.completion && ( | ||||
| <div className="mt-6"> | |||||
| <div className="mt-6 px-6 pb-4"> | |||||
| <GroupName name={t('appDebug.result')} /> | <GroupName name={t('appDebug.result')} /> | ||||
| {(completionRes || isResponsing) && ( | {(completionRes || isResponsing) && ( | ||||
| <TextGeneration | <TextGeneration | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {isShowFormattingChangeConfirm && ( | |||||
| <FormattingChanged | |||||
| onConfirm={handleConfirm} | |||||
| onCancel={handleCancel} | |||||
| /> | |||||
| )} | |||||
| {isShowCannotQueryDataset && ( | {isShowCannotQueryDataset && ( | ||||
| <CannotQueryDataset | <CannotQueryDataset | ||||
| onConfirm={() => setShowCannotQueryDataset(false)} | onConfirm={() => setShowCannotQueryDataset(false)} | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } | ||||
| {isShowFormattingChangeConfirm && ( | |||||
| <FormattingChanged | |||||
| onConfirm={handleConfirm} | |||||
| onCancel={handleCancel} | |||||
| /> | |||||
| )} | |||||
| {!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)} | {!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)} | ||||
| </> | </> | ||||
| ) | ) |
| export const APP_CHAT_WITH_MULTIPLE_MODEL = 'APP_CHAT_WITH_MULTIPLE_MODEL' | 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_CHAT_WITH_MULTIPLE_MODEL_RESTART = 'APP_CHAT_WITH_MULTIPLE_MODEL_RESTART' | ||||
| export const APP_SIDEBAR_SHOULD_COLLAPSE = 'APP_SIDEBAR_SHOULD_COLLAPSE' | export const APP_SIDEBAR_SHOULD_COLLAPSE = 'APP_SIDEBAR_SHOULD_COLLAPSE' | ||||
| export const ORCHESTRATE_CHANGED = 'ORCHESTRATE_CHANGED' |
| import Loading from '../../base/loading' | import Loading from '../../base/loading' | ||||
| import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config' | import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config' | ||||
| import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal' | 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 type { ModelAndParameter } from './debug/types' | ||||
| import { APP_SIDEBAR_SHOULD_COLLAPSE } from './debug/types' | import { APP_SIDEBAR_SHOULD_COLLAPSE } from './debug/types' | ||||
| import PublishWithMultipleModel from './debug/debug-with-multiple-model/publish-with-multiple-model' | import PublishWithMultipleModel from './debug/debug-with-multiple-model/publish-with-multiple-model' | ||||
| import { PromptMode } from '@/models/debug' | import { PromptMode } from '@/models/debug' | ||||
| import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG, supportFunctionCallModels } from '@/config' | 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 SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset' | ||||
| import I18n from '@/context/i18n' | |||||
| import { useModalContext } from '@/context/modal-context' | import { useModalContext } from '@/context/modal-context' | ||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | ||||
| import Drawer from '@/app/components/base/drawer' | import Drawer from '@/app/components/base/drawer' | ||||
| embedding_model_name: '', | embedding_model_name: '', | ||||
| }, | }, | ||||
| }) | }) | ||||
| const formattingChangedDispatcher = useFormattingChangedDispatcher() | |||||
| const setAnnotationConfig = (config: AnnotationReplyConfig, notSetFormatChanged?: boolean) => { | const setAnnotationConfig = (config: AnnotationReplyConfig, notSetFormatChanged?: boolean) => { | ||||
| doSetAnnotationConfig(config) | doSetAnnotationConfig(config) | ||||
| if (!notSetFormatChanged) | if (!notSetFormatChanged) | ||||
| setFormattingChanged(true) | |||||
| formattingChangedDispatcher() | |||||
| } | } | ||||
| const [moderationConfig, setModerationConfig] = useState<ModerationConfig>({ | const [moderationConfig, setModerationConfig] = useState<ModerationConfig>({ | ||||
| return | return | ||||
| } | } | ||||
| setFormattingChanged(true) | |||||
| formattingChangedDispatcher() | |||||
| if (data.find(item => !item.name)) { // has not loaded selected dataset | if (data.find(item => !item.name)) { // has not loaded selected dataset | ||||
| const newSelected = produce(data, (draft: any) => { | const newSelected = produce(data, (draft: any) => { | ||||
| data.forEach((item, index) => { | data.forEach((item, index) => { | ||||
| transfer_methods: config.transfer_methods || [TransferMethod.local_file], | transfer_methods: config.transfer_methods || [TransferMethod.local_file], | ||||
| }) | }) | ||||
| if (!notNoticeFormattingChanged) | if (!notNoticeFormattingChanged) | ||||
| setFormattingChanged(true) | |||||
| formattingChangedDispatcher() | |||||
| } | } | ||||
| const { | const { | ||||
| } | } | ||||
| const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false) | const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false) | ||||
| const { locale } = useContext(I18n) | |||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| const { | const { | ||||
| ) | ) | ||||
| } | } | ||||
| </div> | </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 | <Debug | ||||
| hasSetAPIKEY={hasSettedApiKey} | hasSetAPIKEY={hasSettedApiKey} | ||||
| onSetting={() => setShowAccountSettingModal({ payload: 'provider' })} | onSetting={() => setShowAccountSettingModal({ payload: 'provider' })} |
| </div> | </div> | ||||
| </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 | <TooltipPlus | ||||
| popupContent={t('appDebug.feature.annotation.edit') as string} | popupContent={t('appDebug.feature.annotation.edit') as string} | ||||
| > | > |
| const AgentContent: FC<AgentContentProps> = ({ | const AgentContent: FC<AgentContentProps> = ({ | ||||
| item, | item, | ||||
| }) => { | }) => { | ||||
| const { allToolIcons } = useChatContext() | |||||
| const { | |||||
| allToolIcons, | |||||
| isResponsing, | |||||
| } = useChatContext() | |||||
| const { | const { | ||||
| annotation, | annotation, | ||||
| agent_thoughts, | agent_thoughts, | ||||
| <Thought | <Thought | ||||
| thought={thought} | thought={thought} | ||||
| allToolIcons={allToolIcons || {}} | allToolIcons={allToolIcons || {}} | ||||
| isFinished={!!thought.observation} | |||||
| isFinished={!!thought.observation || !isResponsing} | |||||
| /> | /> | ||||
| )} | )} | ||||
| type AnswerProps = { | type AnswerProps = { | ||||
| item: ChatItem | item: ChatItem | ||||
| question: string | |||||
| index: number | |||||
| } | } | ||||
| const Answer: FC<AnswerProps> = ({ | const Answer: FC<AnswerProps> = ({ | ||||
| item, | item, | ||||
| question, | |||||
| index, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { | const { | ||||
| <div className='relative pr-10'> | <div className='relative pr-10'> | ||||
| <AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' /> | <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'> | <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 && ( | responsing && !content && !hasAgentThoughts && ( | ||||
| <div className='flex items-center justify-center w-6 h-5'> | <div className='flex items-center justify-center w-6 h-5'> | ||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| annotation?.id && !annotation?.logAnnotation && ( | |||||
| annotation?.id && annotation.authorName && ( | |||||
| <EditTitle | <EditTitle | ||||
| className='mt-1' | className='mt-1' | ||||
| title={t('appAnnotation.editBy', { author: annotation.authorName })} | title={t('appAnnotation.editBy', { author: annotation.authorName })} |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useState } from 'react' | |||||
| import type { ChatItem } from '../../types' | import type { ChatItem } from '../../types' | ||||
| import { useCurrentAnswerIsResponsing } from '../hooks' | import { useCurrentAnswerIsResponsing } from '../hooks' | ||||
| import { useChatContext } from '../context' | import { useChatContext } from '../context' | ||||
| import CopyBtn from '@/app/components/app/chat/copy-btn' | import CopyBtn from '@/app/components/app/chat/copy-btn' | ||||
| import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' | import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication' | ||||
| import AudioBtn from '@/app/components/base/audio-btn' | 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 = { | type OperationProps = { | ||||
| item: ChatItem | item: ChatItem | ||||
| question: string | |||||
| index: number | |||||
| } | } | ||||
| const Operation: FC<OperationProps> = ({ | const Operation: FC<OperationProps> = ({ | ||||
| item, | item, | ||||
| question, | |||||
| index, | |||||
| }) => { | }) => { | ||||
| const { config } = useChatContext() | |||||
| const { | |||||
| config, | |||||
| onAnnotationAdded, | |||||
| onAnnotationEdited, | |||||
| onAnnotationRemoved, | |||||
| } = useChatContext() | |||||
| const [isShowReplyModal, setIsShowReplyModal] = useState(false) | |||||
| const responsing = useCurrentAnswerIsResponsing(item.id) | const responsing = useCurrentAnswerIsResponsing(item.id) | ||||
| const { | const { | ||||
| id, | |||||
| isOpeningStatement, | isOpeningStatement, | ||||
| content, | content, | ||||
| annotation, | annotation, | ||||
| } = item | } = item | ||||
| const hasAnnotation = !!annotation?.id | |||||
| return ( | return ( | ||||
| <div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'> | <div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'> | ||||
| className='hidden group-hover:block' | 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 && ( | annotation?.id && ( | ||||
| <div | <div |
| import type { ReactNode } from 'react' | import type { ReactNode } from 'react' | ||||
| import { createContext, useContext } from 'use-context-selector' | 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>({ | const ChatContext = createContext<ChatContextValue>({ | ||||
| chatList: [], | chatList: [], | ||||
| answerIcon, | answerIcon, | ||||
| allToolIcons, | allToolIcons, | ||||
| onSend, | onSend, | ||||
| onAnnotationEdited, | |||||
| onAnnotationAdded, | |||||
| onAnnotationRemoved, | |||||
| }: ChatContextProviderProps) => { | }: ChatContextProviderProps) => { | ||||
| return ( | return ( | ||||
| <ChatContext.Provider value={{ | <ChatContext.Provider value={{ | ||||
| answerIcon, | answerIcon, | ||||
| allToolIcons, | allToolIcons, | ||||
| onSend, | onSend, | ||||
| onAnnotationEdited, | |||||
| onAnnotationAdded, | |||||
| onAnnotationRemoved, | |||||
| }}> | }}> | ||||
| {children} | {children} | ||||
| </ChatContext.Provider> | </ChatContext.Provider> |
| import { | import { | ||||
| useCallback, | |||||
| useEffect, | useEffect, | ||||
| useRef, | useRef, | ||||
| useState, | useState, | ||||
| } from 'react' | } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { produce } from 'immer' | import { produce } from 'immer' | ||||
| import { useGetState } from 'ahooks' | |||||
| import dayjs from 'dayjs' | import dayjs from 'dayjs' | ||||
| import type { | import type { | ||||
| ChatConfig, | ChatConfig, | ||||
| import { useToastContext } from '@/app/components/base/toast' | import { useToastContext } from '@/app/components/base/toast' | ||||
| import { ssePost } from '@/service/base' | import { ssePost } from '@/service/base' | ||||
| import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' | import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' | ||||
| import type { Annotation } from '@/models/log' | |||||
| type GetAbortController = (abortController: AbortController) => void | type GetAbortController = (abortController: AbortController) => void | ||||
| type SendCallback = { | type SendCallback = { | ||||
| onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise<any> | onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise<any> | ||||
| onGetSuggestedQuestions?: (responseItemId: 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 = ( | export const useChat = ( | ||||
| config: ChatConfig, | config: ChatConfig, | ||||
| promptVariablesConfig?: { | promptVariablesConfig?: { | ||||
| const connversationId = useRef('') | const connversationId = useRef('') | ||||
| const hasStopResponded = useRef(false) | const hasStopResponded = useRef(false) | ||||
| const [isResponsing, setIsResponsing] = useState(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 [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 || {}) | return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {}) | ||||
| } | |||||
| }, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables]) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (config.opening_statement && !chatList.some(item => !item.isAnswer)) { | |||||
| setChatList([{ | |||||
| if (config.opening_statement && !chatList.length) { | |||||
| handleUpdateChatList([{ | |||||
| id: `${Date.now()}`, | id: `${Date.now()}`, | ||||
| content: getIntroduction(config.opening_statement), | content: getIntroduction(config.opening_statement), | ||||
| isAnswer: true, | isAnswer: true, | ||||
| suggestedQuestions: config.suggested_questions, | 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 | 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 = '' | connversationId.current = '' | ||||
| setIsResponsing(false) | |||||
| setChatList(config.opening_statement | |||||
| const newChatList = config.opening_statement | |||||
| ? [{ | ? [{ | ||||
| id: `${Date.now()}`, | id: `${Date.now()}`, | ||||
| content: config.opening_statement, | content: config.opening_statement, | ||||
| isOpeningStatement: true, | isOpeningStatement: true, | ||||
| suggestedQuestions: config.suggested_questions, | suggestedQuestions: config.suggested_questions, | ||||
| }] | }] | ||||
| : []) | |||||
| : [] | |||||
| handleUpdateChatList(newChatList) | |||||
| setSuggestQuestions([]) | 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, | url: string, | ||||
| data: any, | data: any, | ||||
| { | { | ||||
| }: SendCallback, | }: SendCallback, | ||||
| ) => { | ) => { | ||||
| setSuggestQuestions([]) | setSuggestQuestions([]) | ||||
| if (isResponsing) { | |||||
| if (isResponsingRef.current) { | |||||
| notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) | notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) | ||||
| return false | 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 questionId = `question-${Date.now()}` | ||||
| const questionItem = { | const questionItem = { | ||||
| isAnswer: true, | isAnswer: true, | ||||
| } | } | ||||
| const newList = [...getChatList(), questionItem, placeholderAnswerItem] | |||||
| setChatList(newList) | |||||
| const newList = [...chatListRef.current, questionItem, placeholderAnswerItem] | |||||
| handleUpdateChatList(newList) | |||||
| // answer | // answer | ||||
| const responseItem: ChatItem = { | const responseItem: ChatItem = { | ||||
| isAnswer: true, | isAnswer: true, | ||||
| } | } | ||||
| setIsResponsing(true) | |||||
| handleResponsing(true) | |||||
| hasStopResponded.current = false | hasStopResponded.current = false | ||||
| const bodyParams = { | const bodyParams = { | ||||
| }, | }, | ||||
| { | { | ||||
| getAbortController: (abortController) => { | getAbortController: (abortController) => { | ||||
| setAbortController(abortController) | |||||
| abortControllerRef.current = abortController | |||||
| }, | }, | ||||
| onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { | onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { | ||||
| if (!isAgentMode) { | if (!isAgentMode) { | ||||
| if (isFirstMessage && newConversationId) | if (isFirstMessage && newConversationId) | ||||
| connversationId.current = newConversationId | connversationId.current = newConversationId | ||||
| setTaskId(taskId) | |||||
| taskIdRef.current = taskId | |||||
| if (messageId) | if (messageId) | ||||
| responseItem.id = messageId | responseItem.id = messageId | ||||
| }) | }) | ||||
| }, | }, | ||||
| async onCompleted(hasError?: boolean) { | async onCompleted(hasError?: boolean) { | ||||
| setIsResponsing(false) | |||||
| handleResponsing(false) | |||||
| if (hasError) | if (hasError) | ||||
| return | return | ||||
| if (connversationId.current) { | |||||
| if (connversationId.current && !hasStopResponded.current) { | |||||
| const { data }: any = await onGetConvesationMessages( | const { data }: any = await onGetConvesationMessages( | ||||
| connversationId.current, | connversationId.current, | ||||
| newAbortController => setConversationMessagesAbortController(newAbortController), | |||||
| newAbortController => conversationMessagesAbortControllerRef.current = newAbortController, | |||||
| ) | ) | ||||
| const newResponseItem = data.find((item: any) => item.id === responseItem.id) | const newResponseItem = data.find((item: any) => item.id === responseItem.id) | ||||
| if (!newResponseItem) | if (!newResponseItem) | ||||
| return | return | ||||
| setChatList(produce(getChatList(), (draft) => { | |||||
| const newChatList = produce(chatListRef.current, (draft) => { | |||||
| const index = draft.findIndex(item => item.id === responseItem.id) | const index = draft.findIndex(item => item.id === responseItem.id) | ||||
| if (index !== -1) { | if (index !== -1) { | ||||
| const requestion = draft[index - 1] | const requestion = draft[index - 1] | ||||
| }, | }, | ||||
| } | } | ||||
| } | } | ||||
| })) | |||||
| }) | |||||
| handleUpdateChatList(newChatList) | |||||
| } | } | ||||
| if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) { | if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) { | ||||
| const { data }: any = await onGetSuggestedQuestions( | const { data }: any = await onGetSuggestedQuestions( | ||||
| responseItem.id, | responseItem.id, | ||||
| newAbortController => setSuggestedQuestionsAbortController(newAbortController), | |||||
| newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController, | |||||
| ) | ) | ||||
| setSuggestQuestions(data) | setSuggestQuestions(data) | ||||
| } | } | ||||
| id: messageEnd.metadata.annotation_reply.id, | id: messageEnd.metadata.annotation_reply.id, | ||||
| authorName: messageEnd.metadata.annotation_reply.account.name, | authorName: messageEnd.metadata.annotation_reply.account.name, | ||||
| }) | }) | ||||
| const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId) | |||||
| const newListWithAnswer = produce( | const newListWithAnswer = produce( | ||||
| getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), | |||||
| baseState, | |||||
| (draft) => { | (draft) => { | ||||
| if (!draft.find(item => item.id === questionId)) | if (!draft.find(item => item.id === questionId)) | ||||
| draft.push({ ...questionItem }) | draft.push({ ...questionItem }) | ||||
| ...responseItem, | ...responseItem, | ||||
| }) | }) | ||||
| }) | }) | ||||
| setChatList(newListWithAnswer) | |||||
| handleUpdateChatList(newListWithAnswer) | |||||
| return | return | ||||
| } | } | ||||
| responseItem.citation = messageEnd.metadata?.retriever_resources || [] | responseItem.citation = messageEnd.metadata?.retriever_resources || [] | ||||
| const newListWithAnswer = produce( | 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) => { | (draft) => { | ||||
| if (!draft.find(item => item.id === questionId)) | if (!draft.find(item => item.id === questionId)) | ||||
| draft.push({ ...questionItem }) | draft.push({ ...questionItem }) | ||||
| draft.push({ ...responseItem }) | draft.push({ ...responseItem }) | ||||
| }) | }) | ||||
| setChatList(newListWithAnswer) | |||||
| handleUpdateChatList(newListWithAnswer) | |||||
| }, | }, | ||||
| onMessageReplace: (messageReplace) => { | onMessageReplace: (messageReplace) => { | ||||
| responseItem.content = messageReplace.answer | responseItem.content = messageReplace.answer | ||||
| }, | }, | ||||
| onError() { | 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) | draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) | ||||
| })) | |||||
| }) | |||||
| handleUpdateChatList(newChatList) | |||||
| }, | }, | ||||
| }) | }) | ||||
| return true | 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 { | return { | ||||
| chatList, | chatList, | ||||
| getChatList, | |||||
| setChatList, | setChatList, | ||||
| conversationId: connversationId.current, | conversationId: connversationId.current, | ||||
| isResponsing, | isResponsing, | ||||
| suggestedQuestions, | suggestedQuestions, | ||||
| handleRestart, | handleRestart, | ||||
| handleStop, | handleStop, | ||||
| handleAnnotationEdited, | |||||
| handleAnnotationAdded, | |||||
| handleAnnotationRemoved, | |||||
| } | } | ||||
| } | } | ||||
| } from 'react' | } from 'react' | ||||
| import { | import { | ||||
| memo, | memo, | ||||
| useEffect, | |||||
| useRef, | useRef, | ||||
| } from 'react' | } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useThrottleEffect } from 'ahooks' | import { useThrottleEffect } from 'ahooks' | ||||
| import type { | import type { | ||||
| ChatConfig, | ChatConfig, | ||||
| import TryToAsk from './try-to-ask' | import TryToAsk from './try-to-ask' | ||||
| import { ChatContextProvider } from './context' | import { ChatContextProvider } from './context' | ||||
| import type { Emoji } from '@/app/components/tools/types' | 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 = { | export type ChatProps = { | ||||
| config: ChatConfig | |||||
| onSend?: OnSend | |||||
| chatList: ChatItem[] | chatList: ChatItem[] | ||||
| isResponsing: boolean | |||||
| config?: ChatConfig | |||||
| isResponsing?: boolean | |||||
| noStopResponding?: boolean | |||||
| onStopResponding?: () => void | |||||
| noChatInput?: boolean | noChatInput?: boolean | ||||
| onSend?: OnSend | |||||
| chatContainerclassName?: string | chatContainerclassName?: string | ||||
| chatFooterClassName?: string | chatFooterClassName?: string | ||||
| suggestedQuestions?: string[] | suggestedQuestions?: string[] | ||||
| questionIcon?: ReactNode | questionIcon?: ReactNode | ||||
| answerIcon?: ReactNode | answerIcon?: ReactNode | ||||
| allToolIcons?: Record<string, string | Emoji> | 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> = ({ | const Chat: FC<ChatProps> = ({ | ||||
| config, | config, | ||||
| onSend, | onSend, | ||||
| chatList, | chatList, | ||||
| isResponsing, | isResponsing, | ||||
| noStopResponding, | |||||
| onStopResponding, | |||||
| noChatInput, | noChatInput, | ||||
| chatContainerclassName, | chatContainerclassName, | ||||
| chatFooterClassName, | chatFooterClassName, | ||||
| questionIcon, | questionIcon, | ||||
| answerIcon, | answerIcon, | ||||
| allToolIcons, | allToolIcons, | ||||
| onAnnotationAdded, | |||||
| onAnnotationEdited, | |||||
| onAnnotationRemoved, | |||||
| }) => { | }) => { | ||||
| const ref = useRef<HTMLDivElement>(null) | |||||
| const { t } = useTranslation() | |||||
| const chatContainerRef = useRef<HTMLDivElement>(null) | |||||
| const chatFooterRef = useRef<HTMLDivElement>(null) | const chatFooterRef = useRef<HTMLDivElement>(null) | ||||
| const handleScrolltoBottom = () => { | |||||
| if (chatContainerRef.current) | |||||
| chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight | |||||
| } | |||||
| useThrottleEffect(() => { | 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 }) | }, [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 ( | return ( | ||||
| <ChatContextProvider | <ChatContextProvider | ||||
| answerIcon={answerIcon} | answerIcon={answerIcon} | ||||
| allToolIcons={allToolIcons} | allToolIcons={allToolIcons} | ||||
| onSend={onSend} | onSend={onSend} | ||||
| onAnnotationAdded={onAnnotationAdded} | |||||
| onAnnotationEdited={onAnnotationEdited} | |||||
| onAnnotationRemoved={onAnnotationRemoved} | |||||
| > | > | ||||
| <div className='relative h-full'> | <div className='relative h-full'> | ||||
| <div | <div | ||||
| ref={ref} | |||||
| ref={chatContainerRef} | |||||
| className={`relative h-full overflow-y-auto ${chatContainerclassName}`} | className={`relative h-full overflow-y-auto ${chatContainerclassName}`} | ||||
| > | > | ||||
| { | { | ||||
| chatList.map((item) => { | |||||
| chatList.map((item, index) => { | |||||
| if (item.isAnswer) { | if (item.isAnswer) { | ||||
| return ( | return ( | ||||
| <Answer | <Answer | ||||
| key={item.id} | key={item.id} | ||||
| item={item} | item={item} | ||||
| question={chatList[index - 1]?.content} | |||||
| index={index} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } | ||||
| ) | ) | ||||
| }) | }) | ||||
| } | } | ||||
| </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> | </div> | ||||
| ) | ) | ||||
| } | } | ||||
| { | |||||
| hasTryToAsk && ( | |||||
| <TryToAsk | |||||
| suggestedQuestions={suggestedQuestions} | |||||
| onSend={onSend} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| !noChatInput && ( | |||||
| <ChatInput | |||||
| visionConfig={config?.file_upload?.image} | |||||
| speechToTextConfig={config?.speech_to_text} | |||||
| onSend={onSend} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </ChatContextProvider> | </ChatContextProvider> |
| enabled: boolean | enabled: boolean | ||||
| } | } | ||||
| export type ChatConfig = Omit<ModelConfig, 'model'> | |||||
| export type ChatConfig = Omit<ModelConfig, 'model'> & { | |||||
| supportAnnotation?: boolean | |||||
| appId?: string | |||||
| } | |||||
| export type ChatItem = IChatItem | export type ChatItem = IChatItem | ||||
| hasSetContextVar: boolean | hasSetContextVar: boolean | ||||
| isShowVisionConfig: boolean | isShowVisionConfig: boolean | ||||
| visionConfig: VisionSettings | visionConfig: VisionSettings | ||||
| setVisionConfig: (visionConfig: VisionSettings) => void | |||||
| setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void | |||||
| } | } | ||||
| const DebugConfigurationContext = createContext<IDebugConfiguration>({ | const DebugConfigurationContext = createContext<IDebugConfiguration>({ |