mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20: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 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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
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 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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
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 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>
|
||||||
|
|
|
||||||
|
|
@ -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} />
|
||||||
|
|
|
||||||
|
|
@ -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> */
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -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
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;
|
||||||
|
|
@ -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
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