Spaces:
Runtime error
Runtime error
| 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<boolean>; | |
| } | |
| export const Chat = memo(({ stopConversationRef }: Props) => { | |
| const { | |
| state: { | |
| selectedConversation, | |
| conversations, | |
| loading | |
| }, | |
| handleUpdateConversation, | |
| dispatch: homeDispatch, | |
| }: any = useContext(HomeContext); | |
| const [currentMessage, setCurrentMessage] = useState<Message>(); | |
| const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true); | |
| const [showSettings, setShowSettings] = useState<boolean>(false); | |
| const [showScrollDownButton, setShowScrollDownButton] = | |
| useState<boolean>(false); | |
| const messagesEndRef = useRef<HTMLDivElement>(null); | |
| const chatContainerRef = useRef<HTMLDivElement>(null); | |
| const textareaRef = useRef<HTMLTextAreaElement>(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('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 ( | |
| <div className="relative flex-1 overflow-hidden bg-white dark:bg-[#343541]"> | |
| <div | |
| className="max-h-full overflow-x-hidden" | |
| ref={chatContainerRef} | |
| onScroll={handleScroll} | |
| > | |
| {selectedConversation?.messages.length === 0 ? ( | |
| <> | |
| <div className="mx-auto flex flex-col space-y-5 md:space-y-10 px-3 pt-5 md:pt-12 sm:max-w-[600px]"> | |
| </div> | |
| </> | |
| ) : ( | |
| <> | |
| <div className="sticky top-0 z-10 flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200"> | |
| <button | |
| className="ml-2 cursor-pointer hover:opacity-50" | |
| onClick={handleSettings} | |
| > | |
| <IconSettings size={18} /> | |
| </button> | |
| <button | |
| className="ml-2 cursor-pointer hover:opacity-50" | |
| onClick={onClearAll} | |
| > | |
| <IconClearAll size={18} /> | |
| </button> | |
| </div> | |
| {loading && <ChatLoader />} | |
| <div | |
| className="h-[162px] bg-white dark:bg-[#343541]" | |
| ref={messagesEndRef} | |
| /> | |
| </> | |
| )} | |
| </div> | |
| <ChatInput | |
| stopConversationRef={stopConversationRef} | |
| textareaRef={textareaRef} | |
| onSend={(message: any) => { | |
| setCurrentMessage(message); | |
| handleSend(message, 0); | |
| }} | |
| onScrollDownClick={handleScrollDown} | |
| onRegenerate={() => { | |
| if (currentMessage) { | |
| handleSend(currentMessage, 2, null); | |
| } | |
| }} | |
| showScrollDownButton={showScrollDownButton} | |
| /> | |
| </div> | |
| ); | |
| }); | |
| Chat.displayName = 'Chat'; |