import { Conversation, Message } from '@/types/chat'; import { IconArrowDown } from '@tabler/icons-react'; import { KeyValuePair } from '@/types/data'; import { ErrorMessage } from '@/types/error'; import { OpenAIModel, OpenAIModelID } from '@/types/openai'; import { Prompt } from '@/types/prompt'; import { throttle } from '@/utils'; import { IconClearAll, IconKey, IconSettings } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { FC, memo, MutableRefObject, useCallback, useEffect, useRef, useState, } from 'react'; import { Spinner } from '../Global/Spinner'; import { ChatInput } from './ChatInput'; import { ChatLoader } from './ChatLoader'; import { ChatMessage } from './ChatMessage'; import { ErrorMessageDiv } from './ErrorMessageDiv'; import { ModelSelect } from './ModelSelect'; import { SystemPrompt } from './SystemPrompt'; interface Props { conversation: Conversation; models: OpenAIModel[]; apiKey: string; serverSideApiKeyIsSet: boolean; defaultModelId: OpenAIModelID; messageIsStreaming: boolean; modelError: ErrorMessage | null; loading: boolean; prompts: Prompt[]; 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, defaultModelId, messageIsStreaming, modelError, loading, prompts, onSend, onUpdateConversation, onEditMessage, stopConversationRef, }) => { const { t } = useTranslation('chat'); 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 scrollToBottom = useCallback(() => { if (autoScrollEnabled) { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); textareaRef.current?.focus(); } }, [autoScrollEnabled]); 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?'))) { onUpdateConversation(conversation, { key: 'messages', value: [] }); } }; const scrollDown = () => { if (autoScrollEnabled) { messagesEndRef.current?.scrollIntoView(true); } }; const throttledScrollDown = throttle(scrollDown, 250); // appear scroll down button only when user scrolls up 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 ? (
) : ( '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 && }
)}
{ setCurrentMessage(message); onSend(message); }} onRegenerate={() => { if (currentMessage) { onSend(currentMessage, 2); } }} /> )} {showScrollDownButton && (
)}
); }, ); Chat.displayName = 'Chat';