complete renaming functions, abstracts more svg, sets title to current convo title, adds a try again feature to errors

This commit is contained in:
Daniel Avila 2023-02-11 10:22:15 -05:00
parent 592b7629aa
commit 5af5a97d8f
24 changed files with 512 additions and 82 deletions

View file

@ -44,6 +44,18 @@ module.exports = {
return { message: 'Error saving conversation' };
}
},
updateConvo: async ({ conversationId, ...update }) => {
try {
return await Conversation.findOneAndUpdate(
{ conversationId },
update,
{ new: true }
).exec();
} catch (error) {
console.log(error);
return { message: 'Error updating conversation' };
}
},
getConvos: async () => await Conversation.find({}).exec(),
deleteConvos: async (filter) => {

52
models/Prompt.js Normal file
View file

@ -0,0 +1,52 @@
const mongoose = require('mongoose');
const promptSchema = mongoose.Schema({
title: {
type: String,
required: true
},
prompt: {
type: String,
required: true
},
category: {
type: String,
},
created: {
type: Date,
default: Date.now
}
});
const Prompt = mongoose.models.Prompt || mongoose.model('Prompt', promptSchema);
module.exports = {
savePrompt: async ({ title, prompt }) => {
try {
await Prompt.create({
title,
prompt
});
return { title, prompt };
} catch (error) {
console.error(error);
return { prompt: 'Error saving prompt' };
}
},
getPrompts: async (filter) => {
try {
return await Prompt.find(filter).exec()
} catch (error) {
console.error(error);
return { prompt: 'Error getting prompts' };
}
},
deletePrompts: async (filter) => {
try {
return await Prompt.deleteMany(filter).exec()
} catch (error) {
console.error(error);
return { prompt: 'Error deleting prompts' };
}
}
}

62
package-lock.json generated
View file

@ -21,6 +21,7 @@
"openai": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-redux": "^8.0.5",
"react-textarea-autosize": "^8.4.0",
"swr": "^2.0.3",
@ -16361,8 +16362,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"optional": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@ -16372,9 +16371,7 @@
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"optional": true,
"peer": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
@ -16603,6 +16600,25 @@
"react": "^18.2.0"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"node_modules/react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.3.0"
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@ -16759,6 +16775,14 @@
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-textarea-autosize": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.0.tgz",
@ -32185,8 +32209,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"optional": true,
"peer": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@ -32196,9 +32218,7 @@
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"optional": true,
"peer": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
},
@ -32369,6 +32389,22 @@
"scheduler": "^0.23.0"
}
},
"react-fast-compare": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
"integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
},
"react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"requires": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
}
},
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@ -32487,6 +32523,12 @@
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
}
},
"react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"requires": {}
},
"react-textarea-autosize": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.4.0.tgz",

View file

@ -33,6 +33,7 @@
"openai": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-redux": "^8.0.5",
"react-textarea-autosize": "^8.4.0",
"swr": "^2.0.3",

View file

@ -2,7 +2,8 @@ const express = require('express');
const dbConnect = require('../models/dbConnect');
const { ask, titleConversation } = require('../app/chatgpt');
const { saveMessage, getMessages, deleteMessages } = require('../models/Message');
const { saveConvo, getConvos, deleteConvos } = require('../models/Conversation');
const { saveConvo, getConvos, deleteConvos, updateConvo } = require('../models/Conversation');
const { savePrompt, getPrompts, deletePrompts } = require('../models/Prompt');
const crypto = require('crypto');
const path = require('path');
const cors = require('cors');
@ -25,6 +26,15 @@ app.get('/convos', async (req, res) => {
res.status(200).send(await getConvos());
});
app.get('/prompts', async (req, res) => {
let filter = {};
// const { search } = req.body.arg;
// if (!!search) {
// filter = { conversationId };
// }
res.status(200).send(await getPrompts(filter));
});
app.get('/messages/:conversationId', async (req, res) => {
const { conversationId } = req.params;
res.status(200).send(await getMessages({ conversationId }));
@ -46,6 +56,18 @@ app.post('/clear_convos', async (req, res) => {
}
});
app.post('/update_convo', async (req, res) => {
const update = req.body.arg;
try {
const dbResponse = await updateConvo(update);
res.status(201).send(dbResponse);
} catch (error) {
console.error(error);
res.status(500).send(error);
}
});
app.post('/ask', async (req, res) => {
console.log(req.body);
const { text, parentMessageId, conversationId } = req.body;

View file

@ -10,6 +10,7 @@ import useDidMountEffect from './hooks/useDidMountEffect';
const App = () => {
const { messages } = useSelector((state) => state.messages);
const { conversationId } = useSelector((state) => state.convo);
const { title } = useSelector((state) => state.convo);
const { data, error, isLoading, mutate } = swr('http://localhost:3050/convos');
useDidMountEffect(() => mutate(), [conversationId]);
@ -19,7 +20,7 @@ const App = () => {
<div className="flex h-full w-full flex-1 flex-col bg-gray-50 md:pl-[260px]">
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden dark:bg-gray-800">
<MobileNav />
<Messages messages={messages} />
<Messages messages={messages} title={title}/>
<TextChat
messages={messages}
reloadConvos={mutate}

View file

@ -0,0 +1,108 @@
import React from 'react';
import RenameButton from './RenameButton';
import DeleteButton from './DeleteButton';
import { useSelector, useDispatch } from 'react-redux';
import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setText } from '~/store/textSlice';
import manualSWR from '~/utils/fetchers';
{
/* <div class="flex py-3 px-3 items-center gap-3 relative rounded-md cursor-pointer hover:pr-14 break-all pr-14 bg-gray-800 hover:bg-gray-800"><svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 flex-shrink-0" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg><input type="text" class="text-sm border-none bg-transparent p-0 m-0 w-full mr-0" value="titleeeeeeeeeeeeeeeee"><div class="absolute flex right-1 z-10 text-gray-300 visible"><button class="p-1 hover:text-white"><svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="20 6 9 17 4 12"></polyline></svg></button><button class="p-1 hover:text-white"><svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button></div></div> */
}
export default function Conversation({ id, parentMessageId, title = 'New conversation' }) {
const dispatch = useDispatch();
const { conversationId } = useSelector((state) => state.convo);
const { trigger, isMutating } = manualSWR(`http://localhost:3050/messages/${id}`, 'get');
const clickHandler = async () => {
if (conversationId === id) {
return;
}
dispatch(setConversation({ error: false, conversationId: id, parentMessageId }));
const data = await trigger();
dispatch(setMessages(data));
dispatch(setText(''));
};
const aProps = {
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'
};
if (conversationId !== id) {
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';
}
return (
<div class="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 hover:pr-14">
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4 flex-shrink-0"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
<input
type="text"
class="m-0 mr-0 w-full border-none bg-transparent p-0 text-sm"
value="titleeeeeeeeeeeeeeeee"
/>
<div class="visible absolute right-1 z-10 flex text-gray-300">
<button class="p-1 hover:text-white">
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</button>
<button class="p-1 hover:text-white">
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="18"
y1="6"
x2="6"
y2="18"
></line>
<line
x1="6"
y1="6"
x2="18"
y2="18"
></line>
</svg>
</button>
</div>
</div>
);
}

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useRef } from 'react';
import RenameButton from './RenameButton';
import DeleteButton from './DeleteButton';
import { useSelector, useDispatch } from 'react-redux';
@ -6,23 +6,56 @@ import { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
import { setText } from '~/store/textSlice';
import manualSWR from '~/utils/fetchers';
import ConvoIcon from '../svg/ConvoIcon';
export default function Conversation({ id, parentMessageId, title = 'New conversation' }) {
const [renaming, setRenaming] = useState(false);
const [titleInput, setTitleInput] = useState(title);
const inputRef = useRef(null);
const dispatch = useDispatch();
const { conversationId } = useSelector((state) => state.convo);
const { trigger, isMutating } = manualSWR(`http://localhost:3050/messages/${id}`, 'get');
const rename = manualSWR(`http://localhost:3050/update_convo`, 'post');
const clickHandler = async () => {
if (conversationId === id) {
return;
}
dispatch(setConversation({ error: false, conversationId: id, parentMessageId }));
dispatch(setConversation({ title, error: false, conversationId: id, parentMessageId }));
const data = await trigger();
dispatch(setMessages(data));
dispatch(setText(''));
};
const renameHandler = (e) => {
e.preventDefault();
setRenaming(true);
setTimeout(() => {
inputRef.current.focus();
}, 25);
};
const cancelHandler = (e) => {
e.preventDefault();
setRenaming(false);
};
const onRename = (e) => {
e.preventDefault();
setRenaming(false);
if (titleInput === title) {
return;
}
rename.trigger({ conversationId, title: titleInput });
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
onRename(e);
}
};
const aProps = {
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'
@ -38,27 +71,35 @@ export default function Conversation({ id, parentMessageId, title = 'New convers
onClick={() => clickHandler()}
{...aProps}
>
<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"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<ConvoIcon />
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis break-all">
{title}
{renaming === true ? (
<input
ref={inputRef}
type="text"
className="m-0 mr-0 w-full leading-tight border border-blue-500 bg-transparent p-0 text-sm outline-none"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
onBlur={onRename}
onKeyPress={handleKeyPress}
/>
) : (
title
)}
</div>
{conversationId === id ? (
<div className="visible absolute right-1 z-10 flex text-gray-300">
<RenameButton conversationId={id} />
<DeleteButton conversationId={id} />
<RenameButton
conversationId={id}
renaming={renaming}
renameHandler={renameHandler}
onRename={onRename}
/>
<DeleteButton
conversationId={id}
renaming={renaming}
cancelHandler={cancelHandler}
/>
</div>
) : (
<div className="absolute inset-y-0 right-0 z-10 w-8 bg-gradient-to-l from-gray-900 group-hover:from-[#2A2B32]" />

View file

@ -1,11 +1,12 @@
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 { setConversation } from '~/store/convoSlice';
import { setMessages } from '~/store/messageSlice';
export default function DeleteButton({ conversationId }) {
export default function DeleteButton({ conversationId, renaming, cancelHandler }) {
const dispatch = useDispatch();
const { trigger, isMutating } = manualSWR(
'http://localhost:3050/clear_convos',
@ -17,13 +18,14 @@ export default function DeleteButton({ conversationId }) {
);
const clickHandler = () => trigger({ conversationId });
const handler = renaming ? cancelHandler : clickHandler;
return (
<button
className="p-1 hover:text-white"
onClick={clickHandler}
onClick={handler}
>
<TrashIcon />
{ renaming ? <CrossIcon/> : <TrashIcon />}
</button>
);
}

View file

@ -1,23 +1,13 @@
import React from 'react';
import RenameIcon from '../svg/RenameIcon';
import CheckMark from '../svg/CheckMark';
export default function RenameButton({ onClick, renaming, renameHandler, onRename }) {
const handler = renaming ? onRename : renameHandler;
export default function RenameButton({ onClick, disabled }) {
return (
<button className="p-1 hover:text-white">
<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"
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg>
<button className="p-1 hover:text-white" onClick={handler}>
{renaming ? <CheckMark /> : <RenameIcon />}
</button>
);
}

View file

@ -10,7 +10,7 @@ export default function NewChat() {
const clickHandler = () => {
dispatch(setText(''));
dispatch(setMessages([]));
dispatch(setConversation({ error: false, conversationId: null, parentMessageId: null }));
dispatch(setConversation({ title: 'New Chat', error: false, conversationId: null, parentMessageId: null }));
};
return (

View file

@ -0,0 +1,20 @@
import React from 'react';
export default function Prompt({ title, prompt, id }) {
return (
<div
// onclick="selectPromptTemplate(0)"
className="flex w-full flex-col gap-2 rounded-md bg-gray-50 p-4 text-left hover:bg-gray-200 dark:bg-white/5 "
>
<h2 className="m-auto flex items-center gap-3 text-lg font-normal md:flex-col md:gap-2">
{ title }
</h2>
<button>
<p className="w-full rounded-md bg-gray-50 p-3 hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-gray-900">
{prompt}
</p>
</button>
<span className="font-medium">Use prompt </span>
</div>
);
}

View file

@ -23,7 +23,7 @@ export default function Templates() {
</span>
<div
// onclick="selectPromptTemplate(0)"
className="flex w-full flex-col gap-2 rounded-md bg-gray-50 p-4 text-left hover:bg-gray-200 dark:bg-white/5 dark:hover:bg-gray-900"
className="flex w-full flex-col gap-2 rounded-md bg-gray-50 p-4 text-left hover:bg-gray-200 dark:bg-white/5 "
>
<h2 className="m-auto flex items-center gap-3 text-lg font-normal md:flex-col md:gap-2">
DAN (Do Anything Now)

View file

@ -1,13 +1,15 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { setText } from '~/store/textSlice';
import Templates from './Templates';
import useDocumentTitle from '~/hooks/useDocumentTitle';
import Templates from '../Prompts/Templates';
import SunIcon from '../svg/SunIcon';
import LightningIcon from '../svg/LightningIcon';
import CautionIcon from '../svg/CautionIcon';
export default function Landing() {
export default function Landing({ title }) {
const dispatch = useDispatch();
useDocumentTitle(title);
const clickHandler = (e) => {
e.preventDefault();

View file

@ -1,12 +1,15 @@
import React, { useEffect, useRef } from 'react';
import useDocumentTitle from '~/hooks/useDocumentTitle';
import Message from './Message';
import Landing from './Landing';
export default function Messages({ messages }) {
export default function Messages({ messages, title }) {
if (messages.length === 0) {
return <Landing />;
return <Landing title={title}/>;
}
useDocumentTitle(title);
const messagesEndRef = useRef(null);
const scrollToBottom = () => {

View file

@ -1,6 +1,7 @@
import React from 'react';
import RegenerateIcon from '../svg/RegenerateIcon';
export default function Regenerate({ submitMessage }) {
export default function Regenerate({ submitMessage, tryAgain }) {
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
@ -8,31 +9,22 @@ export default function Regenerate({ submitMessage }) {
return (
<>
<span className="mb-auto block flex justify-center text-xs md:mb-auto">
<span className="mb-2 block flex justify-center text-xs md:mb-2">
There was an error generating a response
</span>
<button
onClick={clickHandler}
className="btn btn-primary m-auto flex justify-center gap-2"
>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-3 w-3"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
<span className="m-auto flex justify-center">
<button
onClick={clickHandler}
className="btn btn-primary m-auto flex justify-center gap-2"
>
<polyline points="1 4 1 10 7 10" />
<polyline points="23 20 23 14 17 14" />
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
</svg>
Regenerate response
</button>
<RegenerateIcon />
Regenerate response
</button>
<button onClick={tryAgain} className="btn btn-neutral flex justify-center gap-2 border-0 md:border">
<RegenerateIcon />
Try another message
</button>
</span>
</>
);
}

View file

@ -88,6 +88,11 @@ export default function TextChat({ messages, reloadConvos }) {
dispatch(setText(value));
};
const tryAgain = (e) => {
e.preventDefault();
dispatch(setError(false));
};
return (
<div className="md:bg-vert-light-gradient dark:md:bg-vert-dark-gradient absolute bottom-0 left-0 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">
@ -97,7 +102,10 @@ export default function TextChat({ messages, reloadConvos }) {
{/* removed this prop shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] */}
{!!error ? (
<Regenerate submitMessage={submitMessage} />
<Regenerate
submitMessage={submitMessage}
tryAgain={tryAgain}
/>
) : (
<div className="relative flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white py-2 shadow-md dark:border-gray-900/50 dark:bg-gray-700 dark:text-white dark:shadow-lg md:py-3 md:pl-4">
<TextareaAutosize

View file

@ -0,0 +1,20 @@
import React from 'react';
export default function CheckMark() {
return (
<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="20 6 9 17 4 12" />
</svg>
);
}

View file

@ -0,0 +1,20 @@
import React from 'react';
export default function ConvoIcon() {
return (
<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"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
);
}

View file

@ -0,0 +1,31 @@
import React from 'react';
export default function CrossIcon() {
return (
<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"
>
<line
x1="18"
y1="6"
x2="6"
y2="18"
/>
<line
x1="6"
y1="6"
x2="18"
y2="18"
/>
</svg>
);
}

View file

@ -0,0 +1,22 @@
import React from 'react';
export default function Regenerate() {
return (
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-3 w-3"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="1 4 1 10 7 10" />
<polyline points="23 20 23 14 17 14" />
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
</svg>
);
}

View file

@ -0,0 +1,21 @@
import React from 'react';
export default function RenameIcon() {
return (
<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"
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg>
);
}

View file

@ -0,0 +1,20 @@
// useDocumentTitle.js
import { useRef, useEffect } from 'react';
function useDocumentTitle(title, prevailOnUnmount = false) {
const defaultTitle = useRef(document.title);
useEffect(() => {
document.title = title;
}, [title]);
useEffect(
() => () => {
if (!prevailOnUnmount) {
document.title = defaultTitle.current;
}
}, []
);
}
export default useDocumentTitle;

View file

@ -1,8 +1,8 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
active: false,
error: false,
title: 'ChatGPT Clone',
conversationId: null,
parentMessageId: null,
};