diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index df834b116d..0c7af563db 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -110,6 +110,10 @@ export const Chat = memo(() => { return isReplay && !messageIsStreaming && isReplayPaused; }, [isReplay, isReplayPaused, messageIsStreaming]); + const isNotEmptyConversations = selectedConversations.some( + (conv) => conv.messages.length > 0, + ); + useEffect(() => { setIsShowChatSettings(false); @@ -499,12 +503,6 @@ export const Chat = memo(() => { setInputHeight(inputHeight); }, []); - useEffect(() => { - if (showReplayControls) { - setInputHeight(80); - } - }, [showReplayControls]); - return (
{modelError ? ( @@ -786,42 +784,37 @@ export const Chat = memo(() => { ) : ( <> - {showReplayControls ? ( - conv.messages.length === 0, - )} - /> - ) : ( - <> - {!isPlayback && ( - val.messages.length > 0, - )} - showScrollDownButton={showScrollDownButton} - onSend={onSendMessage} - onScrollDownClick={handleScrollDown} - onRegenerate={onRegenerateMessage} - onStopConversation={() => { - dispatch(ConversationsActions.stopStreamMessage()); - }} - onResize={onChatInputResize} + {!isPlayback && ( + { + dispatch(ConversationsActions.stopStreamMessage()); + }} + onResize={onChatInputResize} + isShowInput={!isReplay || isNotEmptyConversations} + > + {showReplayControls && ( + )} + + )} - {isPlayback && ( - - )} - + {isPlayback && ( + )} )} diff --git a/src/components/Chat/ChatInput.tsx b/src/components/Chat/ChatInput.tsx index b1cd4901ec..fc59a2f5c2 100644 --- a/src/components/Chat/ChatInput.tsx +++ b/src/components/Chat/ChatInput.tsx @@ -1,34 +1,16 @@ import { IconPlayerStop } from '@tabler/icons-react'; -import { - KeyboardEvent, - MutableRefObject, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { MutableRefObject, ReactNode, useEffect, useRef } from 'react'; import { useTranslation } from 'next-i18next'; -import { isMobile } from '@/src/utils/app/mobile'; - import { Message } from '@/src/types/chat'; -import { OpenAIEntityModels, defaultModelLimits } from '@/src/types/openai'; -import { Prompt } from '@/src/types/prompt'; import { ConversationsSelectors } from '@/src/store/conversations/conversations.reducers'; import { useAppSelector } from '@/src/store/hooks'; -import { ModelsSelectors } from '@/src/store/models/models.reducers'; -import { PromptsSelectors } from '@/src/store/prompts/prompts.reducers'; -import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import RefreshCWAlt from '../../../public/images/icons/refresh-cw-alt.svg'; -import { FooterMessage } from './FooterMessage'; -import { PromptDialog } from './PromptDialog'; -import { PromptList } from './PromptList'; -import { ScrollDownButton } from './ScrollDownButton'; -import { SendMessageButton } from './SendMessageButton'; +import { ChatInputFooter } from './ChatInputFooter'; +import { ChatInputMessage } from './ChatInputMessage'; interface Props { onSend: (message: Message) => void; @@ -39,6 +21,8 @@ interface Props { textareaRef: MutableRefObject; showScrollDownButton: boolean; isMessagesPresented: boolean; + isShowInput: boolean; + children?: ReactNode; } export const ChatInput = ({ @@ -50,59 +34,17 @@ export const ChatInput = ({ textareaRef, showScrollDownButton, isMessagesPresented, + isShowInput, + children, }: Props) => { const { t } = useTranslation('chat'); - const [content, setContent] = useState(); - const [isTyping, setIsTyping] = useState(false); - const [showPromptList, setShowPromptList] = useState(false); - const [activePromptIndex, setActivePromptIndex] = useState(0); - const [promptInputValue, setPromptInputValue] = useState(''); - const [variables, setVariables] = useState([]); - const [isModalVisible, setIsModalVisible] = useState(false); - const [showPluginSelect, setShowPluginSelect] = useState(false); - - const prompts = useAppSelector(PromptsSelectors.selectPrompts); - const messageIsStreaming = useAppSelector( ConversationsSelectors.selectIsConversationsStreaming, ); - const isIframe = useAppSelector(SettingsSelectors.selectIsIframe); - const selectedConversations = useAppSelector( - ConversationsSelectors.selectSelectedConversations, - ); - - const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); - - const maxLength = useMemo(() => { - const maxLengthArray = selectedConversations.map( - ({ model }) => - modelsMap[model.id]?.maxLength ?? - OpenAIEntityModels[model.id]?.maxLength ?? - defaultModelLimits.maxLength, - ); - - return Math.min(...maxLengthArray); - }, [modelsMap, selectedConversations]); - - const promptListRef = useRef(null); const inputRef = useRef(null); - const [filteredPrompts, setFilteredPrompts] = useState(() => - prompts.filter((prompt) => - prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), - ), - ); - - useEffect(() => { - setFilteredPrompts( - prompts.filter((prompt) => - prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), - ), - ); - }, [prompts, promptInputValue]); - useEffect(() => { if (!inputRef) { return; @@ -118,203 +60,6 @@ export const ChatInput = ({ }; }, [inputRef, onResize]); - const updatePromptListVisibility = useCallback((text: string) => { - const match = text.match(/\/\w*$/); - - if (match) { - setShowPromptList(true); - setPromptInputValue(match[0].slice(1)); - } else { - setShowPromptList(false); - setPromptInputValue(''); - } - }, []); - - const handleChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - - if (maxLength && value.length > maxLength) { - alert( - t( - `Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`, - { maxLength, valueLength: value.length }, - ), - ); - return; - } - - setContent(value); - updatePromptListVisibility(value); - }, - [maxLength, t, updatePromptListVisibility], - ); - - const handleSend = useCallback(() => { - if (messageIsStreaming) { - return; - } - - if (!content) { - alert(t('Please enter a message')); - return; - } - - onSend({ role: 'user', content }); - setContent(''); - - if (window.innerWidth < 640 && textareaRef && textareaRef.current) { - textareaRef.current.blur(); - } - }, [content, messageIsStreaming, onSend, t, textareaRef]); - - const parseVariables = useCallback((content: string) => { - const regex = /{{(.*?)}}/g; - const foundVariables = []; - let match; - - while ((match = regex.exec(content)) !== null) { - foundVariables.push(match[1]); - } - - return foundVariables; - }, []); - - const handlePromptSelect = useCallback( - (prompt: Prompt) => { - if (!prompt.content) { - return; - } - - const parsedVariables = parseVariables(prompt.content); - setVariables(parsedVariables); - - if (parsedVariables.length > 0) { - setIsModalVisible(true); - } else { - setContent((prevContent) => { - const updatedContent = prevContent?.replace( - /\/\w*$/, - prompt.content as string, - ); - return updatedContent; - }); - updatePromptListVisibility(prompt.content); - } - }, - [parseVariables, updatePromptListVisibility], - ); - - const handleInitModal = useCallback(() => { - const selectedPrompt = filteredPrompts[activePromptIndex]; - if (selectedPrompt && !!selectedPrompt.content) { - setContent((prevContent) => { - const newContent = prevContent?.replace( - /\/\w*$/, - selectedPrompt.content as string, - ); - return newContent; - }); - handlePromptSelect(selectedPrompt); - } - setShowPromptList(false); - }, [activePromptIndex, filteredPrompts, handlePromptSelect]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (showPromptList) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - setActivePromptIndex((prevIndex) => - prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex, - ); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - setActivePromptIndex((prevIndex) => - prevIndex > 0 ? prevIndex - 1 : prevIndex, - ); - } else if (e.key === 'Tab') { - e.preventDefault(); - setActivePromptIndex((prevIndex) => - prevIndex < prompts.length - 1 ? prevIndex + 1 : 0, - ); - } else if (e.key === 'Enter') { - e.preventDefault(); - handleInitModal(); - } else if (e.key === 'Escape') { - e.preventDefault(); - setShowPromptList(false); - } else { - setActivePromptIndex(0); - } - } else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } else if (e.key === '/' && e.metaKey) { - e.preventDefault(); - setShowPluginSelect(!showPluginSelect); - } - }, - [ - handleInitModal, - handleSend, - isTyping, - prompts.length, - showPluginSelect, - showPromptList, - ], - ); - - const handleSubmit = useCallback( - (updatedVariables: string[]) => { - const newContent = content?.replace(/{{(.*?)}}/g, (match, variable) => { - const index = variables.indexOf(variable); - return updatedVariables[index]; - }); - - setContent(newContent); - - if (textareaRef && textareaRef.current) { - textareaRef.current.focus(); - } - }, - [content, textareaRef, variables], - ); - - useEffect(() => { - if (promptListRef.current) { - promptListRef.current.scrollTop = activePromptIndex * 30; - } - }, [activePromptIndex]); - - useEffect(() => { - if (textareaRef && textareaRef.current) { - textareaRef.current.style.height = 'inherit'; // reset height - const scrollHeight = textareaRef.current.scrollHeight; // then check scroll height - textareaRef.current.style.height = `${scrollHeight}px`; - textareaRef.current.style.overflow = `${ - scrollHeight > 400 ? 'auto' : 'hidden' - }`; - } - }, [content, textareaRef]); - - useEffect(() => { - const handleOutsideClick = (e: MouseEvent) => { - if ( - promptListRef.current && - !promptListRef.current.contains(e.target as Node) - ) { - setShowPromptList(false); - } - }; - - window.addEventListener('click', handleOutsideClick); - - return () => { - window.removeEventListener('click', handleOutsideClick); - }; - }, []); - return (
)} - {!messageIsStreaming && isMessagesPresented && ( + {!children && !messageIsStreaming && isMessagesPresented && (
- -
-
-