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:
Wentao Lyu 2023-03-28 20:36:21 +08:00
parent d8ccc5b870
commit af3d74b104
33 changed files with 1142 additions and 473 deletions

View file

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

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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 (

View file

@ -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"

View file

@ -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 />

View file

@ -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 (

View file

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

View file

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

View 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 />
</>
);
}

View 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 />
</>
);
}

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

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

View file

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

View 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,
};

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

View 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
View 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,
};

View 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
});

View file

@ -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();
// }