diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index ef6d8b5..e3bce5a 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -1,12 +1,25 @@ -import { Conversation, ErrorMessage, KeyValuePair, Message, OpenAIModel } from "@/types"; -import { FC, 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 { + Conversation, + ErrorMessage, + KeyValuePair, + Message, + OpenAIModel, +} from '@/types'; +import { + FC, + 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'; interface Props { conversation: Conversation; @@ -17,14 +30,31 @@ interface Props { modelError: ErrorMessage | null; messageError: boolean; loading: boolean; - lightMode: "light" | "dark"; + lightMode: 'light' | 'dark'; onSend: (message: Message, deleteCount?: number) => void; - onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void; + onUpdateConversation: ( + conversation: Conversation, + data: KeyValuePair, + ) => void; onEditMessage: (message: Message, messageIndex: number) => void; stopConversationRef: MutableRefObject; } -export const Chat: FC = ({ conversation, models, apiKey, serverSideApiKeyIsSet, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, onEditMessage, stopConversationRef }) => { +export const Chat: FC = ({ + conversation, + models, + apiKey, + serverSideApiKeyIsSet, + messageIsStreaming, + modelError, + messageError, + loading, + lightMode, + onSend, + onUpdateConversation, + onEditMessage, + stopConversationRef, +}) => { const { t } = useTranslation('chat'); const [currentMessage, setCurrentMessage] = useState(); const [autoScrollEnabled, setAutoScrollEnabled] = useState(true); @@ -42,7 +72,8 @@ export const Chat: FC = ({ conversation, models, apiKey, serverSideApiKey const handleScroll = () => { if (chatContainerRef.current) { - const { scrollTop, scrollHeight, clientHeight } = chatContainerRef.current; + const { scrollTop, scrollHeight, clientHeight } = + chatContainerRef.current; const bottomTolerance = 5; if (scrollTop + clientHeight < scrollHeight - bottomTolerance) { @@ -62,43 +93,60 @@ export const Chat: FC = ({ conversation, models, apiKey, serverSideApiKey const chatContainer = chatContainerRef.current; if (chatContainer) { - chatContainer.addEventListener("scroll", handleScroll); + chatContainer.addEventListener('scroll', handleScroll); return () => { - chatContainer.removeEventListener("scroll", handleScroll); + chatContainer.removeEventListener('scroll', handleScroll); }; } }, []); return ( -
+
{!(apiKey || serverSideApiKeyIsSet) ? ( -
-
{t('OpenAI API Key Required')}
-
{t('Please set your OpenAI API key in the bottom left of the sidebar.')}
+
+
+ {t('OpenAI API Key Required')} +
+
+ {t( + 'Please set your OpenAI API key in the bottom left of the sidebar.', + )} +
- ) : modelError ? : ( + ) : modelError ? ( + + ) : ( <> -
+
{conversation.messages.length === 0 ? ( <> -
-
{models.length === 0 ? t("Loading...") : "Chatbot UI"}
+
+
+ {models.length === 0 ? t('Loading...') : 'Chatbot UI'} +
{models.length > 0 && ( -
+
onUpdateConversation(conversation, { key: "model", value: model })} + onModelChange={(model) => + onUpdateConversation(conversation, { + key: 'model', + value: model, + }) + } /> onUpdateConversation(conversation, { key: "prompt", value: prompt })} + onChangePrompt={(prompt) => + onUpdateConversation(conversation, { + key: 'prompt', + value: prompt, + }) + } />
)} @@ -106,7 +154,9 @@ export const Chat: FC = ({ conversation, models, apiKey, serverSideApiKey ) : ( <> -
{t('Model')}: {conversation.model.name}
+
+ {t('Model')}: {conversation.model.name} +
{conversation.messages.map((message, index) => ( = ({ conversation, models, apiKey, serverSideApiKey {loading && }
diff --git a/components/Chat/ChatInput.tsx b/components/Chat/ChatInput.tsx index d205cc7..58797bc 100644 --- a/components/Chat/ChatInput.tsx +++ b/components/Chat/ChatInput.tsx @@ -1,7 +1,13 @@ -import { Message, OpenAIModel, OpenAIModelID } from "@/types"; -import { IconPlayerStop, IconRepeat, IconSend } from "@tabler/icons-react"; -import { FC, KeyboardEvent, MutableRefObject, useEffect, useState } from "react"; -import { useTranslation } from "next-i18next"; +import { Message, OpenAIModel, OpenAIModelID } from '@/types'; +import { IconPlayerStop, IconRepeat, IconSend } from '@tabler/icons-react'; +import { + FC, + KeyboardEvent, + MutableRefObject, + useEffect, + useState, +} from 'react'; +import { useTranslation } from 'next-i18next'; interface Props { messageIsStreaming: boolean; @@ -13,7 +19,15 @@ interface Props { textareaRef: MutableRefObject; } -export const ChatInput: FC = ({ messageIsStreaming, model, messages, onSend, onRegenerate, stopConversationRef, textareaRef }) => { +export const ChatInput: FC = ({ + messageIsStreaming, + model, + messages, + onSend, + onRegenerate, + stopConversationRef, + textareaRef, +}) => { const { t } = useTranslation('chat'); const [content, setContent] = useState(); const [isTyping, setIsTyping] = useState(false); @@ -23,7 +37,12 @@ export const ChatInput: FC = ({ messageIsStreaming, model, messages, onSe const maxLength = model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000; if (value.length > maxLength) { - alert(t(`Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, { maxLength, valueLength: value.length })); + alert( + t( + `Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, + { maxLength, valueLength: value.length }, + ), + ); return; } @@ -36,12 +55,12 @@ export const ChatInput: FC = ({ messageIsStreaming, model, messages, onSe } if (!content) { - alert(t("Please enter a message")); + alert(t('Please enter a message')); return; } - onSend({ role: "user", content }); - setContent(""); + onSend({ role: 'user', content }); + setContent(''); if (window.innerWidth < 640 && textareaRef && textareaRef.current) { textareaRef.current.blur(); @@ -49,14 +68,16 @@ export const ChatInput: FC = ({ messageIsStreaming, model, messages, onSe }; const isMobile = () => { - const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent; - const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i; + const userAgent = + typeof window.navigator === 'undefined' ? '' : navigator.userAgent; + const mobileRegex = + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i; return mobileRegex.test(userAgent); }; const handleKeyDown = (e: KeyboardEvent) => { if (!isTyping) { - if (e.key === "Enter" && !e.shiftKey && !isMobile()) { + if (e.key === 'Enter' && !e.shiftKey && !isMobile()) { e.preventDefault(); handleSend(); } @@ -65,9 +86,11 @@ export const ChatInput: FC = ({ messageIsStreaming, model, messages, onSe useEffect(() => { if (textareaRef && textareaRef.current) { - textareaRef.current.style.height = "inherit"; + textareaRef.current.style.height = 'inherit'; textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; - textareaRef.current.style.overflow = `${textareaRef?.current?.scrollHeight > 400 ? "auto" : "hidden"}`; + textareaRef.current.style.overflow = `${ + textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden' + }`; } }, [content]); @@ -79,45 +102,43 @@ export const ChatInput: FC = ({ messageIsStreaming, model, messages, onSe } return ( -
-
+
+
{messageIsStreaming && ( )} {!messageIsStreaming && messages.length > 0 && ( )} -
+