optimistic ui for message sending and submit state

This commit is contained in:
Danny Avila 2023-02-07 16:22:35 -05:00
parent 9d41ed4615
commit 6842ac880c
19 changed files with 430 additions and 92 deletions

View file

@ -1,5 +1,5 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { getMessages } = require('./Message'); const { getMessages, deleteMessages } = require('./Message');
const convoSchema = mongoose.Schema({ const convoSchema = mongoose.Schema({
conversationId: { conversationId: {
@ -26,7 +26,7 @@ const Conversation =
mongoose.models.Conversation || mongoose.model('Conversation', convoSchema); mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
module.exports = { module.exports = {
saveConversation: async ({ conversationId, parentMessageId, title }) => { saveConvo: async ({ conversationId, parentMessageId, title }) => {
const messages = await getMessages({ conversationId }); const messages = await getMessages({ conversationId });
const update = { parentMessageId, messages }; const update = { parentMessageId, messages };
if (title) { if (title) {
@ -39,5 +39,16 @@ module.exports = {
{ new: true, upsert: true } { new: true, upsert: true }
).exec(); ).exec();
}, },
getConversations: async () => await Conversation.find({}).exec(), getConvos: async () => await Conversation.find({}).exec(),
deleteConvos: async (filter) => {
// const filter = {};
// if (!!conversationId) {
// filter = conversationId;
// }
let deleteCount = await Conversation.deleteMany(filter).exec();
deleteCount.messages = await deleteMessages(filter);
return deleteCount;
}
}; };

View file

@ -1,8 +1,8 @@
const express = require('express'); const express = require('express');
const dbConnect = require('../models/dbConnect'); const dbConnect = require('../models/dbConnect');
const { ask, titleConversation } = require('../app/chatgpt'); const { ask, titleConversation } = require('../app/chatgpt');
const { saveMessage, getMessages, deleteAllMessages } = require('../models/Message'); const { saveMessage, getMessages } = require('../models/Message');
const { saveConversation, getConversations } = require('../models/Conversation'); const { saveConvo, getConvos, deleteConvos } = require('../models/Conversation');
const crypto = require('crypto'); const crypto = require('crypto');
const path = require('path'); const path = require('path');
const cors = require('cors'); const cors = require('cors');
@ -22,7 +22,7 @@ app.get('/', function (req, res) {
}); });
app.get('/convos', async (req, res) => { app.get('/convos', async (req, res) => {
res.status(200).send(await getConversations()); res.status(200).send(await getConvos());
}); });
app.get('/messages/:conversationId', async (req, res) => { app.get('/messages/:conversationId', async (req, res) => {
@ -31,10 +31,20 @@ app.get('/messages/:conversationId', async (req, res) => {
}); });
app.post('/clear_convos', async (req, res) => { app.post('/clear_convos', async (req, res) => {
const { conversationId } = req.body; let filter = {};
const { conversationId } = req.body.arg;
console.log('conversationId', conversationId); console.log('conversationId', conversationId);
const filter = {}; if (!!conversationId) {
res.status(201).send(await deleteAllMessages(filter)); filter = { conversationId };
}
try {
const dbResponse = await deleteConvos(filter);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
}); });
app.post('/ask', async (req, res) => { app.post('/ask', async (req, res) => {
@ -51,6 +61,8 @@ app.post('/ask', async (req, res) => {
'X-Accel-Buffering': 'no' 'X-Accel-Buffering': 'no'
}); });
// res.write(`event: message\ndata: ${JSON.stringify('')}\n\n`);
let i = 0; let i = 0;
const progressCallback = async (partial) => { const progressCallback = async (partial) => {
// console.log('partial', partial); // console.log('partial', partial);
@ -74,7 +86,7 @@ app.post('/ask', async (req, res) => {
gptResponse.sender = 'GPT'; gptResponse.sender = 'GPT';
await saveMessage(gptResponse); await saveMessage(gptResponse);
await saveConversation(gptResponse); await saveConvo(gptResponse);
res.write(`event: message\ndata: ${JSON.stringify(gptResponse)}\n\n`); res.write(`event: message\ndata: ${JSON.stringify(gptResponse)}\n\n`);
res.end(); res.end();

View file

@ -4,14 +4,14 @@ import Messages from './components/main/Messages';
import TextChat from './components/main/TextChat'; import TextChat from './components/main/TextChat';
import Nav from './components/Nav'; import Nav from './components/Nav';
import MobileNav from './components/Nav/MobileNav'; import MobileNav from './components/Nav/MobileNav';
import useSWR from 'swr'; import { swr } from './utils/fetchers';
import useDidMountEffect from './hooks/useDidMountEffect';
const fetcher = (url) => fetch(url).then((res) => res.json());
// const postRequest = async (url, { arg }) => await axios.post(url, { arg });
const App = () => { const App = () => {
const messages = useSelector((state) => state.messages); const { messages } = useSelector((state) => state.messages);
const { data, error, isLoading, mutate } = useSWR('http://localhost:3050/convos', fetcher); const convo = useSelector((state) => state.convo);
const { data, error, isLoading, mutate } = swr('http://localhost:3050/convos');
useDidMountEffect(() => mutate(), [convo]);
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
@ -22,7 +22,10 @@ const App = () => {
{/* <main className="relative h-full w-full transition-width flex flex-col overflow-hidden items-stretch flex-1"> */} {/* <main className="relative h-full w-full transition-width flex flex-col overflow-hidden items-stretch flex-1"> */}
<MobileNav /> <MobileNav />
<Messages messages={messages} /> <Messages messages={messages} />
<TextChat messages={messages} reloadConvos={mutate} /> <TextChat
messages={messages}
reloadConvos={mutate}
/>
{/* </main> */} {/* </main> */}
</div> </div>
</div> </div>

View file

@ -4,32 +4,29 @@ import DeleteButton from './DeleteButton';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice'; import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice'; import { setMessages } from '~/store/messageSlice';
import useSWRMutation from 'swr/mutation'; import manualSWR from '~/utils/fetchers';
const fetcher = (url) => fetch(url).then((res) => res.json());
export default function Conversation({ id, parentMessageId, title = 'New conversation' }) { export default function Conversation({ id, parentMessageId, title = 'New conversation' }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const conversationId = useSelector((state) => state.convo.conversationId); const conversationId = useSelector((state) => state.convo.conversationId);
const { trigger, isMutating } = manualSWR(
const { trigger, isMutating } = useSWRMutation(
`http://localhost:3050/messages/${id}`, `http://localhost:3050/messages/${id}`,
fetcher, 'get',
{ (res) => dispatch(setMessages(res))
onSuccess: function (res) {
dispatch(setMessages(res));
}
}
); );
const onConvoClick = (id, parentMessageId) => { const clickHandler = () => {
if (conversationId === id) {
return;
}
dispatch(setConversation({ conversationId: id, parentMessageId })); dispatch(setConversation({ conversationId: id, parentMessageId }));
trigger(); trigger();
}; };
return ( return (
<a <a
onClick={() => onConvoClick(id, parentMessageId)} onClick={() => clickHandler()}
className="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" className="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"
> >
<svg <svg
@ -50,8 +47,8 @@ export default function Conversation({ id, parentMessageId, title = 'New convers
{title} {title}
</div> </div>
<div className="visible absolute right-1 z-10 flex text-gray-300"> <div className="visible absolute right-1 z-10 flex text-gray-300">
{id === conversationId && <RenameButton />} {id === conversationId && <RenameButton conversationId={id} />}
{id === conversationId && <DeleteButton />} {id === conversationId && <DeleteButton conversationId={id} />}
</div> </div>
</a> </a>
); );

View file

@ -1,37 +1,29 @@
import React from 'react'; import React from 'react';
import TrashIcon from '../svg/TrashIcon'; import TrashIcon from '../svg/TrashIcon';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
export default function DeleteButton({ conversationId }) {
const dispatch = useDispatch();
const { trigger, isMutating } = manualSWR(
'http://localhost:3050/clear_convos',
'post',
() => {
dispatch(setMessages([]));
dispatch(setConversation({ conversationId: null, parentMessageId: null }));
}
);
const clickHandler = () => trigger({ conversationId });
export default function DeleteButton({ onClick, disabled }) {
return ( return (
<button className="p-1 hover:text-white"> <button
className="p-1 hover:text-white"
onClick={clickHandler}
>
<TrashIcon /> <TrashIcon />
{/* <svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<line
x1="10"
y1="11"
x2="10"
y2="17"
/>
<line
x1="14"
y1="11"
x2="14"
y2="17"
/>
</svg> */}
</button> </button>
); );
} }

View file

@ -1,10 +1,14 @@
import React from 'react'; import React from 'react';
export default function NavLink({ svg, text }) { export default function NavLink({ svg, text, clickHandler}) {
return ( // const props
<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"> // if (clickHandler) {
{svg()}
{text} // }
</a> // return (
); // <a {clickHandler && onClick={clickHandler}} 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">
// {svg()}
// {text}
// </a>
// );
} }

View file

@ -3,13 +3,31 @@ import NavLink from './NavLink';
import TrashIcon from '../svg/TrashIcon'; import TrashIcon from '../svg/TrashIcon';
import DarkModeIcon from '../svg/DarkModeIcon'; import DarkModeIcon from '../svg/DarkModeIcon';
import LogOutIcon from '../svg/LogOutIcon'; import LogOutIcon from '../svg/LogOutIcon';
import manualSWR from '~/utils/fetchers';
import { useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
export default function NavLinks() { export default function NavLinks() {
const dispatch = useDispatch();
const { trigger, isMutating } = manualSWR(
'http://localhost:3050/clear_convos',
'post',
() => {
dispatch(setMessages([]));
dispatch(setConversation({ conversationId: null, parentMessageId: null }));
}
);
const clickHandler = () => trigger({});
return ( return (
<> <>
<NavLink <NavLink
svg={TrashIcon} svg={TrashIcon}
text="Clear conversations" text="Clear conversations"
onClick={clickHandler}
/> />
<NavLink <NavLink
svg={DarkModeIcon} svg={DarkModeIcon}

View file

@ -1,8 +1,21 @@
import React from 'react'; import React from 'react';
import { useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
export default function NewChat() { export default function NewChat() {
const dispatch = useDispatch();
const clickHandler = () => {
dispatch(setMessages([]));
dispatch(setConversation({ conversationId: null, parentMessageId: null }));
};
return ( return (
<a className="mb-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"> <a
onClick={clickHandler}
className="mb-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10"
>
<svg <svg
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"

View file

@ -0,0 +1,170 @@
import React from 'react';
export default function Landing() {
return (
<div className="flex h-full flex-col items-center text-sm dark:bg-gray-800">
<div className="w-full px-6 text-gray-800 dark:text-gray-100 md:flex md:max-w-2xl md:flex-col lg:max-w-3xl">
<h1 className="mt-6 ml-auto mr-auto mb-10 flex items-center justify-center gap-2 text-center text-4xl font-semibold sm:mt-[20vh] sm:mb-16">
ChatGPT Clone
</h1>
<div className="items-start gap-3.5 text-center md:flex">
<div className="mb-8 flex flex-1 flex-col gap-3.5 md:mb-auto">
<h2 className="m-auto flex items-center gap-3 text-lg font-normal md:flex-col md:gap-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-6 w-6"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="5"
></circle>
<line
x1="12"
y1="1"
x2="12"
y2="3"
/>
<line
x1="12"
y1="21"
x2="12"
y2="23"
/>
<line
x1="4.22"
y1="4.22"
x2="5.64"
y2="5.64"
/>
<line
x1="18.36"
y1="18.36"
x2="19.78"
y2="19.78"
/>
<line
x1="1"
y1="12"
x2="3"
y2="12"
/>
<line
x1="21"
y1="12"
x2="23"
y2="12"
/>
<line
x1="4.22"
y1="19.78"
x2="5.64"
y2="18.36"
/>
<line
x1="18.36"
y1="5.64"
x2="19.78"
y2="4.22"
/>
</svg>
Examples
</h2>
<ul className="m-auto flex w-full flex-col gap-3.5 sm:max-w-md">
<button className="w-full rounded-md bg-gray-50 p-3 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-gray-900">
"Explain quantum computing in simple terms"
</button>
<button className="w-full rounded-md bg-gray-50 p-3 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-gray-900">
"Got any creative ideas for a 10 year olds birthday?"
</button>
<button className="w-full rounded-md bg-gray-50 p-3 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-gray-900">
"How do I make an HTTP request in Javascript?"
</button>
</ul>
</div>
<div className="mb-8 flex flex-1 flex-col gap-3.5 md:mb-auto">
<h2 className="m-auto flex items-center gap-3 text-lg font-normal md:flex-col md:gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
aria-hidden="true"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z"
/>
</svg>
Capabilities
</h2>
<ul className="m-auto flex w-full flex-col gap-3.5 sm:max-w-md">
<li className="w-full rounded-md bg-gray-50 p-3 dark:bg-white/5">
Remembers what user said earlier in the conversation
</li>
<li className="w-full rounded-md bg-gray-50 p-3 dark:bg-white/5">
Allows user to provide follow-up corrections
</li>
<li className="w-full rounded-md bg-gray-50 p-3 dark:bg-white/5">
Trained to decline inappropriate requests
</li>
</ul>
</div>
<div className="mb-8 flex flex-1 flex-col gap-3.5 md:mb-auto">
<h2 className="m-auto flex items-center gap-3 text-lg font-normal md:flex-col md:gap-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-6 w-6"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line
x1="12"
y1="9"
x2="12"
y2="13"
/>
<line
x1="12"
y1="17"
x2="12.01"
y2="17"
/>
</svg>
Limitations
</h2>
<ul className="m-auto flex w-full flex-col gap-3.5 sm:max-w-md">
<li className="w-full rounded-md bg-gray-50 p-3 dark:bg-white/5">
May occasionally generate incorrect information
</li>
<li className="w-full rounded-md bg-gray-50 p-3 dark:bg-white/5">
May occasionally produce harmful instructions or biased content
</li>
<li className="w-full rounded-md bg-gray-50 p-3 dark:bg-white/5">
Limited knowledge of world and events after 2021
</li>
</ul>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
export default function Message({ sender, text }) { export default function Message({ sender, text, last = false}) {
const { isSubmitting } = useSelector((state) => state.submit);
const props = { const props = {
className: className:
'group w-full border-b border-black/10 text-gray-800 dark:border-gray-900/50 dark:bg-gray-800 dark:text-gray-100' 'group w-full border-b border-black/10 text-gray-800 dark:border-gray-900/50 dark:bg-gray-800 dark:text-gray-100'
@ -16,7 +18,10 @@ export default function Message({ sender, text }) {
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl"> <div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<strong className="relative flex w-[30px] flex-col items-end">{sender}:</strong> <strong className="relative flex w-[30px] flex-col items-end">{sender}:</strong>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]"> <div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
{text} <span>
{text}
{isSubmitting && last && sender === 'GPT' && <span className="blink"></span>}
</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,13 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import Message from './Message'; import Message from './Message';
import Landing from './Landing';
export default function Messages({ messages }) { export default function Messages({ messages }) {
if (messages.length === 0) {
return <Landing />
};
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const scrollToBottom = () => { const scrollToBottom = () => {
@ -23,6 +29,7 @@ export default function Messages({ messages }) {
key={i} key={i}
sender={message.sender} sender={message.sender}
text={message.text} text={message.text}
last={i === messages.length - 1}
/> />
))} ))}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />

View file

@ -1,8 +1,30 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import useDidMountEffect from '~/hooks/useDidMountEffect';
export default function SubmitButton({ onClick, disabled }) { export default function SubmitButton({ submitMessage }) {
const { isSubmitting } = useSelector((state) => state.submit);
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
};
if (isSubmitting) {
return (
<button className="absolute bottom-1.5 right-1 rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:bottom-0.5 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:bottom-2.5 md:right-2 md:disabled:bottom-1">
<div className="text-2xl">
<span >·</span>
<span className="blink">·</span>
<span className="blink2">·</span>
</div>
</button>
);
}
return ( return (
<button onClick={onClick} className="absolute bottom-1.5 right-1 rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:bottom-2.5 md:right-2"> <button
onClick={clickHandler}
className="absolute bottom-1.5 right-1 rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:bottom-2.5 md:right-2"
>
<svg <svg
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
@ -26,3 +48,7 @@ export default function SubmitButton({ onClick, disabled }) {
</button> </button>
); );
} }
{
/* <div class="text-2xl"><span class="">·</span><span class="">·</span><span class="invisible">·</span></div> */
}

View file

@ -5,16 +5,23 @@ import handleSubmit from '~/utils/handleSubmit';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice'; import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice'; import { setMessages } from '~/store/messageSlice';
import { setSubmitState } from '~/store/submitSlice';
export default function TextChat({ messages, reloadConvos }) { export default function TextChat({ messages, reloadConvos }) {
const [text, setText] = useState(''); const [text, setText] = useState('');
const dispatch = useDispatch(); const dispatch = useDispatch();
const convo = useSelector((state) => state.convo); const convo = useSelector((state) => state.convo);
const { isSubmitting } = useSelector((state) => state.submit);
const submitMessage = () => { const submitMessage = () => {
if (!!isSubmitting || text.trim() === '') {
return;
}
dispatch(setSubmitState(true));
const payload = text.trim(); const payload = text.trim();
const currentMsg = { sender: 'user', text: payload, current: true }; const currentMsg = { sender: 'user', text: payload, current: true };
dispatch(setMessages([...messages, currentMsg])); const initialResponse = { sender: 'GPT', text: '' };
dispatch(setMessages([...messages, currentMsg, initialResponse]));
setText(''); setText('');
const messageHandler = (data) => { const messageHandler = (data) => {
dispatch(setMessages([...messages, currentMsg, { sender: 'GPT', text: data }])); dispatch(setMessages([...messages, currentMsg, { sender: 'GPT', text: data }]));
@ -26,6 +33,7 @@ export default function TextChat({ messages, reloadConvos }) {
} }
reloadConvos(); reloadConvos();
dispatch(setSubmitState(false));
}; };
console.log('User Input:', payload); console.log('User Input:', payload);
handleSubmit(payload, messageHandler, convo, convoHandler); handleSubmit(payload, messageHandler, convo, convoHandler);
@ -37,18 +45,22 @@ export default function TextChat({ messages, reloadConvos }) {
} }
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
if (!!isSubmitting) {
return;
}
submitMessage(); submitMessage();
} }
}; };
// <> const changeHandler = (e) => {
// <textarea // console.log('changeHandler', JSON.stringify(e.target.value));
// className="m-10 h-16 p-4" const { value } = e.target;
// value={text} if (isSubmitting && (value === '' || value === '\n')) {
// onKeyUp={handleKeyPress} return;
// onChange={(e) => setText(e.target.value)} }
// /> setText(value);
// </> };
return ( return (
<div className="md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient w-full border-t bg-white dark:border-white/20 dark:bg-gray-800 md:border-t-0 md:border-transparent md:!bg-transparent md:dark:border-transparent"> <div className="md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient w-full border-t bg-white dark:border-white/20 dark:bg-gray-800 md:border-t-0 md:border-transparent md:!bg-transparent md:dark:border-transparent">
<form className="stretch mx-2 flex flex-row gap-3 pt-2 last:mb-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6"> <form className="stretch mx-2 flex flex-row gap-3 pt-2 last:mb-2 md:last:mb-6 lg:mx-auto lg:max-w-3xl lg:pt-6">
@ -61,11 +73,11 @@ export default function TextChat({ messages, reloadConvos }) {
rows="1" rows="1"
value={text} value={text}
onKeyUp={handleKeyPress} onKeyUp={handleKeyPress}
onChange={(e) => setText(e.target.value)} onChange={changeHandler}
placeholder="" placeholder=""
className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-2 pr-7 leading-6 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent md:pl-0" className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-2 pr-7 leading-6 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent md:pl-0"
/> />
<SubmitButton onClick={() => submitMessage()} /> <SubmitButton submitMessage={submitMessage} />
</div> </div>
</div> </div>
</form> </form>

View file

@ -11,9 +11,7 @@ const currentSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
setConversation: (state, action) => { setConversation: (state, action) => {
const { conversationId, parentMessageId } = action.payload; return { ...state, ...action.payload };
state.conversationId = conversationId;
state.parentMessageId = parentMessageId;
}, },
} }
}); });

View file

@ -2,10 +2,12 @@ import { configureStore } from '@reduxjs/toolkit';
import convoReducer from './convoSlice.js'; import convoReducer from './convoSlice.js';
import messageReducer from './messageSlice.js' import messageReducer from './messageSlice.js'
import submitReducer from './submitSlice.js'
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
convo: convoReducer, convo: convoReducer,
messages: messageReducer, messages: messageReducer,
submit: submitReducer,
}, },
}); });

View file

@ -1,18 +1,19 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
const initialState = []; const initialState = {
messages: [],
};
const currentSlice = createSlice({ const currentSlice = createSlice({
name: 'messages', name: 'messages',
initialState, initialState,
reducers: { reducers: {
setMessages: (state, action) => { setMessages: (state, action) => {
const { payload } = action; state.messages = [...action.payload];
return [...payload];
}, },
} }
}); });
export const { setMessages } = currentSlice.actions; export const { setMessages, setSubmitState } = currentSlice.actions;
export default currentSlice.reducer; export default currentSlice.reducer;

19
src/store/submitSlice.js Normal file
View file

@ -0,0 +1,19 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isSubmitting: false,
};
const currentSlice = createSlice({
name: 'submit',
initialState,
reducers: {
setSubmitState: (state, action) => {
state.isSubmitting = action.payload;
},
}
});
export const { setSubmitState } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -5,4 +5,34 @@
/* * { /* * {
box-sizing: border-box; box-sizing: border-box;
outline: 1px solid limegreen !important; outline: 1px solid limegreen !important;
} */ } */
.blink {
animation: blink 1s linear infinite;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.blink2 {
animation: blink 1500ms linear infinite;
}
@keyframes blink2 {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}

18
src/utils/fetchers.js Normal file
View file

@ -0,0 +1,18 @@
import axios from 'axios';
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
const fetcher = (url) => fetch(url).then((res) => res.json());
const postRequest = async (url, { arg }) => {
return await axios.post(url, { arg });
};
export const swr = (path) => useSWR(path, fetcher);
export default function manualSWR(path, type, successCallback) {
const fetchFunction = type === 'get' ? fetcher : postRequest;
return useSWRMutation(path, fetchFunction, {
onSuccess: successCallback
});
};