import { Conversation, ErrorMessage, KeyValuePair, Message, OpenAIModel, } from '@/types'; import { FC, memo, MutableRefObject, useCallback, useEffect, useRef, useState, } from 'react'; import { useTranslation } from 'next-i18next'; import { ChatInput } from './ChatInput'; import { ChatLoader } from './ChatLoader'; import { ChatMessage } from './ChatMessage'; import { ErrorMessageDiv } from './ErrorMessageDiv'; import { ModelSelect } from './ModelSelect'; import { SystemPrompt } from './SystemPrompt'; import { IconSettings } from '@tabler/icons-react'; import { throttle } from '@/utils'; interface Props { conversation: Conversation; models: OpenAIModel[]; apiKey: string; serverSideApiKeyIsSet: boolean; messageIsStreaming: boolean; modelError: ErrorMessage | null; messageError: boolean; loading: boolean; onSend: (message: Message, deleteCount?: number) => void; onUpdateConversation: ( conversation: Conversation, data: KeyValuePair, ) => void; onEditMessage: (message: Message, messageIndex: number) => void; stopConversationRef: MutableRefObject; } export const Chat: FC = memo( ({ conversation, models, apiKey, serverSideApiKeyIsSet, messageIsStreaming, modelError, messageError, loading, onSend, onUpdateConversation, onEditMessage, stopConversationRef, }) => { const { t } = useTranslation('chat'); const [currentMessage, setCurrentMessage] = useState(); const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); const [showSettings, setShowSettings] = useState(false); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); const textareaRef = useRef(null); const handleSettings = () => { setShowSettings(!showSettings); }; const scrollDown = () => { if (autoScrollEnabled) { messagesEndRef.current?.scrollIntoView(true); } }; const throttledScrollDown = throttle(scrollDown, 250); useEffect(() => { throttledScrollDown(); setCurrentMessage( conversation.messages[conversation.messages.length - 2], ); }, [conversation.messages, 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 (
{!(apiKey || serverSideApiKeyIsSet) ? (
{t('OpenAI API Key Required')}
{t( 'Please set your OpenAI API key in the bottom left of the sidebar.', )}
{t("If you don't have an OpenAI API key, you can get one here: ")} openai.com
) : modelError ? ( ) : ( <>
{conversation.messages.length === 0 ? ( <>
{models.length === 0 ? t('Loading...') : 'Chatbot UI'}
{models.length > 0 && (
onUpdateConversation(conversation, { key: 'model', value: model, }) } /> onUpdateConversation(conversation, { key: 'prompt', value: prompt, }) } />
)}
) : ( <>
{t('Model')}: {conversation.model.name}
{showSettings && (
onUpdateConversation(conversation, { key: 'model', value: model, }) } />
)} {conversation.messages.map((message, index) => ( ))} {loading && }
)}
0} model={conversation.model} onSend={(message) => { setCurrentMessage(message); onSend(message); }} onRegenerate={() => { if (currentMessage) { onSend(currentMessage, 2); } }} /> )}
); }, ); Chat.displayName = 'Chat';