mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20:15 +01:00
refactor: nav and search.
feat: use recoil to replace redux feat: use react-native THIS IS NOT FINISHED. DONT USE THIS
This commit is contained in:
parent
d8ccc5b870
commit
af3d74b104
33 changed files with 1142 additions and 473 deletions
|
|
@ -1,19 +1,21 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { Provider } from 'react-redux';
|
// import { Provider } from 'react-redux';
|
||||||
import { store } from './src/store';
|
// import { store } from './src/store';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { ThemeProvider } from './src/hooks/ThemeContext';
|
import { ThemeProvider } from './src/hooks/ThemeContext';
|
||||||
import App from './src/App';
|
import App from './src/App';
|
||||||
import './src/style.css';
|
import './src/style.css';
|
||||||
import './src/mobile.css'
|
import './src/mobile.css';
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<Provider store={store}>
|
<RecoilRoot>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<App />
|
<App />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Provider>
|
</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
|
@ -36,9 +36,11 @@
|
||||||
"react-lazy-load": "^4.0.1",
|
"react-lazy-load": "^4.0.1",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
"react-redux": "^8.0.5",
|
"react-redux": "^8.0.5",
|
||||||
|
"react-router-dom": "^6.9.0",
|
||||||
"react-string-replace": "^1.1.0",
|
"react-string-replace": "^1.1.0",
|
||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.4.0",
|
||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
|
"recoil": "^0.7.7",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"rehype-raw": "^6.1.1",
|
"rehype-raw": "^6.1.1",
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,52 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Messages from './components/Messages';
|
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
|
||||||
import Landing from './components/Main/Landing';
|
import Root from './routes/Root';
|
||||||
import TextChat from './components/Main/TextChat';
|
// import Chat from './routes/Chat';
|
||||||
import Nav from './components/Nav';
|
import store from './store';
|
||||||
import MobileNav from './components/Nav/MobileNav';
|
|
||||||
import useDocumentTitle from '~/hooks/useDocumentTitle';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import userAuth from './utils/userAuth';
|
import userAuth from './utils/userAuth';
|
||||||
import { setUser } from './store/userReducer';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { setSearchState } from './store/searchSlice';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const App = () => {
|
const router = createBrowserRouter([
|
||||||
const dispatch = useDispatch();
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <Root />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: (
|
||||||
|
<Navigate
|
||||||
|
to="/chat/new"
|
||||||
|
replace={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'chat/:conversationId',
|
||||||
|
element: null //<Chat />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
const { messages, messageTree } = useSelector((state) => state.messages);
|
const App = () => {
|
||||||
const { user } = useSelector((state) => state.user);
|
const [user, setUser] = useRecoilState(store.user);
|
||||||
const { title } = useSelector((state) => state.convo);
|
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
|
||||||
const [navVisible, setNavVisible] = useState(false);
|
|
||||||
useDocumentTitle(title);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get('/api/search/enable').then((res) => { console.log(res.data); dispatch(setSearchState(res.data))});
|
axios.get('/api/search/enable').then(res => {
|
||||||
|
setIsSearchEnabled(res.data);
|
||||||
|
});
|
||||||
userAuth()
|
userAuth()
|
||||||
.then((user) => dispatch(setUser(user)))
|
.then(user => setUser(user))
|
||||||
.catch((err) => console.log(err));
|
.catch(err => console.log(err));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (user)
|
if (user)
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen">
|
<div>
|
||||||
<Nav
|
<RouterProvider router={router} />
|
||||||
navVisible={navVisible}
|
|
||||||
setNavVisible={setNavVisible}
|
|
||||||
/>
|
|
||||||
<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 bg-white dark:bg-gray-800">
|
|
||||||
<MobileNav setNavVisible={setNavVisible} />
|
|
||||||
{messages.length === 0 && title.toLowerCase() === 'chatgpt clone' ? (
|
|
||||||
<Landing title={title} />
|
|
||||||
) : (
|
|
||||||
<Messages
|
|
||||||
messages={messages}
|
|
||||||
messageTree={messageTree}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<TextChat messages={messages} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
else return <div className="flex h-screen"></div>;
|
else return <div className="flex h-screen"></div>;
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,125 @@
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
|
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import RenameButton from './RenameButton';
|
import RenameButton from './RenameButton';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import { setConversation } from '~/store/convoSlice';
|
|
||||||
import { setSubmission, setStopStream, setCustomGpt, setModel, setCustomModel } from '~/store/submitSlice';
|
|
||||||
import { setMessages, setEmptyMessage } from '~/store/messageSlice';
|
|
||||||
import { setText } from '~/store/textSlice';
|
|
||||||
import manualSWR from '~/utils/fetchers';
|
|
||||||
import ConvoIcon from '../svg/ConvoIcon';
|
import ConvoIcon from '../svg/ConvoIcon';
|
||||||
import { refreshConversation } from '../../store/convoSlice';
|
import manualSWR from '~/utils/fetchers';
|
||||||
|
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
export default function Conversation({ conversation, retainView }) {
|
||||||
|
const [currentConversation, setCurrentConversation] = useRecoilState(store.conversation);
|
||||||
|
const setMessages = useSetRecoilState(store.messages);
|
||||||
|
const setSubmission = useSetRecoilState(store.submission);
|
||||||
|
const resetLatestMessage = useResetRecoilState(store.latestMessage);
|
||||||
|
|
||||||
|
const { refreshConversations } = store.useConversations();
|
||||||
|
|
||||||
export default function Conversation({
|
|
||||||
id,
|
|
||||||
model,
|
|
||||||
parentMessageId,
|
|
||||||
conversationId,
|
|
||||||
title,
|
|
||||||
chatGptLabel = null,
|
|
||||||
promptPrefix = null,
|
|
||||||
bingData,
|
|
||||||
retainView,
|
|
||||||
}) {
|
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
const [titleInput, setTitleInput] = useState(title);
|
const [titleInput, setTitleInput] = useState(title);
|
||||||
const { stopStream } = useSelector((state) => state.submit);
|
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { trigger } = manualSWR(`/api/messages/${id}`, 'get');
|
const {
|
||||||
|
model,
|
||||||
|
parentMessageId,
|
||||||
|
conversationId,
|
||||||
|
title,
|
||||||
|
chatGptLabel = null,
|
||||||
|
promptPrefix = null,
|
||||||
|
jailbreakConversationId,
|
||||||
|
conversationSignature,
|
||||||
|
clientId,
|
||||||
|
invocationId,
|
||||||
|
toneStyle
|
||||||
|
} = conversation;
|
||||||
|
|
||||||
const rename = manualSWR(`/api/convos/update`, 'post');
|
const rename = manualSWR(`/api/convos/update`, 'post');
|
||||||
|
|
||||||
|
const bingData = conversationSignature
|
||||||
|
? {
|
||||||
|
jailbreakConversationId: jailbreakConversationId,
|
||||||
|
conversationSignature: conversationSignature,
|
||||||
|
parentMessageId: parentMessageId || null,
|
||||||
|
clientId: clientId,
|
||||||
|
invocationId: invocationId,
|
||||||
|
toneStyle: toneStyle
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
const clickHandler = async () => {
|
const clickHandler = async () => {
|
||||||
if (conversationId === id) {
|
if (currentConversation?.conversationId === conversationId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stopStream) {
|
// stop existing submission
|
||||||
dispatch(setStopStream(true));
|
setSubmission(null);
|
||||||
dispatch(setSubmission({}));
|
|
||||||
}
|
|
||||||
dispatch(setEmptyMessage());
|
|
||||||
|
|
||||||
const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
|
// set conversation to the new conversation
|
||||||
|
setCurrentConversation(conversation);
|
||||||
|
setMessages(null);
|
||||||
|
resetLatestMessage();
|
||||||
|
|
||||||
if (bingData) {
|
// if (!stopStream) {
|
||||||
const {
|
// dispatch(setStopStream(true));
|
||||||
parentMessageId,
|
// dispatch(setSubmission({}));
|
||||||
conversationSignature,
|
// }
|
||||||
jailbreakConversationId,
|
// dispatch(setEmptyMessage());
|
||||||
clientId,
|
|
||||||
invocationId,
|
|
||||||
toneStyle,
|
|
||||||
} = bingData;
|
|
||||||
dispatch(
|
|
||||||
setConversation({
|
|
||||||
...convo,
|
|
||||||
parentMessageId,
|
|
||||||
jailbreakConversationId,
|
|
||||||
conversationSignature,
|
|
||||||
clientId,
|
|
||||||
invocationId,
|
|
||||||
toneStyle,
|
|
||||||
latestMessage: null
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
dispatch(
|
|
||||||
setConversation({
|
|
||||||
...convo,
|
|
||||||
parentMessageId,
|
|
||||||
jailbreakConversationId: null,
|
|
||||||
conversationSignature: null,
|
|
||||||
clientId: null,
|
|
||||||
invocationId: null,
|
|
||||||
toneStyle: null,
|
|
||||||
latestMessage: null
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const data = await trigger();
|
|
||||||
|
|
||||||
if (chatGptLabel) {
|
// const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
|
||||||
dispatch(setModel('chatgptCustom'));
|
|
||||||
dispatch(setCustomModel(chatGptLabel.toLowerCase()));
|
|
||||||
} else {
|
|
||||||
dispatch(setModel(model));
|
|
||||||
dispatch(setCustomModel(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch(setMessages(data));
|
// if (bingData) {
|
||||||
dispatch(setCustomGpt(convo));
|
// const {
|
||||||
dispatch(setText(''));
|
// parentMessageId,
|
||||||
dispatch(setStopStream(false));
|
// conversationSignature,
|
||||||
|
// jailbreakConversationId,
|
||||||
|
// clientId,
|
||||||
|
// invocationId,
|
||||||
|
// toneStyle
|
||||||
|
// } = bingData;
|
||||||
|
// dispatch(
|
||||||
|
// setConversation({
|
||||||
|
// ...convo,
|
||||||
|
// parentMessageId,
|
||||||
|
// jailbreakConversationId,
|
||||||
|
// conversationSignature,
|
||||||
|
// clientId,
|
||||||
|
// invocationId,
|
||||||
|
// toneStyle,
|
||||||
|
// latestMessage: null
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// dispatch(
|
||||||
|
// setConversation({
|
||||||
|
// ...convo,
|
||||||
|
// parentMessageId,
|
||||||
|
// jailbreakConversationId: null,
|
||||||
|
// conversationSignature: null,
|
||||||
|
// clientId: null,
|
||||||
|
// invocationId: null,
|
||||||
|
// toneStyle: null,
|
||||||
|
// latestMessage: null
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// const data = await trigger();
|
||||||
|
|
||||||
|
// if (chatGptLabel) {
|
||||||
|
// dispatch(setModel('chatgptCustom'));
|
||||||
|
// dispatch(setCustomModel(chatGptLabel.toLowerCase()));
|
||||||
|
// } else {
|
||||||
|
// dispatch(setModel(model));
|
||||||
|
// dispatch(setCustomModel(null));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// dispatch(setMessages(data));
|
||||||
|
// dispatch(setCustomGpt(convo));
|
||||||
|
// dispatch(setText(''));
|
||||||
|
// dispatch(setStopStream(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const renameHandler = (e) => {
|
const renameHandler = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setTitleInput(title);
|
setTitleInput(title);
|
||||||
setRenaming(true);
|
setRenaming(true);
|
||||||
|
|
@ -102,24 +128,28 @@ export default function Conversation({
|
||||||
}, 25);
|
}, 25);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelHandler = (e) => {
|
const cancelHandler = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setRenaming(false);
|
setRenaming(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRename = (e) => {
|
const onRename = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setRenaming(false);
|
setRenaming(false);
|
||||||
if (titleInput === title) {
|
if (titleInput === title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rename.trigger({ conversationId, title: titleInput })
|
rename.trigger({ conversationId, title: titleInput }).then(() => {
|
||||||
.then(() => {
|
refreshConversations();
|
||||||
dispatch(refreshConversation())
|
if (conversationId == currentConversation?.conversationId)
|
||||||
});
|
setCurrentConversation(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
title: titleInput
|
||||||
|
}));
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e) => {
|
const handleKeyDown = e => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
onRename(e);
|
onRename(e);
|
||||||
}
|
}
|
||||||
|
|
@ -130,7 +160,7 @@ export default function Conversation({
|
||||||
'animate-flash group relative flex cursor-pointer items-center gap-3 break-all rounded-md bg-gray-800 py-3 px-3 pr-14 hover:bg-gray-800'
|
'animate-flash group relative flex cursor-pointer items-center gap-3 break-all rounded-md bg-gray-800 py-3 px-3 pr-14 hover:bg-gray-800'
|
||||||
};
|
};
|
||||||
|
|
||||||
if (conversationId !== id) {
|
if (currentConversation?.conversationId !== conversationId) {
|
||||||
aProps.className =
|
aProps.className =
|
||||||
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-[#2A2B32] hover:pr-4';
|
'group relative flex cursor-pointer items-center gap-3 break-all rounded-md py-3 px-3 hover:bg-[#2A2B32] hover:pr-4';
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +178,7 @@ export default function Conversation({
|
||||||
type="text"
|
type="text"
|
||||||
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
|
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
|
||||||
value={titleInput}
|
value={titleInput}
|
||||||
onChange={(e) => setTitleInput(e.target.value)}
|
onChange={e => setTitleInput(e.target.value)}
|
||||||
onBlur={onRename}
|
onBlur={onRename}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|
@ -156,16 +186,16 @@ export default function Conversation({
|
||||||
title
|
title
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{conversationId === id ? (
|
{currentConversation?.conversationId === conversationId ? (
|
||||||
<div className="visible absolute right-1 z-10 flex text-gray-300">
|
<div className="visible absolute right-1 z-10 flex text-gray-300">
|
||||||
<RenameButton
|
<RenameButton
|
||||||
conversationId={id}
|
conversationId={conversationId}
|
||||||
renaming={renaming}
|
renaming={renaming}
|
||||||
renameHandler={renameHandler}
|
renameHandler={renameHandler}
|
||||||
onRename={onRename}
|
onRename={onRename}
|
||||||
/>
|
/>
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
conversationId={id}
|
conversationId={conversationId}
|
||||||
renaming={renaming}
|
renaming={renaming}
|
||||||
cancelHandler={cancelHandler}
|
cancelHandler={cancelHandler}
|
||||||
retainView={retainView}
|
retainView={retainView}
|
||||||
|
|
|
||||||
|
|
@ -2,24 +2,19 @@ import React from 'react';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
import CrossIcon from '../svg/CrossIcon';
|
import CrossIcon from '../svg/CrossIcon';
|
||||||
import manualSWR from '~/utils/fetchers';
|
import manualSWR from '~/utils/fetchers';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { setNewConvo, removeConvo } from '~/store/convoSlice';
|
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import store from '~/store';
|
||||||
import { setSubmission } from '~/store/submitSlice';
|
|
||||||
|
|
||||||
export default function DeleteButton({ conversationId, renaming, cancelHandler, retainView }) {
|
export default function DeleteButton({ conversationId, renaming, cancelHandler, retainView }) {
|
||||||
const dispatch = useDispatch();
|
const currentConversation = useRecoilValue(store.conversation) || {};
|
||||||
const { trigger } = manualSWR(
|
const { newConversation } = store.useConversation();
|
||||||
`/api/convos/clear`,
|
const { refreshConversations } = store.useConversations();
|
||||||
'post',
|
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
|
||||||
() => {
|
if (currentConversation?.conversationId == conversationId) newConversation();
|
||||||
dispatch(setMessages([]));
|
refreshConversations();
|
||||||
dispatch(removeConvo(conversationId));
|
retainView();
|
||||||
dispatch(setNewConvo());
|
});
|
||||||
dispatch(setSubmission({}));
|
|
||||||
retainView();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const clickHandler = () => trigger({ conversationId });
|
const clickHandler = () => trigger({ conversationId });
|
||||||
const handler = renaming ? cancelHandler : clickHandler;
|
const handler = renaming ? cancelHandler : clickHandler;
|
||||||
|
|
@ -29,7 +24,7 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
|
||||||
className="p-1 hover:text-white"
|
className="p-1 hover:text-white"
|
||||||
onClick={handler}
|
onClick={handler}
|
||||||
>
|
>
|
||||||
{ renaming ? <CrossIcon/> : <TrashIcon />}
|
{renaming ? <CrossIcon /> : <TrashIcon />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default function Pages({ pageNumber, pages, nextPage, previousPage }) {
|
export default function Pages({ pageNumber, pages, nextPage, previousPage }) {
|
||||||
const clickHandler = (func) => async (e) => {
|
const clickHandler = func => async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await func();
|
await func();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return pageNumber == 1 && pages == 1 ? null : (
|
||||||
<div className="m-auto mt-4 mb-2 flex items-center justify-center gap-2">
|
<div className="m-auto mt-4 mb-2 flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={clickHandler(previousPage)}
|
onClick={clickHandler(previousPage)}
|
||||||
|
|
|
||||||
|
|
@ -2,34 +2,15 @@ import React from 'react';
|
||||||
import Conversation from './Conversation';
|
import Conversation from './Conversation';
|
||||||
|
|
||||||
export default function Conversations({ conversations, conversationId, moveToTop }) {
|
export default function Conversations({ conversations, conversationId, moveToTop }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{conversations &&
|
{conversations &&
|
||||||
conversations.length > 0 &&
|
conversations.length > 0 &&
|
||||||
conversations.map((convo) => {
|
conversations.map(convo => {
|
||||||
const bingData = convo.conversationSignature
|
|
||||||
? {
|
|
||||||
jailbreakConversationId: convo.jailbreakConversationId,
|
|
||||||
conversationSignature: convo.conversationSignature,
|
|
||||||
parentMessageId: convo.parentMessageId || null,
|
|
||||||
clientId: convo.clientId,
|
|
||||||
invocationId: convo.invocationId,
|
|
||||||
toneStyle: convo.toneStyle,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Conversation
|
<Conversation
|
||||||
key={convo.conversationId}
|
key={convo.conversationId}
|
||||||
id={convo.conversationId}
|
conversation={convo}
|
||||||
model={convo.model}
|
|
||||||
parentMessageId={convo.parentMessageId}
|
|
||||||
title={convo.title}
|
|
||||||
conversationId={conversationId}
|
|
||||||
chatGptLabel={convo.chatGptLabel}
|
|
||||||
promptPrefix={convo.promptPrefix}
|
|
||||||
bingData={bingData}
|
|
||||||
retainView={moveToTop}
|
retainView={moveToTop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
305
client/src/components/MessageHandler/index.jsx
Normal file
305
client/src/components/MessageHandler/index.jsx
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
|
||||||
|
import { SSE } from "~/utils/sse";
|
||||||
|
import { useMessageHandler } from "../../utils/handleSubmit";
|
||||||
|
import createPayload from "~/utils/createPayload";
|
||||||
|
|
||||||
|
import store from "~/store";
|
||||||
|
|
||||||
|
export default function MessageHandler({ messages }) {
|
||||||
|
const [submission, setSubmission] = useRecoilState(store.submission);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmitting);
|
||||||
|
const setMessages = useSetRecoilState(store.messages);
|
||||||
|
const setConversation = useSetRecoilState(store.conversation);
|
||||||
|
const resetLatestMessage = useResetRecoilState(store.latestMessage);
|
||||||
|
|
||||||
|
const { refreshConversations } = store.useConversations();
|
||||||
|
|
||||||
|
const messageHandler = (data, submission) => {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
message,
|
||||||
|
initialResponse,
|
||||||
|
isRegenerate = false,
|
||||||
|
} = submission;
|
||||||
|
|
||||||
|
if (isRegenerate)
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
...initialResponse,
|
||||||
|
text: data,
|
||||||
|
parentMessageId: message?.overrideParentMessageId,
|
||||||
|
messageId: message?.overrideParentMessageId + "_",
|
||||||
|
submitting: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
else
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
...initialResponse,
|
||||||
|
text: data,
|
||||||
|
parentMessageId: message?.messageId,
|
||||||
|
messageId: message?.messageId + "_",
|
||||||
|
submitting: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelHandler = (data, submission) => {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
message,
|
||||||
|
initialResponse,
|
||||||
|
isRegenerate = false,
|
||||||
|
} = submission;
|
||||||
|
|
||||||
|
if (isRegenerate)
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
...initialResponse,
|
||||||
|
text: data,
|
||||||
|
parentMessageId: message?.overrideParentMessageId,
|
||||||
|
messageId: message?.overrideParentMessageId + "_",
|
||||||
|
cancelled: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
else
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
...initialResponse,
|
||||||
|
text: data,
|
||||||
|
parentMessageId: message?.messageId,
|
||||||
|
messageId: message?.messageId + "_",
|
||||||
|
cancelled: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdHandler = (data, submission) => {
|
||||||
|
const {
|
||||||
|
messages,
|
||||||
|
message,
|
||||||
|
initialResponse,
|
||||||
|
isRegenerate = false,
|
||||||
|
} = submission;
|
||||||
|
|
||||||
|
if (isRegenerate)
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
...initialResponse,
|
||||||
|
parentMessageId: message?.overrideParentMessageId,
|
||||||
|
messageId: message?.overrideParentMessageId + "_",
|
||||||
|
submitting: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
else
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
...initialResponse,
|
||||||
|
parentMessageId: message?.messageId,
|
||||||
|
messageId: message?.messageId + "_",
|
||||||
|
submitting: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { conversationId } = message;
|
||||||
|
setConversation((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
conversationId,
|
||||||
|
}));
|
||||||
|
resetLatestMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalHandler = (data, submission) => {
|
||||||
|
const {
|
||||||
|
conversation,
|
||||||
|
messages,
|
||||||
|
message,
|
||||||
|
initialResponse,
|
||||||
|
isRegenerate = false,
|
||||||
|
} = submission;
|
||||||
|
|
||||||
|
const { requestMessage, responseMessage } = data;
|
||||||
|
const { conversationId } = requestMessage;
|
||||||
|
|
||||||
|
// update the messages
|
||||||
|
if (isRegenerate) setMessages([...messages, responseMessage]);
|
||||||
|
else setMessages([...messages, requestMessage, responseMessage]);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
|
||||||
|
// refresh title
|
||||||
|
if (
|
||||||
|
requestMessage.parentMessageId == "00000000-0000-0000-0000-000000000000"
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshConversations();
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// in case it takes too long.
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshConversations();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { model, chatGptLabel, promptPrefix } = conversation;
|
||||||
|
const isBing = model === "bingai" || model === "sydney";
|
||||||
|
|
||||||
|
if (!isBing) {
|
||||||
|
const { title } = data;
|
||||||
|
const { conversationId } = responseMessage;
|
||||||
|
setConversation((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
title,
|
||||||
|
conversationId,
|
||||||
|
jailbreakConversationId: null,
|
||||||
|
conversationSignature: null,
|
||||||
|
clientId: null,
|
||||||
|
invocationId: null,
|
||||||
|
chatGptLabel,
|
||||||
|
promptPrefix,
|
||||||
|
latestMessage: null,
|
||||||
|
}));
|
||||||
|
} else if (model === "bingai") {
|
||||||
|
const { title } = data;
|
||||||
|
const { conversationSignature, clientId, conversationId, invocationId } =
|
||||||
|
responseMessage;
|
||||||
|
setConversation((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
title,
|
||||||
|
conversationId,
|
||||||
|
jailbreakConversationId: null,
|
||||||
|
conversationSignature,
|
||||||
|
clientId,
|
||||||
|
invocationId,
|
||||||
|
chatGptLabel,
|
||||||
|
promptPrefix,
|
||||||
|
latestMessage: null,
|
||||||
|
}));
|
||||||
|
} else if (model === "sydney") {
|
||||||
|
const { title } = data;
|
||||||
|
const {
|
||||||
|
jailbreakConversationId,
|
||||||
|
parentMessageId,
|
||||||
|
conversationSignature,
|
||||||
|
clientId,
|
||||||
|
conversationId,
|
||||||
|
invocationId,
|
||||||
|
} = responseMessage;
|
||||||
|
setConversation((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
title,
|
||||||
|
conversationId,
|
||||||
|
jailbreakConversationId,
|
||||||
|
conversationSignature,
|
||||||
|
clientId,
|
||||||
|
invocationId,
|
||||||
|
chatGptLabel,
|
||||||
|
promptPrefix,
|
||||||
|
latestMessage: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorHandler = (data, submission) => {
|
||||||
|
const {
|
||||||
|
conversation,
|
||||||
|
messages,
|
||||||
|
message,
|
||||||
|
initialResponse,
|
||||||
|
isRegenerate = false,
|
||||||
|
} = submission;
|
||||||
|
|
||||||
|
console.log("Error:", data);
|
||||||
|
const errorResponse = {
|
||||||
|
...data,
|
||||||
|
error: true,
|
||||||
|
parentMessageId: message?.messageId,
|
||||||
|
};
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setMessages([...messages, message, errorResponse]);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (submission === null) return;
|
||||||
|
if (Object.keys(submission).length === 0) return;
|
||||||
|
|
||||||
|
const { messages, initialResponse, isRegenerate = false } = submission;
|
||||||
|
let { message } = submission;
|
||||||
|
|
||||||
|
const { server, payload } = createPayload(submission);
|
||||||
|
|
||||||
|
const events = new SSE(server, {
|
||||||
|
payload: JSON.stringify(payload),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
let latestResponseText = "";
|
||||||
|
events.onmessage = (e) => {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (data.final) {
|
||||||
|
finalHandler(data, { ...submission, message });
|
||||||
|
console.log("final", data);
|
||||||
|
}
|
||||||
|
if (data.created) {
|
||||||
|
message = {
|
||||||
|
...data.message,
|
||||||
|
model: message?.model,
|
||||||
|
chatGptLabel: message?.chatGptLabel,
|
||||||
|
promptPrefix: message?.promptPrefix,
|
||||||
|
overrideParentMessageId: message?.overrideParentMessageId,
|
||||||
|
};
|
||||||
|
createdHandler(data, { ...submission, message });
|
||||||
|
console.log("created", message);
|
||||||
|
} else {
|
||||||
|
let text = data.text || data.response;
|
||||||
|
if (data.initial) console.log(data);
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
latestResponseText = text;
|
||||||
|
messageHandler(text, { ...submission, message });
|
||||||
|
}
|
||||||
|
// console.log('dataStream', data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
events.onopen = () => console.log("connection is opened");
|
||||||
|
|
||||||
|
events.oncancel = (e) =>
|
||||||
|
cancelHandler(latestResponseText, { ...submission, message });
|
||||||
|
|
||||||
|
events.onerror = function (e) {
|
||||||
|
console.log("error in opening conn.");
|
||||||
|
events.close();
|
||||||
|
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
|
||||||
|
errorHandler(data, { ...submission, message });
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
events.stream();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const isCancelled = events.readyState <= 1;
|
||||||
|
events.close();
|
||||||
|
if (isCancelled) {
|
||||||
|
const e = new Event("cancel");
|
||||||
|
events.dispatchEvent(e);
|
||||||
|
}
|
||||||
|
setIsSubmitting(false);
|
||||||
|
};
|
||||||
|
}, [submission]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
@ -2,50 +2,31 @@ import React from 'react';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
import { useSWRConfig } from 'swr';
|
import { useSWRConfig } from 'swr';
|
||||||
import manualSWR from '~/utils/fetchers';
|
import manualSWR from '~/utils/fetchers';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { setNewConvo, removeAll } from '~/store/convoSlice';
|
import store from '~/store';
|
||||||
import { setMessages } from '~/store/messageSlice';
|
|
||||||
import { setSubmission } from '~/store/submitSlice';
|
|
||||||
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
|
|
||||||
import DialogTemplate from '../ui/DialogTemplate';
|
|
||||||
|
|
||||||
export default function ClearConvos() {
|
export default function ClearConvos() {
|
||||||
const dispatch = useDispatch();
|
const { newConversation } = store.useConversation();
|
||||||
|
const { refreshConversations } = store.useConversations();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
|
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
|
||||||
dispatch(setMessages([]));
|
newConversation();
|
||||||
dispatch(setNewConvo());
|
refreshConversations();
|
||||||
dispatch(setSubmission({}));
|
|
||||||
mutate(`/api/convos`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
console.log('Clearing conversations...');
|
console.log('Clearing conversations...');
|
||||||
dispatch(removeAll());
|
|
||||||
trigger({});
|
trigger({});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<a
|
<a
|
||||||
className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
|
||||||
// onClick={clickHandler}
|
onClick={clickHandler}
|
||||||
>
|
>
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
Clear conversations
|
Clear conversations
|
||||||
</a>
|
</a>
|
||||||
</DialogTrigger>
|
|
||||||
<DialogTemplate
|
|
||||||
title="Clear conversations"
|
|
||||||
description="Are you sure you want to clear all conversations? This is irreversible."
|
|
||||||
selection={{
|
|
||||||
selectHandler: clickHandler,
|
|
||||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
|
||||||
selectText: 'Clear',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useState, useContext } from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import LogOutIcon from '../svg/LogOutIcon';
|
import LogOutIcon from '../svg/LogOutIcon';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
export default function Logout() {
|
export default function Logout() {
|
||||||
const { user } = useSelector((state) => state.user);
|
const user = useRecoilValue(store.user);
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
window.location.href = "/auth/logout";
|
window.location.href = '/auth/logout';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { setNewConvo } from '~/store/convoSlice';
|
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import store from '~/store';
|
||||||
import { setSubmission } from '~/store/submitSlice';
|
|
||||||
import { setText } from '~/store/textSlice';
|
|
||||||
|
|
||||||
export default function MobileNav({ setNavVisible }) {
|
export default function MobileNav({ setNavVisible }) {
|
||||||
const dispatch = useDispatch();
|
const conversation = useRecoilValue(store.conversation);
|
||||||
const { conversationId, convos, title } = useSelector((state) => state.convo);
|
const { newConversation } = store.useConversation();
|
||||||
|
const { title = 'New Chat' } = conversation || {};
|
||||||
const toggleNavVisible = () => {
|
|
||||||
setNavVisible((prev) => {
|
|
||||||
return !prev
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const newConvo = () => {
|
|
||||||
dispatch(setText(''));
|
|
||||||
dispatch(setMessages([]));
|
|
||||||
dispatch(setNewConvo());
|
|
||||||
dispatch(setSubmission({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed top-0 left-0 right-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
|
<div className="fixed top-0 left-0 right-0 z-10 flex items-center border-b border-white/20 bg-gray-800 pl-1 pt-1 text-gray-200 sm:pl-3 md:hidden">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white"
|
className="-ml-0.5 -mt-0.5 inline-flex h-10 w-10 items-center justify-center rounded-md hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white dark:hover:text-white"
|
||||||
onClick={toggleNavVisible}
|
onClick={() => setNavVisible(prev => !prev)}
|
||||||
>
|
>
|
||||||
<span className="sr-only">Open sidebar</span>
|
<span className="sr-only">Open sidebar</span>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -66,7 +52,7 @@ export default function MobileNav({ setNavVisible }) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="px-3"
|
className="px-3"
|
||||||
onClick={newConvo}
|
onClick={() => newConversation()}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,17 @@ import SearchBar from './SearchBar';
|
||||||
import ClearConvos from './ClearConvos';
|
import ClearConvos from './ClearConvos';
|
||||||
import DarkMode from './DarkMode';
|
import DarkMode from './DarkMode';
|
||||||
import Logout from './Logout';
|
import Logout from './Logout';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
|
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
|
||||||
const { searchEnabled } = useSelector((state) => state.search);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ !!searchEnabled && <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/>}
|
{!!isSearchEnabled && (
|
||||||
|
<SearchBar
|
||||||
|
fetch={fetch}
|
||||||
|
onSuccess={onSearchSuccess}
|
||||||
|
clearSearch={clearSearch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<DarkMode />
|
<DarkMode />
|
||||||
<ClearConvos />
|
<ClearConvos />
|
||||||
<Logout />
|
<Logout />
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import store from '~/store';
|
||||||
import { setNewConvo, refreshConversation } from '~/store/convoSlice';
|
|
||||||
import { setMessages } from '~/store/messageSlice';
|
|
||||||
import { setSubmission, setDisabled } from '~/store/submitSlice';
|
|
||||||
import { setText } from '~/store/textSlice';
|
|
||||||
import { setInputValue, setQuery } from '~/store/searchSlice';
|
|
||||||
|
|
||||||
export default function NewChat() {
|
export default function NewChat() {
|
||||||
const dispatch = useDispatch();
|
const { newConversation } = store.useConversation();
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
dispatch(setText(''));
|
// dispatch(setInputValue(''));
|
||||||
dispatch(setMessages([]));
|
// dispatch(setQuery(''));
|
||||||
dispatch(setNewConvo());
|
newConversation();
|
||||||
dispatch(refreshConversation());
|
|
||||||
dispatch(setSubmission({}));
|
|
||||||
dispatch(setDisabled(false));
|
|
||||||
dispatch(setInputValue(''));
|
|
||||||
dispatch(setQuery(''));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,44 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { setInputValue, setQuery } from '~/store/searchSlice';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
export default function SearchBar({ fetch, clearSearch }) {
|
export default function SearchBar({ fetch, clearSearch }) {
|
||||||
const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
const { inputValue } = useSelector((state) => state.search);
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const setSearchQuery = useSetRecoilState(store.searchQuery);
|
||||||
|
|
||||||
// const [inputValue, setInputValue] = useState('');
|
// const [inputValue, setInputValue] = useState('');
|
||||||
|
|
||||||
const debouncedChangeHandler = useCallback(
|
const debouncedChangeHandler = useCallback(
|
||||||
debounce((q) => {
|
debounce(q => {
|
||||||
dispatch(setQuery(q));
|
setSearchQuery(q);
|
||||||
if (q.length > 0) {
|
if (q.length > 0) {
|
||||||
fetch(q, 1);
|
fetch(q, 1);
|
||||||
}
|
}
|
||||||
}, 750),
|
}, 750),
|
||||||
[dispatch]
|
[setSearchQuery]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyUp = (e) => {
|
const handleKeyUp = e => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
if (e.keyCode === 8 && value === '') {
|
if (e.keyCode === 8 && value === '') {
|
||||||
// Value after clearing input: ""
|
// Value after clearing input: ""
|
||||||
console.log(`Value after clearing input: "${value}"`);
|
console.log(`Value after clearing input: "${value}"`);
|
||||||
dispatch(setQuery(''));
|
setSearchQuery('');
|
||||||
clearSearch();
|
clearSearch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeHandler = (e) => {
|
const changeHandler = e => {
|
||||||
let q = e.target.value;
|
let q = e.target.value;
|
||||||
dispatch(setInputValue(q));
|
setInputValue(q);
|
||||||
q = q.trim();
|
q = q.trim();
|
||||||
|
|
||||||
if (q === '') {
|
if (q === '') {
|
||||||
dispatch(setQuery(''));
|
setSearchQuery('');
|
||||||
clearSearch();
|
clearSearch();
|
||||||
} else {
|
} else {
|
||||||
debouncedChangeHandler(q);
|
debouncedChangeHandler(q);
|
||||||
|
|
|
||||||
|
|
@ -6,67 +6,121 @@ import Pages from '../Conversations/Pages';
|
||||||
import Conversations from '../Conversations';
|
import Conversations from '../Conversations';
|
||||||
import NavLinks from './NavLinks';
|
import NavLinks from './NavLinks';
|
||||||
import { searchFetcher, swr } from '~/utils/fetchers';
|
import { searchFetcher, swr } from '~/utils/fetchers';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { setConvos, setNewConvo, refreshConversation } from '~/store/convoSlice';
|
|
||||||
import { setMessages } from '~/store/messageSlice';
|
import store from '~/store';
|
||||||
import { setDisabled } from '~/store/submitSlice';
|
|
||||||
|
|
||||||
export default function Nav({ navVisible, setNavVisible }) {
|
export default function Nav({ navVisible, setNavVisible }) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const [isHovering, setIsHovering] = useState(false);
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const scrollPositionRef = useRef(null);
|
||||||
|
|
||||||
|
// const dispatch = useDispatch();
|
||||||
|
const [conversations, setConversations] = useState([]);
|
||||||
|
// current page
|
||||||
|
const [pageNumber, setPageNumber] = useState(1);
|
||||||
|
// total pages
|
||||||
const [pages, setPages] = useState(1);
|
const [pages, setPages] = useState(1);
|
||||||
const [pageNumber, setPage] = useState(1);
|
|
||||||
const { search, query } = useSelector((state) => state.search);
|
// search
|
||||||
const { conversationId, convos, refreshConvoHint } = useSelector((state) => state.convo);
|
const searchQuery = useRecoilValue(store.searchQuery);
|
||||||
|
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
|
||||||
|
const isSearching = useRecoilValue(store.isSearching);
|
||||||
|
const { newConversation } = store.useConversation();
|
||||||
|
|
||||||
|
// current conversation
|
||||||
|
const conversation = useRecoilValue(store.conversation);
|
||||||
|
const { conversationId } = conversation || {};
|
||||||
|
const setMessages = useSetRecoilState(store.messages);
|
||||||
|
|
||||||
|
// refreshConversationsHint is used for other components to ask refresh of Nav
|
||||||
|
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
|
||||||
|
|
||||||
|
const { refreshConversations } = store.useConversations();
|
||||||
|
|
||||||
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
|
||||||
const onSuccess = (data, searchFetch = false) => {
|
const onSuccess = (data, searchFetch = false) => {
|
||||||
if (search) {
|
if (isSearching) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { conversations, pages } = data;
|
let { conversations, pages } = data;
|
||||||
if (pageNumber > pages) {
|
if (pageNumber > pages) {
|
||||||
setPage(pages);
|
setPageNumber(pages);
|
||||||
} else {
|
} else {
|
||||||
dispatch(setConvos({ convos: conversations, searchFetch }));
|
if (!searchFetch)
|
||||||
|
conversations = conversations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
setConversations(conversations);
|
||||||
setPages(pages);
|
setPages(pages);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSearchSuccess = (data, expectedPage) => {
|
const onSearchSuccess = (data, expectedPage) => {
|
||||||
const res = data;
|
const res = data;
|
||||||
dispatch(setConvos({ convos: res.conversations, searchFetch: true }));
|
setConversations(res.conversations);
|
||||||
if (expectedPage) {
|
if (expectedPage) {
|
||||||
setPage(expectedPage);
|
setPageNumber(expectedPage);
|
||||||
}
|
}
|
||||||
setPage(res.pageNumber);
|
setPageNumber(res.pageNumber);
|
||||||
setPages(res.pages);
|
setPages(res.pages);
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
if (res.messages?.length > 0) {
|
if (res.messages?.length > 0) {
|
||||||
dispatch(setMessages(res.messages));
|
setMessages(res.messages);
|
||||||
dispatch(setDisabled(true));
|
// dispatch(setDisabled(true));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetch = useCallback(_.partialRight(searchFetcher.bind(null, () => setIsFetching(true)), onSearchSuccess), [dispatch]);
|
// TODO: dont need this
|
||||||
|
const fetch = useCallback(
|
||||||
|
_.partialRight(
|
||||||
|
searchFetcher.bind(null, () => setIsFetching(true)),
|
||||||
|
onSearchSuccess
|
||||||
|
),
|
||||||
|
[setIsFetching]
|
||||||
|
);
|
||||||
|
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
setPage(1);
|
setPageNumber(1);
|
||||||
dispatch(refreshConversation());
|
refreshConversations();
|
||||||
if (!conversationId) {
|
if (conversationId == 'search') {
|
||||||
dispatch(setNewConvo());
|
newConversation();
|
||||||
dispatch(setMessages([]));
|
|
||||||
}
|
}
|
||||||
dispatch(setDisabled(false));
|
// dispatch(setDisabled(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
|
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
|
||||||
revalidateOnMount: false,
|
revalidateOnMount: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
const nextPage = async () => {
|
||||||
const scrollPositionRef = useRef(null);
|
moveToTop();
|
||||||
|
|
||||||
|
if (!isSearching) {
|
||||||
|
setPageNumber(prev => prev + 1);
|
||||||
|
await mutate();
|
||||||
|
} else {
|
||||||
|
await fetch(searchQuery, +pageNumber + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previousPage = async () => {
|
||||||
|
moveToTop();
|
||||||
|
|
||||||
|
if (!isSearching) {
|
||||||
|
setPageNumber(prev => prev - 1);
|
||||||
|
await mutate();
|
||||||
|
} else {
|
||||||
|
await fetch(searchQuery, +pageNumber - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSearching) {
|
||||||
|
mutate();
|
||||||
|
}
|
||||||
|
}, [pageNumber, conversationId, refreshConversationsHint]);
|
||||||
|
|
||||||
const moveToTop = () => {
|
const moveToTop = () => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
|
|
@ -75,35 +129,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextPage = async () => {
|
const moveTo = () => {
|
||||||
moveToTop();
|
|
||||||
|
|
||||||
if (!search) {
|
|
||||||
setPage((prev) => prev + 1);
|
|
||||||
await mutate();
|
|
||||||
} else {
|
|
||||||
await fetch(query, +pageNumber + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const previousPage = async () => {
|
|
||||||
moveToTop();
|
|
||||||
|
|
||||||
if (!search) {
|
|
||||||
setPage((prev) => prev - 1);
|
|
||||||
await mutate();
|
|
||||||
} else {
|
|
||||||
await fetch(query, +pageNumber - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!search) {
|
|
||||||
mutate();
|
|
||||||
}
|
|
||||||
}, [pageNumber, conversationId, refreshConvoHint]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
|
|
||||||
if (container && scrollPositionRef.current !== null) {
|
if (container && scrollPositionRef.current !== null) {
|
||||||
|
|
@ -112,18 +138,20 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
|
|
||||||
container.scrollTop = Math.min(maxScrollTop, scrollPositionRef.current);
|
container.scrollTop = Math.min(maxScrollTop, scrollPositionRef.current);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNavVisible = () => {
|
||||||
|
setNavVisible(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
moveTo();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNavVisible(false);
|
setNavVisible(false);
|
||||||
}, [conversationId]);
|
}, [conversationId]);
|
||||||
|
|
||||||
const toggleNavVisible = () => {
|
|
||||||
setNavVisible((prev) => {
|
|
||||||
return !prev;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerClasses =
|
const containerClasses =
|
||||||
isLoading && pageNumber === 1
|
isLoading && pageNumber === 1
|
||||||
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
|
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
|
||||||
|
|
@ -151,11 +179,11 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
>
|
>
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{/* {(isLoading && pageNumber === 1) ? ( */}
|
{/* {(isLoading && pageNumber === 1) ? ( */}
|
||||||
{(isLoading && pageNumber === 1) || (isFetching) ? (
|
{(isLoading && pageNumber === 1) || isFetching ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<Conversations
|
<Conversations
|
||||||
conversations={convos}
|
conversations={conversations}
|
||||||
conversationId={conversationId}
|
conversationId={conversationId}
|
||||||
moveToTop={moveToTop}
|
moveToTop={moveToTop}
|
||||||
/>
|
/>
|
||||||
|
|
@ -172,6 +200,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
fetch={fetch}
|
fetch={fetch}
|
||||||
onSearchSuccess={onSearchSuccess}
|
onSearchSuccess={onSearchSuccess}
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
|
isSearchEnabled={isSearchEnabled}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
67
client/src/routes/Chat.jsx
Normal file
67
client/src/routes/Chat.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
|
||||||
|
|
||||||
|
import Landing from "../components/ui/Landing";
|
||||||
|
import Messages from "../components/Messages";
|
||||||
|
import TextChat from "../components/Input";
|
||||||
|
|
||||||
|
import store from "~/store";
|
||||||
|
import manualSWR from "~/utils/fetchers";
|
||||||
|
// import TextChat from './components/Main/TextChat';
|
||||||
|
|
||||||
|
// {/* <TextChat messages={messages} /> */}
|
||||||
|
|
||||||
|
export default function Chat() {
|
||||||
|
const [conversation, setConversation] = useRecoilState(store.conversation);
|
||||||
|
const setMessages = useSetRecoilState(store.messages);
|
||||||
|
const messagesTree = useRecoilValue(store.messagesTree);
|
||||||
|
const { newConversation } = store.useConversation();
|
||||||
|
const { conversationId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { trigger: messagesTrigger } = manualSWR(
|
||||||
|
`/api/messages/${conversation?.conversationId}`,
|
||||||
|
"get"
|
||||||
|
);
|
||||||
|
|
||||||
|
const { trigger: conversationTrigger } = manualSWR(
|
||||||
|
`/api/convos/${conversationId}`,
|
||||||
|
"get"
|
||||||
|
);
|
||||||
|
|
||||||
|
// when conversation changed or conversationId (in url) changed
|
||||||
|
useEffect(() => {
|
||||||
|
if (conversation === null) {
|
||||||
|
// no current conversation, we need to do something
|
||||||
|
if (conversationId == "new") {
|
||||||
|
// create new
|
||||||
|
newConversation();
|
||||||
|
} else {
|
||||||
|
// fetch it from server
|
||||||
|
conversationTrigger().then(setConversation);
|
||||||
|
setMessages(null);
|
||||||
|
console.log("NEED TO FETCH DATA");
|
||||||
|
}
|
||||||
|
} else if (conversation?.conversationId !== conversationId)
|
||||||
|
// conversationId (in url) should always follow conversation?.conversationId, unless conversation is null
|
||||||
|
navigate(`/chat/${conversation?.conversationId}`);
|
||||||
|
}, [conversation, conversationId]);
|
||||||
|
|
||||||
|
// when messagesTree is null (<=> messages is null)
|
||||||
|
// we need to fetch message list from server
|
||||||
|
useEffect(() => {
|
||||||
|
if (messagesTree === null) {
|
||||||
|
messagesTrigger().then(setMessages);
|
||||||
|
}
|
||||||
|
}, [conversation?.conversationId]);
|
||||||
|
|
||||||
|
if (conversation?.conversationId !== conversationId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{conversationId == "new" ? <Landing /> : <Messages />}
|
||||||
|
<TextChat />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
client/src/routes/Root.jsx
Normal file
29
client/src/routes/Root.jsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import MessageHandler from '../components/MessageHandler';
|
||||||
|
import Nav from '../components/Nav';
|
||||||
|
import MobileNav from '../components/Nav/MobileNav';
|
||||||
|
|
||||||
|
export default function Root() {
|
||||||
|
const [navVisible, setNavVisible] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex h-screen">
|
||||||
|
<Nav
|
||||||
|
navVisible={navVisible}
|
||||||
|
setNavVisible={setNavVisible}
|
||||||
|
/>
|
||||||
|
<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 bg-white pt-10 dark:bg-gray-800 md:pt-0">
|
||||||
|
<MobileNav setNavVisible={setNavVisible} />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MessageHandler />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
106
client/src/store/conversation.js
Normal file
106
client/src/store/conversation.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import models from './models';
|
||||||
|
import { atom, selector, useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
|
||||||
|
import buildTree from '~/utils/buildTree';
|
||||||
|
|
||||||
|
// current conversation, can be null (need to be fetched from server)
|
||||||
|
// sample structure
|
||||||
|
// {
|
||||||
|
// conversationId: "new",
|
||||||
|
// title: "New Chat",
|
||||||
|
// jailbreakConversationId: null,
|
||||||
|
// conversationSignature: null,
|
||||||
|
// clientId: null,
|
||||||
|
// invocationId: null,
|
||||||
|
// model: "chatgpt",
|
||||||
|
// chatGptLabel: null,
|
||||||
|
// promptPrefix: null,
|
||||||
|
// user: null,
|
||||||
|
// suggestions: [],
|
||||||
|
// toneStyle: null,
|
||||||
|
// }
|
||||||
|
const conversation = atom({
|
||||||
|
key: 'conversation',
|
||||||
|
default: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// current messages of the conversation, must be an array
|
||||||
|
// sample structure
|
||||||
|
// [{text, sender, messageId, parentMessageId, isCreatedByUser}]
|
||||||
|
const messages = atom({
|
||||||
|
key: 'messages',
|
||||||
|
default: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const messagesTree = selector({
|
||||||
|
key: 'messagesTree',
|
||||||
|
get: ({ get }) => {
|
||||||
|
return buildTree(get(messages));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const latestMessage = atom({
|
||||||
|
key: 'latestMessage',
|
||||||
|
default: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const useConversation = () => {
|
||||||
|
const modelsFilter = useRecoilValue(models.modelsFilter);
|
||||||
|
const setConversation = useSetRecoilState(conversation);
|
||||||
|
const setMessages = useSetRecoilState(messages);
|
||||||
|
const resetLatestMessage = useResetRecoilState(latestMessage);
|
||||||
|
|
||||||
|
const newConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => {
|
||||||
|
const getDefaultModel = () => {
|
||||||
|
try {
|
||||||
|
// try to read latest selected model from local storage
|
||||||
|
const lastSelected = JSON.parse(localStorage.getItem('model'));
|
||||||
|
const { model: _model, chatGptLabel: _chatGptLabel, promptPrefix: _promptPrefix } = lastSelected;
|
||||||
|
|
||||||
|
if (modelsFilter[_model]) {
|
||||||
|
model = _model;
|
||||||
|
chatGptLabel = _chatGptLabel;
|
||||||
|
promptPrefix = _promptPrefix;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
// if anything happens, reset to default model
|
||||||
|
if (modelsFilter?.chatgpt) model = 'chatgpt';
|
||||||
|
else if (modelsFilter?.bingai) model = 'bingai';
|
||||||
|
else if (modelsFilter?.chatgptBrowser) model = 'chatgptBrowser';
|
||||||
|
chatGptLabel = null;
|
||||||
|
promptPrefix = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (model === null)
|
||||||
|
// get the default model
|
||||||
|
getDefaultModel();
|
||||||
|
|
||||||
|
setConversation({
|
||||||
|
conversationId: 'new',
|
||||||
|
title: 'New Chat',
|
||||||
|
jailbreakConversationId: null,
|
||||||
|
conversationSignature: null,
|
||||||
|
clientId: null,
|
||||||
|
invocationId: null,
|
||||||
|
model: model,
|
||||||
|
chatGptLabel: chatGptLabel,
|
||||||
|
promptPrefix: promptPrefix,
|
||||||
|
user: null,
|
||||||
|
suggestions: [],
|
||||||
|
toneStyle: null
|
||||||
|
});
|
||||||
|
setMessages([]);
|
||||||
|
resetLatestMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
return { newConversation };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
conversation,
|
||||||
|
messages,
|
||||||
|
messagesTree,
|
||||||
|
latestMessage,
|
||||||
|
useConversation
|
||||||
|
};
|
||||||
27
client/src/store/conversations.js
Normal file
27
client/src/store/conversations.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
RecoilRoot,
|
||||||
|
atom,
|
||||||
|
selector,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
useSetRecoilState,
|
||||||
|
} from "recoil";
|
||||||
|
|
||||||
|
const refreshConversationsHint = atom({
|
||||||
|
key: "refreshConversationsHint",
|
||||||
|
default: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const useConversations = () => {
|
||||||
|
const setRefreshConversationsHint = useSetRecoilState(
|
||||||
|
refreshConversationsHint
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshConversations = () =>
|
||||||
|
setRefreshConversationsHint((prevState) => prevState + 1);
|
||||||
|
|
||||||
|
return { refreshConversations };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { refreshConversationsHint, useConversations };
|
||||||
|
|
@ -1,22 +1,15 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
import conversation from './conversation';
|
||||||
|
import conversations from './conversations';
|
||||||
|
import models from './models';
|
||||||
|
import user from './user';
|
||||||
|
import submission from './submission';
|
||||||
|
import search from './search';
|
||||||
|
|
||||||
import convoReducer from './convoSlice.js';
|
export default {
|
||||||
import messageReducer from './messageSlice.js';
|
...conversation,
|
||||||
import modelReducer from './modelSlice.js';
|
...conversations,
|
||||||
import submitReducer from './submitSlice.js';
|
...models,
|
||||||
import textReducer from './textSlice.js';
|
...user,
|
||||||
import userReducer from './userReducer.js';
|
...submission,
|
||||||
import searchReducer from './searchSlice.js';
|
...search
|
||||||
|
};
|
||||||
export const store = configureStore({
|
|
||||||
reducer: {
|
|
||||||
convo: convoReducer,
|
|
||||||
messages: messageReducer,
|
|
||||||
models: modelReducer,
|
|
||||||
text: textReducer,
|
|
||||||
submit: submitReducer,
|
|
||||||
user: userReducer,
|
|
||||||
search: searchReducer
|
|
||||||
},
|
|
||||||
devTools: true
|
|
||||||
});
|
|
||||||
|
|
|
||||||
80
client/src/store/models.js
Normal file
80
client/src/store/models.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
RecoilRoot,
|
||||||
|
atom,
|
||||||
|
selector,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
} from "recoil";
|
||||||
|
|
||||||
|
const customGPTModels = atom({
|
||||||
|
key: "customGPTModels",
|
||||||
|
default: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const models = selector({
|
||||||
|
key: "models",
|
||||||
|
get: ({ get }) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
_id: "0",
|
||||||
|
name: "ChatGPT",
|
||||||
|
value: "chatgpt",
|
||||||
|
model: "chatgpt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "1",
|
||||||
|
name: "CustomGPT",
|
||||||
|
value: "chatgptCustom",
|
||||||
|
model: "chatgptCustom",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "2",
|
||||||
|
name: "BingAI",
|
||||||
|
value: "bingai",
|
||||||
|
model: "bingai",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "3",
|
||||||
|
name: "Sydney",
|
||||||
|
value: "sydney",
|
||||||
|
model: "sydney",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "4",
|
||||||
|
name: "ChatGPT",
|
||||||
|
value: "chatgptBrowser",
|
||||||
|
model: "chatgptBrowser",
|
||||||
|
},
|
||||||
|
...get(customGPTModels),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelsFilter = atom({
|
||||||
|
key: "modelsFilter",
|
||||||
|
default: {
|
||||||
|
chatgpt: false,
|
||||||
|
chatgptCustom: false,
|
||||||
|
bingai: false,
|
||||||
|
sydney: false,
|
||||||
|
chatgptBrowser: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableModels = selector({
|
||||||
|
key: "availableModels",
|
||||||
|
get: ({ get }) => {
|
||||||
|
const m = get(models);
|
||||||
|
const f = get(modelsFilter);
|
||||||
|
return m.filter(({ model }) => f[model]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// const modelAvailable
|
||||||
|
|
||||||
|
export default {
|
||||||
|
customGPTModels,
|
||||||
|
models,
|
||||||
|
modelsFilter,
|
||||||
|
availableModels,
|
||||||
|
};
|
||||||
25
client/src/store/search.js
Normal file
25
client/src/store/search.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { atom, selector } from 'recoil';
|
||||||
|
|
||||||
|
const isSearchEnabled = atom({
|
||||||
|
key: 'isSearchEnabled',
|
||||||
|
default: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchQuery = atom({
|
||||||
|
key: 'searchQuery',
|
||||||
|
default: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSearching = selector({
|
||||||
|
key: 'isSearching',
|
||||||
|
get: ({ get }) => {
|
||||||
|
const data = get(searchQuery);
|
||||||
|
return !!data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
isSearchEnabled,
|
||||||
|
isSearching,
|
||||||
|
searchQuery
|
||||||
|
};
|
||||||
37
client/src/store/submission.js
Normal file
37
client/src/store/submission.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
RecoilRoot,
|
||||||
|
atom,
|
||||||
|
selector,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
useSetRecoilState,
|
||||||
|
} from "recoil";
|
||||||
|
import buildTree from "~/utils/buildTree";
|
||||||
|
|
||||||
|
// current submission
|
||||||
|
// submit any new value to this state will cause new message to be send.
|
||||||
|
// set to null to give up any submission
|
||||||
|
// {
|
||||||
|
// conversation, // target submission, must have: model, chatGptLabel, promptPrefix
|
||||||
|
// messages, // old messages
|
||||||
|
// message, // request message
|
||||||
|
// initialResponse, // response message
|
||||||
|
// isRegenerate=false, // isRegenerate?
|
||||||
|
// }
|
||||||
|
|
||||||
|
const submission = atom({
|
||||||
|
key: "submission",
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSubmitting = atom({
|
||||||
|
key: "isSubmitting",
|
||||||
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
submission,
|
||||||
|
isSubmitting,
|
||||||
|
};
|
||||||
17
client/src/store/user.js
Normal file
17
client/src/store/user.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
RecoilRoot,
|
||||||
|
atom,
|
||||||
|
selector,
|
||||||
|
useRecoilState,
|
||||||
|
useRecoilValue,
|
||||||
|
} from "recoil";
|
||||||
|
|
||||||
|
const user = atom({
|
||||||
|
key: "user",
|
||||||
|
default: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
user,
|
||||||
|
};
|
||||||
22
client/src/store2/index.js
Normal file
22
client/src/store2/index.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import convoReducer from './convoSlice.js';
|
||||||
|
import messageReducer from './messageSlice.js';
|
||||||
|
import modelReducer from './modelSlice.js';
|
||||||
|
import submitReducer from './submitSlice.js';
|
||||||
|
import textReducer from './textSlice.js';
|
||||||
|
import userReducer from './userReducer.js';
|
||||||
|
import searchReducer from './searchSlice.js';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
convo: convoReducer,
|
||||||
|
messages: messageReducer,
|
||||||
|
models: modelReducer,
|
||||||
|
text: textReducer,
|
||||||
|
submit: submitReducer,
|
||||||
|
user: userReducer,
|
||||||
|
search: searchReducer
|
||||||
|
},
|
||||||
|
devTools: true
|
||||||
|
});
|
||||||
|
|
@ -1,164 +1,123 @@
|
||||||
import resetConvo from './resetConvo';
|
// import resetConvo from './resetConvo';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
// import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { setNewConvo } from '~/store/convoSlice';
|
// import { setNewConvo } 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';
|
||||||
import { setText } from '~/store/textSlice';
|
// import { setText } from '~/store/textSlice';
|
||||||
import { setError } from '~/store/convoSlice';
|
// import { setError } from '~/store/convoSlice';
|
||||||
import {v4} from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
const useMessageHandler = () => {
|
const useMessageHandler = () => {
|
||||||
const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
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 { messages } = useSelector((state) => state.messages);
|
// const { messages } = useSelector((state) => state.messages);
|
||||||
const { model, chatGptLabel, promptPrefix, isSubmitting } = useSelector((state) => state.submit);
|
// const { model, chatGptLabel, promptPrefix, isSubmitting } = useSelector((state) => state.submit);
|
||||||
const { latestMessage, error } = convo;
|
// const { latestMessage, error } = convo;
|
||||||
|
|
||||||
const ask = ({ text, parentMessageId=null, conversationId=null, messageId=null}, { isRegenerate=false }={}) => {
|
const [currentConversation, setCurrentConversation] = useRecoilState(store.conversation) || {};
|
||||||
if (error) {
|
const setSubmission = useSetRecoilState(store.submission);
|
||||||
dispatch(setError(false));
|
const isSubmitting = useRecoilValue(store.isSubmitting);
|
||||||
}
|
|
||||||
|
|
||||||
|
const latestMessage = useRecoilValue(store.latestMessage);
|
||||||
|
const { error } = currentConversation;
|
||||||
|
|
||||||
|
const [messages, setMessages] = useRecoilState(store.messages);
|
||||||
|
|
||||||
|
const ask = (
|
||||||
|
{ text, parentMessageId = null, conversationId = null, messageId = null },
|
||||||
|
{ isRegenerate = false } = {}
|
||||||
|
) => {
|
||||||
if (!!isSubmitting || text === '') {
|
if (!!isSubmitting || text === '') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine the model to be used
|
||||||
|
const { model = null, chatGptLabel = null, promptPrefix = null } = currentConversation;
|
||||||
|
|
||||||
|
// construct the query message
|
||||||
// this is not a real messageId, it is used as placeholder before real messageId returned
|
// this is not a real messageId, it is used as placeholder before real messageId returned
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
const fakeMessageId = v4();
|
const fakeMessageId = v4();
|
||||||
const isCustomModel = model === 'chatgptCustom' || !initial[model];
|
// const isCustomModel = model === 'chatgptCustom' || !initial[model];
|
||||||
const sender = model === 'chatgptCustom' ? chatGptLabel : model;
|
// const sender = model === 'chatgptCustom' ? chatGptLabel : model;
|
||||||
parentMessageId = parentMessageId || latestMessage?.messageId || '00000000-0000-0000-0000-000000000000';
|
parentMessageId = parentMessageId || latestMessage?.messageId || '00000000-0000-0000-0000-000000000000';
|
||||||
let currentMessages = messages;
|
let currentMessages = messages;
|
||||||
if (resetConvo(currentMessages, sender)) {
|
conversationId = conversationId || currentConversation?.conversationId;
|
||||||
parentMessageId = '00000000-0000-0000-0000-000000000000';
|
if (conversationId == 'search') {
|
||||||
conversationId = null;
|
console.error('cannot send any message under search view!');
|
||||||
dispatch(setNewConvo());
|
return;
|
||||||
currentMessages = [];
|
|
||||||
}
|
}
|
||||||
const currentMsg = { sender: 'User', text, current: true, isCreatedByUser: true, parentMessageId, conversationId, messageId: fakeMessageId };
|
if (conversationId == 'new') {
|
||||||
const initialResponse = { sender, text: '', parentMessageId: isRegenerate?messageId:fakeMessageId, messageId: (isRegenerate?messageId:fakeMessageId) + '_', submitting: true };
|
parentMessageId = '00000000-0000-0000-0000-000000000000';
|
||||||
|
currentMessages = [];
|
||||||
|
conversationId = null;
|
||||||
|
}
|
||||||
|
const currentMsg = {
|
||||||
|
sender: 'User',
|
||||||
|
text,
|
||||||
|
current: true,
|
||||||
|
isCreatedByUser: true,
|
||||||
|
parentMessageId,
|
||||||
|
conversationId,
|
||||||
|
messageId: fakeMessageId
|
||||||
|
};
|
||||||
|
|
||||||
|
// construct the placeholder response message
|
||||||
|
const initialResponse = {
|
||||||
|
sender: chatGptLabel || model,
|
||||||
|
text: '',
|
||||||
|
parentMessageId: isRegenerate ? messageId : fakeMessageId,
|
||||||
|
messageId: (isRegenerate ? messageId : fakeMessageId) + '_',
|
||||||
|
conversationId,
|
||||||
|
submitting: true
|
||||||
|
};
|
||||||
|
|
||||||
const submission = {
|
const submission = {
|
||||||
convo,
|
conversation: {
|
||||||
isCustomModel,
|
...currentConversation,
|
||||||
|
conversationId,
|
||||||
|
model,
|
||||||
|
chatGptLabel,
|
||||||
|
promptPrefix
|
||||||
|
},
|
||||||
message: {
|
message: {
|
||||||
...currentMsg,
|
...currentMsg,
|
||||||
model,
|
model,
|
||||||
chatGptLabel,
|
chatGptLabel,
|
||||||
promptPrefix,
|
promptPrefix,
|
||||||
overrideParentMessageId: isRegenerate?messageId:null
|
overrideParentMessageId: isRegenerate ? messageId : null
|
||||||
},
|
},
|
||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
isRegenerate,
|
isRegenerate,
|
||||||
initialResponse,
|
initialResponse
|
||||||
sender,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('User Input:', text);
|
console.log('User Input:', text);
|
||||||
|
|
||||||
if (isRegenerate) {
|
if (isRegenerate) {
|
||||||
dispatch(setMessages([...currentMessages, initialResponse]));
|
setMessages([...currentMessages, initialResponse]);
|
||||||
} else {
|
} else {
|
||||||
dispatch(setMessages([...currentMessages, currentMsg, initialResponse]));
|
setMessages([...currentMessages, currentMsg, initialResponse]);
|
||||||
dispatch(setText(''));
|
|
||||||
}
|
}
|
||||||
dispatch(setSubmitState(true));
|
setSubmission(submission);
|
||||||
dispatch(setSubmission(submission));
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const regenerate = ({ parentMessageId }) => {
|
const regenerate = ({ parentMessageId }) => {
|
||||||
const parentMessage = messages?.find(element => element.messageId == parentMessageId);
|
const parentMessage = messages?.find(element => element.messageId == parentMessageId);
|
||||||
|
|
||||||
if (parentMessage && parentMessage.isCreatedByUser)
|
if (parentMessage && parentMessage.isCreatedByUser) ask({ ...parentMessage }, { isRegenerate: true });
|
||||||
ask({ ...parentMessage }, { isRegenerate: true })
|
else console.error('Failed to regenerate the message: parentMessage not found or not created by user.');
|
||||||
else
|
};
|
||||||
console.error('Failed to regenerate the message: parentMessage not found or not created by user.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopGenerating = () => {
|
const stopGenerating = () => {
|
||||||
dispatch(setSubmission({}));
|
setSubmission(null);
|
||||||
}
|
};
|
||||||
|
|
||||||
return { ask, regenerate, stopGenerating }
|
return { ask, regenerate, stopGenerating };
|
||||||
}
|
};
|
||||||
|
|
||||||
export { useMessageHandler };
|
export { useMessageHandler };
|
||||||
|
|
||||||
// deprecated
|
|
||||||
// export default function handleSubmit({
|
|
||||||
// model,
|
|
||||||
// text,
|
|
||||||
// convo,
|
|
||||||
// messageHandler,
|
|
||||||
// convoHandler,
|
|
||||||
// errorHandler,
|
|
||||||
// chatGptLabel,
|
|
||||||
// promptPrefix
|
|
||||||
// }) {
|
|
||||||
// const endpoint = `/api/ask`;
|
|
||||||
// let payload = { model, text, chatGptLabel, promptPrefix };
|
|
||||||
// if (convo.conversationId && convo.parentMessageId) {
|
|
||||||
// payload = {
|
|
||||||
// ...payload,
|
|
||||||
// conversationId: convo.conversationId,
|
|
||||||
// parentMessageId: convo.parentMessageId
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const isBing = model === 'bingai' || model === 'sydney';
|
|
||||||
// if (isBing && convo.conversationId) {
|
|
||||||
|
|
||||||
// payload = {
|
|
||||||
// ...payload,
|
|
||||||
// jailbreakConversationId: convo.jailbreakConversationId,
|
|
||||||
// conversationId: convo.conversationId,
|
|
||||||
// conversationSignature: convo.conversationSignature,
|
|
||||||
// clientId: convo.clientId,
|
|
||||||
// invocationId: convo.invocationId,
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let server = endpoint;
|
|
||||||
// server = model === 'bingai' ? server + '/bing' : server;
|
|
||||||
// server = model === 'sydney' ? server + '/sydney' : server;
|
|
||||||
|
|
||||||
// const events = new SSE(server, {
|
|
||||||
// payload: JSON.stringify(payload),
|
|
||||||
// headers: { 'Content-Type': 'application/json' }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// events.onopen = function () {
|
|
||||||
// console.log('connection is opened');
|
|
||||||
// };
|
|
||||||
|
|
||||||
// events.onmessage = function (e) {
|
|
||||||
// const data = JSON.parse(e.data);
|
|
||||||
// let text = data.text || data.response;
|
|
||||||
// if (data.message) {
|
|
||||||
// messageHandler(text, events);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (data.final) {
|
|
||||||
// convoHandler(data);
|
|
||||||
// console.log('final', data);
|
|
||||||
// } else {
|
|
||||||
// // console.log('dataStream', data);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// events.onerror = function (e) {
|
|
||||||
// console.log('error in opening conn.');
|
|
||||||
// events.close();
|
|
||||||
// errorHandler(e);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// events.addEventListener('stop', () => {
|
|
||||||
// // Close the SSE stream
|
|
||||||
// console.log('stop event received');
|
|
||||||
// events.close();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// events.stream();
|
|
||||||
// }
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue