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 { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './src/store';
// import { Provider } from 'react-redux';
// import { store } from './src/store';
import { RecoilRoot } from 'recoil';
import { ThemeProvider } from './src/hooks/ThemeContext';
import App from './src/App';
import './src/style.css';
import './src/mobile.css'
import './src/mobile.css';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(
<Provider store={store}>
<RecoilRoot>
<ThemeProvider>
<App />
</ThemeProvider>
</Provider>
);
</RecoilRoot>
);

View file

@ -36,9 +36,11 @@
"react-lazy-load": "^4.0.1",
"react-markdown": "^8.0.5",
"react-redux": "^8.0.5",
"react-router-dom": "^6.9.0",
"react-string-replace": "^1.1.0",
"react-textarea-autosize": "^8.4.0",
"react-transition-group": "^4.4.5",
"recoil": "^0.7.7",
"rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.2",
"rehype-raw": "^6.1.1",

View file

@ -1,53 +1,52 @@
import React, { useEffect, useState } from 'react';
import Messages from './components/Messages';
import Landing from './components/Main/Landing';
import TextChat from './components/Main/TextChat';
import Nav from './components/Nav';
import MobileNav from './components/Nav/MobileNav';
import useDocumentTitle from '~/hooks/useDocumentTitle';
import { useSelector, useDispatch } from 'react-redux';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import Root from './routes/Root';
// import Chat from './routes/Chat';
import store from './store';
import userAuth from './utils/userAuth';
import { setUser } from './store/userReducer';
import { setSearchState } from './store/searchSlice';
import { useRecoilState, useSetRecoilState } from 'recoil';
import axios from 'axios';
const App = () => {
const dispatch = useDispatch();
const router = createBrowserRouter([
{
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 { user } = useSelector((state) => state.user);
const { title } = useSelector((state) => state.convo);
const [navVisible, setNavVisible] = useState(false);
useDocumentTitle(title);
const App = () => {
const [user, setUser] = useRecoilState(store.user);
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
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()
.then((user) => dispatch(setUser(user)))
.catch((err) => console.log(err));
.then(user => setUser(user))
.catch(err => console.log(err));
}, []);
if (user)
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 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>
<RouterProvider router={router} />
</div>
);
else return <div className="flex h-screen"></div>;

View file

@ -1,99 +1,125 @@
import React, { useState, useRef } from 'react';
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import RenameButton from './RenameButton';
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 { 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 [titleInput, setTitleInput] = useState(title);
const { stopStream } = useSelector((state) => state.submit);
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 bingData = conversationSignature
? {
jailbreakConversationId: jailbreakConversationId,
conversationSignature: conversationSignature,
parentMessageId: parentMessageId || null,
clientId: clientId,
invocationId: invocationId,
toneStyle: toneStyle
}
: null;
const clickHandler = async () => {
if (conversationId === id) {
if (currentConversation?.conversationId === conversationId) {
return;
}
if (!stopStream) {
dispatch(setStopStream(true));
dispatch(setSubmission({}));
}
dispatch(setEmptyMessage());
// stop existing submission
setSubmission(null);
const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
// set conversation to the new conversation
setCurrentConversation(conversation);
setMessages(null);
resetLatestMessage();
if (bingData) {
const {
parentMessageId,
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 (!stopStream) {
// dispatch(setStopStream(true));
// dispatch(setSubmission({}));
// }
// dispatch(setEmptyMessage());
if (chatGptLabel) {
dispatch(setModel('chatgptCustom'));
dispatch(setCustomModel(chatGptLabel.toLowerCase()));
} else {
dispatch(setModel(model));
dispatch(setCustomModel(null));
}
// const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
dispatch(setMessages(data));
dispatch(setCustomGpt(convo));
dispatch(setText(''));
dispatch(setStopStream(false));
// if (bingData) {
// const {
// parentMessageId,
// 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();
setTitleInput(title);
setRenaming(true);
@ -102,24 +128,28 @@ export default function Conversation({
}, 25);
};
const cancelHandler = (e) => {
const cancelHandler = e => {
e.preventDefault();
setRenaming(false);
};
const onRename = (e) => {
const onRename = e => {
e.preventDefault();
setRenaming(false);
if (titleInput === title) {
return;
}
rename.trigger({ conversationId, title: titleInput })
.then(() => {
dispatch(refreshConversation())
});
rename.trigger({ conversationId, title: titleInput }).then(() => {
refreshConversations();
if (conversationId == currentConversation?.conversationId)
setCurrentConversation(prevState => ({
...prevState,
title: titleInput
}));
});
};
const handleKeyDown = (e) => {
const handleKeyDown = e => {
if (e.key === 'Enter') {
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'
};
if (conversationId !== id) {
if (currentConversation?.conversationId !== conversationId) {
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';
}
@ -148,7 +178,7 @@ export default function Conversation({
type="text"
className="m-0 mr-0 w-full border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onChange={e => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyDown={handleKeyDown}
/>
@ -156,16 +186,16 @@ export default function Conversation({
title
)}
</div>
{conversationId === id ? (
{currentConversation?.conversationId === conversationId ? (
<div className="visible absolute right-1 z-10 flex text-gray-300">
<RenameButton
conversationId={id}
conversationId={conversationId}
renaming={renaming}
renameHandler={renameHandler}
onRename={onRename}
/>
<DeleteButton
conversationId={id}
conversationId={conversationId}
renaming={renaming}
cancelHandler={cancelHandler}
retainView={retainView}

View file

@ -2,24 +2,19 @@ import React from 'react';
import TrashIcon from '../svg/TrashIcon';
import CrossIcon from '../svg/CrossIcon';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setNewConvo, removeConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function DeleteButton({ conversationId, renaming, cancelHandler, retainView }) {
const dispatch = useDispatch();
const { trigger } = manualSWR(
`/api/convos/clear`,
'post',
() => {
dispatch(setMessages([]));
dispatch(removeConvo(conversationId));
dispatch(setNewConvo());
dispatch(setSubmission({}));
retainView();
}
);
const currentConversation = useRecoilValue(store.conversation) || {};
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
if (currentConversation?.conversationId == conversationId) newConversation();
refreshConversations();
retainView();
});
const clickHandler = () => trigger({ conversationId });
const handler = renaming ? cancelHandler : clickHandler;
@ -29,7 +24,7 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
className="p-1 hover:text-white"
onClick={handler}
>
{ renaming ? <CrossIcon/> : <TrashIcon />}
{renaming ? <CrossIcon /> : <TrashIcon />}
</button>
);
}

View file

@ -1,12 +1,12 @@
import React from 'react';
export default function Pages({ pageNumber, pages, nextPage, previousPage }) {
const clickHandler = (func) => async (e) => {
const clickHandler = func => async e => {
e.preventDefault();
await func();
};
return (
return pageNumber == 1 && pages == 1 ? null : (
<div className="m-auto mt-4 mb-2 flex items-center justify-center gap-2">
<button
onClick={clickHandler(previousPage)}

View file

@ -2,34 +2,15 @@ import React from 'react';
import Conversation from './Conversation';
export default function Conversations({ conversations, conversationId, moveToTop }) {
return (
<>
{conversations &&
conversations.length > 0 &&
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;
conversations.map(convo => {
return (
<Conversation
key={convo.conversationId}
id={convo.conversationId}
model={convo.model}
parentMessageId={convo.parentMessageId}
title={convo.title}
conversationId={conversationId}
chatGptLabel={convo.chatGptLabel}
promptPrefix={convo.promptPrefix}
bingData={bingData}
conversation={convo}
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 { useSWRConfig } from 'swr';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setNewConvo, removeAll } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
import DialogTemplate from '../ui/DialogTemplate';
import store from '~/store';
export default function ClearConvos() {
const dispatch = useDispatch();
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { mutate } = useSWRConfig();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
mutate(`/api/convos`);
newConversation();
refreshConversations();
});
const clickHandler = () => {
console.log('Clearing conversations...');
dispatch(removeAll());
trigger({});
};
return (
<Dialog>
<DialogTrigger asChild>
<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"
// onClick={clickHandler}
onClick={clickHandler}
>
<TrashIcon />
Clear conversations
</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 { useSelector } from 'react-redux';
import React from 'react';
import LogOutIcon from '../svg/LogOutIcon';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function Logout() {
const { user } = useSelector((state) => state.user);
const user = useRecoilValue(store.user);
const clickHandler = () => {
window.location.href = "/auth/logout";
window.location.href = '/auth/logout';
};
return (

View file

@ -1,33 +1,19 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { setNewConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmission } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { useRecoilValue } from 'recoil';
import store from '~/store';
export default function MobileNav({ setNavVisible }) {
const dispatch = useDispatch();
const { conversationId, convos, title } = useSelector((state) => state.convo);
const toggleNavVisible = () => {
setNavVisible((prev) => {
return !prev
})
}
const newConvo = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(setSubmission({}));
}
const conversation = useRecoilValue(store.conversation);
const { newConversation } = store.useConversation();
const { title = 'New Chat' } = conversation || {};
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">
<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"
onClick={toggleNavVisible}
onClick={() => setNavVisible(prev => !prev)}
>
<span className="sr-only">Open sidebar</span>
<svg
@ -66,7 +52,7 @@ export default function MobileNav({ setNavVisible }) {
<button
type="button"
className="px-3"
onClick={newConvo}
onClick={() => newConversation()}
>
<svg
stroke="currentColor"

View file

@ -3,13 +3,17 @@ import SearchBar from './SearchBar';
import ClearConvos from './ClearConvos';
import DarkMode from './DarkMode';
import Logout from './Logout';
import { useSelector } from 'react-redux';
export default function NavLinks({ fetch, onSearchSuccess, clearSearch }) {
const { searchEnabled } = useSelector((state) => state.search);
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
return (
<>
{ !!searchEnabled && <SearchBar fetch={fetch} onSuccess={onSearchSuccess} clearSearch={clearSearch}/>}
{!!isSearchEnabled && (
<SearchBar
fetch={fetch}
onSuccess={onSearchSuccess}
clearSearch={clearSearch}
/>
)}
<DarkMode />
<ClearConvos />
<Logout />

View file

@ -1,23 +1,13 @@
import React from 'react';
import { useDispatch } from 'react-redux';
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';
import store from '~/store';
export default function NewChat() {
const dispatch = useDispatch();
const { newConversation } = store.useConversation();
const clickHandler = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setNewConvo());
dispatch(refreshConversation());
dispatch(setSubmission({}));
dispatch(setDisabled(false));
dispatch(setInputValue(''));
dispatch(setQuery(''));
// dispatch(setInputValue(''));
// dispatch(setQuery(''));
newConversation();
};
return (

View file

@ -1,41 +1,44 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import { debounce } from 'lodash';
import { useSelector, useDispatch } from 'react-redux';
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 }) {
const dispatch = useDispatch();
const { inputValue } = useSelector((state) => state.search);
// const dispatch = useDispatch();
const [inputValue, setInputValue] = useState('');
const setSearchQuery = useSetRecoilState(store.searchQuery);
// const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback(
debounce((q) => {
dispatch(setQuery(q));
debounce(q => {
setSearchQuery(q);
if (q.length > 0) {
fetch(q, 1);
}
}, 750),
[dispatch]
[setSearchQuery]
);
const handleKeyUp = (e) => {
const handleKeyUp = e => {
const { value } = e.target;
if (e.keyCode === 8 && value === '') {
if (e.keyCode === 8 && value === '') {
// Value after clearing input: ""
console.log(`Value after clearing input: "${value}"`);
dispatch(setQuery(''));
setSearchQuery('');
clearSearch();
}
}
};
const changeHandler = (e) => {
const changeHandler = e => {
let q = e.target.value;
dispatch(setInputValue(q));
setInputValue(q);
q = q.trim();
if (q === '') {
dispatch(setQuery(''));
setSearchQuery('');
clearSearch();
} else {
debouncedChangeHandler(q);

View file

@ -6,67 +6,121 @@ import Pages from '../Conversations/Pages';
import Conversations from '../Conversations';
import NavLinks from './NavLinks';
import { searchFetcher, swr } from '~/utils/fetchers';
import { useDispatch, useSelector } from 'react-redux';
import { setConvos, setNewConvo, refreshConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setDisabled } from '~/store/submitSlice';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import store from '~/store';
export default function Nav({ navVisible, setNavVisible }) {
const dispatch = useDispatch();
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 [pageNumber, setPage] = useState(1);
const { search, query } = useSelector((state) => state.search);
const { conversationId, convos, refreshConvoHint } = useSelector((state) => state.convo);
// search
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) => {
if (search) {
if (isSearching) {
return;
}
const { conversations, pages } = data;
let { conversations, pages } = data;
if (pageNumber > pages) {
setPage(pages);
setPageNumber(pages);
} 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);
}
};
const onSearchSuccess = (data, expectedPage) => {
const res = data;
dispatch(setConvos({ convos: res.conversations, searchFetch: true }));
setConversations(res.conversations);
if (expectedPage) {
setPage(expectedPage);
setPageNumber(expectedPage);
}
setPage(res.pageNumber);
setPageNumber(res.pageNumber);
setPages(res.pages);
setIsFetching(false);
if (res.messages?.length > 0) {
dispatch(setMessages(res.messages));
dispatch(setDisabled(true));
setMessages(res.messages);
// 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 = () => {
setPage(1);
dispatch(refreshConversation());
if (!conversationId) {
dispatch(setNewConvo());
dispatch(setMessages([]));
setPageNumber(1);
refreshConversations();
if (conversationId == 'search') {
newConversation();
}
dispatch(setDisabled(false));
// dispatch(setDisabled(false));
};
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
revalidateOnMount: false,
revalidateOnMount: false
});
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
const nextPage = async () => {
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 container = containerRef.current;
@ -75,35 +129,7 @@ export default function Nav({ navVisible, setNavVisible }) {
}
};
const nextPage = async () => {
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 moveTo = () => {
const container = containerRef.current;
if (container && scrollPositionRef.current !== null) {
@ -112,18 +138,20 @@ export default function Nav({ navVisible, setNavVisible }) {
container.scrollTop = Math.min(maxScrollTop, scrollPositionRef.current);
}
};
const toggleNavVisible = () => {
setNavVisible(prev => !prev);
};
useEffect(() => {
moveTo();
}, [data]);
useEffect(() => {
setNavVisible(false);
}, [conversationId]);
const toggleNavVisible = () => {
setNavVisible((prev) => {
return !prev;
});
};
const containerClasses =
isLoading && pageNumber === 1
? '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}>
{/* {(isLoading && pageNumber === 1) ? ( */}
{(isLoading && pageNumber === 1) || (isFetching) ? (
{(isLoading && pageNumber === 1) || isFetching ? (
<Spinner />
) : (
<Conversations
conversations={convos}
conversations={conversations}
conversationId={conversationId}
moveToTop={moveToTop}
/>
@ -172,6 +200,7 @@ export default function Nav({ navVisible, setNavVisible }) {
fetch={fetch}
onSearchSuccess={onSearchSuccess}
clearSearch={clearSearch}
isSearchEnabled={isSearchEnabled}
/>
</nav>
</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';
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
});
export default {
...conversation,
...conversations,
...models,
...user,
...submission,
...search
};

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 { useSelector, useDispatch } from 'react-redux';
import { setNewConvo } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setSubmitState, setSubmission } from '~/store/submitSlice';
import { setText } from '~/store/textSlice';
import { setError } from '~/store/convoSlice';
import {v4} from 'uuid';
// import resetConvo from './resetConvo';
// import { useSelector, useDispatch } from 'react-redux';
// import { setNewConvo } from '~/store/convoSlice';
// import { setMessages } from '~/store/messageSlice';
// import { setSubmitState, setSubmission } from '~/store/submitSlice';
// import { setText } from '~/store/textSlice';
// import { setError } from '~/store/convoSlice';
import { v4 } from 'uuid';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import store from '~/store';
const useMessageHandler = () => {
const dispatch = useDispatch();
const convo = useSelector((state) => state.convo);
const { initial } = useSelector((state) => state.models);
const { messages } = useSelector((state) => state.messages);
const { model, chatGptLabel, promptPrefix, isSubmitting } = useSelector((state) => state.submit);
const { latestMessage, error } = convo;
// const dispatch = useDispatch();
// const convo = useSelector((state) => state.convo);
// const { initial } = useSelector((state) => state.models);
// const { messages } = useSelector((state) => state.messages);
// const { model, chatGptLabel, promptPrefix, isSubmitting } = useSelector((state) => state.submit);
// const { latestMessage, error } = convo;
const ask = ({ text, parentMessageId=null, conversationId=null, messageId=null}, { isRegenerate=false }={}) => {
if (error) {
dispatch(setError(false));
}
const [currentConversation, setCurrentConversation] = useRecoilState(store.conversation) || {};
const setSubmission = useSetRecoilState(store.submission);
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 === '') {
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
text = text.trim();
const fakeMessageId = v4();
const isCustomModel = model === 'chatgptCustom' || !initial[model];
const sender = model === 'chatgptCustom' ? chatGptLabel : model;
// const isCustomModel = model === 'chatgptCustom' || !initial[model];
// const sender = model === 'chatgptCustom' ? chatGptLabel : model;
parentMessageId = parentMessageId || latestMessage?.messageId || '00000000-0000-0000-0000-000000000000';
let currentMessages = messages;
if (resetConvo(currentMessages, sender)) {
parentMessageId = '00000000-0000-0000-0000-000000000000';
conversationId = null;
dispatch(setNewConvo());
currentMessages = [];
conversationId = conversationId || currentConversation?.conversationId;
if (conversationId == 'search') {
console.error('cannot send any message under search view!');
return;
}
const currentMsg = { sender: 'User', text, current: true, isCreatedByUser: true, parentMessageId, conversationId, messageId: fakeMessageId };
const initialResponse = { sender, text: '', parentMessageId: isRegenerate?messageId:fakeMessageId, messageId: (isRegenerate?messageId:fakeMessageId) + '_', submitting: true };
if (conversationId == 'new') {
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 = {
convo,
isCustomModel,
message: {
conversation: {
...currentConversation,
conversationId,
model,
chatGptLabel,
promptPrefix
},
message: {
...currentMsg,
model,
chatGptLabel,
promptPrefix,
overrideParentMessageId: isRegenerate?messageId:null
overrideParentMessageId: isRegenerate ? messageId : null
},
messages: currentMessages,
isRegenerate,
initialResponse,
sender,
initialResponse
};
console.log('User Input:', text);
if (isRegenerate) {
dispatch(setMessages([...currentMessages, initialResponse]));
setMessages([...currentMessages, initialResponse]);
} else {
dispatch(setMessages([...currentMessages, currentMsg, initialResponse]));
dispatch(setText(''));
setMessages([...currentMessages, currentMsg, initialResponse]);
}
dispatch(setSubmitState(true));
dispatch(setSubmission(submission));
}
setSubmission(submission);
};
const regenerate = ({ parentMessageId }) => {
const parentMessage = messages?.find(element => element.messageId == parentMessageId);
if (parentMessage && parentMessage.isCreatedByUser)
ask({ ...parentMessage }, { isRegenerate: true })
else
console.error('Failed to regenerate the message: parentMessage not found or not created by user.');
}
if (parentMessage && parentMessage.isCreatedByUser) ask({ ...parentMessage }, { isRegenerate: true });
else console.error('Failed to regenerate the message: parentMessage not found or not created by user.');
};
const stopGenerating = () => {
dispatch(setSubmission({}));
}
setSubmission(null);
};
return { ask, regenerate, stopGenerating }
}
return { ask, regenerate, stopGenerating };
};
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();
// }