diff --git a/models/Conversation.js b/models/Conversation.js
index d964f1874c..c0387f54f1 100644
--- a/models/Conversation.js
+++ b/models/Conversation.js
@@ -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) => {
diff --git a/models/Prompt.js b/models/Prompt.js
new file mode 100644
index 0000000000..612278d038
--- /dev/null
+++ b/models/Prompt.js
@@ -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' };
+ }
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0d2a121aab..c859f1d7b2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 3525b27baf..b01f3230af 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/server/index.js b/server/index.js
index e4efa46c20..2469632fb0 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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;
diff --git a/src/App.jsx b/src/App.jsx
index 9efa7c0128..6352197fdd 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -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 = () => {
*/
+}
+
+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 (
+
+ );
+}
diff --git a/src/components/Conversations/Conversation.jsx b/src/components/Conversations/Conversation.jsx
index 53c2c56aac..4dfa96006b 100644
--- a/src/components/Conversations/Conversation.jsx
+++ b/src/components/Conversations/Conversation.jsx
@@ -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}
>
-
+
- {title}
+ {renaming === true ? (
+ setTitleInput(e.target.value)}
+ onBlur={onRename}
+ onKeyPress={handleKeyPress}
+ />
+ ) : (
+ title
+ )}
{conversationId === id ? (
-
-
+
+
) : (
diff --git a/src/components/Conversations/DeleteButton.jsx b/src/components/Conversations/DeleteButton.jsx
index 7fd38fd69f..64ea642c32 100644
--- a/src/components/Conversations/DeleteButton.jsx
+++ b/src/components/Conversations/DeleteButton.jsx
@@ -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 (
);
}
diff --git a/src/components/Conversations/RenameButton.jsx b/src/components/Conversations/RenameButton.jsx
index 362dfb6c09..d13a5e5db6 100644
--- a/src/components/Conversations/RenameButton.jsx
+++ b/src/components/Conversations/RenameButton.jsx
@@ -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 (
-