import { IconClearAll, IconSettings } from '@tabler/icons-react'; import { MutableRefObject, memo, useCallback, useContext, useEffect, useRef, useState, } from 'react'; import { saveConversation, saveConversations, updateConversation, throttle } from '@/utils'; import { ChatBody, Conversation, Message } from '@/types/chat'; import HomeContext from '@/pages/api/home.context'; import { ChatInput } from './ChatInput'; import { ChatLoader } from './ChatLoader'; interface Props { stopConversationRef: MutableRefObject; } export const Chat = memo(({ stopConversationRef }: Props) => { const { state: { selectedConversation, conversations, loading }, dispatch: homeDispatch, }: any = useContext(HomeContext); const [currentMessage, setCurrentMessage] = useState(); const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); const [showSettings, setShowSettings] = useState(false); const [showScrollDownButton, setShowScrollDownButton] = useState(false); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); const textareaRef = useRef(null); const handleSend = useCallback( async (message: Message, deleteCount = 0, plugin: Plugin | null = null) => { if (selectedConversation) { let updatedConversation: Conversation; if (deleteCount) { const updatedMessages = [...selectedConversation.messages]; for (let i = 0; i < deleteCount; i++) { updatedMessages.pop(); } updatedConversation = { ...selectedConversation, messages: [...updatedMessages, message], }; } else { updatedConversation = { ...selectedConversation, messages: [...selectedConversation.messages, message], }; } homeDispatch({ field: 'selectedConversation', value: updatedConversation, }); homeDispatch({ field: 'loading', value: true }); homeDispatch({ field: 'messageIsStreaming', value: true }); const chatBody: ChatBody = { model: updatedConversation.model, messages: updatedConversation.messages, prompt: updatedConversation.prompt }; const endpoint = "/v1/api/create" const body = JSON.stringify(chatBody); const controller = new AbortController(); const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', }, signal: controller.signal, body, }); if (!response.ok) { homeDispatch({ field: 'loading', value: false }); homeDispatch({ field: 'messageIsStreaming', value: false }); console.error(response.statusText); return; } const data = response.body; if (!data) { homeDispatch({ field: 'loading', value: false }); homeDispatch({ field: 'messageIsStreaming', value: false }); return; } if (!plugin) { if (updatedConversation.messages.length === 1) { const { content } = message; const customName = content.length > 30 ? content.substring(0, 30) + '...' : content; updatedConversation = { ...updatedConversation, name: customName, }; } homeDispatch({ field: 'loading', value: false }); const reader = data.getReader(); const decoder = new TextDecoder(); let done = false; let isFirst = true; let text = ''; while (!done) { if (stopConversationRef.current === true) { controller.abort(); done = true; break; } const { value, done: doneReading } = await reader.read(); done = doneReading; const chunkValue = decoder.decode(value); text += chunkValue; if (isFirst) { isFirst = false; const updatedMessages: Message[] = [ ...updatedConversation.messages, { role: 'assistant', content: chunkValue }, ]; updatedConversation = { ...updatedConversation, messages: updatedMessages, }; homeDispatch({ field: 'selectedConversation', value: updatedConversation, }); } else { const updatedMessages: Message[] = updatedConversation.messages.map((message: any, index: number) => { if (index === updatedConversation.messages.length - 1) { return { ...message, content: text, }; } return message; }); updatedConversation = { ...updatedConversation, messages: updatedMessages, }; homeDispatch({ field: 'selectedConversation', value: updatedConversation, }); } } saveConversation(updatedConversation); const updatedConversations: Conversation[] = conversations.map( (conversation: { id: any; }) => { if (conversation.id === selectedConversation.id) { return updatedConversation; } return conversation; }, ); if (updatedConversations.length === 0) { updatedConversations.push(updatedConversation); } homeDispatch({ field: 'conversations', value: updatedConversations }); saveConversations(updatedConversations); homeDispatch({ field: 'messageIsStreaming', value: false }); } else { const { answer } = await response.json(); const updatedMessages: Message[] = [ ...updatedConversation.messages, { role: 'assistant', content: answer }, ]; updatedConversation = { ...updatedConversation, messages: updatedMessages, }; homeDispatch({ field: 'selectedConversation', value: updateConversation, }); saveConversation(updatedConversation); const updatedConversations: Conversation[] = conversations.map( (conversation: { id: any; }) => { if (conversation.id === selectedConversation.id) { return updatedConversation; } return conversation; }, ); if (updatedConversations.length === 0) { updatedConversations.push(updatedConversation); } homeDispatch({ field: 'conversations', value: updatedConversations }); saveConversations(updatedConversations); homeDispatch({ field: 'loading', value: false }); homeDispatch({ field: 'messageIsStreaming', value: false }); } } }, [ conversations, selectedConversation, stopConversationRef, ], ); const handleScroll = () => { if (chatContainerRef.current) { const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current; const bottomTolerance = 30; if (scrollTop + clientHeight < scrollHeight - bottomTolerance) { setAutoScrollEnabled(false); setShowScrollDownButton(true); } else { setAutoScrollEnabled(true); setShowScrollDownButton(false); } } }; const handleScrollDown = () => { chatContainerRef.current?.scrollTo({ top: chatContainerRef.current.scrollHeight, behavior: 'smooth', }); }; const handleSettings = () => { setShowSettings(!showSettings); }; const onClearAll = () => { if ( confirm(t('Are you sure you want to clear all messages?')) && selectedConversation ) { handleUpdateConversation(selectedConversation, { key: 'messages', value: [], }); } }; const scrollDown = () => { if (autoScrollEnabled) { messagesEndRef.current?.scrollIntoView(true); } }; const throttledScrollDown = throttle(scrollDown, 250); useEffect(() => { throttledScrollDown(); selectedConversation && setCurrentMessage( selectedConversation.messages[selectedConversation.messages.length - 2], ); }, [selectedConversation, throttledScrollDown]); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { setAutoScrollEnabled(entry.isIntersecting); if (entry.isIntersecting) { textareaRef.current?.focus(); } }, { root: null, threshold: 0.5, }, ); const messagesEndElement = messagesEndRef.current; if (messagesEndElement) { observer.observe(messagesEndElement); } return () => { if (messagesEndElement) { observer.unobserve(messagesEndElement); } }; }, [messagesEndRef]); return (
{selectedConversation?.messages.length === 0 ? ( <>
) : ( <>
{loading && }
)}
{ setCurrentMessage(message); handleSend(message, 0, plugin); }} onScrollDownClick={handleScrollDown} onRegenerate={() => { if (currentMessage) { handleSend(currentMessage, 2, null); } }} showScrollDownButton={showScrollDownButton} />
); }); Chat.displayName = 'Chat';