mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
optimistic ui for message sending and submit state
This commit is contained in:
parent
9d41ed4615
commit
6842ac880c
19 changed files with 430 additions and 92 deletions
|
|
@ -1,5 +1,5 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { getMessages } = require('./Message');
|
||||
const { getMessages, deleteMessages } = require('./Message');
|
||||
|
||||
const convoSchema = mongoose.Schema({
|
||||
conversationId: {
|
||||
|
|
@ -26,7 +26,7 @@ const Conversation =
|
|||
mongoose.models.Conversation || mongoose.model('Conversation', convoSchema);
|
||||
|
||||
module.exports = {
|
||||
saveConversation: async ({ conversationId, parentMessageId, title }) => {
|
||||
saveConvo: async ({ conversationId, parentMessageId, title }) => {
|
||||
const messages = await getMessages({ conversationId });
|
||||
const update = { parentMessageId, messages };
|
||||
if (title) {
|
||||
|
|
@ -39,5 +39,16 @@ module.exports = {
|
|||
{ new: true, upsert: true }
|
||||
).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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
const express = require('express');
|
||||
const dbConnect = require('../models/dbConnect');
|
||||
const { ask, titleConversation } = require('../app/chatgpt');
|
||||
const { saveMessage, getMessages, deleteAllMessages } = require('../models/Message');
|
||||
const { saveConversation, getConversations } = require('../models/Conversation');
|
||||
const { saveMessage, getMessages } = require('../models/Message');
|
||||
const { saveConvo, getConvos, deleteConvos } = require('../models/Conversation');
|
||||
const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
|
|
@ -22,7 +22,7 @@ app.get('/', function (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) => {
|
||||
|
|
@ -31,10 +31,20 @@ app.get('/messages/:conversationId', 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);
|
||||
const filter = {};
|
||||
res.status(201).send(await deleteAllMessages(filter));
|
||||
if (!!conversationId) {
|
||||
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) => {
|
||||
|
|
@ -51,6 +61,8 @@ app.post('/ask', async (req, res) => {
|
|||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
|
||||
// res.write(`event: message\ndata: ${JSON.stringify('')}\n\n`);
|
||||
|
||||
let i = 0;
|
||||
const progressCallback = async (partial) => {
|
||||
// console.log('partial', partial);
|
||||
|
|
@ -74,7 +86,7 @@ app.post('/ask', async (req, res) => {
|
|||
|
||||
gptResponse.sender = 'GPT';
|
||||
await saveMessage(gptResponse);
|
||||
await saveConversation(gptResponse);
|
||||
await saveConvo(gptResponse);
|
||||
|
||||
res.write(`event: message\ndata: ${JSON.stringify(gptResponse)}\n\n`);
|
||||
res.end();
|
||||
|
|
|
|||
17
src/App.jsx
17
src/App.jsx
|
|
@ -4,14 +4,14 @@ import Messages from './components/main/Messages';
|
|||
import TextChat from './components/main/TextChat';
|
||||
import Nav from './components/Nav';
|
||||
import MobileNav from './components/Nav/MobileNav';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||
// const postRequest = async (url, { arg }) => await axios.post(url, { arg });
|
||||
import { swr } from './utils/fetchers';
|
||||
import useDidMountEffect from './hooks/useDidMountEffect';
|
||||
|
||||
const App = () => {
|
||||
const messages = useSelector((state) => state.messages);
|
||||
const { data, error, isLoading, mutate } = useSWR('http://localhost:3050/convos', fetcher);
|
||||
const { messages } = useSelector((state) => state.messages);
|
||||
const convo = useSelector((state) => state.convo);
|
||||
const { data, error, isLoading, mutate } = swr('http://localhost:3050/convos');
|
||||
useDidMountEffect(() => mutate(), [convo]);
|
||||
|
||||
return (
|
||||
<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"> */}
|
||||
<MobileNav />
|
||||
<Messages messages={messages} />
|
||||
<TextChat messages={messages} reloadConvos={mutate} />
|
||||
<TextChat
|
||||
messages={messages}
|
||||
reloadConvos={mutate}
|
||||
/>
|
||||
{/* </main> */}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,32 +4,29 @@ import DeleteButton from './DeleteButton';
|
|||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { setConversation } from '~/store/convoSlice';
|
||||
import { setMessages } from '~/store/messageSlice';
|
||||
import useSWRMutation from 'swr/mutation';
|
||||
|
||||
const fetcher = (url) => fetch(url).then((res) => res.json());
|
||||
import manualSWR from '~/utils/fetchers';
|
||||
|
||||
export default function Conversation({ id, parentMessageId, title = 'New conversation' }) {
|
||||
const dispatch = useDispatch();
|
||||
const conversationId = useSelector((state) => state.convo.conversationId);
|
||||
|
||||
const { trigger, isMutating } = useSWRMutation(
|
||||
const { trigger, isMutating } = manualSWR(
|
||||
`http://localhost:3050/messages/${id}`,
|
||||
fetcher,
|
||||
{
|
||||
onSuccess: function (res) {
|
||||
dispatch(setMessages(res));
|
||||
}
|
||||
}
|
||||
'get',
|
||||
(res) => dispatch(setMessages(res))
|
||||
);
|
||||
|
||||
const onConvoClick = (id, parentMessageId) => {
|
||||
const clickHandler = () => {
|
||||
if (conversationId === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setConversation({ conversationId: id, parentMessageId }));
|
||||
trigger();
|
||||
};
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<svg
|
||||
|
|
@ -50,8 +47,8 @@ export default function Conversation({ id, parentMessageId, title = 'New convers
|
|||
{title}
|
||||
</div>
|
||||
<div className="visible absolute right-1 z-10 flex text-gray-300">
|
||||
{id === conversationId && <RenameButton />}
|
||||
{id === conversationId && <DeleteButton />}
|
||||
{id === conversationId && <RenameButton conversationId={id} />}
|
||||
{id === conversationId && <DeleteButton conversationId={id} />}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,37 +1,29 @@
|
|||
import React from 'react';
|
||||
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 (
|
||||
<button className="p-1 hover:text-white">
|
||||
<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"
|
||||
<button
|
||||
className="p-1 hover:text-white"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<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> */}
|
||||
<TrashIcon />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function NavLink({ svg, text }) {
|
||||
return (
|
||||
<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">
|
||||
{svg()}
|
||||
{text}
|
||||
</a>
|
||||
);
|
||||
export default function NavLink({ svg, text, clickHandler}) {
|
||||
// const props
|
||||
// if (clickHandler) {
|
||||
|
||||
// }
|
||||
// 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>
|
||||
// );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,31 @@ import NavLink from './NavLink';
|
|||
import TrashIcon from '../svg/TrashIcon';
|
||||
import DarkModeIcon from '../svg/DarkModeIcon';
|
||||
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() {
|
||||
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 (
|
||||
<>
|
||||
<NavLink
|
||||
svg={TrashIcon}
|
||||
text="Clear conversations"
|
||||
onClick={clickHandler}
|
||||
/>
|
||||
<NavLink
|
||||
svg={DarkModeIcon}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setConversation } from '~/store/convoSlice';
|
||||
import { setMessages } from '~/store/messageSlice';
|
||||
|
||||
export default function NewChat() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const clickHandler = () => {
|
||||
dispatch(setMessages([]));
|
||||
dispatch(setConversation({ conversationId: null, parentMessageId: null }));
|
||||
};
|
||||
|
||||
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
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
|
|
|
|||
170
src/components/main/Landing.jsx
Normal file
170
src/components/main/Landing.jsx
Normal 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 old’s 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
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 = {
|
||||
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'
|
||||
|
|
@ -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">
|
||||
<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)]">
|
||||
<span>
|
||||
{text}
|
||||
{isSubmitting && last && sender === 'GPT' && <span className="blink">█</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import Message from './Message';
|
||||
import Landing from './Landing';
|
||||
|
||||
export default function Messages({ messages }) {
|
||||
|
||||
if (messages.length === 0) {
|
||||
return <Landing />
|
||||
};
|
||||
|
||||
const messagesEndRef = useRef(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
|
|
@ -23,6 +29,7 @@ export default function Messages({ messages }) {
|
|||
key={i}
|
||||
sender={message.sender}
|
||||
text={message.text}
|
||||
last={i === messages.length - 1}
|
||||
/>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
|
|
|
|||
|
|
@ -1,8 +1,30 @@
|
|||
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 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 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 (
|
||||
<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
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
|
|
@ -26,3 +48,7 @@ export default function SubmitButton({ onClick, disabled }) {
|
|||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
/* <div class="text-2xl"><span class="">·</span><span class="">·</span><span class="invisible">·</span></div> */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,23 @@ import handleSubmit from '~/utils/handleSubmit';
|
|||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { setConversation } from '~/store/convoSlice';
|
||||
import { setMessages } from '~/store/messageSlice';
|
||||
import { setSubmitState } from '~/store/submitSlice';
|
||||
|
||||
export default function TextChat({ messages, reloadConvos }) {
|
||||
const [text, setText] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const convo = useSelector((state) => state.convo);
|
||||
const { isSubmitting } = useSelector((state) => state.submit);
|
||||
|
||||
const submitMessage = () => {
|
||||
if (!!isSubmitting || text.trim() === '') {
|
||||
return;
|
||||
}
|
||||
dispatch(setSubmitState(true));
|
||||
const payload = text.trim();
|
||||
const currentMsg = { sender: 'user', text: payload, current: true };
|
||||
dispatch(setMessages([...messages, currentMsg]));
|
||||
const initialResponse = { sender: 'GPT', text: '' };
|
||||
dispatch(setMessages([...messages, currentMsg, initialResponse]));
|
||||
setText('');
|
||||
const messageHandler = (data) => {
|
||||
dispatch(setMessages([...messages, currentMsg, { sender: 'GPT', text: data }]));
|
||||
|
|
@ -26,6 +33,7 @@ export default function TextChat({ messages, reloadConvos }) {
|
|||
}
|
||||
|
||||
reloadConvos();
|
||||
dispatch(setSubmitState(false));
|
||||
};
|
||||
console.log('User Input:', payload);
|
||||
handleSubmit(payload, messageHandler, convo, convoHandler);
|
||||
|
|
@ -37,18 +45,22 @@ export default function TextChat({ messages, reloadConvos }) {
|
|||
}
|
||||
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (!!isSubmitting) {
|
||||
return;
|
||||
}
|
||||
submitMessage();
|
||||
}
|
||||
};
|
||||
|
||||
// <>
|
||||
// <textarea
|
||||
// className="m-10 h-16 p-4"
|
||||
// value={text}
|
||||
// onKeyUp={handleKeyPress}
|
||||
// onChange={(e) => setText(e.target.value)}
|
||||
// />
|
||||
// </>
|
||||
const changeHandler = (e) => {
|
||||
// console.log('changeHandler', JSON.stringify(e.target.value));
|
||||
const { value } = e.target;
|
||||
if (isSubmitting && (value === '' || value === '\n')) {
|
||||
return;
|
||||
}
|
||||
setText(value);
|
||||
};
|
||||
|
||||
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">
|
||||
<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"
|
||||
value={text}
|
||||
onKeyUp={handleKeyPress}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onChange={changeHandler}
|
||||
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"
|
||||
/>
|
||||
<SubmitButton onClick={() => submitMessage()} />
|
||||
<SubmitButton submitMessage={submitMessage} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ const currentSlice = createSlice({
|
|||
initialState,
|
||||
reducers: {
|
||||
setConversation: (state, action) => {
|
||||
const { conversationId, parentMessageId } = action.payload;
|
||||
state.conversationId = conversationId;
|
||||
state.parentMessageId = parentMessageId;
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import { configureStore } from '@reduxjs/toolkit';
|
|||
|
||||
import convoReducer from './convoSlice.js';
|
||||
import messageReducer from './messageSlice.js'
|
||||
import submitReducer from './submitSlice.js'
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
convo: convoReducer,
|
||||
messages: messageReducer,
|
||||
submit: submitReducer,
|
||||
},
|
||||
});
|
||||
|
|
@ -1,18 +1,19 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = [];
|
||||
const initialState = {
|
||||
messages: [],
|
||||
};
|
||||
|
||||
const currentSlice = createSlice({
|
||||
name: 'messages',
|
||||
initialState,
|
||||
reducers: {
|
||||
setMessages: (state, action) => {
|
||||
const { payload } = action;
|
||||
return [...payload];
|
||||
state.messages = [...action.payload];
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const { setMessages } = currentSlice.actions;
|
||||
export const { setMessages, setSubmitState } = currentSlice.actions;
|
||||
|
||||
export default currentSlice.reducer;
|
||||
|
|
|
|||
19
src/store/submitSlice.js
Normal file
19
src/store/submitSlice.js
Normal 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;
|
||||
|
|
@ -6,3 +6,33 @@
|
|||
box-sizing: border-box;
|
||||
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
18
src/utils/fetchers.js
Normal 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
|
||||
});
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue