From 96ca7835172c6c0d38ef81bc62b10f5f3bb0a3cf Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Mar 2023 10:42:45 -0400 Subject: [PATCH 1/6] chore: re-organize message modules, fix icon size, convo reset properly rebuilds Tree --- .gitignore | 1 - client/src/App.jsx | 3 +- client/src/components/Main/TextChat.jsx | 8 +- client/src/components/Messages/Message.jsx | 45 ++--------- .../src/components/Messages/MultiMessage.jsx | 40 ++++++++++ client/src/components/Messages/index.jsx | 76 ++++++++----------- client/src/store/messageSlice.js | 3 + client/src/utils/buildTree.js | 17 +++++ 8 files changed, 103 insertions(+), 90 deletions(-) create mode 100644 client/src/components/Messages/MultiMessage.jsx create mode 100644 client/src/utils/buildTree.js 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/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..af60fc1a9a 100644 --- a/client/src/components/Messages/Message.jsx +++ b/client/src/components/Messages/Message.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import TextWrapper from './TextWrapper'; +import MultiMessage from './MultiMessage'; import { useSelector, useDispatch } from 'react-redux'; import HoverButtons from './HoverButtons'; import SiblingSwitch from './SiblingSwitch'; @@ -11,42 +12,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, @@ -84,9 +49,9 @@ export default function Message({ dispatch(setConversation({parentMessageId: message?.messageId})) }, [last, ]) - if (sender === '') { - return ; - } + // if (sender === '') { + // return ; + // } const enterEdit = (cancel) => setCurrentEditId(cancel?-1:message.messageId) @@ -167,7 +132,7 @@ export default function Message({ >
-
+
{typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? ( {icon} ) : ( 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/index.jsx b/client/src/components/Messages/index.jsx index 85c671d691..eaf0a0cb6d 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -1,12 +1,13 @@ 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 buildTree from '~/utils/buildTree'; 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 +24,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 +60,33 @@ const Messages = ({ messages }) => { onScroll={debouncedHandleScroll} > {/*
*/} -
-
- - - {() => showScrollButton && } - - +
+
+ {messageTree.length === 0 ? ( + + ) : ( + <> + + + {() => showScrollButton && } + + + )}
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/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 From 2fd50c99b83978ce6420aadc01cdbfe84bd0ba2c Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Mar 2023 12:47:30 -0400 Subject: [PATCH 2/6] fix: debounce title request and handle error with default title --- api/app/titleConvo.js | 39 +++++++++++++-------- api/server/routes/ask.js | 44 +++++------------------- api/server/routes/askBing.js | 18 +++++----- api/server/routes/askSydney.js | 18 +++++----- api/server/routes/convos.js | 29 ---------------- api/server/routes/handlers.js | 21 +++++++++-- client/src/components/Messages/index.jsx | 1 - 7 files changed, 70 insertions(+), 100 deletions(-) diff --git a/api/app/titleConvo.js b/api/app/titleConvo.js index 88e8c75074..344af6f893 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,37 @@ const proxyEnvToAxiosProxy = (proxyString) => { port: port ? parseInt(port) : undefined, auth: username && password ? { username, password } : undefined }; - - return proxyConfig -} + + 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) }); + 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, ''); }; -module.exports = titleConvo; +const debouncedTitleConvo = _.debounce(titleConvo, 500); + +module.exports = debouncedTitleConvo; diff --git a/api/server/routes/ask.js b/api/server/routes/ask.js index 265672db8b..1d497c86b8 100644 --- a/api/server/routes/ask.js +++ b/api/server/routes/ask.js @@ -3,15 +3,15 @@ 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 { askClient, browserClient, customClient } = require('../../app/'); const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models'); -const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); +const { + handleError, + sendMessage, + createOnProgress, + genTitle, + handleText +} = require('./handlers'); const { getMessages } = require('../../models/Message'); router.use('/bing', askBing); @@ -42,15 +42,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 +85,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 ({ @@ -212,13 +192,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 genTitle({ model, text, response: gptResponse }); await saveConvo({ conversationId, diff --git a/api/server/routes/askBing.js b/api/server/routes/askBing.js index 066b64faec..681332ec37 100644 --- a/api/server/routes/askBing.js +++ b/api/server/routes/askBing.js @@ -1,9 +1,15 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { titleConvo, getCitations, citeText, askBing } = require('../../app/'); +const { getCitations, citeText, askBing } = require('../../app/'); const { saveMessage, getConvoTitle, saveConvo } = require('../../models'); -const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); +const { + handleError, + sendMessage, + createOnProgress, + genTitle, + handleText +} = require('./handlers'); const citationRegex = /\[\^\d+?\^]/g; router.post('/', async (req, res) => { @@ -146,13 +152,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 genTitle({ model, text, response }); await saveConvo({ conversationId, diff --git a/api/server/routes/askSydney.js b/api/server/routes/askSydney.js index 03e3479be4..40aa967bcc 100644 --- a/api/server/routes/askSydney.js +++ b/api/server/routes/askSydney.js @@ -1,9 +1,15 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { titleConvo, getCitations, citeText, askSydney } = require('../../app/'); +const { getCitations, citeText, askSydney } = require('../../app/'); const { saveMessage, saveConvo, getConvoTitle } = require('../../models'); -const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); +const { + handleError, + sendMessage, + createOnProgress, + genTitle, + handleText +} = require('./handlers'); const citationRegex = /\[\^\d+?\^]/g; router.post('/', async (req, res) => { @@ -158,13 +164,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 genTitle({ 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..7c6d6ce315 100644 --- a/api/server/routes/handlers.js +++ b/api/server/routes/handlers.js @@ -1,4 +1,4 @@ -const { citeText, detectCode } = require('../../app/'); +const { titleConvo, citeText, detectCode } = require('../../app/'); const _ = require('lodash'); const sanitizeHtml = require('sanitize-html'); @@ -14,6 +14,23 @@ const sendMessage = (res, message) => { res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`); }; +const genTitle = async ({ model, text, response }) => { + let title = 'New Chat'; + try { + title = await titleConvo({ + model, + message: text, + response: JSON.stringify(response?.text) + }); + } catch (e) { + console.error(e); + console.log('There was an issue generating title, see error above'); + } + + console.log('CONVERSATION TITLE', title); + return title; +}; + const createOnProgress = () => { let i = 0; let tokens = ''; @@ -56,4 +73,4 @@ const handleText = async (input) => { return text; }; -module.exports = { handleError, sendMessage, createOnProgress, handleText }; \ No newline at end of file +module.exports = { handleError, sendMessage, createOnProgress, genTitle, handleText }; diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index eaf0a0cb6d..d9b5b2a80b 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -3,7 +3,6 @@ import Spinner from '../svg/Spinner'; import { CSSTransition } from 'react-transition-group'; import ScrollToBottom from './ScrollToBottom'; import MultiMessage from './MultiMessage'; -import buildTree from '~/utils/buildTree'; import { useSelector } from 'react-redux'; const Messages = ({ messages, messageTree }) => { From a8aad30fc8f4d6bc75347cf7898f91946d1a9aab Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Mar 2023 14:36:17 -0400 Subject: [PATCH 3/6] chore: memoized Messages component, will require custom equality check --- api/server/routes/handlers.js | 21 +++++++++++++++------ client/src/components/Messages/index.jsx | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/api/server/routes/handlers.js b/api/server/routes/handlers.js index 7c6d6ce315..e3bd72583f 100644 --- a/api/server/routes/handlers.js +++ b/api/server/routes/handlers.js @@ -1,6 +1,7 @@ -const { titleConvo, citeText, detectCode } = require('../../app/'); const _ = require('lodash'); const sanitizeHtml = require('sanitize-html'); +const { titleConvo, 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`); @@ -42,8 +43,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) { @@ -65,9 +71,12 @@ const createOnProgress = () => { const handleText = async (input) => { let text = input; text = await detectCode(text); - // if (text.includes('```')) { - // text = sanitizeHtml(text); - // text = text.replaceAll(') =>', ') =>'); + // 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; diff --git a/client/src/components/Messages/index.jsx b/client/src/components/Messages/index.jsx index d9b5b2a80b..611a0231d3 100644 --- a/client/src/components/Messages/index.jsx +++ b/client/src/components/Messages/index.jsx @@ -95,4 +95,4 @@ const Messages = ({ messages, messageTree }) => { ); }; -export default Messages; +export default React.memo(Messages); From a0c94715ce99321441d0276e8733ca372d03ea42 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Mar 2023 15:21:04 -0400 Subject: [PATCH 4/6] chore: refactor titleConvo --- api/app/titleConvo.js | 63 ++++++++++++++++++++-------------- api/server/routes/ask.js | 12 ++----- api/server/routes/askBing.js | 12 ++----- api/server/routes/askSydney.js | 12 ++----- api/server/routes/handlers.js | 19 +--------- 5 files changed, 47 insertions(+), 71 deletions(-) diff --git a/api/app/titleConvo.js b/api/app/titleConvo.js index 344af6f893..e37fcb3b0c 100644 --- a/api/app/titleConvo.js +++ b/api/app/titleConvo.js @@ -16,33 +16,44 @@ const proxyEnvToAxiosProxy = (proxyString) => { 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) } - ); +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 - return completion.data.choices[0].message.content.replace(/["\.]/g, ''); + //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 debouncedTitleConvo = _.debounce(titleConvo, 500); +const throttledTitleConvo = _.throttle(titleConvo, 1000); -module.exports = debouncedTitleConvo; +module.exports = throttledTitleConvo; diff --git a/api/server/routes/ask.js b/api/server/routes/ask.js index 1d497c86b8..69c180c8a7 100644 --- a/api/server/routes/ask.js +++ b/api/server/routes/ask.js @@ -3,15 +3,9 @@ const crypto = require('crypto'); const router = express.Router(); const askBing = require('./askBing'); const askSydney = require('./askSydney'); -const { askClient, browserClient, customClient } = require('../../app/'); +const { titleConvo, askClient, browserClient, customClient } = require('../../app/'); const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models'); -const { - handleError, - sendMessage, - createOnProgress, - genTitle, - handleText -} = require('./handlers'); +const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const { getMessages } = require('../../models/Message'); router.use('/bing', askBing); @@ -192,7 +186,7 @@ const ask = async ({ res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { - const title = await genTitle({ model, text, response: gptResponse }); + 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 681332ec37..aa052c31d7 100644 --- a/api/server/routes/askBing.js +++ b/api/server/routes/askBing.js @@ -1,15 +1,9 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { getCitations, citeText, askBing } = require('../../app/'); +const { titleConvo, getCitations, citeText, askBing } = require('../../app/'); const { saveMessage, getConvoTitle, saveConvo } = require('../../models'); -const { - handleError, - sendMessage, - createOnProgress, - genTitle, - handleText -} = require('./handlers'); +const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const citationRegex = /\[\^\d+?\^]/g; router.post('/', async (req, res) => { @@ -152,7 +146,7 @@ const ask = async ({ res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { - const title = await genTitle({ model, text, response }); + 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 40aa967bcc..7fecc2a450 100644 --- a/api/server/routes/askSydney.js +++ b/api/server/routes/askSydney.js @@ -1,15 +1,9 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); -const { getCitations, citeText, askSydney } = require('../../app/'); +const { titleConvo, getCitations, citeText, askSydney } = require('../../app/'); const { saveMessage, saveConvo, getConvoTitle } = require('../../models'); -const { - handleError, - sendMessage, - createOnProgress, - genTitle, - handleText -} = require('./handlers'); +const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const citationRegex = /\[\^\d+?\^]/g; router.post('/', async (req, res) => { @@ -164,7 +158,7 @@ const ask = async ({ res.end(); if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { - const title = await genTitle({ model, text, response }); + const title = await titleConvo({ model, text, response }); await saveConvo({ conversationId, diff --git a/api/server/routes/handlers.js b/api/server/routes/handlers.js index e3bd72583f..31213f3080 100644 --- a/api/server/routes/handlers.js +++ b/api/server/routes/handlers.js @@ -15,23 +15,6 @@ const sendMessage = (res, message) => { res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`); }; -const genTitle = async ({ model, text, response }) => { - let title = 'New Chat'; - try { - title = await titleConvo({ - model, - message: text, - response: JSON.stringify(response?.text) - }); - } catch (e) { - console.error(e); - console.log('There was an issue generating title, see error above'); - } - - console.log('CONVERSATION TITLE', title); - return title; -}; - const createOnProgress = () => { let i = 0; let tokens = ''; @@ -82,4 +65,4 @@ const handleText = async (input) => { return text; }; -module.exports = { handleError, sendMessage, createOnProgress, genTitle, handleText }; +module.exports = { handleError, sendMessage, createOnProgress, handleText }; From 84b104e65fad1dfa328787ff03eeba554e680022 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Mar 2023 15:44:48 -0400 Subject: [PATCH 5/6] chore: delegate response text parsing to one location --- api/server/routes/ask.js | 2 +- api/server/routes/askBing.js | 10 ++-------- api/server/routes/askSydney.js | 11 ++-------- api/server/routes/handlers.js | 20 +++++++++++++++---- .../src/components/Messages/TextWrapper.jsx | 2 +- 5 files changed, 22 insertions(+), 23 deletions(-) diff --git a/api/server/routes/ask.js b/api/server/routes/ask.js index 69c180c8a7..ad2a7178c2 100644 --- a/api/server/routes/ask.js +++ b/api/server/routes/ask.js @@ -162,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; diff --git a/api/server/routes/askBing.js b/api/server/routes/askBing.js index aa052c31d7..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, { diff --git a/api/server/routes/askSydney.js b/api/server/routes/askSydney.js index 7fecc2a450..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 }); diff --git a/api/server/routes/handlers.js b/api/server/routes/handlers.js index 31213f3080..3ba4e81243 100644 --- a/api/server/routes/handlers.js +++ b/api/server/routes/handlers.js @@ -1,7 +1,8 @@ const _ = require('lodash'); const sanitizeHtml = require('sanitize-html'); -const { titleConvo, 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 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`); @@ -51,9 +52,20 @@ const createOnProgress = () => { return onProgress; }; -const handleText = async (input) => { - let text = input; +const handleText = async (response, bing = false) => { + let { text } = response; text = await detectCode(text); + 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) => { diff --git a/client/src/components/Messages/TextWrapper.jsx b/client/src/components/Messages/TextWrapper.jsx index af38b56a5c..5719ee876b 100644 --- a/client/src/components/Messages/TextWrapper.jsx +++ b/client/src/components/Messages/TextWrapper.jsx @@ -142,7 +142,7 @@ export default function TextWrapper({ text }) { // 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 } else { return {text}; } From 8c6340aed05b87784e43f9ae13c8b55efcf9dbba Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 15 Mar 2023 16:38:01 -0400 Subject: [PATCH 6/6] chore: refactor cursor blink, debugging --- client/src/components/Messages/Message.jsx | 179 +++++++++++------- .../src/components/Messages/TextWrapper.jsx | 19 +- client/src/style.css | 1 - 3 files changed, 121 insertions(+), 78 deletions(-) diff --git a/client/src/components/Messages/Message.jsx b/client/src/components/Messages/Message.jsx index af60fc1a9a..622b907454 100644 --- a/client/src/components/Messages/Message.jsx +++ b/client/src/components/Messages/Message.jsx @@ -1,10 +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'; @@ -22,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) { @@ -45,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) { @@ -67,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)); @@ -90,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])); @@ -105,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); }; @@ -131,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/TextWrapper.jsx b/client/src/components/Messages/TextWrapper.jsx index 5719ee876b..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/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,