Merge pull request #3 from wtlyu/final-adj

Final adjustments
This commit is contained in:
Danny Avila 2023-03-15 17:08:18 -04:00 committed by GitHub
commit ff45511011
16 changed files with 303 additions and 293 deletions

1
.gitignore vendored
View file

@ -47,7 +47,6 @@ bower_components/
.env .env
cache.json cache.json
api/data/ api/data/
.eslintrc.js
owner.yml owner.yml
archive archive
.vscode/settings.json .vscode/settings.json

View file

@ -1,4 +1,5 @@
const { Configuration, OpenAIApi } = require('openai'); const { Configuration, OpenAIApi } = require('openai');
const _ = require('lodash');
const proxyEnvToAxiosProxy = (proxyString) => { const proxyEnvToAxiosProxy = (proxyString) => {
if (!proxyString) return null; if (!proxyString) return null;
@ -12,15 +13,18 @@ const proxyEnvToAxiosProxy = (proxyString) => {
auth: username && password ? { username, password } : undefined auth: username && password ? { username, password } : undefined
}; };
return proxyConfig return proxyConfig;
} };
const titleConvo = async ({ message, response, model }) => { const titleConvo = async ({ model, text, response }) => {
let title = 'New Chat';
try {
const configuration = new Configuration({ const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY apiKey: process.env.OPENAI_KEY
}); });
const openai = new OpenAIApi(configuration); const openai = new OpenAIApi(configuration);
const completion = await openai.createChatCompletion({ const completion = await openai.createChatCompletion(
{
model: 'gpt-3.5-turbo', model: 'gpt-3.5-turbo',
messages: [ messages: [
{ {
@ -28,12 +32,28 @@ const titleConvo = async ({ message, response, model }) => {
content: 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.' '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: ` }, {
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) }); },
{ proxy: proxyEnvToAxiosProxy(process.env.PROXY || null) }
);
//eslint-disable-next-line //eslint-disable-next-line
return completion.data.choices[0].message.content.replace(/["\.]/g, ''); 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;
}; };
module.exports = titleConvo; const throttledTitleConvo = _.throttle(titleConvo, 1000);
module.exports = throttledTitleConvo;

View file

@ -3,13 +3,7 @@ const crypto = require('crypto');
const router = express.Router(); const router = express.Router();
const askBing = require('./askBing'); const askBing = require('./askBing');
const askSydney = require('./askSydney'); const askSydney = require('./askSydney');
const { const { titleConvo, askClient, browserClient, customClient } = require('../../app/');
titleConvo,
askClient,
browserClient,
customClient
// detectCode
} = require('../../app/');
const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models'); const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const { getMessages } = require('../../models/Message'); const { getMessages } = require('../../models/Message');
@ -42,15 +36,6 @@ router.post('/', async (req, res) => {
...convo ...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 saveMessage(userMessage);
await saveConvo({ ...userMessage, model, ...convo }); await saveConvo({ ...userMessage, model, ...convo });
@ -94,17 +79,6 @@ router.post('/regenerate', async (req, res) => {
res res
}); });
} else return handleError(res, { text: 'Parent message not found' }); } 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 ({ const ask = async ({
@ -188,7 +162,7 @@ const ask = async ({
gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model; gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model;
gptResponse.model = model; gptResponse.model = model;
// gptResponse.final = true; // gptResponse.final = true;
gptResponse.text = await handleText(gptResponse.text); gptResponse.text = await handleText(gptResponse);
if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') { if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') {
gptResponse.chatGptLabel = convo.chatGptLabel; gptResponse.chatGptLabel = convo.chatGptLabel;
@ -212,13 +186,7 @@ const ask = async ({
res.end(); res.end();
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ const title = await titleConvo({ model, text, response: gptResponse });
model,
message: text,
response: JSON.stringify(gptResponse?.text)
});
console.log('CONVERSATION TITLE', title);
await saveConvo({ await saveConvo({
conversationId, conversationId,

View file

@ -1,10 +1,9 @@
const express = require('express'); const express = require('express');
const crypto = require('crypto'); const crypto = require('crypto');
const router = express.Router(); const router = express.Router();
const { titleConvo, getCitations, citeText, askBing } = require('../../app/'); const { titleConvo, askBing } = require('../../app/');
const { saveMessage, getConvoTitle, saveConvo } = require('../../models'); const { saveMessage, getConvoTitle, saveConvo } = require('../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const citationRegex = /\[\^\d+?\^]/g;
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
const { const {
@ -129,12 +128,7 @@ const ask = async ({
response.parentMessageId = response.parentMessageId =
overrideParentMessageId || response.parentMessageId || userMessageId; overrideParentMessageId || response.parentMessageId || userMessageId;
const links = getCitations(response); response.text = await handleText(response, true);
response.text =
citeText(response) +
(links?.length > 0 && hasCitations ? `\n<small>${links}</small>` : '');
response.text = await handleText(response.text);
await saveMessage(response); await saveMessage(response);
await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo }); await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
sendMessage(res, { sendMessage(res, {
@ -146,13 +140,7 @@ const ask = async ({
res.end(); res.end();
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ const title = await titleConvo({ model, text, response });
model,
message: text,
response: JSON.stringify(response?.text)
});
console.log('CONVERSATION TITLE', title);
await saveConvo({ await saveConvo({
conversationId, conversationId,

View file

@ -1,10 +1,9 @@
const express = require('express'); const express = require('express');
const crypto = require('crypto'); const crypto = require('crypto');
const router = express.Router(); const router = express.Router();
const { titleConvo, getCitations, citeText, askSydney } = require('../../app/'); const { titleConvo, askSydney } = require('../../app/');
const { saveMessage, saveConvo, getConvoTitle } = require('../../models'); const { saveMessage, saveConvo, getConvoTitle } = require('../../models');
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers'); const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
const citationRegex = /\[\^\d+?\^]/g;
router.post('/', async (req, res) => { router.post('/', async (req, res) => {
const { const {
@ -97,7 +96,6 @@ const ask = async ({
console.log('SYDNEY RESPONSE', response); console.log('SYDNEY RESPONSE', response);
// console.dir(response, { depth: null }); // console.dir(response, { depth: null });
const hasCitations = response.response.match(citationRegex)?.length > 0;
userMessage.conversationSignature = userMessage.conversationSignature =
convo.conversationSignature || response.conversationSignature; convo.conversationSignature || response.conversationSignature;
@ -125,12 +123,6 @@ const ask = async ({
response.parentMessageId = response.parentMessageId =
overrideParentMessageId || response.parentMessageId || userMessageId; overrideParentMessageId || response.parentMessageId || userMessageId;
const links = getCitations(response);
response.text =
citeText(response) +
(links?.length > 0 && hasCitations ? `\n<small>${links}</small>` : '');
response.text = await handleText(response.text);
// Save user message // Save user message
userMessage.conversationId = response.conversationId || conversationId; userMessage.conversationId = response.conversationId || conversationId;
await saveMessage(userMessage); await saveMessage(userMessage);
@ -146,6 +138,7 @@ const ask = async ({
}); });
conversationId = userMessage.conversationId; conversationId = userMessage.conversationId;
response.text = await handleText(response, true);
// Save sydney response & convo, then send // Save sydney response & convo, then send
await saveMessage(response); await saveMessage(response);
await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo }); await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
@ -158,13 +151,7 @@ const ask = async ({
res.end(); res.end();
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') { if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
const title = await titleConvo({ const title = await titleConvo({ model, text, response });
model,
message: text,
response: JSON.stringify(response?.text)
});
console.log('CONVERSATION TITLE', title);
await saveConvo({ await saveConvo({
conversationId, conversationId,

View file

@ -1,41 +1,12 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const { titleConvo } = require('../../app/');
const { getConvo, saveConvo, getConvoTitle } = require('../../models');
const { getConvosByPage, deleteConvos, updateConvo } = require('../../models/Conversation'); const { getConvosByPage, deleteConvos, updateConvo } = require('../../models/Conversation');
const { getMessages } = require('../../models/Message');
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
const pageNumber = req.query.pageNumber || 1; const pageNumber = req.query.pageNumber || 1;
res.status(200).send(await getConvosByPage(pageNumber)); 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) => { router.post('/clear', async (req, res) => {
let filter = {}; let filter = {};
const { conversationId } = req.body.arg; const { conversationId } = req.body.arg;

View file

@ -1,6 +1,8 @@
const { citeText, detectCode } = require('../../app/');
const _ = require('lodash'); const _ = require('lodash');
const sanitizeHtml = require('sanitize-html'); 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) => { const handleError = (res, message) => {
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`); res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
@ -25,8 +27,13 @@ const createOnProgress = () => {
if (tokens.match(/^\n/)) { if (tokens.match(/^\n/)) {
tokens = tokens.replace(/^\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) { if (bing) {
@ -45,12 +52,26 @@ const createOnProgress = () => {
return onProgress; return onProgress;
}; };
const handleText = async (input) => { const handleText = async (response, bing = false) => {
let text = input; let { text } = response;
text = await detectCode(text); text = await detectCode(text);
// if (text.includes('```')) { response.text = text;
// text = sanitizeHtml(text);
// text = text.replaceAll(') =&gt;', ') =>'); 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<small>${links}</small>` : '';
}
// 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; return text;

View file

@ -8,7 +8,7 @@ import useDocumentTitle from '~/hooks/useDocumentTitle';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
const App = () => { const App = () => {
const { messages } = useSelector((state) => state.messages); const { messages, messageTree } = useSelector((state) => state.messages);
const { title } = useSelector((state) => state.convo); const { title } = useSelector((state) => state.convo);
const { conversationId } = useSelector((state) => state.convo); const { conversationId } = useSelector((state) => state.convo);
const [ navVisible, setNavVisible ]= useState(false) const [ navVisible, setNavVisible ]= useState(false)
@ -25,6 +25,7 @@ const App = () => {
) : ( ) : (
<Messages <Messages
messages={messages} messages={messages}
messageTree={messageTree}
/> />
)} )}
<TextChat messages={messages} /> <TextChat messages={messages} />

View file

@ -157,15 +157,17 @@ export default function TextChat({ messages }) {
const message = text.trim(); const message = text.trim();
const sender = model === 'chatgptCustom' ? chatGptLabel : model; const sender = model === 'chatgptCustom' ? chatGptLabel : model;
let parentMessageId = convo.parentMessageId || '00000000-0000-0000-0000-000000000000'; 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'; parentMessageId = '00000000-0000-0000-0000-000000000000';
dispatch(setNewConvo()); dispatch(setNewConvo());
currentMessages = [];
} }
const currentMsg = { sender: 'User', text: message, current: true, isCreatedByUser: true, parentMessageId , messageId: fakeMessageId }; const currentMsg = { sender: 'User', text: message, current: true, isCreatedByUser: true, parentMessageId , messageId: fakeMessageId };
const initialResponse = { sender, text: '', parentMessageId: fakeMessageId, submitting: true }; const initialResponse = { sender, text: '', parentMessageId: fakeMessageId, submitting: true };
dispatch(setSubmitState(true)); dispatch(setSubmitState(true));
dispatch(setMessages([...messages, currentMsg, initialResponse])); dispatch(setMessages([...currentMessages, currentMsg, initialResponse]));
dispatch(setText('')); dispatch(setText(''));
const submission = { const submission = {
@ -177,7 +179,7 @@ export default function TextChat({ messages }) {
chatGptLabel, chatGptLabel,
promptPrefix, promptPrefix,
}, },
messages, messages: currentMessages,
currentMsg, currentMsg,
initialResponse, initialResponse,
sender, sender,

View file

@ -1,9 +1,9 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import TextWrapper from './TextWrapper'; import TextWrapper from './TextWrapper';
import MultiMessage from './MultiMessage';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import HoverButtons from './HoverButtons'; import HoverButtons from './HoverButtons';
import SiblingSwitch from './SiblingSwitch'; import SiblingSwitch from './SiblingSwitch';
import Spinner from '../svg/Spinner';
import { setError } from '~/store/convoSlice'; import { setError } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice'; import { setMessages } from '~/store/messageSlice';
import { setSubmitState, setSubmission } from '~/store/submitSlice'; import { setSubmitState, setSubmission } from '~/store/submitSlice';
@ -11,42 +11,6 @@ import { setText } from '~/store/textSlice';
import { setConversation } from '../../store/convoSlice'; import { setConversation } from '../../store/convoSlice';
import { getIconOfModel } from '../../utils'; 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 <Message
key={messageList[messageList.length - siblingIdx - 1].messageId}
message={messageList[messageList.length - siblingIdx - 1]}
messages={messages}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
siblingIdx={messageList.length - siblingIdx - 1}
siblingCount={messageList.length}
setSiblingIdx={setSiblingIdxRev}
/>
}
export { MultiMessage };
export default function Message({ export default function Message({
message, message,
messages, messages,
@ -57,21 +21,28 @@ export default function Message({
siblingCount, siblingCount,
setSiblingIdx 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 [abortScroll, setAbort] = useState(false);
const { sender, text, isCreatedByUser, error, submitting } = message const { sender, text, isCreatedByUser, error, submitting } = message;
const textEditor = useRef(null) const textEditor = useRef(null);
const convo = useSelector((state) => state.convo); const convo = useSelector((state) => state.convo);
const { initial } = useSelector((state) => state.models); const { initial } = useSelector((state) => state.models);
const { error: convoError } = convo; const { error: convoError } = convo;
const last = !message?.children?.length const last = !message?.children?.length;
const edit = message.messageId == currentEditId; const edit = message.messageId == currentEditId;
const dispatch = useDispatch(); const dispatch = useDispatch();
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user'; // const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
const blinker = submitting && isSubmitting && last && !isCreatedByUser; const blinker = submitting && isSubmitting && last && !isCreatedByUser;
const generateCursor = useCallback(() => {
if (!blinker) {
return '';
}
return <span className="result-streaming"></span>;
}, [blinker]);
useEffect(() => { useEffect(() => {
if (blinker && !abortScroll) { if (blinker && !abortScroll) {
@ -80,15 +51,10 @@ export default function Message({
}, [isSubmitting, text, blinker, scrollToBottom, abortScroll]); }, [isSubmitting, text, blinker, scrollToBottom, abortScroll]);
useEffect(() => { useEffect(() => {
if (last) if (last) dispatch(setConversation({ parentMessageId: message?.messageId }));
dispatch(setConversation({parentMessageId: message?.messageId})) }, [last]);
}, [last, ])
if (sender === '') { const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId);
return <Spinner />;
}
const enterEdit = (cancel) => setCurrentEditId(cancel?-1:message.messageId)
const handleWheel = () => { const handleWheel = () => {
if (blinker) { if (blinker) {
@ -103,16 +69,23 @@ export default function Message({
'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' '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) if (!isCreatedByUser)
props.className = 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]'; '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) => <TextWrapper text={text} />; // const wrapText = (text) => <TextWrapper text={text} generateCursor={generateCursor}/>;
const resubmitMessage = () => { const resubmitMessage = () => {
const text = textEditor.current.innerText const text = textEditor.current.innerText;
if (convoError) { if (convoError) {
dispatch(setError(false)); dispatch(setError(false));
@ -126,13 +99,22 @@ export default function Message({
const fakeMessageId = crypto.randomUUID(); const fakeMessageId = crypto.randomUUID();
const isCustomModel = model === 'chatgptCustom' || !initial[model]; const isCustomModel = model === 'chatgptCustom' || !initial[model];
const currentMsg = { const currentMsg = {
sender: 'User', text: text.trim(), current: true, isCreatedByUser: true, sender: 'User',
text: text.trim(),
current: true,
isCreatedByUser: true,
parentMessageId: message?.parentMessageId, parentMessageId: message?.parentMessageId,
conversationId: message?.conversationId, conversationId: message?.conversationId,
messageId: fakeMessageId }; messageId: fakeMessageId
};
const sender = model === 'chatgptCustom' ? chatGptLabel : model; 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(setSubmitState(true));
dispatch(setMessages([...messages, currentMsg, initialResponse])); dispatch(setMessages([...messages, currentMsg, initialResponse]));
@ -144,18 +126,18 @@ export default function Message({
...currentMsg, ...currentMsg,
model, model,
chatGptLabel, chatGptLabel,
promptPrefix, promptPrefix
}, },
messages: messages, messages: messages,
currentMsg, currentMsg,
initialResponse, initialResponse,
sender, sender
}; };
console.log('User Input:', currentMsg?.text); console.log('User Input:', currentMsg?.text);
// handleSubmit(submission); // handleSubmit(submission);
dispatch(setSubmission(submission)); dispatch(setSubmission(submission));
setSiblingIdx(siblingCount - 1) setSiblingIdx(siblingCount - 1);
enterEdit(true); enterEdit(true);
}; };
@ -166,42 +148,48 @@ export default function Message({
onWheel={handleWheel} onWheel={handleWheel}
> >
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> <div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
<div className="relative flex w-[30px] flex-col items-end text-right text-xs md:text-sm">
{typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? ( {typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? (
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span> <span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
) : ( ) : (
icon icon
)} )}
<SiblingSwitch siblingIdx={siblingIdx} siblingCount={siblingCount} setSiblingIdx={setSiblingIdx} /> <SiblingSwitch
siblingIdx={siblingIdx}
siblingCount={siblingCount}
setSiblingIdx={setSiblingIdx}
/>
</div> </div>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]"> <div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
<div className="flex flex-grow flex-col gap-3"> <div className="flex flex-grow flex-col gap-3">
{error ? ( {error ? (
<div className="flex flex min-h-[20px] flex-col flex-grow items-start gap-4 gap-2 whitespace-pre-wrap text-red-500"> <div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500">
<div className="rounded-md border border-red-500 bg-red-500/10 py-2 px-3 text-sm text-gray-600 dark:text-gray-100"> <div className="rounded-md border border-red-500 bg-red-500/10 py-2 px-3 text-sm text-gray-600 dark:text-gray-100">
{`An error occurred. Please try again in a few moments.\n\nError message: ${text}`} {`An error occurred. Please try again in a few moments.\n\nError message: ${text}`}
</div> </div>
</div> </div>
) : ) : edit ? (
edit ? ( <div className="flex min-h-[20px] flex-grow flex-col items-start gap-4 whitespace-pre-wrap">
<div className="flex min-h-[20px] flex-col flex-grow items-start gap-4 whitespace-pre-wrap">
{/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */} {/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */}
<div className="markdown prose dark:prose-invert light w-full break-words border-none focus:outline-none" <div
contentEditable={true} ref={textEditor} suppressContentEditableWarning={true}> className="markdown prose dark:prose-invert light w-full break-words border-none focus:outline-none"
contentEditable={true}
ref={textEditor}
suppressContentEditableWarning={true}
>
{text} {text}
</div> </div>
<div className="text-center mt-2 flex w-full justify-center"> <div className="mt-2 flex w-full justify-center text-center">
<button <button
className="btn relative btn-primary mr-2" className="btn btn-primary relative mr-2"
disabled={isSubmitting} disabled={isSubmitting}
onClick={resubmitMessage} onClick={resubmitMessage}
> >
Save & Submit Save & Submit
</button> </button>
<button <button
className="btn relative btn-neutral" className="btn btn-neutral relative"
onClick={() => enterEdit(true)} onClick={() => enterEdit(true)}
> >
Cancel Cancel
@ -209,16 +197,26 @@ export default function Message({
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex min-h-[20px] flex-col flex-grow items-start gap-4 whitespace-pre-wrap"> <div className="flex min-h-[20px] flex-grow flex-col items-start gap-4 whitespace-pre-wrap">
{/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */} {/* <div className={`${blinker ? 'result-streaming' : ''} markdown prose dark:prose-invert light w-full break-words`}> */}
<div className="markdown prose dark:prose-invert light w-full break-words"> <div className="markdown prose dark:prose-invert light w-full break-words">
{!isCreatedByUser ? wrapText(text) : text} {!isCreatedByUser ? (
{blinker && <span className="result-streaming"></span>} <TextWrapper
text={text}
generateCursor={generateCursor}
/>
) : (
text
)}
</div> </div>
</div> </div>
)} )}
</div> </div>
<HoverButtons model={model} visible={!error && isCreatedByUser && !edit} onClick={() => enterEdit()}/> <HoverButtons
model={model}
visible={!error && isCreatedByUser && !edit}
onClick={() => enterEdit()}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -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 (
<Message
key={messageList[messageList.length - siblingIdx - 1].messageId}
message={messageList[messageList.length - siblingIdx - 1]}
messages={messages}
scrollToBottom={scrollToBottom}
currentEditId={currentEditId}
setCurrentEditId={setCurrentEditId}
siblingIdx={messageList.length - siblingIdx - 1}
siblingCount={messageList.length}
setSiblingIdx={setSiblingIdxRev}
/>
);
}

View file

@ -46,8 +46,9 @@ const inLineWrap = (parts) => {
}); });
}; };
export default function TextWrapper({ text }) { export default function TextWrapper({ text, generateCursor }) {
let embedTest = false; let embedTest = false;
let result = null;
// to match unenclosed code blocks // to match unenclosed code blocks
if (text.match(/```/g)?.length === 1) { 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)) { } else if (text.match(markupRegex)) {
// map over the parts and wrap any text between tildes with <code> tags // map over the parts and wrap any text between tildes with <code> tags
const parts = text.split(markupRegex); const parts = text.split(markupRegex);
const codeParts = inLineWrap(parts); const codeParts = inLineWrap(parts);
return <>{codeParts}</>; // return the wrapped text // return <>{codeParts}</>; // return the wrapped text
result = <>{codeParts}</>;
} else { } else {
return <Markdown options={mdOptions}>{text}</Markdown>; // return <Markdown options={mdOptions}>{text}</Markdown>;
result = <Markdown options={mdOptions}>{text}</Markdown>;
} }
return (
<>
{result}
{(<>{generateCursor()}</>)}
</>
);
} }

View file

@ -1,12 +1,12 @@
import React, { useEffect, useState, useRef, useMemo } from 'react'; import React, { useEffect, useState, useRef, useMemo } from 'react';
import Spinner from '../svg/Spinner';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import ScrollToBottom from './ScrollToBottom'; import ScrollToBottom from './ScrollToBottom';
import { MultiMessage } from './Message'; import MultiMessage from './MultiMessage';
import Conversation from '../Conversations/Conversation';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
const Messages = ({ messages }) => { const Messages = ({ messages, messageTree }) => {
const [currentEditId, setCurrentEditId] = useState(-1) const [currentEditId, setCurrentEditId] = useState(-1);
const { conversationId } = useSelector((state) => state.convo); const { conversationId } = useSelector((state) => state.convo);
const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollButton, setShowScrollButton] = useState(false);
const scrollableRef = useRef(null); const scrollableRef = useRef(null);
@ -24,26 +24,6 @@ const Messages = ({ messages }) => {
}; };
}, [messages]); }, [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 = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
setShowScrollButton(false); setShowScrollButton(false);
@ -79,8 +59,12 @@ const Messages = ({ messages }) => {
onScroll={debouncedHandleScroll} onScroll={debouncedHandleScroll}
> >
{/* <div className="flex-1 overflow-hidden"> */} {/* <div className="flex-1 overflow-hidden"> */}
<div className="h-full dark:gpt-dark-gray"> <div className="dark:gpt-dark-gray h-full">
<div className="flex h-full flex-col items-center text-sm dark:gpt-dark-gray"> <div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm">
{messageTree.length === 0 ? (
<Spinner />
) : (
<>
<MultiMessage <MultiMessage
key={conversationId} // avoid internal state mixture key={conversationId} // avoid internal state mixture
messageList={messageTree} messageList={messageTree}
@ -98,9 +82,10 @@ const Messages = ({ messages }) => {
> >
{() => showScrollButton && <ScrollToBottom scrollHandler={scrollHandler} />} {() => showScrollButton && <ScrollToBottom scrollHandler={scrollHandler} />}
</CSSTransition> </CSSTransition>
</>
)}
<div <div
className="group h-32 w-full flex-shrink-0 dark:border-gray-900/50 dark:gpt-dark-gray md:h-48" className="dark:gpt-dark-gray group h-32 w-full flex-shrink-0 dark:border-gray-900/50 md:h-48"
ref={messagesEndRef} ref={messagesEndRef}
/> />
</div> </div>
@ -110,4 +95,4 @@ const Messages = ({ messages }) => {
); );
}; };
export default Messages; export default React.memo(Messages);

View file

@ -1,7 +1,9 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import buildTree from '~/utils/buildTree';
const initialState = { const initialState = {
messages: [], messages: [],
messageTree: []
}; };
const currentSlice = createSlice({ const currentSlice = createSlice({
@ -10,6 +12,7 @@ const currentSlice = createSlice({
reducers: { reducers: {
setMessages: (state, action) => { setMessages: (state, action) => {
state.messages = action.payload; state.messages = action.payload;
state.messageTree = buildTree(action.payload);
}, },
setEmptyMessage: (state) => { setEmptyMessage: (state) => {
state.messages = [ state.messages = [

View file

@ -1251,7 +1251,6 @@ html {
vertical-align: baseline; vertical-align: baseline;
} }
/* .result-streaming>:not(ol):not(ul):not(pre):last-child:after, /* .result-streaming>:not(ol):not(ul):not(pre):last-child:after,
.result-streaming>ol:last-child li:last-child:after, .result-streaming>ol:last-child li:last-child:after,
.result-streaming>pre:last-child code:after, .result-streaming>pre:last-child code:after,

View file

@ -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;
}