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