diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index ea78b87..ad0c285 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -1,644 +1,598 @@ -import { - IconChevronDown, - IconClearAll, - IconSettings, -} from '@tabler/icons-react'; -import { - MutableRefObject, - memo, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from 'react'; - -import { useTranslation } from 'next-i18next'; - -import { DEFAULT_TEMPERATURE } from '@/utils/app/const'; -import { - saveConversation, - saveConversations, - updateConversation, -} from '@/utils/app/conversation'; -import { throttle } from '@/utils/data/throttle'; -import { ChatStream, ChatWithoutStream } from '@/utils/server'; - -import { ChatBody, Conversation, Message } from '@/types/chat'; -import { Plugin } from '@/types/plugin'; +import {memo, MutableRefObject, useCallback, useContext, useEffect, useRef, useState,} from 'react'; -import HomeContext from '@/pages/api/home/home.context'; +import {useTranslation} from 'next-i18next'; + +import {DEFAULT_TEMPERATURE} from '@/utils/app/const'; +import {saveConversation, saveConversations, updateConversation,} from '@/utils/app/conversation'; +import {throttle} from '@/utils/data/throttle'; +import {ChatStream, ChatWithoutStream} from '@/utils/server'; + +import {ChatBody, Conversation, Message} from '@/types/chat'; +import {Plugin} from '@/types/plugin'; -import Spinner from '../Spinner'; -import { ChatInput } from './ChatInput'; -import { ChatLoader } from './ChatLoader'; -import { ErrorMessageDiv } from './ErrorMessageDiv'; -import { MemoizedChatMessage } from './MemoizedChatMessage'; -import { ModelSelect } from './ModelSelect'; -import { SystemPrompt } from './SystemPrompt'; +import HomeContext from '@/pages/api/home/home.context'; +import {ChatInput} from './ChatInput'; +import {ChatLoader} from './ChatLoader'; +import {MemoizedChatMessage} from './MemoizedChatMessage'; +import {SystemPrompt} from './SystemPrompt'; import SystemNodes from './SystemNodes' import cn from 'classnames'; interface Props { - stopConversationRef: MutableRefObject; + stopConversationRef: MutableRefObject; } -export const Chat = memo(({ stopConversationRef }: Props) => { - const maxImg = 1; - - const { - state: { - selectedConversation, - conversations, - models, - isStream, - lightMode, - api, - apiKey, - pluginKeys, - modelError, - loading, - prompts, - defaultModelId, - }, - handleUpdateConversation, - dispatch: homeDispatch, - } = useContext(HomeContext); - const { t } = useTranslation('chat'); - - const [currentMessage, setCurrentMessage] = useState(); - const [imageCount, setImageCount] = useState(0); - 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 textListRef = useRef([]); - let text: string = ''; - let isFirst: boolean = true; - const queryDoneRef = useRef(false); - let showDone: boolean = true; - let updatedConversation: Conversation; - - async function delay(time: number | undefined) { - return new Promise((resolve) => setTimeout(resolve, time)); - } - - async function showText() { - if (showDone && selectedConversation && textListRef.current.length > 0) { - showDone = false; - for (let i = 0; i < textListRef.current.length; i++) { - text += textListRef.current.shift(); - text = text.replace(/<\|.*?\|>/g, ''); - if (isFirst) { - isFirst = false; - homeDispatch({ field: 'loading', value: false }); - const updatedMessages: Message[] = [ - ...updatedConversation.messages, - { role: 'assistant', content: text || '' }, - ]; - updatedConversation = { - ...updatedConversation, - messages: updatedMessages, - }; - homeDispatch({ - field: 'selectedConversation', - value: updatedConversation, - }); - } else { - const updatedMessages: Message[] = updatedConversation.messages.map( - (message, index) => { - if (index === updatedConversation.messages.length - 1) { - return { - ...message, - content: text, - }; - } - return message; - }, - ); - updatedConversation = { - ...updatedConversation, - messages: updatedMessages, - }; - saveConversation(updatedConversation); - homeDispatch({ - field: 'selectedConversation', - value: updatedConversation, - }); - const updatedConversations: Conversation[] = conversations.map( - (conversation) => { - if (conversation.id === selectedConversation.id) { - return updatedConversation; - } - return conversation; - }, - ); - if (updatedConversations.length === 0) { - updatedConversations.push(updatedConversation); - } - homeDispatch({ - field: 'conversations', - value: updatedConversations, - }); - saveConversations(updatedConversations); - if (!queryDoneRef.current) { - // await delay(textListRef.current.length > 10 ? 300 : 100); - } else { - // await delay(20) - } - } - } - showDone = true; +export const Chat = memo(({stopConversationRef}: Props) => { + const maxImg = 1; + + const { + state: { + selectedConversation, + conversations, + isStream, + api, + apiKey, + pluginKeys, + loading, + prompts, + selectedNode, + }, + handleUpdateConversation, + handleNewConversation, + dispatch: homeDispatch, + } = useContext(HomeContext); + + const {t} = useTranslation('chat'); + + const [currentMessage, setCurrentMessage] = useState(); + const [imageCount, setImageCount] = useState(0); + 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 textListRef = useRef([]); + let text: string = ''; + let isFirst: boolean = true; + const queryDoneRef = useRef(false); + let showDone: boolean = true; + let updatedConversation: Conversation; + + async function delay(time: number | undefined) { + return new Promise((resolve) => setTimeout(resolve, time)); } - } - const whileShowText = async () => { - while (textListRef.current.length !== 0) { - await showText(); + async function showText() { + if (showDone && selectedConversation && textListRef.current.length > 0) { + showDone = false; + for (let i = 0; i < textListRef.current.length; i++) { + text += textListRef.current.shift(); + text = text.replace(/<\|.*?\|>/g, ''); + if (isFirst) { + isFirst = false; + homeDispatch({field: 'loading', value: false}); + const updatedMessages: Message[] = [ + ...updatedConversation.messages, + {role: 'assistant', content: text || ''}, + ]; + updatedConversation = { + ...updatedConversation, + messages: updatedMessages, + }; + homeDispatch({ + field: 'selectedConversation', + value: updatedConversation, + }); + } else { + const updatedMessages: Message[] = updatedConversation.messages.map( + (message, index) => { + if (index === updatedConversation.messages.length - 1) { + return { + ...message, + content: text, + }; + } + return message; + }, + ); + updatedConversation = { + ...updatedConversation, + messages: updatedMessages, + }; + saveConversation(updatedConversation); + homeDispatch({ + field: 'selectedConversation', + value: updatedConversation, + }); + const updatedConversations: Conversation[] = conversations.map( + (conversation) => { + if (conversation.id === selectedConversation.id) { + return updatedConversation; + } + return conversation; + }, + ); + if (updatedConversations.length === 0) { + updatedConversations.push(updatedConversation); + } + homeDispatch({ + field: 'conversations', + value: updatedConversations, + }); + saveConversations(updatedConversations, selectedNode?.subdomain); + if (!queryDoneRef.current) { + // await delay(textListRef.current.length > 10 ? 300 : 100); + } else { + // await delay(20) + } + } + } + showDone = true; + } } - }; - - const handleSend = useCallback( - async (message: Message, deleteCount = 0, plugin: Plugin | null = null) => { - if (selectedConversation) { - let sandMessages: Message[]; - 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], - }; - if (selectedConversation.messages.length === 0) { - updatedConversation = { - ...updatedConversation, - name: - typeof message.content === 'string' - ? message.content - : message.content.find((item) => item.type === 'text') - ?.text || '', - }; - } + + const whileShowText = async () => { + while (textListRef.current.length !== 0) { + await showText(); } - sandMessages = updatedConversation.messages.map((item) => { - if (typeof item.content !== 'string') { - return { - ...item, - content: item.content.map((data) => { - if ( - data.type === 'image_url' && - data.image_url && - data.image_url.url.startsWith('data:image/') - ) { - return { - ...data, - image_url: { url: data.image_url.url.split(',')[1] }, - }; + }; + + const handleSend = useCallback( + async (message: Message, deleteCount = 0, plugin: Plugin | null = null) => { + if (selectedConversation) { + let sandMessages: Message[]; + if (deleteCount) { + const updatedMessages = [...selectedConversation.messages]; + for (let i = 0; i < deleteCount; i++) { + updatedMessages.pop(); + } + updatedConversation = { + ...selectedConversation, + messages: [...updatedMessages, message], + }; } else { - return data; - } - }), - }; - } else { - return item; - } - }); - homeDispatch({ - field: 'selectedConversation', - value: updatedConversation, - }); - homeDispatch({ field: 'loading', value: true }); - homeDispatch({ field: 'messageIsStreaming', value: true }); - const chatBody: ChatBody = { - model: updatedConversation.model, - messages: sandMessages, - key: apiKey, - prompt: updatedConversation.prompt, - temperature: updatedConversation.temperature, - }; - const controller = new AbortController(); - - try { - const { model, messages, key, prompt, temperature } = chatBody; - - let promptToSend = prompt; - - let temperatureToUse = temperature; - if (temperatureToUse == null) { - temperatureToUse = DEFAULT_TEMPERATURE; - } - - let messagesToSend: Message[] = []; - - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - messagesToSend = [message, ...messagesToSend]; - } - if (isStream) { - queryDoneRef.current = false; - let response: ReadableStream | null = await ChatStream( - model, - promptToSend, - temperatureToUse, - api, - key, - messagesToSend, - ); - if (response) { - let notFinishData = ''; - const decoder = new TextDecoder(); - const reader = response.getReader(); - while (!queryDoneRef.current) { - const { value, done } = await reader.read(); - if (done) { - queryDoneRef.current = true; - } - let chunkValue = decoder.decode(value); - if (chunkValue) { - const parts = chunkValue.split('\n\n'); - parts.forEach((part) => { - let isError = false; - part = part.trim(); - if (part.startsWith('data: ')) { - part = part.substring(6).trim(); + updatedConversation = { + ...selectedConversation, + messages: [...selectedConversation.messages, message], + }; + if (selectedConversation.messages.length === 0) { + updatedConversation = { + ...updatedConversation, + name: + typeof message.content === 'string' + ? message.content + : message.content.find((item) => item.type === 'text') + ?.text || '', + }; } - if (part === '[DONE]') { - queryDoneRef.current = true; + } + sandMessages = updatedConversation.messages.map((item) => { + if (typeof item.content !== 'string') { + return { + ...item, + content: item.content.map((data) => { + if ( + data.type === 'image_url' && + data.image_url && + data.image_url.url.startsWith('data:image/') + ) { + return { + ...data, + image_url: {url: data.image_url.url.split(',')[1]}, + }; + } else { + return data; + } + }), + }; } else { - if (!part.startsWith('{')) { - if (notFinishData) { - part = notFinishData + part; - notFinishData = ''; - } else { - isError = true; - } - } else if (!part.endsWith('}')) { - notFinishData = part; - isError = true; - } + return item; + } + }); + homeDispatch({ + field: 'selectedConversation', + value: updatedConversation, + }); + homeDispatch({field: 'loading', value: true}); + homeDispatch({field: 'messageIsStreaming', value: true}); + const chatBody: ChatBody = { + node: updatedConversation.node, + messages: sandMessages, + key: apiKey, + prompt: updatedConversation.prompt, + temperature: updatedConversation.temperature, + }; + const controller = new AbortController(); + + try { + const {node, messages, key, prompt, temperature} = chatBody; + + let promptToSend = prompt; + + let temperatureToUse = temperature; + if (temperatureToUse == null) { + temperatureToUse = DEFAULT_TEMPERATURE; } - if (!isError && !queryDoneRef.current) { - try { - if (part) { - const obj = JSON.parse(part); - if (obj && obj['choices']) { - obj['choices'].forEach( - (obj1: { [x: string]: { [x: string]: any } }) => { - if (obj1) { - if ( - obj1['delta'] && - obj1['delta']['content'] - ) { - textListRef.current.push( - obj1['delta']['content'], - ); - } + let messagesToSend: Message[] = []; + + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + messagesToSend = [message, ...messagesToSend]; + } + if (isStream) { + queryDoneRef.current = false; + let response: ReadableStream | null = await ChatStream( + node, + promptToSend, + temperatureToUse, + api, + key, + messagesToSend, + ); + if (response) { + let notFinishData = ''; + const decoder = new TextDecoder(); + const reader = response.getReader(); + while (!queryDoneRef.current) { + const {value, done} = await reader.read(); + if (done) { + queryDoneRef.current = true; + } + let chunkValue = decoder.decode(value); + if (chunkValue) { + const parts = chunkValue.split('\n\n'); + parts.forEach((part) => { + let isError = false; + part = part.trim(); + if (part.startsWith('data: ')) { + part = part.substring(6).trim(); + } + if (part === '[DONE]') { + queryDoneRef.current = true; + } else { + if (!part.startsWith('{')) { + if (notFinishData) { + part = notFinishData + part; + notFinishData = ''; + } else { + isError = true; + } + } else if (!part.endsWith('}')) { + notFinishData = part; + isError = true; + } + } + + if (!isError && !queryDoneRef.current) { + try { + if (part) { + const obj = JSON.parse(part); + if (obj && obj['choices']) { + obj['choices'].forEach( + (obj1: { [x: string]: { [x: string]: any } }) => { + if (obj1) { + if ( + obj1['delta'] && + obj1['delta']['content'] + ) { + textListRef.current.push( + obj1['delta']['content'], + ); + } + } + }, + ); + } + if (obj && obj['usage']) { + console.log("prompt:", obj['usage']['prompt_tokens']); + console.log("completion:", obj['usage']['completion_tokens']); + console.log("total:", obj['usage']['total_tokens']); + } + } + } catch (e) { + console.log('error JSON', part); + } + } + }); + } + if (showDone) { + whileShowText(); } - }, + } + homeDispatch({field: 'messageIsStreaming', value: false}); + controller.abort(); + } else { + homeDispatch({field: 'loading', value: false}); + homeDispatch({field: 'messageIsStreaming', value: false}); + } + } else { + let response = await ChatWithoutStream( + node, + promptToSend, + temperatureToUse, + api, + key, + messagesToSend, + ); + if (response) { + const data = response.choices[0].message; + const updatedMessages: Message[] = [ + ...updatedConversation.messages, + {role: 'assistant', content: data.content}, + ]; + updatedConversation = { + ...updatedConversation, + messages: updatedMessages, + }; + + homeDispatch({ + field: 'selectedConversation', + value: updateConversation, + }); + saveConversation(updatedConversation); + const updatedConversations: Conversation[] = conversations.map( + (conversation) => { + if (conversation.id === selectedConversation.id) { + return updatedConversation; + } + return conversation; + }, ); - } - if(obj && obj['usage']){ - console.log("prompt:",obj['usage']['prompt_tokens']); - console.log("completion:",obj['usage']['completion_tokens']); - console.log("total:",obj['usage']['total_tokens']); - } + if (updatedConversations.length === 0) { + updatedConversations.push(updatedConversation); + } + homeDispatch({ + field: 'conversations', + value: updatedConversations, + }); + saveConversations(updatedConversations, selectedNode?.subdomain); + homeDispatch({field: 'loading', value: false}); + homeDispatch({field: 'messageIsStreaming', value: false}); + homeDispatch({ + field: 'selectedConversation', + value: updatedConversation, + }); } - } catch (e) { - console.log('error JSON', part); - } } - }); - } - if (showDone) { - whileShowText(); + } catch (error) { + console.error(error); } - } - homeDispatch({ field: 'messageIsStreaming', value: false }); - controller.abort(); - } else { - homeDispatch({ field: 'loading', value: false }); - homeDispatch({ field: 'messageIsStreaming', value: false }); } - } else { - let response = await ChatWithoutStream( - model, - promptToSend, - temperatureToUse, - api, - key, - messagesToSend, - ); - if (response) { - const data = response.choices[0].message; - const updatedMessages: Message[] = [ - ...updatedConversation.messages, - { role: 'assistant', content: data.content }, - ]; - updatedConversation = { - ...updatedConversation, - messages: updatedMessages, - }; - - homeDispatch({ - field: 'selectedConversation', - value: updateConversation, - }); - saveConversation(updatedConversation); - const updatedConversations: Conversation[] = conversations.map( - (conversation) => { - 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 }); - homeDispatch({ - field: 'selectedConversation', - value: updatedConversation, - }); + }, + [ + api, + apiKey, + conversations, + pluginKeys, + isStream, + selectedConversation, + stopConversationRef, + ], + ); + + 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); } - } - } catch (error) { - console.error(error); } - } - }, - [ - api, - apiKey, - conversations, - pluginKeys, - isStream, - selectedConversation, - stopConversationRef, - ], - ); - - 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?')) && - selectedConversation - ) { - handleUpdateConversation(selectedConversation, { - key: 'messages', - value: [], - }); - } - }; + }; - const scrollDown = () => { - if (autoScrollEnabled) { - messagesEndRef.current?.scrollIntoView(true); - } - }; - const throttledScrollDown = throttle(scrollDown, 250); - - // useEffect(() => { - // if (currentMessage) { - // handleSend(currentMessage); - // homeDispatch({ field: 'currentMessage', value: undefined }); - // } - // }, [currentMessage]); - - useEffect(() => { - throttledScrollDown(); - if (selectedConversation) { - setImageCount( - selectedConversation?.messages.reduce((total, currentItem) => { - if (Array.isArray(currentItem.content)) { - total += currentItem.content.reduce((acc, item) => { - if (item.type === 'image_url' && item['image_url']) { - acc++; - } - return acc; - }, 0); - } - return total; - }, 0), - ); - setCurrentMessage( - selectedConversation.messages[selectedConversation.messages.length - 2], - ); - } - }, [selectedConversation, throttledScrollDown]); - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => { - setAutoScrollEnabled(entry.isIntersecting); - if (entry.isIntersecting) { - textareaRef.current?.focus(); + 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?')) && + selectedConversation + ) { + handleUpdateConversation(selectedConversation, { + key: 'messages', + value: [], + }); } - }, - { - root: null, - threshold: 0.5, - }, - ); - const messagesEndElement = messagesEndRef.current; - if (messagesEndElement) { - observer.observe(messagesEndElement); - } - return () => { - if (messagesEndElement) { - observer.unobserve(messagesEndElement); - } }; - }, [messagesEndRef]); - - return ( -
- {!(apiKey || api) ? ( -
-
- Welcome to GaiaNet-AI Chat -
-
-
- GaiaNet-AI Chat allows you to plug in your API key to use this UI - with their API. -
-
- It is only used to communicate - with their API. -
-
-
- ) : ( - // modelError ? ( - // - // ) : - <> -
-
- {/*
+ + const scrollDown = () => { + if (autoScrollEnabled) { + messagesEndRef.current?.scrollIntoView(true); + } + }; + const throttledScrollDown = throttle(scrollDown, 250); + + // useEffect(() => { + // if (currentMessage) { + // handleSend(currentMessage); + // homeDispatch({ field: 'currentMessage', value: undefined }); + // } + // }, [currentMessage]); + + useEffect(() => { + throttledScrollDown(); + if (selectedConversation) { + setImageCount( + selectedConversation?.messages.reduce((total, currentItem) => { + if (Array.isArray(currentItem.content)) { + total += currentItem.content.reduce((acc, item) => { + if (item.type === 'image_url' && item['image_url']) { + acc++; + } + return acc; + }, 0); + } + return total; + }, 0), + ); + 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 ( +
+
+
+ {/*

{selectedConversation?.model?.id || defaultModelId}

*/} - - - {selectedConversation?.promptState !== 2 && - selectedConversation && ( - - handleUpdateConversation(selectedConversation, { - key: 'prompt', - value: prompt, - }) - } - /> - )} -
- {selectedConversation?.messages?.length !== 0 && ( - <> - {/*
- {t('Model')}: {selectedConversation?.model?.id} | {t('Temp')}:{' '} - {selectedConversation?.temperature} | - - + + + {selectedConversation?.promptState !== 2 && selectedConversation && ( + + handleUpdateConversation(selectedConversation, { + key: 'prompt', + value: prompt, + }) + } + /> + )}
- {showSettings && ( -
-
- + {!selectedConversation && ( +
+ +
+ Welcome to Gaia AI Chat +
+
+ Gaia AI Chat lets you interact with various open-source LLMs through an easy-to-use + interface. +
+
+ +
+
+ )} + {selectedConversation?.messages?.length !== 0 && ( +
+ + {selectedConversation?.messages && + selectedConversation.messages.map((message, index) => ( + { + if (currentMessage) { + handleSend( + currentMessage, + selectedConversation.messages.length - index + 1, + null, + ); + } + }} + onEdit={(editedMessage) => { + setCurrentMessage(editedMessage); + // discard edited message and the ones that come after then resend + handleSend( + editedMessage, + selectedConversation?.messages.length - index, + ); + }} + /> + ))} + + {loading && }
-
- )} */} - - - {selectedConversation?.messages && - selectedConversation.messages.map((message, index) => ( - { + )} +
+ {(selectedConversation?.promptState !== 1 || + selectedConversation?.prompt !== '') && ( + { + setCurrentMessage(message); + handleSend(message, 0, plugin); + }} + onScrollDownClick={handleScrollDown} + onRegenerate={() => { if (currentMessage) { - handleSend( - currentMessage, - selectedConversation.messages.length - index + 1, - null, - ); + handleSend(currentMessage, 2, null); } - }} - onEdit={(editedMessage) => { - setCurrentMessage(editedMessage); - // discard edited message and the ones that come after then resend - handleSend( - editedMessage, - selectedConversation?.messages.length - index, - ); - }} - /> - ))} - - {loading && } -
- )} -
- {(selectedConversation?.promptState !== 1 || - selectedConversation?.prompt !== '') && ( - { - setCurrentMessage(message); - handleSend(message, 0, plugin); - }} - onScrollDownClick={handleScrollDown} - onRegenerate={() => { - if (currentMessage) { - handleSend(currentMessage, 2, null); - } - }} - showScrollDownButton={showScrollDownButton} - /> - )} - - )} -
- ); +
+ ); }); Chat.displayName = 'Chat'; diff --git a/components/Chat/ChatInput.tsx b/components/Chat/ChatInput.tsx index f564d8d..4e1e9fd 100644 --- a/components/Chat/ChatInput.tsx +++ b/components/Chat/ChatInput.tsx @@ -1,615 +1,573 @@ /* eslint-disable @next/next/no-img-element */ -import { - IconArrowDown, - IconBolt, - IconSend, - IconUpload, - IconWriting, -} from '@tabler/icons-react'; -import React, { - KeyboardEvent, - MutableRefObject, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from 'react'; - -import { useTranslation } from 'next-i18next'; - -import { Content, Message } from '@/types/chat'; -import { Plugin } from '@/types/plugin'; -import { Prompt } from '@/types/prompt'; +import {IconArrowDown, IconBolt,} from '@tabler/icons-react'; +import React, {KeyboardEvent, MutableRefObject, useCallback, useContext, useEffect, useRef, useState,} from 'react'; + +import {useTranslation} from 'next-i18next'; + +import {Content, Message} from '@/types/chat'; +import {Plugin} from '@/types/plugin'; +import {Prompt} from '@/types/prompt'; import HomeContext from '@/pages/api/home/home.context'; -import { PromptList } from './PromptList'; -import { VariableModal } from './VariableModal'; +import {PromptList} from './PromptList'; +import {VariableModal} from './VariableModal'; interface Props { - onSend: (message: Message, plugin: Plugin | null) => void; - maxImg: number; - onRegenerate: () => void; - onScrollDownClick: () => void; - stopConversationRef: MutableRefObject; - textareaRef: MutableRefObject; - showScrollDownButton: boolean; + onSend: (message: Message, plugin: Plugin | null) => void; + maxImg: number; + onRegenerate: () => void; + onScrollDownClick: () => void; + stopConversationRef: MutableRefObject; + textareaRef: MutableRefObject; + showScrollDownButton: boolean; } export const ChatInput = ({ - onSend, - onRegenerate, - maxImg, - onScrollDownClick, - stopConversationRef, - textareaRef, - showScrollDownButton, -}: Props) => { - const { t } = useTranslation('chat'); - - const { - state: { selectedConversation, messageIsStreaming, prompts }, - - dispatch: homeDispatch, - } = useContext(HomeContext); - - 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 [plugin, setPlugin] = useState(null); - const [urlInputShow, setUrlInputShow] = useState(false); - const [inputUrl, setInputUrl] = useState(''); - - const promptListRef = useRef(null); - - const filteredPrompts = prompts.filter((prompt) => - prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), - ); - - const handleChange = (e: React.ChangeEvent) => { - const value = e.target.value; - const maxLength = selectedConversation?.model.maxLength; - - 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); - }; - - const handleSend = () => { - let thisList: string[] = imageSrcList; - if (urlInputShow) { - thisList = uploadInput(); - } - - if (messageIsStreaming) { - return; - } - - if (!content) { - alert(t('Please enter a message')); - return; - } - - let finalContent: string | Content[] = content; - - if (thisList.length > 0) { - finalContent = [{ type: 'text', text: content }]; - thisList.forEach((item) => { - if (Array.isArray(finalContent)) { - finalContent.push({ - type: 'image_url', - image_url: { url: item }, - }); + onSend, + onRegenerate, + maxImg, + onScrollDownClick, + stopConversationRef, + textareaRef, + showScrollDownButton, + }: Props) => { + const {t} = useTranslation('chat'); + + const { + state: {selectedConversation, messageIsStreaming, prompts}, + + dispatch: homeDispatch, + } = useContext(HomeContext); + + 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 [plugin, setPlugin] = useState(null); + const [urlInputShow, setUrlInputShow] = useState(false); + const [inputUrl, setInputUrl] = useState(''); + + const promptListRef = useRef(null); + + const filteredPrompts = prompts.filter((prompt) => + prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()), + ); + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setContent(value); + updatePromptListVisibility(value); + }; + + const handleSend = () => { + let thisList: string[] = imageSrcList; + if (urlInputShow) { + thisList = uploadInput(); + } + + if (messageIsStreaming) { + return; + } + + if (!content) { + alert(t('Please enter a message')); + return; + } + + let finalContent: string | Content[] = content; + + if (thisList.length > 0) { + finalContent = [{type: 'text', text: content}]; + thisList.forEach((item) => { + if (Array.isArray(finalContent)) { + finalContent.push({ + type: 'image_url', + image_url: {url: item}, + }); + } + }); + } + + onSend({role: 'user', content: finalContent}, plugin); + setContent(''); + setImageSrcList([]); + setPlugin(null); + + if (window.innerWidth < 640 && textareaRef && textareaRef.current) { + textareaRef.current.blur(); + } + }; + + const handleStopConversation = () => { + stopConversationRef.current = true; + setTimeout(() => { + stopConversationRef.current = false; + }, 1000); + }; + + 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; + return mobileRegex.test(userAgent); + }; + + const handleInitModal = () => { + const selectedPrompt = filteredPrompts[activePromptIndex]; + if (selectedPrompt) { + setContent((prevContent) => { + return prevContent?.replace(/\/\w*$/, selectedPrompt.content); + }); + handlePromptSelect(selectedPrompt); } - }); - } - - onSend({ role: 'user', content: finalContent }, plugin); - setContent(''); - setImageSrcList([]); - setPlugin(null); - - if (window.innerWidth < 640 && textareaRef && textareaRef.current) { - textareaRef.current.blur(); - } - }; - - const handleStopConversation = () => { - stopConversationRef.current = true; - setTimeout(() => { - stopConversationRef.current = false; - }, 1000); - }; - - 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; - return mobileRegex.test(userAgent); - }; - - const handleInitModal = () => { - const selectedPrompt = filteredPrompts[activePromptIndex]; - if (selectedPrompt) { - setContent((prevContent) => { - return prevContent?.replace(/\/\w*$/, selectedPrompt.content); - }); - handlePromptSelect(selectedPrompt); - } - setShowPromptList(false); - }; - - const handleKeyDown = (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); - } - }; - - const parseVariables = (content: string) => { - const regex = /{{(.*?)}}/g; - const foundVariables = []; - let match; - - while ((match = regex.exec(content)) !== null) { - foundVariables.push(match[1]); - } - - return foundVariables; - }; - - const updatePromptListVisibility = useCallback((text: string) => { - const match = text.match(/\/\w*$/); - - if (match) { - setShowPromptList(true); - setPromptInputValue(match[0].slice(1)); - } else { - setShowPromptList(false); - setPromptInputValue(''); - } - }, []); - - const handlePromptSelect = (prompt: Prompt) => { - const parsedVariables = parseVariables(prompt.content); - setVariables(parsedVariables); - - if (parsedVariables.length > 0) { - setIsModalVisible(true); - } else { - setContent((prevContent) => { - return prevContent?.replace(/\/\w*$/, prompt.content); - }); - updatePromptListVisibility(prompt.content); - } - }; - - const handleSubmit = (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(); - } - }; - - useEffect(() => { - if (promptListRef.current) { - promptListRef.current.scrollTop = activePromptIndex * 30; - } - }, [activePromptIndex]); - - useEffect(() => { - if (textareaRef && textareaRef.current) { - textareaRef.current.style.height = 'inherit'; - textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`; - textareaRef.current.style.overflow = `${ - textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden' - }`; - } - }, [content]); - - useEffect(() => { - const handleOutsideClick = (e: MouseEvent) => { - if ( - promptListRef.current && - !promptListRef.current.contains(e.target as Node) - ) { setShowPromptList(false); - } }; - window.addEventListener('click', handleOutsideClick); + const handleKeyDown = (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); + } + }; + + const parseVariables = (content: string) => { + const regex = /{{(.*?)}}/g; + const foundVariables = []; + let match; + + while ((match = regex.exec(content)) !== null) { + foundVariables.push(match[1]); + } - return () => { - window.removeEventListener('click', handleOutsideClick); + return foundVariables; }; - }, []); - - const [imageSrcList, setImageSrcList] = useState([]); - - const handleImageChange = (e: React.ChangeEvent) => { - const files = e.target.files; - if (files) { - const newImages: string[] = []; - const readerPromises: Promise[] = []; - for (let i = 0; i < files.length; i++) { - const file = files[i]; - const reader = new FileReader(); - readerPromises.push( - new Promise((resolve) => { - reader.onloadend = () => { - newImages.push(reader.result as string); - resolve(); - }; - }), - ); - reader.readAsDataURL(file); - } - Promise.all(readerPromises).then(() => { - addImg(newImages); - }); - } - }; - - const addImg = (imageList: string[]) => { - let list: string[]; - if (imageSrcList.length + imageList.length <= maxImg) { - list = [...imageSrcList, ...imageList]; - } else { - const newList = [...imageList, ...imageList]; - list = newList.slice(-maxImg); - } - setImageSrcList(list); - return list; - }; - - const handleImageRemove = (index: number) => { - setImageSrcList((prevImages) => { - const newImages = [...prevImages]; - newImages.splice(index, 1); - return newImages; - }); - }; - - const uploadInput = () => { - let list: string[] = []; - setUrlInputShow(false); - if (inputUrl) { - list = addImg([inputUrl]); - setInputUrl(''); - } - return list; - }; - - return ( -
- -
-
0 ? ' mt-4' : '') - } - > -
- - {urlInputShow ? ( - <> - { - setInputUrl(e.target.value); - }} - id="imgUrlInputBox" - autoFocus - style={{ height: '2.25rem' }} - className="px-2 h-full bg-[#ffffff] border-l border-t border-b outline-none rounded-l-lg" - /> - - - ) : ( - - )} -
- For Llava series models, please submit an image URL for processing - first. -
-
-
-
-
- {/*{messageIsStreaming && (*/} - {/* */} - {/* {t('Stop Generating')}*/} - {/* */} - {/*)}*/} - - {/*{!messageIsStreaming &&*/} - {/* selectedConversation &&*/} - {/* selectedConversation.messages.length > 0 && (*/} - {/* */} - {/* {t('Regenerate response')}*/} - {/* */} - {/*)}*/} - -
- {/* setShowPluginSelect(!showPluginSelect)}*/} - {/* // onKeyDown={(e) => {}}*/} - {/*>*/} -
- {/*{plugin ? : }*/} - -
- {/**/} - - {/*{showPluginSelect && (*/} - {/*
*/} - {/* {*/} - {/* if (e.key === 'Escape') {*/} - {/* e.preventDefault();*/} - {/* setShowPluginSelect(false);*/} - {/* textareaRef.current?.focus();*/} - {/* }*/} - {/* }}*/} - {/* onPluginChange={(plugin: Plugin) => {*/} - {/* setPlugin(plugin);*/} - {/* setShowPluginSelect(false);*/} - - {/* if (textareaRef && textareaRef.current) {*/} - {/* textareaRef.current.focus();*/} - {/* }*/} - {/* }}*/} - {/* />*/} - {/*
*/} - {/*)}*/} - -