mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 03:10:15 +01:00
refactor: nav and search.
feat: use recoil to replace redux feat: use react-native THIS IS NOT FINISHED. DONT USE THIS
This commit is contained in:
parent
d8ccc5b870
commit
af3d74b104
33 changed files with 1142 additions and 473 deletions
|
|
@ -1,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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
305
client/src/components/MessageHandler/index.jsx
Normal file
305
client/src/components/MessageHandler/index.jsx
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from "recoil";
|
||||
import { SSE } from "~/utils/sse";
|
||||
import { useMessageHandler } from "../../utils/handleSubmit";
|
||||
import createPayload from "~/utils/createPayload";
|
||||
|
||||
import store from "~/store";
|
||||
|
||||
export default function MessageHandler({ messages }) {
|
||||
const [submission, setSubmission] = useRecoilState(store.submission);
|
||||
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmitting);
|
||||
const setMessages = useSetRecoilState(store.messages);
|
||||
const setConversation = useSetRecoilState(store.conversation);
|
||||
const resetLatestMessage = useResetRecoilState(store.latestMessage);
|
||||
|
||||
const { refreshConversations } = store.useConversations();
|
||||
|
||||
const messageHandler = (data, submission) => {
|
||||
const {
|
||||
messages,
|
||||
message,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
if (isRegenerate)
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
parentMessageId: message?.overrideParentMessageId,
|
||||
messageId: message?.overrideParentMessageId + "_",
|
||||
submitting: true,
|
||||
},
|
||||
]);
|
||||
else
|
||||
setMessages([
|
||||
...messages,
|
||||
message,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
parentMessageId: message?.messageId,
|
||||
messageId: message?.messageId + "_",
|
||||
submitting: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const cancelHandler = (data, submission) => {
|
||||
const {
|
||||
messages,
|
||||
message,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
if (isRegenerate)
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
parentMessageId: message?.overrideParentMessageId,
|
||||
messageId: message?.overrideParentMessageId + "_",
|
||||
cancelled: true,
|
||||
},
|
||||
]);
|
||||
else
|
||||
setMessages([
|
||||
...messages,
|
||||
message,
|
||||
{
|
||||
...initialResponse,
|
||||
text: data,
|
||||
parentMessageId: message?.messageId,
|
||||
messageId: message?.messageId + "_",
|
||||
cancelled: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const createdHandler = (data, submission) => {
|
||||
const {
|
||||
messages,
|
||||
message,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
if (isRegenerate)
|
||||
setMessages([
|
||||
...messages,
|
||||
{
|
||||
...initialResponse,
|
||||
parentMessageId: message?.overrideParentMessageId,
|
||||
messageId: message?.overrideParentMessageId + "_",
|
||||
submitting: true,
|
||||
},
|
||||
]);
|
||||
else
|
||||
setMessages([
|
||||
...messages,
|
||||
message,
|
||||
{
|
||||
...initialResponse,
|
||||
parentMessageId: message?.messageId,
|
||||
messageId: message?.messageId + "_",
|
||||
submitting: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const { conversationId } = message;
|
||||
setConversation((prevState) => ({
|
||||
...prevState,
|
||||
conversationId,
|
||||
}));
|
||||
resetLatestMessage();
|
||||
};
|
||||
|
||||
const finalHandler = (data, submission) => {
|
||||
const {
|
||||
conversation,
|
||||
messages,
|
||||
message,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
const { requestMessage, responseMessage } = data;
|
||||
const { conversationId } = requestMessage;
|
||||
|
||||
// update the messages
|
||||
if (isRegenerate) setMessages([...messages, responseMessage]);
|
||||
else setMessages([...messages, requestMessage, responseMessage]);
|
||||
setIsSubmitting(false);
|
||||
|
||||
// refresh title
|
||||
if (
|
||||
requestMessage.parentMessageId == "00000000-0000-0000-0000-000000000000"
|
||||
) {
|
||||
setTimeout(() => {
|
||||
refreshConversations();
|
||||
}, 2000);
|
||||
|
||||
// in case it takes too long.
|
||||
setTimeout(() => {
|
||||
refreshConversations();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
const { model, chatGptLabel, promptPrefix } = conversation;
|
||||
const isBing = model === "bingai" || model === "sydney";
|
||||
|
||||
if (!isBing) {
|
||||
const { title } = data;
|
||||
const { conversationId } = responseMessage;
|
||||
setConversation((prevState) => ({
|
||||
...prevState,
|
||||
title,
|
||||
conversationId,
|
||||
jailbreakConversationId: null,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: null,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
latestMessage: null,
|
||||
}));
|
||||
} else if (model === "bingai") {
|
||||
const { title } = data;
|
||||
const { conversationSignature, clientId, conversationId, invocationId } =
|
||||
responseMessage;
|
||||
setConversation((prevState) => ({
|
||||
...prevState,
|
||||
title,
|
||||
conversationId,
|
||||
jailbreakConversationId: null,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
latestMessage: null,
|
||||
}));
|
||||
} else if (model === "sydney") {
|
||||
const { title } = data;
|
||||
const {
|
||||
jailbreakConversationId,
|
||||
parentMessageId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
conversationId,
|
||||
invocationId,
|
||||
} = responseMessage;
|
||||
setConversation((prevState) => ({
|
||||
...prevState,
|
||||
title,
|
||||
conversationId,
|
||||
jailbreakConversationId,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
invocationId,
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
latestMessage: null,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const errorHandler = (data, submission) => {
|
||||
const {
|
||||
conversation,
|
||||
messages,
|
||||
message,
|
||||
initialResponse,
|
||||
isRegenerate = false,
|
||||
} = submission;
|
||||
|
||||
console.log("Error:", data);
|
||||
const errorResponse = {
|
||||
...data,
|
||||
error: true,
|
||||
parentMessageId: message?.messageId,
|
||||
};
|
||||
setIsSubmitting(false);
|
||||
setMessages([...messages, message, errorResponse]);
|
||||
return;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (submission === null) return;
|
||||
if (Object.keys(submission).length === 0) return;
|
||||
|
||||
const { messages, initialResponse, isRegenerate = false } = submission;
|
||||
let { message } = submission;
|
||||
|
||||
const { server, payload } = createPayload(submission);
|
||||
|
||||
const events = new SSE(server, {
|
||||
payload: JSON.stringify(payload),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
let latestResponseText = "";
|
||||
events.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
if (data.final) {
|
||||
finalHandler(data, { ...submission, message });
|
||||
console.log("final", data);
|
||||
}
|
||||
if (data.created) {
|
||||
message = {
|
||||
...data.message,
|
||||
model: message?.model,
|
||||
chatGptLabel: message?.chatGptLabel,
|
||||
promptPrefix: message?.promptPrefix,
|
||||
overrideParentMessageId: message?.overrideParentMessageId,
|
||||
};
|
||||
createdHandler(data, { ...submission, message });
|
||||
console.log("created", message);
|
||||
} else {
|
||||
let text = data.text || data.response;
|
||||
if (data.initial) console.log(data);
|
||||
|
||||
if (data.message) {
|
||||
latestResponseText = text;
|
||||
messageHandler(text, { ...submission, message });
|
||||
}
|
||||
// console.log('dataStream', data);
|
||||
}
|
||||
};
|
||||
|
||||
events.onopen = () => console.log("connection is opened");
|
||||
|
||||
events.oncancel = (e) =>
|
||||
cancelHandler(latestResponseText, { ...submission, message });
|
||||
|
||||
events.onerror = function (e) {
|
||||
console.log("error in opening conn.");
|
||||
events.close();
|
||||
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
errorHandler(data, { ...submission, message });
|
||||
};
|
||||
|
||||
setIsSubmitting(true);
|
||||
events.stream();
|
||||
|
||||
return () => {
|
||||
const isCancelled = events.readyState <= 1;
|
||||
events.close();
|
||||
if (isCancelled) {
|
||||
const e = new Event("cancel");
|
||||
events.dispatchEvent(e);
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
}, [submission]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2,50 +2,31 @@ import React from 'react';
|
|||
import TrashIcon from '../svg/TrashIcon';
|
||||
import { 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue