diff --git a/.gitignore b/.gitignore index fa70c3c4c4..e201974879 100644 --- a/.gitignore +++ b/.gitignore @@ -47,7 +47,6 @@ bower_components/ .env cache.json api/data/ -.eslintrc.js owner.yml archive .vscode/settings.json diff --git a/api/app/titleConvo.js b/api/app/titleConvo.js index 88e8c75074..e37fcb3b0c 100644 --- a/api/app/titleConvo.js +++ b/api/app/titleConvo.js @@ -1,4 +1,5 @@ const { Configuration, OpenAIApi } = require('openai'); +const _ = require('lodash'); const proxyEnvToAxiosProxy = (proxyString) => { if (!proxyString) return null; @@ -11,29 +12,48 @@ const proxyEnvToAxiosProxy = (proxyString) => { port: port ? parseInt(port) : undefined, auth: username && password ? { username, password } : undefined }; - - return proxyConfig -} -const titleConvo = async ({ message, response, model }) => { - const configuration = new Configuration({ - apiKey: process.env.OPENAI_KEY - }); - const openai = new OpenAIApi(configuration); - const completion = await openai.createChatCompletion({ - model: 'gpt-3.5-turbo', - messages: [ - { - role: 'system', - content: - 'You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user in title case, using the same language.' - }, - { role: 'user', content: `In 5 words or less, summarize the conversation below with a title in title case using the language the user writes in. Don't refer to the participants of the conversation by name. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${message}"\n\n${model}: "${response}"\n\nTitle: ` }, - ] - }, { proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) }); - - //eslint-disable-next-line - return completion.data.choices[0].message.content.replace(/["\.]/g, ''); + return proxyConfig; }; -module.exports = titleConvo; +const titleConvo = async ({ model, text, response }) => { + let title = 'New Chat'; + try { + const configuration = new Configuration({ + apiKey: process.env.OPENAI_KEY + }); + const openai = new OpenAIApi(configuration); + const completion = await openai.createChatCompletion( + { + model: 'gpt-3.5-turbo', + messages: [ + { + role: 'system', + content: + 'You are a title-generator with one job: giving a conversation, detect the language and titling the conversation provided by a user in title case, using the same language.' + }, + { + role: 'user', + content: `In 5 words or less, summarize the conversation below with a title in title case using the language the user writes in. Don't refer to the participants of the conversation by name. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${text}"\n\n${model}: "${JSON.stringify( + response?.text + )}"\n\nTitle: ` + } + ] + }, + { proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) } + ); + + //eslint-disable-next-line + title = completion.data.choices[0].message.content.replace(/["\.]/g, ''); + } catch (e) { + console.error(e); + console.log('There was an issue generating title, see error above'); + } + + console.log('CONVERSATION TITLE', title); + return title; +}; + +const throttledTitleConvo = _.throttle(titleConvo, 1000); + +module.exports = throttledTitleConvo; diff --git a/api/server/routes/ask.js b/api/server/routes/ask.js index 265672db8b..ad2a7178c2 100644 --- a/api/server/routes/ask.js +++ b/api/server/routes/ask.js @@ -3,13 +3,7 @@ const crypto = require('crypto'); const router = express.Router(); const askBing = require('./askBing'); const askSydney = require('./askSydney'); -const { - titleConvo, - askClient, - browserClient, - customClient - // detectCode -} = require('../../app/'); +const { titleConvo, askClient, browserClient, customClient } = require('../../app/'); const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const { getMessages } = require('../../models/Message'); @@ -42,15 +36,6 @@ router.post('/', async (req, res) => { ...convo }); - // if (model === 'chatgptCustom' && !chatGptLabel && conversationId) { - // const convo = await getConvo({ conversationId }); - // if (convo) { - // console.log('found convo for custom gpt', { convo }) - // chatGptLabel = convo.chatGptLabel; - // promptPrefix = convo.promptPrefix; - // } - // } - await saveMessage(userMessage); await saveConvo({ ...userMessage, model, ...convo }); @@ -94,17 +79,6 @@ router.post('/regenerate', async (req, res) => { res }); } else return handleError(res, { text: 'Parent message not found' }); - - // if (model === 'chatgptCustom' && !chatGptLabel && conversationId) { - // const convo = await getConvo({ conversationId }); - // if (convo) { - // console.log('found convo for custom gpt', { convo }) - // chatGptLabel = convo.chatGptLabel; - // promptPrefix = convo.promptPrefix; - // } - // } - - // await saveConvo({ ...userMessage, model, chatGptLabel, promptPrefix }); }); const ask = async ({ @@ -188,7 +162,7 @@ const ask = async ({ gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model; gptResponse.model = model; // gptResponse.final = true; - gptResponse.text = await handleText(gptResponse.text); + gptResponse.text = await handleText(gptResponse); if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') { gptResponse.chatGptLabel = convo.chatGptLabel; @@ -212,13 +186,7 @@ const ask = async ({ res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { - const title = await titleConvo({ - model, - message: text, - response: JSON.stringify(gptResponse?.text) - }); - - console.log('CONVERSATION TITLE', title); + const title = await titleConvo({ model, text, response: gptResponse }); await saveConvo({ conversationId, diff --git a/api/server/routes/askBing.js b/api/server/routes/askBing.js index 066b64faec..cfbac77185 100644 --- a/api/server/routes/askBing.js +++ b/api/server/routes/askBing.js @@ -1,10 +1,9 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { titleConvo, getCitations, citeText, askBing } = require('../../app/'); +const { titleConvo, askBing } = require('../../app/'); const { saveMessage, getConvoTitle, saveConvo } = require('../../models'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); -const citationRegex = /\[\^\d+?\^]/g; router.post('/', async (req, res) => { const { @@ -129,12 +128,7 @@ const ask = async ({ response.parentMessageId = overrideParentMessageId || response.parentMessageId || userMessageId; - const links = getCitations(response); - response.text = - citeText(response) + - (links?.length > 0 && hasCitations ? `\n${links}` : ''); - response.text = await handleText(response.text); - + response.text = await handleText(response, true); await saveMessage(response); await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo }); sendMessage(res, { @@ -146,13 +140,7 @@ const ask = async ({ res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { - const title = await titleConvo({ - model, - message: text, - response: JSON.stringify(response?.text) - }); - - console.log('CONVERSATION TITLE', title); + const title = await titleConvo({ model, text, response }); await saveConvo({ conversationId, diff --git a/api/server/routes/askSydney.js b/api/server/routes/askSydney.js index 03e3479be4..00f287fa7b 100644 --- a/api/server/routes/askSydney.js +++ b/api/server/routes/askSydney.js @@ -1,10 +1,9 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { titleConvo, getCitations, citeText, askSydney } = require('../../app/'); +const { titleConvo, askSydney } = require('../../app/'); const { saveMessage, saveConvo, getConvoTitle } = require('../../models'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); -const citationRegex = /\[\^\d+?\^]/g; router.post('/', async (req, res) => { const { @@ -97,7 +96,6 @@ const ask = async ({ console.log('SYDNEY RESPONSE', response); // console.dir(response, { depth: null }); - const hasCitations = response.response.match(citationRegex)?.length > 0; userMessage.conversationSignature = convo.conversationSignature || response.conversationSignature; @@ -125,12 +123,6 @@ const ask = async ({ response.parentMessageId = overrideParentMessageId || response.parentMessageId || userMessageId; - const links = getCitations(response); - response.text = - citeText(response) + - (links?.length > 0 && hasCitations ? `\n${links}` : ''); - response.text = await handleText(response.text); - // Save user message userMessage.conversationId = response.conversationId || conversationId; await saveMessage(userMessage); @@ -146,6 +138,7 @@ const ask = async ({ }); conversationId = userMessage.conversationId; + response.text = await handleText(response, true); // Save sydney response & convo, then send await saveMessage(response); await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo }); @@ -158,13 +151,7 @@ const ask = async ({ res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { - const title = await titleConvo({ - model, - message: text, - response: JSON.stringify(response?.text) - }); - - console.log('CONVERSATION TITLE', title); + const title = await titleConvo({ model, text, response }); await saveConvo({ conversationId, diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 9a5f71cee3..99fb824708 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,41 +1,12 @@ const express = require('express'); const router = express.Router(); -const { titleConvo } = require('../../app/'); -const { getConvo, saveConvo, getConvoTitle } = require('../../models'); const { getConvosByPage, deleteConvos, updateConvo } = require('../../models/Conversation'); -const { getMessages } = require('../../models/Message'); router.get('/', async (req, res) => { const pageNumber = req.query.pageNumber || 1; res.status(200).send(await getConvosByPage(pageNumber)); }); -router.post('/gen_title', async (req, res) => { - const { conversationId } = req.body.arg; - - const convo = await getConvo(conversationId) - const firstMessage = (await getMessages({ conversationId }))[0] - const secondMessage = (await getMessages({ conversationId }))[1] - - // if (convo.title == 'New Chat') { - // const title = await titleConvo({ - // model: convo?.model, - // message: firstMessage?.text, - // response: JSON.stringify(secondMessage?.text || '') - // }); - - // console.log('CONVERSATION TITLE', title); - - // await saveConvo({ - // conversationId, - // title - // }) - - // res.status(200).send(title); - // } else - return res.status(200).send(convo.title); -}); - router.post('/clear', async (req, res) => { let filter = {}; const { conversationId } = req.body.arg; diff --git a/api/server/routes/handlers.js b/api/server/routes/handlers.js index d3e613bccc..3ba4e81243 100644 --- a/api/server/routes/handlers.js +++ b/api/server/routes/handlers.js @@ -1,6 +1,8 @@ -const { citeText, detectCode } = require('../../app/'); const _ = require('lodash'); const sanitizeHtml = require('sanitize-html'); +const citationRegex = /\[\^\d+?\^]/g; +const { getCitations, citeText, detectCode } = require('../../app/'); +// const htmlTagRegex = /(<\/?\s*[a-zA-Z]*\s*(?:\s+[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*(?:\/?)>|<\s*[a-zA-Z]+\s*(?:\s+[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*(?:\/?>|<\/?>))/g; const handleError = (res, message) => { res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`); @@ -25,8 +27,13 @@ const createOnProgress = () => { if (tokens.match(/^\n/)) { tokens = tokens.replace(/^\n/, ''); } - // if (tokens.includes('```')) { - // tokens = sanitizeHtml(tokens); + + // const htmlTags = tokens.match(htmlTagRegex); + // if (tokens.includes('```') && htmlTags && htmlTags.length > 0) { + // htmlTags.forEach((tag) => { + // const sanitizedTag = sanitizeHtml(tag); + // tokens = tokens.replaceAll(tag, sanitizedTag); + // }); // } if (bing) { @@ -45,15 +52,29 @@ const createOnProgress = () => { return onProgress; }; -const handleText = async (input) => { - let text = input; +const handleText = async (response, bing = false) => { + let { text } = response; text = await detectCode(text); - // if (text.includes('```')) { - // text = sanitizeHtml(text); - // text = text.replaceAll(') =>', ') =>'); + response.text = text; + + if (bing) { + // const hasCitations = response.response.match(citationRegex)?.length > 0; + const links = getCitations(response); + if (response.text.match(citationRegex)?.length > 0) { + text = citeText(response); + } + text += links?.length > 0 ? `\n${links}` : ''; + } + + // const htmlTags = text.match(htmlTagRegex); + // if (text.includes('```') && htmlTags && htmlTags.length > 0) { + // htmlTags.forEach((tag) => { + // const sanitizedTag = sanitizeHtml(tag); + // text = text.replaceAll(tag, sanitizedTag); + // }); // } return text; }; -module.exports = { handleError, sendMessage, createOnProgress, handleText }; \ No newline at end of file +module.exports = { handleError, sendMessage, createOnProgress, handleText }; diff --git a/client/src/App.jsx b/client/src/App.jsx index 9bab1f5939..5f1446fd0b 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -8,7 +8,7 @@ import useDocumentTitle from '~/hooks/useDocumentTitle'; import { useSelector } from 'react-redux'; const App = () => { - const { messages } = useSelector((state) => state.messages); + const { messages, messageTree } = useSelector((state) => state.messages); const { title } = useSelector((state) => state.convo); const { conversationId } = useSelector((state) => state.convo); const [ navVisible, setNavVisible ]= useState(false) @@ -25,6 +25,7 @@ const App = () => { ) : ( )} diff --git a/client/src/components/Main/TextChat.jsx b/client/src/components/Main/TextChat.jsx index 3671833ef0..e0363cd8b1 100644 --- a/client/src/components/Main/TextChat.jsx +++ b/client/src/components/Main/TextChat.jsx @@ -157,15 +157,17 @@ export default function TextChat({ messages }) { const message = text.trim(); const sender = model === 'chatgptCustom' ? chatGptLabel : model; let parentMessageId = convo.parentMessageId || '00000000-0000-0000-0000-000000000000'; - if (resetConvo(messages, sender)) { + let currentMessages = messages; + if (resetConvo(currentMessages, sender)) { parentMessageId = '00000000-0000-0000-0000-000000000000'; dispatch(setNewConvo()); + currentMessages = []; } const currentMsg = { sender: 'User', text: message, current: true, isCreatedByUser: true, parentMessageId , messageId: fakeMessageId }; const initialResponse = { sender, text: '', parentMessageId: fakeMessageId, submitting: true }; dispatch(setSubmitState(true)); - dispatch(setMessages([...messages, currentMsg, initialResponse])); + dispatch(setMessages([...currentMessages, currentMsg, initialResponse])); dispatch(setText('')); const submission = { @@ -177,7 +179,7 @@ export default function TextChat({ messages }) { chatGptLabel, promptPrefix, }, - messages, + messages: currentMessages, currentMsg, initialResponse, sender, diff --git a/client/src/components/Messages/Message.jsx b/client/src/components/Messages/Message.jsx index cfda8e524f..622b907454 100644 --- a/client/src/components/Messages/Message.jsx +++ b/client/src/components/Messages/Message.jsx @@ -1,9 +1,9 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import TextWrapper from './TextWrapper'; +import MultiMessage from './MultiMessage'; import { useSelector, useDispatch } from 'react-redux'; import HoverButtons from './HoverButtons'; import SiblingSwitch from './SiblingSwitch'; -import Spinner from '../svg/Spinner'; import { setError } from '~/store/convoSlice'; import { setMessages } from '~/store/messageSlice'; import { setSubmitState, setSubmission } from '~/store/submitSlice'; @@ -11,42 +11,6 @@ import { setText } from '~/store/textSlice'; import { setConversation } from '../../store/convoSlice'; import { getIconOfModel } from '../../utils'; -const MultiMessage = ({ - messageList, - messages, - scrollToBottom, - currentEditId, - setCurrentEditId -}) => { - const [siblingIdx, setSiblingIdx] = useState(0) - - const setSiblingIdxRev = (value) => { - setSiblingIdx(messageList?.length - value - 1) - } - - if (!messageList?.length) return null; - - if (siblingIdx >= messageList?.length) { - setSiblingIdx(0) - return null - } - - return -} - -export { MultiMessage }; - export default function Message({ message, messages, @@ -57,21 +21,28 @@ export default function Message({ siblingCount, setSiblingIdx }) { - const { isSubmitting, model, chatGptLabel, promptPrefix } = useSelector((state) => state.submit); + const { isSubmitting, model, chatGptLabel, promptPrefix } = useSelector( + (state) => state.submit + ); const [abortScroll, setAbort] = useState(false); - const { sender, text, isCreatedByUser, error, submitting } = message - const textEditor = useRef(null) + const { sender, text, isCreatedByUser, error, submitting } = message; + const textEditor = useRef(null); const convo = useSelector((state) => state.convo); const { initial } = useSelector((state) => state.models); const { error: convoError } = convo; - const last = !message?.children?.length - + const last = !message?.children?.length; const edit = message.messageId == currentEditId; - const dispatch = useDispatch(); // const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user'; const blinker = submitting && isSubmitting && last && !isCreatedByUser; + const generateCursor = useCallback(() => { + if (!blinker) { + return ''; + } + + return ; + }, [blinker]); useEffect(() => { if (blinker && !abortScroll) { @@ -80,15 +51,10 @@ export default function Message({ }, [isSubmitting, text, blinker, scrollToBottom, abortScroll]); useEffect(() => { - if (last) - dispatch(setConversation({parentMessageId: message?.messageId})) - }, [last, ]) + if (last) dispatch(setConversation({ parentMessageId: message?.messageId })); + }, [last]); - if (sender === '') { - return ; - } - - const enterEdit = (cancel) => setCurrentEditId(cancel?-1:message.messageId) + const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId); const handleWheel = () => { if (blinker) { @@ -102,17 +68,24 @@ export default function Message({ className: 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800' }; - - const icon = getIconOfModel({ sender, isCreatedByUser, model, chatGptLabel, promptPrefix, error }); - + + const icon = getIconOfModel({ + sender, + isCreatedByUser, + model, + chatGptLabel, + promptPrefix, + error + }); + if (!isCreatedByUser) props.className = 'w-full border-b border-black/10 bg-gray-50 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]'; - const wrapText = (text) => ; + // const wrapText = (text) => ; const resubmitMessage = () => { - const text = textEditor.current.innerText + const text = textEditor.current.innerText; if (convoError) { dispatch(setError(false)); @@ -125,14 +98,23 @@ export default function Message({ // this is not a real messageId, it is used as placeholder before real messageId returned const fakeMessageId = crypto.randomUUID(); const isCustomModel = model === 'chatgptCustom' || !initial[model]; - const currentMsg = { - sender: 'User', text: text.trim(), current: true, isCreatedByUser: true, - parentMessageId: message?.parentMessageId, - conversationId: message?.conversationId, - messageId: fakeMessageId }; + const currentMsg = { + sender: 'User', + text: text.trim(), + current: true, + isCreatedByUser: true, + parentMessageId: message?.parentMessageId, + conversationId: message?.conversationId, + messageId: fakeMessageId + }; const sender = model === 'chatgptCustom' ? chatGptLabel : model; - const initialResponse = { sender, text: '', parentMessageId: fakeMessageId, submitting: true }; + const initialResponse = { + sender, + text: '', + parentMessageId: fakeMessageId, + submitting: true + }; dispatch(setSubmitState(true)); dispatch(setMessages([...messages, currentMsg, initialResponse])); @@ -140,22 +122,22 @@ export default function Message({ const submission = { isCustomModel, - message: { - ...currentMsg, + message: { + ...currentMsg, model, chatGptLabel, - promptPrefix, + promptPrefix }, messages: messages, currentMsg, initialResponse, - sender, + sender }; console.log('User Input:', currentMsg?.text); // handleSubmit(submission); dispatch(setSubmission(submission)); - setSiblingIdx(siblingCount - 1) + setSiblingIdx(siblingCount - 1); enterEdit(true); }; @@ -166,68 +148,84 @@ export default function Message({ onWheel={handleWheel} >
- -
+
{typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? ( {icon} ) : ( icon )} - +
{error ? ( -
+
{`An error occurred. Please try again in a few moments.\n\nError message: ${text}`}
- ) : - edit ? ( -
- {/*
*/} - -
- {text} -
-
- - -
+ ) : edit ? ( +
+ {/*
*/} + +
+ {text}
- ) : ( -
- {/*
*/} -
- {!isCreatedByUser ? wrapText(text) : text} - {blinker && } -
+
+ +
- )} +
+ ) : ( +
+ {/*
*/} +
+ {!isCreatedByUser ? ( + + ) : ( + text + )} +
+
+ )}
- enterEdit()}/> + enterEdit()} + />
); diff --git a/client/src/components/Messages/MultiMessage.jsx b/client/src/components/Messages/MultiMessage.jsx new file mode 100644 index 0000000000..24ab761eb6 --- /dev/null +++ b/client/src/components/Messages/MultiMessage.jsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import Message from './Message'; + +export default function MultiMessage({ + messageList, + messages, + scrollToBottom, + currentEditId, + setCurrentEditId +}) { + const [siblingIdx, setSiblingIdx] = useState(0); + + const setSiblingIdxRev = (value) => { + setSiblingIdx(messageList?.length - value - 1); + }; + + // if (!messageList?.length) return null; + if (!(messageList && messageList.length)) { + return null; + } + + if (siblingIdx >= messageList?.length) { + setSiblingIdx(0); + return null; + } + + return ( + + ); +} diff --git a/client/src/components/Messages/TextWrapper.jsx b/client/src/components/Messages/TextWrapper.jsx index af38b56a5c..75766bca0c 100644 --- a/client/src/components/Messages/TextWrapper.jsx +++ b/client/src/components/Messages/TextWrapper.jsx @@ -46,8 +46,9 @@ const inLineWrap = (parts) => { }); }; -export default function TextWrapper({ text }) { +export default function TextWrapper({ text, generateCursor }) { let embedTest = false; + let result = null; // to match unenclosed code blocks if (text.match(/```/g)?.length === 1) { @@ -137,13 +138,23 @@ export default function TextWrapper({ text }) { } }); - return <>{codeParts}; // return the wrapped text + // return <>{codeParts}; // return the wrapped text + result = <>{codeParts}; } else if (text.match(markupRegex)) { // map over the parts and wrap any text between tildes with tags const parts = text.split(markupRegex); const codeParts = inLineWrap(parts); - return <>{codeParts}; // return the wrapped text + // return <>{codeParts}; // return the wrapped text + result = <>{codeParts}; } else { - return {text}; + // return {text}; + result = {text}; } + + return ( + <> + {result} + {(<>{generateCursor()})} + + ); } diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index 85c671d691..611a0231d3 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -1,12 +1,12 @@ import React, { useEffect, useState, useRef, useMemo } from 'react'; +import Spinner from '../svg/Spinner'; import { CSSTransition } from 'react-transition-group'; import ScrollToBottom from './ScrollToBottom'; -import { MultiMessage } from './Message'; -import Conversation from '../Conversations/Conversation'; +import MultiMessage from './MultiMessage'; import { useSelector } from 'react-redux'; -const Messages = ({ messages }) => { - const [currentEditId, setCurrentEditId] = useState(-1) +const Messages = ({ messages, messageTree }) => { + const [currentEditId, setCurrentEditId] = useState(-1); const { conversationId } = useSelector((state) => state.convo); const [showScrollButton, setShowScrollButton] = useState(false); const scrollableRef = useRef(null); @@ -23,26 +23,6 @@ const Messages = ({ messages }) => { clearTimeout(timeoutId); }; }, [messages]); - - const messageTree = useMemo(() => buildTree(messages), [messages, ]); - - function buildTree(messages) { - let messageMap = {}; - let rootMessages = []; - - // Traverse the messages array and store each element in messageMap. - messages.forEach(message => { - messageMap[message.messageId] = {...message, children: []}; - - const parentMessage = messageMap[message.parentMessageId]; - if (parentMessage) - parentMessage.children.push(messageMap[message.messageId]); - else - rootMessages.push(messageMap[message.messageId]); - }); - - return rootMessages; - } const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); @@ -79,28 +59,33 @@ const Messages = ({ messages }) => { onScroll={debouncedHandleScroll} > {/*
*/} -
-
- - - {() => showScrollButton && } - - +
+
+ {messageTree.length === 0 ? ( + + ) : ( + <> + + + {() => showScrollButton && } + + + )}
@@ -110,4 +95,4 @@ const Messages = ({ messages }) => { ); }; -export default Messages; +export default React.memo(Messages); diff --git a/client/src/store/messageSlice.js b/client/src/store/messageSlice.js index 137c329822..955d0c98f8 100644 --- a/client/src/store/messageSlice.js +++ b/client/src/store/messageSlice.js @@ -1,7 +1,9 @@ import { createSlice } from '@reduxjs/toolkit'; +import buildTree from '~/utils/buildTree'; const initialState = { messages: [], + messageTree: [] }; const currentSlice = createSlice({ @@ -10,6 +12,7 @@ const currentSlice = createSlice({ reducers: { setMessages: (state, action) => { state.messages = action.payload; + state.messageTree = buildTree(action.payload); }, setEmptyMessage: (state) => { state.messages = [ diff --git a/client/src/style.css b/client/src/style.css index c4a03408b7..30fae19b40 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1251,7 +1251,6 @@ html { vertical-align: baseline; } - /* .result-streaming>:not(ol):not(ul):not(pre):last-child:after, .result-streaming>ol:last-child li:last-child:after, .result-streaming>pre:last-child code:after, diff --git a/client/src/utils/buildTree.js b/client/src/utils/buildTree.js new file mode 100644 index 0000000000..a030509bca --- /dev/null +++ b/client/src/utils/buildTree.js @@ -0,0 +1,17 @@ +export default function buildTree(messages) { + let messageMap = {}; + let rootMessages = []; + + // Traverse the messages array and store each element in messageMap. + messages.forEach(message => { + messageMap[message.messageId] = {...message, children: []}; + + const parentMessage = messageMap[message.parentMessageId]; + if (parentMessage) + parentMessage.children.push(messageMap[message.messageId]); + else + rootMessages.push(messageMap[message.messageId]); + }); + + return rootMessages; +} \ No newline at end of file