mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
refactors server route for brevity, state working, styling matching to chatgpt
This commit is contained in:
parent
dcf4f2a524
commit
acaef39d12
10 changed files with 75 additions and 53 deletions
File diff suppressed because one or more lines are too long
|
|
@ -6,18 +6,25 @@ const { askClient } = require('../../app/chatgpt-client');
|
||||||
const { saveMessage, deleteMessages } = require('../../models/Message');
|
const { saveMessage, deleteMessages } = require('../../models/Message');
|
||||||
const { saveConvo } = require('../../models/Conversation');
|
const { saveConvo } = require('../../models/Conversation');
|
||||||
|
|
||||||
|
const handleError = (res, errorMessage) => {
|
||||||
|
res.status(500).write(`event: error\ndata: ${errorMessage}`);
|
||||||
|
res.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = (res, message) => {
|
||||||
|
res.write(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
||||||
|
};
|
||||||
|
|
||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const { text, parentMessageId, conversationId } = req.body;
|
const { text, parentMessageId, conversationId } = req.body;
|
||||||
if (!text.trim().includes(' ') && text.length < 5) {
|
if (!text.trim().includes(' ') && text.length < 5) {
|
||||||
res.status(500).write('Prompt empty or too short');
|
return handleError(res, 'Prompt empty or too short');
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessageId = crypto.randomUUID();
|
const userMessageId = crypto.randomUUID();
|
||||||
let userMessage = { id: userMessageId, sender: 'User', text };
|
let userMessage = { id: userMessageId, sender: 'User', text };
|
||||||
|
|
||||||
console.log('ask log', userMessage);
|
console.log('ask log', { ...userMessage, parentMessageId, conversationId });
|
||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
Connection: 'keep-alive',
|
Connection: 'keep-alive',
|
||||||
|
|
@ -35,71 +42,58 @@ router.post('/', async (req, res) => {
|
||||||
userMessage.parentMessageId = parentMessageId ? parentMessageId : partial.id;
|
userMessage.parentMessageId = parentMessageId ? parentMessageId : partial.id;
|
||||||
userMessage.conversationId = conversationId ? conversationId : partial.conversationId;
|
userMessage.conversationId = conversationId ? conversationId : partial.conversationId;
|
||||||
await saveMessage(userMessage);
|
await saveMessage(userMessage);
|
||||||
res.write(
|
sendMessage(res, { ...partial, initial: true });
|
||||||
`event: message\ndata: ${JSON.stringify({ ...partial, initial: true })}\n\n`
|
|
||||||
);
|
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof partial === 'object') {
|
if (typeof partial === 'object') {
|
||||||
const data = JSON.stringify({ ...partial, message: true });
|
sendMessage(res, { ...partial, message: true });
|
||||||
res.write(`event: message\ndata: ${data}\n\n`);
|
|
||||||
} else {
|
} else {
|
||||||
tokens += partial;
|
tokens += partial;
|
||||||
res.write(`event: message\ndata: ${JSON.stringify({ text: tokens, message: true })}\n\n`);
|
sendMessage(res, { text: tokens, message: true });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let gptResponse = await askClient(text, progressCallback, { parentMessageId, conversationId });
|
let gptResponse = await askClient(text, progressCallback, {
|
||||||
|
parentMessageId,
|
||||||
|
conversationId
|
||||||
|
});
|
||||||
|
|
||||||
console.log('CLIENT RESPONSE', gptResponse);
|
console.log('CLIENT RESPONSE', gptResponse);
|
||||||
|
|
||||||
if (!!parentMessageId) {
|
if (!parentMessageId) {
|
||||||
gptResponse = { ...gptResponse, parentMessageId };
|
|
||||||
} else {
|
|
||||||
gptResponse.title = await titleConvo(text, gptResponse.text);
|
gptResponse.title = await titleConvo(text, gptResponse.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gptResponse.parentMessageId && !parentMessageId) {
|
if (!gptResponse.parentMessageId) {
|
||||||
userMessage.parentMessageId = gptResponse.messageId;
|
|
||||||
gptResponse.parentMessageId = gptResponse.messageId;
|
|
||||||
userMessage.conversationId = gptResponse.conversationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = gptResponse.text || gptResponse.response;
|
|
||||||
|
|
||||||
if (gptResponse.response) {
|
|
||||||
await saveMessage(userMessage);
|
|
||||||
gptResponse.text = gptResponse.response;
|
gptResponse.text = gptResponse.response;
|
||||||
gptResponse.id = gptResponse.messageId;
|
gptResponse.id = gptResponse.messageId;
|
||||||
|
gptResponse.parentMessageId = gptResponse.messageId;
|
||||||
|
userMessage.parentMessageId = parentMessageId ? parentMessageId : gptResponse.messageId;
|
||||||
|
userMessage.conversationId = conversationId ? conversationId : gptResponse.conversationId;
|
||||||
|
await saveMessage(userMessage);
|
||||||
delete gptResponse.response;
|
delete gptResponse.response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(response.includes('2023') && !response.trim().includes(' ')) ||
|
(gptResponse.text.includes('2023') && !gptResponse.text.trim().includes(' ')) ||
|
||||||
response.toLowerCase().includes('no response') ||
|
gptResponse.text.toLowerCase().includes('no response') ||
|
||||||
response.toLowerCase().includes('no answer')
|
gptResponse.text.toLowerCase().includes('no answer')
|
||||||
) {
|
) {
|
||||||
res.status(500).write('event: error\ndata: Prompt empty or too short');
|
return handleError(res, 'Prompt empty or too short');
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gptResponse.sender = 'GPT';
|
gptResponse.sender = 'GPT';
|
||||||
gptResponse.final = true;
|
gptResponse.final = true;
|
||||||
// console.log('gptResponse', gptResponse);
|
|
||||||
|
|
||||||
await saveMessage(gptResponse);
|
await saveMessage(gptResponse);
|
||||||
await saveConvo(gptResponse);
|
await saveConvo(gptResponse);
|
||||||
|
sendMessage(res, gptResponse);
|
||||||
res.write(`event: message\ndata: ${JSON.stringify(gptResponse)}\n\n`);
|
|
||||||
res.end();
|
res.end();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
await deleteMessages({ id: userMessageId });
|
await deleteMessages({ id: userMessageId });
|
||||||
res.status(500).write('event: error\ndata: ' + error.message);
|
handleError(res, error.message);
|
||||||
res.end();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
11
src/App.jsx
11
src/App.jsx
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import Messages from './components/main/Messages';
|
import Messages from './components/main/Messages';
|
||||||
import TextChat from './components/main/TextChat';
|
import TextChat from './components/main/TextChat';
|
||||||
import Nav from './components/Nav';
|
import Nav from './components/Nav';
|
||||||
import MobileNav from './components/Nav/MobileNav';
|
import MobileNav from './components/Nav/MobileNav';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
|
||||||
const { messages } = useSelector((state) => state.messages);
|
const { messages } = useSelector((state) => state.messages);
|
||||||
const { title } = useSelector((state) => state.convo);
|
const convo = useSelector((state) => state.convo);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
|
|
@ -15,10 +16,8 @@ const App = () => {
|
||||||
<div className="flex h-full w-full flex-1 flex-col bg-gray-50 md:pl-[260px]">
|
<div className="flex h-full w-full flex-1 flex-col bg-gray-50 md:pl-[260px]">
|
||||||
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden dark:bg-gray-800">
|
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden dark:bg-gray-800">
|
||||||
<MobileNav />
|
<MobileNav />
|
||||||
<Messages messages={messages} title={title}/>
|
<Messages messages={messages} title={convo.title}/>
|
||||||
<TextChat
|
<TextChat messages={messages}/>
|
||||||
messages={messages}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
|
import { useSWRConfig } from "swr"
|
||||||
import manualSWR from '~/utils/fetchers';
|
import manualSWR from '~/utils/fetchers';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { setConversation } from '~/store/convoSlice';
|
import { setConversation } from '~/store/convoSlice';
|
||||||
|
|
@ -7,6 +8,7 @@ import { setMessages } from '~/store/messageSlice';
|
||||||
|
|
||||||
export default function ClearConvos() {
|
export default function ClearConvos() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { mutate } = useSWRConfig()
|
||||||
|
|
||||||
const { trigger, isMutating } = manualSWR(
|
const { trigger, isMutating } = manualSWR(
|
||||||
'http://localhost:3050/convos/clear',
|
'http://localhost:3050/convos/clear',
|
||||||
|
|
@ -14,6 +16,7 @@ export default function ClearConvos() {
|
||||||
() => {
|
() => {
|
||||||
dispatch(setMessages([]));
|
dispatch(setMessages([]));
|
||||||
dispatch(setConversation({ error: false, conversationId: null, parentMessageId: null }));
|
dispatch(setConversation({ error: false, conversationId: null, parentMessageId: null }));
|
||||||
|
mutate('http://localhost:3050/convos');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ export default function Message({ sender, text, last = false, error = false }) {
|
||||||
'w-full border-b border-black/10 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 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blinker = isSubmitting && last && sender === 'GPT';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
<div className="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="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">
|
||||||
|
|
@ -32,7 +34,7 @@ export default function Message({ sender, text, last = false, error = false }) {
|
||||||
<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-row flex-col items-start gap-4 gap-2 whitespace-pre-wrap text-red-500">
|
<div className="flex flex min-h-[20px] flex-row 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">
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,7 +42,7 @@ export default function Message({ sender, text, last = false, error = false }) {
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
{text}
|
{text}
|
||||||
{isSubmitting && last && sender === 'GPT' && <span className="cursorBlink">█</span>}
|
{blinker && <span className="result-streaming">█</span>}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
||||||
|
import useDidMountEffect from '~/hooks/useDidMountEffect';
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
import Landing from './Landing';
|
import Landing from './Landing';
|
||||||
|
|
||||||
export default function Messages({ messages, title }) {
|
export default function Messages({ title, messages }) {
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
return <Landing title={title}/>;
|
return <Landing title={title}/>;
|
||||||
}
|
}
|
||||||
|
|
@ -18,9 +20,11 @@ export default function Messages({ messages, title }) {
|
||||||
|
|
||||||
// this useEffect triggers the following warning:
|
// this useEffect triggers the following warning:
|
||||||
// Warning: Internal React error: Expected static flag was missing.
|
// Warning: Internal React error: Expected static flag was missing.
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
scrollToBottom();
|
// scrollToBottom();
|
||||||
}, [messages]);
|
// }, [messages]);
|
||||||
|
|
||||||
|
useDidMountEffect(() => scrollToBottom(), [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 overflow-y-auto ">
|
<div className="flex-1 overflow-y-auto ">
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { setMessages } from '~/store/messageSlice';
|
||||||
import { setSubmitState } from '~/store/submitSlice';
|
import { setSubmitState } from '~/store/submitSlice';
|
||||||
import { setText } from '~/store/textSlice';
|
import { setText } from '~/store/textSlice';
|
||||||
|
|
||||||
export default function TextChat({ messages, reloadConvos }) {
|
export default function TextChat({ messages }) {
|
||||||
const [errorMessage, setErrorMessage] = useState('');
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const convo = useSelector((state) => state.convo);
|
const convo = useSelector((state) => state.convo);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice, current } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
error: false,
|
error: false,
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ export const store = configureStore({
|
||||||
messages: messageReducer,
|
messages: messageReducer,
|
||||||
text: textReducer,
|
text: textReducer,
|
||||||
submit: submitReducer,
|
submit: submitReducer,
|
||||||
}
|
},
|
||||||
|
devTools: true,
|
||||||
});
|
});
|
||||||
|
|
@ -587,4 +587,23 @@ body,html {
|
||||||
.dark .disabled\:dark\:hover\:text-gray-400:hover:disabled {
|
.dark .disabled\:dark\:hover\:text-gray-400:hover:disabled {
|
||||||
--tw-text-opacity:1;
|
--tw-text-opacity:1;
|
||||||
color:rgba(172,172,190,var(--tw-text-opacity))
|
color:rgba(172,172,190,var(--tw-text-opacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .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,
|
||||||
|
.result-streaming>ul:last-child li:last-child:after {
|
||||||
|
-webkit-animation:blink 1s steps(5,start) infinite;
|
||||||
|
animation:blink 1s steps(5,start) infinite;
|
||||||
|
content:"▋";
|
||||||
|
margin-left:.25rem;
|
||||||
|
vertical-align:baseline
|
||||||
|
} */
|
||||||
|
|
||||||
|
.result-streaming {
|
||||||
|
-webkit-animation:blink 1s steps(5,start) infinite;
|
||||||
|
animation:blink 1s steps(5,start) infinite;
|
||||||
|
/* content:"▋"; */
|
||||||
|
margin-left:.25rem;
|
||||||
|
vertical-align:baseline
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue