mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
commit
ff45511011
16 changed files with 303 additions and 293 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(') =>', ') =>');
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
40
client/src/components/Messages/MultiMessage.jsx
Normal file
40
client/src/components/Messages/MultiMessage.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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()}</>)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
17
client/src/utils/buildTree.js
Normal file
17
client/src/utils/buildTree.js
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue