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 | |
}, | |
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(t<string>('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, plugin: any) => { | |
setCurrentMessage(message); | |
handleSend(message, 0, plugin); | |
}} | |
onScrollDownClick={handleScrollDown} | |
onRegenerate={() => { | |
if (currentMessage) { | |
handleSend(currentMessage, 2, null); | |
} | |
}} | |
showScrollDownButton={showScrollDownButton} | |
/> | |
</div> | |
); | |
}); | |
Chat.displayName = 'Chat'; |