import { useChatContext } from '@/components/chat/chat/context'; import { toast } from 'sonner'; import { ERROR_CODE_NO_LIMIT } from '@/components/chat/lib/const'; import { useMessageAnimation } from './message-animation-provider'; import { useChatMessagesContext } from './messages-provider'; import { useResponseFormatContext } from './response-format-provider'; import { useSuggestionsContext } from './suggestions-provider'; import { AuthorType, ChatMessageMetadata, GetChatMessagesPayload, MessageType, OutputContent, OutputLayout, RepeatedChatMessage, ResponseFormat, } from '@/components/chat/types'; import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'; import { usePromptModal } from './prompt-modal-provider'; interface MessagesHandlerContextTypes { fetchMessages: (payload?: GetChatMessagesPayload) => Promise; submitQuestion: (message: string) => Promise; regenerateAnswer: (questionId: number) => Promise; fetchAnswerStream: ( questionId: number, format?: ResponseFormat, onMessage?: (text: string, done?: boolean) => void ) => Promise; cancelAnswerStream: () => void; questionSending: boolean; answerApplying: boolean; selectedModelName?: string; setSelectedModelName?: (modelName: string) => void; } export const MessagesHandlerContext = createContext(undefined); function useMessagesHandler() { const { chatId, requestInstance, currentUser } = useChatContext(); // Get the current model from chat settings const [selectedModelName, setSelectedModelName] = useState(); // Load initial model from settings useEffect(() => { const loadModel = async () => { try { const settings = await requestInstance.getChatSettings(); const model = settings.metadata?.ai_model as string | undefined; if (model) { setSelectedModelName(model); } } catch (error) { console.error('Failed to load chat model settings:', error); } }; if (chatId) { void loadModel(); } }, [chatId, requestInstance]); const { messageIds, addMessages, insertMessage, removeMessages, saveMessageContent, getMessage } = useChatMessagesContext(); const { setResponseFormatWithId } = useResponseFormatContext(); const { currentPromptId } = usePromptModal(); const { registerAnimation } = useMessageAnimation(); const { registerFetchSuggestions, startFetchSuggestions } = useSuggestionsContext(); const [questionSending, setQuestionSending] = useState(false); const [answerApplying, setAnswerApplying] = useState(false); const cancelStreamRef = useRef<() => void>(); useEffect(() => { return () => { setQuestionSending(false); setAnswerApplying(false); cancelStreamRef.current = undefined; }; }, [chatId]); const fetchMessages = useCallback( async (payload?: GetChatMessagesPayload) => { try { const data = await requestInstance.getChatMessages(payload); const messages = data.messages; addMessages(messages); return data; // eslint-disable-next-line } catch (e: any) { toast.error(e.message); return Promise.reject(e); } }, [requestInstance, addMessages] ); const createAssistantMessage = useCallback( (answerId: number, index: number) => { registerAnimation(answerId); insertMessage( { message_id: answerId, content: '', author: { author_uuid: 'assistant', author_type: AuthorType.Assistant, }, }, index ); }, [insertMessage, registerAnimation] ); const regenerateAnswer = useCallback( async (questionId: number) => { const question = getMessage(questionId); const answerId = question?.reply_message_id || questionId + 1; const newMessages = removeMessages([answerId]); setTimeout(() => { const index = newMessages.map((message) => message.message_id).indexOf(questionId); createAssistantMessage(answerId, index); }, 200); }, [createAssistantMessage, getMessage, removeMessages] ); const submitQuestion = useCallback( async (message: string) => { try { setQuestionSending(true); // insert fake message to show user message const fakeMessageId = Date.now(); const author = { author_uuid: currentUser?.uuid || '', author_type: AuthorType.Human, }; insertMessage( { message_id: fakeMessageId, content: message, author, }, 0 ); registerAnimation(fakeMessageId); const promptId = currentPromptId || undefined; const question = await requestInstance.submitQuestion({ content: message, message_type: MessageType.User, prompt_id: promptId, model_name: selectedModelName, }); const answerId = question.reply_message_id || question.message_id + 1; // remove fake message removeMessages([fakeMessageId]); insertMessage( { ...question, author, }, 0 ); // clear suggestions and set placeholder for this question registerFetchSuggestions(question.message_id); // create assistant message after user message setTimeout(() => { createAssistantMessage(answerId, 0); }, 200); void (async () => { try { const view = await requestInstance.getCurrentView(); if ((!messageIds || messageIds.length === 0) && view) { await requestInstance.updateViewName(view, message); } // eslint-disable-next-line } catch (e: any) { toast.error(e.message); } })(); // eslint-disable-next-line } catch (e: any) { toast.error(e.message); return Promise.reject(e); } finally { setQuestionSending(false); } }, [ currentUser?.uuid, insertMessage, registerAnimation, currentPromptId, requestInstance, removeMessages, registerFetchSuggestions, createAssistantMessage, messageIds, selectedModelName, ] ); const saveAnswer = useCallback( async (questionId: number, content: string, metadata: ChatMessageMetadata[]) => { try { const answer = await requestInstance.saveAnswer({ question_message_id: questionId, content, ...(metadata.length !== 0 && { meta_data: metadata }), }); saveMessageContent(answer.message_id, content, metadata); // eslint-disable-next-line } catch (e: any) { toast.error(e.message); } }, [requestInstance, saveMessageContent] ); const removeAssistantMessage = useCallback( (messageId: number) => { removeMessages([messageId]); }, [removeMessages] ); const fetchAnswerStream = useCallback( async (questionId: number, format?: ResponseFormat, onMessage?: (text: string, done?: boolean) => void) => { const question = getMessage(questionId); let answerId = question?.reply_message_id; if (!answerId) { answerId = questionId + 1; } if (format) { setResponseFormatWithId(answerId, format); } const handleMessageProgress = (message: string, metadata: ChatMessageMetadata[], done?: boolean) => { onMessage?.(message, done); if (done) { if (message) { void (async () => { await saveAnswer(questionId, message, metadata); setAnswerApplying(false); if (answerId && messageIds.indexOf(answerId) === 0) { await startFetchSuggestions(questionId); } })(); } else { if (answerId) { removeAssistantMessage(answerId); } setAnswerApplying(false); } return; } }; try { setAnswerApplying(true); const { cancel, streamPromise } = await requestInstance.fetchAnswerStream( { question_id: questionId, format: format || { output_layout: OutputLayout.Paragraph, output_content: OutputContent.TEXT, }, model_name: selectedModelName, }, handleMessageProgress ); cancelStreamRef.current = cancel; await streamPromise; // eslint-disable-next-line } catch (e: any) { toast.error(e.message); setAnswerApplying(false); const code = e.code; if (code !== ERROR_CODE_NO_LIMIT) { // remove assistant message if error is not no limit if (answerId) { removeAssistantMessage(answerId); } return; } // if error is no limit, show assistant message with error return Promise.reject(e); } }, [ getMessage, setResponseFormatWithId, saveAnswer, messageIds, startFetchSuggestions, removeAssistantMessage, requestInstance, selectedModelName, ] ); const cancelAnswerStream = useCallback(() => { if (cancelStreamRef.current) { cancelStreamRef.current(); } }, []); // Update local state and persist to chat settings const updateSelectedModel = useCallback(async (modelName: string) => { setSelectedModelName(modelName); try { await requestInstance.updateChatSettings({ metadata: { ai_model: modelName } }); } catch (error) { console.error('Failed to update chat model settings:', error); } }, [requestInstance]); return { fetchMessages, submitQuestion, regenerateAnswer, fetchAnswerStream, cancelAnswerStream, questionSending, answerApplying, selectedModelName, setSelectedModelName: updateSelectedModel, }; } export function MessagesHandlerProvider({ children }: { children: ReactNode }) { const value = useMessagesHandler(); return {children}; } export function useMessagesHandlerContext() { const context = useContext(MessagesHandlerContext); if (!context) { throw new Error('useMessagesHandlerContext must be used within a MessagesHandlerProvider'); } return context; }