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
|
||||
cache.json
|
||||
api/data/
|
||||
.eslintrc.js
|
||||
owner.yml
|
||||
archive
|
||||
.vscode/settings.json
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { Configuration, OpenAIApi } = require('openai');
|
||||
const _ = require('lodash');
|
||||
|
||||
const proxyEnvToAxiosProxy = (proxyString) => {
|
||||
if (!proxyString) return null;
|
||||
|
|
@ -12,15 +13,18 @@ const proxyEnvToAxiosProxy = (proxyString) => {
|
|||
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({
|
||||
apiKey: process.env.OPENAI_KEY
|
||||
});
|
||||
const openai = new OpenAIApi(configuration);
|
||||
const completion = await openai.createChatCompletion({
|
||||
const completion = await openai.createChatCompletion(
|
||||
{
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [
|
||||
{
|
||||
|
|
@ -28,12 +32,28 @@ const titleConvo = async ({ message, response, model }) => {
|
|||
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: ` },
|
||||
{
|
||||
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
|
||||
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 askBing = require('./askBing');
|
||||
const askSydney = require('./askSydney');
|
||||
const {
|
||||
titleConvo,
|
||||
askClient,
|
||||
browserClient,
|
||||
customClient
|
||||
// detectCode
|
||||
} = require('../../app/');
|
||||
const { titleConvo, askClient, browserClient, customClient } = require('../../app/');
|
||||
const { getConvo, saveMessage, getConvoTitle, saveConvo } = require('../../models');
|
||||
const { handleError, sendMessage, createOnProgress, handleText } = require('./handlers');
|
||||
const { getMessages } = require('../../models/Message');
|
||||
|
|
@ -42,15 +36,6 @@ router.post('/', async (req, res) => {
|
|||
...convo
|
||||
});
|
||||
|
||||
// if (model === 'chatgptCustom' && !chatGptLabel && conversationId) {
|
||||
// const convo = await getConvo({ conversationId });
|
||||
// if (convo) {
|
||||
// console.log('found convo for custom gpt', { convo })
|
||||
// chatGptLabel = convo.chatGptLabel;
|
||||
// promptPrefix = convo.promptPrefix;
|
||||
// }
|
||||
// }
|
||||
|
||||
await saveMessage(userMessage);
|
||||
await saveConvo({ ...userMessage, model, ...convo });
|
||||
|
||||
|
|
@ -94,17 +79,6 @@ router.post('/regenerate', async (req, res) => {
|
|||
res
|
||||
});
|
||||
} else return handleError(res, { text: 'Parent message not found' });
|
||||
|
||||
// if (model === 'chatgptCustom' && !chatGptLabel && conversationId) {
|
||||
// const convo = await getConvo({ conversationId });
|
||||
// if (convo) {
|
||||
// console.log('found convo for custom gpt', { convo })
|
||||
// chatGptLabel = convo.chatGptLabel;
|
||||
// promptPrefix = convo.promptPrefix;
|
||||
// }
|
||||
// }
|
||||
|
||||
// await saveConvo({ ...userMessage, model, chatGptLabel, promptPrefix });
|
||||
});
|
||||
|
||||
const ask = async ({
|
||||
|
|
@ -188,7 +162,7 @@ const ask = async ({
|
|||
gptResponse.sender = model === 'chatgptCustom' ? convo.chatGptLabel : model;
|
||||
gptResponse.model = model;
|
||||
// gptResponse.final = true;
|
||||
gptResponse.text = await handleText(gptResponse.text);
|
||||
gptResponse.text = await handleText(gptResponse);
|
||||
|
||||
if (convo.chatGptLabel?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.chatGptLabel = convo.chatGptLabel;
|
||||
|
|
@ -212,13 +186,7 @@ const ask = async ({
|
|||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({
|
||||
model,
|
||||
message: text,
|
||||
response: JSON.stringify(gptResponse?.text)
|
||||
});
|
||||
|
||||
console.log('CONVERSATION TITLE', title);
|
||||
const title = await titleConvo({ model, text, response: gptResponse });
|
||||
|
||||
await saveConvo({
|
||||
conversationId,
|
||||
|
|
|
|||
|
|
@ -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<small>${links}</small>` : '');
|
||||
response.text = await handleText(response.text);
|
||||
|
||||
response.text = await handleText(response, true);
|
||||
await saveMessage(response);
|
||||
await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
|
||||
sendMessage(res, {
|
||||
|
|
@ -146,13 +140,7 @@ const ask = async ({
|
|||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({
|
||||
model,
|
||||
message: text,
|
||||
response: JSON.stringify(response?.text)
|
||||
});
|
||||
|
||||
console.log('CONVERSATION TITLE', title);
|
||||
const title = await titleConvo({ model, text, response });
|
||||
|
||||
await saveConvo({
|
||||
conversationId,
|
||||
|
|
|
|||
|
|
@ -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<small>${links}</small>` : '');
|
||||
response.text = await handleText(response.text);
|
||||
|
||||
// Save user message
|
||||
userMessage.conversationId = response.conversationId || conversationId;
|
||||
await saveMessage(userMessage);
|
||||
|
|
@ -146,6 +138,7 @@ const ask = async ({
|
|||
});
|
||||
conversationId = userMessage.conversationId;
|
||||
|
||||
response.text = await handleText(response, true);
|
||||
// Save sydney response & convo, then send
|
||||
await saveMessage(response);
|
||||
await saveConvo({ ...response, model, chatGptLabel: null, promptPrefix: null, ...convo });
|
||||
|
|
@ -158,13 +151,7 @@ const ask = async ({
|
|||
res.end();
|
||||
|
||||
if (userParentMessageId == '00000000-0000-0000-0000-000000000000') {
|
||||
const title = await titleConvo({
|
||||
model,
|
||||
message: text,
|
||||
response: JSON.stringify(response?.text)
|
||||
});
|
||||
|
||||
console.log('CONVERSATION TITLE', title);
|
||||
const title = await titleConvo({ model, text, response });
|
||||
|
||||
await saveConvo({
|
||||
conversationId,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const { citeText, detectCode } = require('../../app/');
|
||||
const _ = require('lodash');
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
const citationRegex = /\[\^\d+?\^]/g;
|
||||
const { getCitations, citeText, detectCode } = require('../../app/');
|
||||
// const htmlTagRegex = /(<\/?\s*[a-zA-Z]*\s*(?:\s+[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*(?:\/?)>|<\s*[a-zA-Z]+\s*(?:\s+[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*(?:\/?>|<\/?>))/g;
|
||||
|
||||
const handleError = (res, message) => {
|
||||
res.write(`event: error\ndata: ${JSON.stringify(message)}\n\n`);
|
||||
|
|
@ -25,8 +27,13 @@ const createOnProgress = () => {
|
|||
if (tokens.match(/^\n/)) {
|
||||
tokens = tokens.replace(/^\n/, '');
|
||||
}
|
||||
// if (tokens.includes('```')) {
|
||||
// tokens = sanitizeHtml(tokens);
|
||||
|
||||
// const htmlTags = tokens.match(htmlTagRegex);
|
||||
// if (tokens.includes('```') && htmlTags && htmlTags.length > 0) {
|
||||
// htmlTags.forEach((tag) => {
|
||||
// const sanitizedTag = sanitizeHtml(tag);
|
||||
// tokens = tokens.replaceAll(tag, sanitizedTag);
|
||||
// });
|
||||
// }
|
||||
|
||||
if (bing) {
|
||||
|
|
@ -45,12 +52,26 @@ const createOnProgress = () => {
|
|||
return onProgress;
|
||||
};
|
||||
|
||||
const handleText = async (input) => {
|
||||
let text = input;
|
||||
const handleText = async (response, bing = false) => {
|
||||
let { text } = response;
|
||||
text = await detectCode(text);
|
||||
// if (text.includes('```')) {
|
||||
// text = sanitizeHtml(text);
|
||||
// text = text.replaceAll(') =>', ') =>');
|
||||
response.text = text;
|
||||
|
||||
if (bing) {
|
||||
// const hasCitations = response.response.match(citationRegex)?.length > 0;
|
||||
const links = getCitations(response);
|
||||
if (response.text.match(citationRegex)?.length > 0) {
|
||||
text = citeText(response);
|
||||
}
|
||||
text += links?.length > 0 ? `\n<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;
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
) : (
|
||||
<Messages
|
||||
messages={messages}
|
||||
messageTree={messageTree}
|
||||
/>
|
||||
)}
|
||||
<TextChat messages={messages} />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import TextWrapper from './TextWrapper';
|
||||
import MultiMessage from './MultiMessage';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import HoverButtons from './HoverButtons';
|
||||
import SiblingSwitch from './SiblingSwitch';
|
||||
import Spinner from '../svg/Spinner';
|
||||
import { setError } from '~/store/convoSlice';
|
||||
import { setMessages } from '~/store/messageSlice';
|
||||
import { setSubmitState, setSubmission } from '~/store/submitSlice';
|
||||
|
|
@ -11,42 +11,6 @@ import { setText } from '~/store/textSlice';
|
|||
import { setConversation } from '../../store/convoSlice';
|
||||
import { getIconOfModel } from '../../utils';
|
||||
|
||||
const MultiMessage = ({
|
||||
messageList,
|
||||
messages,
|
||||
scrollToBottom,
|
||||
currentEditId,
|
||||
setCurrentEditId
|
||||
}) => {
|
||||
const [siblingIdx, setSiblingIdx] = useState(0)
|
||||
|
||||
const setSiblingIdxRev = (value) => {
|
||||
setSiblingIdx(messageList?.length - value - 1)
|
||||
}
|
||||
|
||||
if (!messageList?.length) return null;
|
||||
|
||||
if (siblingIdx >= messageList?.length) {
|
||||
setSiblingIdx(0)
|
||||
return null
|
||||
}
|
||||
|
||||
return <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({
|
||||
message,
|
||||
messages,
|
||||
|
|
@ -57,21 +21,28 @@ export default function Message({
|
|||
siblingCount,
|
||||
setSiblingIdx
|
||||
}) {
|
||||
const { isSubmitting, model, chatGptLabel, promptPrefix } = useSelector((state) => state.submit);
|
||||
const { isSubmitting, model, chatGptLabel, promptPrefix } = useSelector(
|
||||
(state) => state.submit
|
||||
);
|
||||
const [abortScroll, setAbort] = useState(false);
|
||||
const { sender, text, isCreatedByUser, error, submitting } = message
|
||||
const textEditor = useRef(null)
|
||||
const { sender, text, isCreatedByUser, error, submitting } = message;
|
||||
const textEditor = useRef(null);
|
||||
const convo = useSelector((state) => state.convo);
|
||||
const { initial } = useSelector((state) => state.models);
|
||||
const { error: convoError } = convo;
|
||||
const last = !message?.children?.length
|
||||
|
||||
const last = !message?.children?.length;
|
||||
const edit = message.messageId == currentEditId;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// const notUser = !isCreatedByUser; // sender.toLowerCase() !== 'user';
|
||||
const blinker = submitting && isSubmitting && last && !isCreatedByUser;
|
||||
const generateCursor = useCallback(() => {
|
||||
if (!blinker) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return <span className="result-streaming">█</span>;
|
||||
}, [blinker]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blinker && !abortScroll) {
|
||||
|
|
@ -80,15 +51,10 @@ export default function Message({
|
|||
}, [isSubmitting, text, blinker, scrollToBottom, abortScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
if (last)
|
||||
dispatch(setConversation({parentMessageId: message?.messageId}))
|
||||
}, [last, ])
|
||||
if (last) dispatch(setConversation({ parentMessageId: message?.messageId }));
|
||||
}, [last]);
|
||||
|
||||
if (sender === '') {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const enterEdit = (cancel) => setCurrentEditId(cancel?-1:message.messageId)
|
||||
const enterEdit = (cancel) => setCurrentEditId(cancel ? -1 : message.messageId);
|
||||
|
||||
const handleWheel = () => {
|
||||
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'
|
||||
};
|
||||
|
||||
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) => <TextWrapper text={text} />;
|
||||
// const wrapText = (text) => <TextWrapper text={text} generateCursor={generateCursor}/>;
|
||||
|
||||
const resubmitMessage = () => {
|
||||
const text = textEditor.current.innerText
|
||||
const text = textEditor.current.innerText;
|
||||
|
||||
if (convoError) {
|
||||
dispatch(setError(false));
|
||||
|
|
@ -126,13 +99,22 @@ export default function Message({
|
|||
const fakeMessageId = crypto.randomUUID();
|
||||
const isCustomModel = model === 'chatgptCustom' || !initial[model];
|
||||
const currentMsg = {
|
||||
sender: 'User', text: text.trim(), current: true, isCreatedByUser: true,
|
||||
sender: 'User',
|
||||
text: text.trim(),
|
||||
current: true,
|
||||
isCreatedByUser: true,
|
||||
parentMessageId: message?.parentMessageId,
|
||||
conversationId: message?.conversationId,
|
||||
messageId: fakeMessageId };
|
||||
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]));
|
||||
|
|
@ -144,18 +126,18 @@ export default function Message({
|
|||
...currentMsg,
|
||||
model,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
promptPrefix
|
||||
},
|
||||
messages: messages,
|
||||
currentMsg,
|
||||
initialResponse,
|
||||
sender,
|
||||
sender
|
||||
};
|
||||
console.log('User Input:', currentMsg?.text);
|
||||
// handleSubmit(submission);
|
||||
dispatch(setSubmission(submission));
|
||||
|
||||
setSiblingIdx(siblingCount - 1)
|
||||
setSiblingIdx(siblingCount - 1);
|
||||
enterEdit(true);
|
||||
};
|
||||
|
||||
|
|
@ -166,42 +148,48 @@ export default function Message({
|
|||
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 flex w-[30px] flex-col items-end text-right text-xs md:text-sm">
|
||||
<div className="relative flex h-[30px] w-[30px] flex-col items-end text-right text-xs md:text-sm">
|
||||
{typeof icon === 'string' && icon.match(/[^\u0000-\u007F]+/) ? (
|
||||
<span className=" direction-rtl w-40 overflow-x-scroll">{icon}</span>
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
<SiblingSwitch siblingIdx={siblingIdx} siblingCount={siblingCount} setSiblingIdx={setSiblingIdx} />
|
||||
<SiblingSwitch
|
||||
siblingIdx={siblingIdx}
|
||||
siblingCount={siblingCount}
|
||||
setSiblingIdx={setSiblingIdx}
|
||||
/>
|
||||
</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="flex flex-grow flex-col gap-3">
|
||||
{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">
|
||||
{`An error occurred. Please try again in a few moments.\n\nError message: ${text}`}
|
||||
</div>
|
||||
</div>
|
||||
) :
|
||||
edit ? (
|
||||
<div className="flex min-h-[20px] flex-col flex-grow items-start gap-4 whitespace-pre-wrap">
|
||||
) : edit ? (
|
||||
<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="markdown prose dark:prose-invert light w-full break-words border-none focus:outline-none"
|
||||
contentEditable={true} ref={textEditor} suppressContentEditableWarning={true}>
|
||||
<div
|
||||
className="markdown prose dark:prose-invert light w-full break-words border-none focus:outline-none"
|
||||
contentEditable={true}
|
||||
ref={textEditor}
|
||||
suppressContentEditableWarning={true}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
<div className="text-center mt-2 flex w-full justify-center">
|
||||
<div className="mt-2 flex w-full justify-center text-center">
|
||||
<button
|
||||
className="btn relative btn-primary mr-2"
|
||||
className="btn btn-primary relative mr-2"
|
||||
disabled={isSubmitting}
|
||||
onClick={resubmitMessage}
|
||||
>
|
||||
Save & Submit
|
||||
</button>
|
||||
<button
|
||||
className="btn relative btn-neutral"
|
||||
className="btn btn-neutral relative"
|
||||
onClick={() => enterEdit(true)}
|
||||
>
|
||||
Cancel
|
||||
|
|
@ -209,16 +197,26 @@ export default function Message({
|
|||
</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="markdown prose dark:prose-invert light w-full break-words">
|
||||
{!isCreatedByUser ? wrapText(text) : text}
|
||||
{blinker && <span className="result-streaming">█</span>}
|
||||
{!isCreatedByUser ? (
|
||||
<TextWrapper
|
||||
text={text}
|
||||
generateCursor={generateCursor}
|
||||
/>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<HoverButtons model={model} visible={!error && isCreatedByUser && !edit} onClick={() => enterEdit()}/>
|
||||
<HoverButtons
|
||||
model={model}
|
||||
visible={!error && isCreatedByUser && !edit}
|
||||
onClick={() => enterEdit()}
|
||||
/>
|
||||
</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 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 <code> 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 <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 Spinner from '../svg/Spinner';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
import ScrollToBottom from './ScrollToBottom';
|
||||
import { MultiMessage } from './Message';
|
||||
import Conversation from '../Conversations/Conversation';
|
||||
import MultiMessage from './MultiMessage';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const Messages = ({ messages }) => {
|
||||
const [currentEditId, setCurrentEditId] = useState(-1)
|
||||
const Messages = ({ messages, messageTree }) => {
|
||||
const [currentEditId, setCurrentEditId] = useState(-1);
|
||||
const { conversationId } = useSelector((state) => state.convo);
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const scrollableRef = useRef(null);
|
||||
|
|
@ -24,26 +24,6 @@ const 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 = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
setShowScrollButton(false);
|
||||
|
|
@ -79,8 +59,12 @@ const Messages = ({ messages }) => {
|
|||
onScroll={debouncedHandleScroll}
|
||||
>
|
||||
{/* <div className="flex-1 overflow-hidden"> */}
|
||||
<div className="h-full dark:gpt-dark-gray">
|
||||
<div className="flex h-full flex-col items-center text-sm dark:gpt-dark-gray">
|
||||
<div className="dark:gpt-dark-gray h-full">
|
||||
<div className="dark:gpt-dark-gray flex h-full flex-col items-center text-sm">
|
||||
{messageTree.length === 0 ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<MultiMessage
|
||||
key={conversationId} // avoid internal state mixture
|
||||
messageList={messageTree}
|
||||
|
|
@ -98,9 +82,10 @@ const Messages = ({ messages }) => {
|
|||
>
|
||||
{() => showScrollButton && <ScrollToBottom scrollHandler={scrollHandler} />}
|
||||
</CSSTransition>
|
||||
|
||||
</>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
</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 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 = [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
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