mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
customGpts persist through localStorage
This commit is contained in:
parent
62bb6ea8f8
commit
9c3a78f96b
22 changed files with 405 additions and 220 deletions
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
## Updates
|
||||
<details open>
|
||||
<summary><strong>2023-03-04</strong></summary>
|
||||
|
||||
<details>
|
||||
<details>
|
||||
<summary><strong>2023-03-01</strong></summary>
|
||||
Official ChatGPT API is out! Removed davinci since the official API is extremely fast and 10x less expensive. Since user labeling and prompt prefixing is officially supported, I will add a View feature so you can set this within chat, which gives the UI an added use case. I've kept the BrowserClient, since it's free to use like the official site.
|
||||
|
||||
|
|
@ -43,7 +47,7 @@ Here are my planned/recently finished features.
|
|||
- [x] Markdown handling
|
||||
- [x] Language Detection for code blocks
|
||||
- [x] 'Copy to clipboard' button for code blocks
|
||||
- [ ] Customize prompt prefix/label (custom ChatGPT using official API)
|
||||
- [x] Customize prompt prefix/label (custom ChatGPT using official API)
|
||||
- [ ] AI model change handling (whether to pseudo-persist convos or start new convos within existing convo)
|
||||
- [ ] Server convo pagination (limit fetch and load more with 'show more' button)
|
||||
- [ ] Bing AI Styling (for suggested responses, convo end, etc.)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ const convoSchema = mongoose.Schema({
|
|||
invocationId: {
|
||||
type: String
|
||||
},
|
||||
chatGptLabel: {
|
||||
type: String
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String
|
||||
},
|
||||
model: {
|
||||
type: String
|
||||
},
|
||||
|
|
|
|||
85
models/CustomGpt.js
Normal file
85
models/CustomGpt.js
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const customGptSchema = mongoose.Schema({
|
||||
chatGptLabel: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
promptPrefix: {
|
||||
type: String
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
created: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
const CustomGpt = mongoose.models.CustomGpt || mongoose.model('CustomGpt', customGptSchema);
|
||||
|
||||
const createCustomGpt = async ({ chatGptLabel, promptPrefix, value }) => {
|
||||
try {
|
||||
await CustomGpt.create({
|
||||
chatGptLabel,
|
||||
promptPrefix,
|
||||
value
|
||||
});
|
||||
return { chatGptLabel, promptPrefix, value };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error saving customGpt' };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCustomGpts: async (filter) => {
|
||||
try {
|
||||
return await CustomGpt.find(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error getting customGpts' };
|
||||
}
|
||||
},
|
||||
// updateCustomGpt: async ({ _id, ...update }) => {
|
||||
// try {
|
||||
// console.log('updateCustomGpt', _id, update);
|
||||
// return await CustomGpt.findOneAndUpdate({ _id }, update, {
|
||||
// new: true,
|
||||
// upsert: true
|
||||
// }).exec();
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// return { message: 'Error updating customGpt' };
|
||||
// }
|
||||
// },
|
||||
updateCustomGpt: async ({ value, ...update }) => {
|
||||
try {
|
||||
console.log('updateCustomGpt', value, update);
|
||||
|
||||
const customGpt = await CustomGpt.findOne({ value }).exec();
|
||||
|
||||
if (!customGpt) {
|
||||
return await createCustomGpt({ value, ...update });
|
||||
} else {
|
||||
return await CustomGpt.findOneAndUpdate({ value }, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
}).exec();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { message: 'Error updating customGpt' };
|
||||
}
|
||||
},
|
||||
deleteCustomGpts: async (filter) => {
|
||||
try {
|
||||
return await CustomGpt.deleteMany(filter).exec();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return { customGpt: 'Error deleting customGpts' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -1,8 +1,12 @@
|
|||
const { saveMessage, deleteMessages } = require('./Message');
|
||||
const { getCustomGpts, updateCustomGpt, deleteCustomGpts } = require('./CustomGpt');
|
||||
const { saveConvo } = require('./Conversation');
|
||||
|
||||
module.exports = {
|
||||
saveMessage,
|
||||
deleteMessages,
|
||||
saveConvo,
|
||||
};
|
||||
getCustomGpts,
|
||||
updateCustomGpt,
|
||||
deleteCustomGpts
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ app.get('/', function (req, res) {
|
|||
app.use('/ask', routes.ask);
|
||||
app.use('/messages', routes.messages);
|
||||
app.use('/convos', routes.convos);
|
||||
app.use('/customGpts', routes.customGpts);
|
||||
app.use('/prompts', routes.prompts);
|
||||
|
||||
app.listen(port, () => {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,15 @@ router.post('/', async (req, res) => {
|
|||
gptResponse.sender = model === 'chatgptCustom' ? chatGptLabel : model;
|
||||
gptResponse.final = true;
|
||||
gptResponse.text = await detectCode(gptResponse.text);
|
||||
|
||||
if (chatGptLabel?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.chatGptLabel = chatGptLabel;
|
||||
}
|
||||
|
||||
if (promptPrefix?.length > 0 && model === 'chatgptCustom') {
|
||||
gptResponse.promptPrefix = promptPrefix;
|
||||
}
|
||||
|
||||
await saveMessage(gptResponse);
|
||||
await saveConvo(gptResponse);
|
||||
sendMessage(res, gptResponse);
|
||||
|
|
|
|||
56
server/routes/customGpts.js
Normal file
56
server/routes/customGpts.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getCustomGpts, updateCustomGpt, deleteCustomGpts } = require('../../models');
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const models = (await getCustomGpts()).map(model => {
|
||||
model = model.toObject();
|
||||
model._id = model._id.toString();
|
||||
return model;
|
||||
});
|
||||
// console.log(models);
|
||||
res.status(200).send(models);
|
||||
});
|
||||
|
||||
router.post('/delete/:_id', async (req, res) => {
|
||||
const { _id } = req.params;
|
||||
let filter = {};
|
||||
|
||||
if (_id) {
|
||||
filter = { _id };
|
||||
}
|
||||
|
||||
try {
|
||||
const dbResponse = await deleteCustomGpts(filter);
|
||||
res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
// router.post('/create', async (req, res) => {
|
||||
// const payload = req.body.arg;
|
||||
|
||||
// try {
|
||||
// const dbResponse = await createCustomGpt(payload);
|
||||
// res.status(201).send(dbResponse);
|
||||
// } catch (error) {
|
||||
// console.error(error);
|
||||
// res.status(500).send(error);
|
||||
// }
|
||||
// });
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const update = req.body.arg;
|
||||
|
||||
try {
|
||||
const dbResponse = await updateCustomGpt(update);
|
||||
res.status(201).send(dbResponse);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).send(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const ask = require('./ask');
|
||||
const messages = require('./messages');
|
||||
const convos = require('./convos');
|
||||
const customGpts = require('./customGpts');
|
||||
const prompts = require('./prompts');
|
||||
|
||||
module.exports = { ask, messages, convos, prompts };
|
||||
module.exports = { ask, messages, convos, customGpts, prompts };
|
||||
|
|
@ -12,11 +12,13 @@ const App = () => {
|
|||
const { title } = useSelector((state) => state.convo);
|
||||
useDocumentTitle(title);
|
||||
|
||||
// bg-color: #343541 instead of bg-gray-800
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<Nav />
|
||||
<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/90">
|
||||
<div className="transition-width relative flex h-full w-full flex-1 flex-col items-stretch overflow-hidden bg-white dark:bg-gray-800/90">
|
||||
<MobileNav />
|
||||
{messages.length === 0 ? (
|
||||
<Landing title={title} />
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@ export default function Conversation({
|
|||
parentMessageId,
|
||||
conversationId,
|
||||
title = 'New conversation',
|
||||
bingData
|
||||
bingData,
|
||||
chatGptLabel = null,
|
||||
promptPrefix = null,
|
||||
}) {
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [titleInput, setTitleInput] = useState(title);
|
||||
const inputRef = useRef(null);
|
||||
const dispatch = useDispatch();
|
||||
// const { trigger, isMutating } = manualSWR(`http://localhost:3050/messages/${id}`, 'get');
|
||||
const { trigger } = manualSWR(`http://localhost:3050/messages/${id}`, 'get');
|
||||
const rename = manualSWR(`http://localhost:3050/convos/update`, 'post');
|
||||
|
||||
|
|
@ -28,13 +29,13 @@ export default function Conversation({
|
|||
return;
|
||||
}
|
||||
|
||||
const convo = { title, error: false, conversationId: id, chatGptLabel, promptPrefix };
|
||||
|
||||
if (bingData) {
|
||||
const { conversationSignature, clientId, invocationId } = bingData;
|
||||
dispatch(
|
||||
setConversation({
|
||||
title,
|
||||
error: false,
|
||||
conversationId: id,
|
||||
...convo,
|
||||
parentMessageId: null,
|
||||
conversationSignature,
|
||||
clientId,
|
||||
|
|
@ -44,9 +45,7 @@ export default function Conversation({
|
|||
} else {
|
||||
dispatch(
|
||||
setConversation({
|
||||
title,
|
||||
error: false,
|
||||
conversationId: id,
|
||||
...convo,
|
||||
parentMessageId,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export default function Conversations({ conversations, conversationId }) {
|
|||
parentMessageId={convo.parentMessageId}
|
||||
title={convo.title}
|
||||
conversationId={conversationId}
|
||||
chatGptLabel={convo.chatGptLabel}
|
||||
promptPrefix={convo.promptPrefix}
|
||||
bingData={bingData}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export default function Message({
|
|||
|
||||
const props = {
|
||||
className:
|
||||
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group dark:bg-gray-800'
|
||||
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
|
||||
};
|
||||
|
||||
const bgColors = {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function ClearConvos() {
|
|||
const dispatch = useDispatch();
|
||||
const { mutate } = useSWRConfig()
|
||||
|
||||
const { trigger, isMutating } = manualSWR(
|
||||
const { trigger } = manualSWR(
|
||||
'http://localhost:3050/convos/clear',
|
||||
'post',
|
||||
() => {
|
||||
|
|
|
|||
20
src/components/main/MenuItems.jsx
Normal file
20
src/components/main/MenuItems.jsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
// import { setModel, setDisabled } from '~/store/submitSlice';
|
||||
// import { swr } from '~/utils/fetchers';
|
||||
// import { setModels } from '~/store/modelSlice';
|
||||
import ModelItem from './ModelItem';
|
||||
|
||||
export default function MenuItems({ models }) {
|
||||
return (
|
||||
<>
|
||||
{models.map((modelItem, i) => (
|
||||
<ModelItem
|
||||
key={i}
|
||||
// id={modelItem._id}
|
||||
modelName={modelItem.name}
|
||||
value={modelItem.value}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
135
src/components/main/ModelDialog.jsx
Normal file
135
src/components/main/ModelDialog.jsx
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setModel, setDisabled, setCustomGpt } from '~/store/submitSlice';
|
||||
import manualSWR from '~/utils/fetchers';
|
||||
import { Button } from '../ui/Button.tsx';
|
||||
import { Input } from '../ui/Input.tsx';
|
||||
import { Label } from '../ui/Label.tsx';
|
||||
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '../ui/Dialog.tsx';
|
||||
|
||||
export default function ModelDialog({ mutate }) {
|
||||
const dispatch = useDispatch();
|
||||
const [chatGptLabel, setChatGptLabel] = useState('');
|
||||
const [promptPrefix, setPromptPrefix] = useState('');
|
||||
const [saveText, setSaveText] = useState('Save');
|
||||
const [required, setRequired] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const updateCustomGpt = manualSWR('http://localhost:3050/customGpts/', 'post');
|
||||
|
||||
const submitHandler = (e) => {
|
||||
if (chatGptLabel.length === 0) {
|
||||
e.preventDefault();
|
||||
setRequired(true);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
||||
dispatch(setModel('chatgptCustom'));
|
||||
dispatch(setDisabled(false));
|
||||
};
|
||||
|
||||
const saveHandler = (e) => {
|
||||
e.preventDefault();
|
||||
const value = chatGptLabel.toLowerCase();
|
||||
|
||||
if (chatGptLabel.length === 0) {
|
||||
setRequired(true);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
updateCustomGpt.trigger({ value, chatGptLabel, promptPrefix });
|
||||
|
||||
mutate();
|
||||
setSaveText('Saved!');
|
||||
setTimeout(() => {
|
||||
setSaveText('Save');
|
||||
}, 2500);
|
||||
|
||||
// dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
||||
// dispatch(setModel('chatgptCustom'));
|
||||
// dispatch(setDisabled(false));
|
||||
};
|
||||
|
||||
const requiredProp = required ? { required: true } : {};
|
||||
|
||||
return (
|
||||
<DialogContent className="dark:bg-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Customize ChatGPT</DialogTitle>
|
||||
<DialogDescription>
|
||||
Note: important instructions are often better placed in your message rather than the
|
||||
prefix.{' '}
|
||||
<a
|
||||
href="https://platform.openai.com/docs/guides/chat/instructing-chat-models"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<u>More info here</u>
|
||||
</a>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-right"
|
||||
>
|
||||
Custom Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={chatGptLabel}
|
||||
ref={inputRef}
|
||||
onChange={(e) => setChatGptLabel(e.target.value)}
|
||||
placeholder="Set a custom name for ChatGPT"
|
||||
className="col-span-3 shadow-[0_0_10px_rgba(0,0,0,0.10)] invalid:border-red-400 invalid:text-red-600 invalid:placeholder-red-600
|
||||
invalid:placeholder-opacity-70 invalid:ring-opacity-20 focus:ring-opacity-20 focus:invalid:border-red-400 focus:invalid:ring-red-400 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:invalid:border-red-600 dark:invalid:text-red-300 dark:invalid:placeholder-opacity-80 dark:focus:invalid:ring-red-600 dark:focus:invalid:ring-opacity-50"
|
||||
{...requiredProp}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="promptPrefix"
|
||||
className="text-right"
|
||||
>
|
||||
Prompt Prefix
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
value={promptPrefix}
|
||||
onChange={(e) => setPromptPrefix(e.target.value)}
|
||||
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
|
||||
className="col-span-3 flex h-20 w-full resize-none rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose>Cancel</DialogClose>
|
||||
<Button
|
||||
style={{ backgroundColor: 'rgb(16, 163, 127)' }}
|
||||
onClick={saveHandler}
|
||||
className="inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold text-white transition-colors"
|
||||
>
|
||||
{saveText}
|
||||
</Button>
|
||||
<DialogClose
|
||||
onClick={submitHandler}
|
||||
className="inline-flex h-10 items-center justify-center rounded-md border-none bg-slate-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900"
|
||||
>
|
||||
Submit
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,50 +1,60 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { setModel, setDisabled, setCustomGpt } from '~/store/submitSlice';
|
||||
// import { setModels } from '~/store/modelSlice';
|
||||
import ModelItem from './ModelItem';
|
||||
import { setModel, setDisabled } from '~/store/submitSlice';
|
||||
import { setConversation } from '~/store/convoSlice';
|
||||
import ModelDialog from './ModelDialog';
|
||||
import MenuItems from './MenuItems';
|
||||
// import useDidMountEffect from '~/hooks/useDidMountEffect';
|
||||
// import { swr } from '~/utils/fetchers';
|
||||
import manualSWR from '~/utils/fetchers';
|
||||
// import { setMessages } from '~/store/messageSlice';
|
||||
import { setModels } from '~/store/modelSlice';
|
||||
// import ModelItem from './ModelItem';
|
||||
import GPTIcon from '../svg/GPTIcon';
|
||||
import BingIcon from '../svg/BingIcon';
|
||||
import { Button } from '../ui/Button.tsx';
|
||||
import { Input } from '../ui/Input.tsx';
|
||||
import { Label } from '../ui/Label.tsx';
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
// DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '../ui/DropdownMenu.tsx';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
// DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle
|
||||
} from '../ui/Dialog.tsx';
|
||||
import { Dialog } from '../ui/Dialog.tsx';
|
||||
|
||||
export default function ModelMenu() {
|
||||
const dispatch = useDispatch();
|
||||
const { model } = useSelector((state) => state.submit);
|
||||
const { models } = useSelector((state) => state.models);
|
||||
const [chatGptLabel, setChatGptLabel] = useState('');
|
||||
const [promptPrefix, setPromptPrefix] = useState('');
|
||||
const [required, setRequired] = useState(false);
|
||||
const inputRef = useRef(null);
|
||||
const { trigger } = manualSWR('http://localhost:3050/customGpts', 'get', (res) => {
|
||||
console.log('models data (response)', res);
|
||||
if (models.length + res.length === models.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchedModels = res.map((modelItem) => ({
|
||||
...modelItem,
|
||||
name: modelItem.chatGptLabel
|
||||
}));
|
||||
|
||||
dispatch(setModels(fetchedModels));
|
||||
});
|
||||
|
||||
// useDidMountEffect(() => mutate(), [chatGptLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
const lastSelectedModel = JSON.parse(localStorage.getItem('model'));
|
||||
if (lastSelectedModel && lastSelectedModel !== 'chatgptCustom') {
|
||||
dispatch(setModel(lastSelectedModel));
|
||||
}
|
||||
|
||||
const cachedModels = JSON.parse(localStorage.getItem('models'));
|
||||
if (cachedModels) {
|
||||
dispatch(setModels(cachedModels));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
|
@ -52,25 +62,28 @@ export default function ModelMenu() {
|
|||
localStorage.setItem('model', JSON.stringify(model));
|
||||
}, [model]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('models', JSON.stringify(models.slice(4)));
|
||||
}, [models]);
|
||||
|
||||
const onChange = (value) => {
|
||||
if (value === 'chatgptCustom') {
|
||||
// dispatch(setDisabled(true));
|
||||
if (!value) {
|
||||
return;
|
||||
} else if (value === 'chatgptCustom') {
|
||||
// dispatch(setMessages([]));
|
||||
} else {
|
||||
dispatch(setModel(value));
|
||||
dispatch(setDisabled(false));
|
||||
}
|
||||
};
|
||||
|
||||
const submitHandler = (e) => {
|
||||
if (chatGptLabel.length === 0) {
|
||||
e.preventDefault();
|
||||
setRequired(true);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
|
||||
dispatch(setModel('chatgptCustom'));
|
||||
dispatch(setDisabled(false));
|
||||
// Set new conversation
|
||||
dispatch(
|
||||
setConversation({
|
||||
title: 'New Chat',
|
||||
error: false,
|
||||
conversationId: null,
|
||||
parentMessageId: null
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const defaultColorProps = [
|
||||
|
|
@ -80,7 +93,6 @@ export default function ModelMenu() {
|
|||
'dark:hover:bg-opacity-20',
|
||||
'dark:hover:bg-gray-900',
|
||||
'dark:hover:text-gray-400',
|
||||
// 'dark:data-[state=open]:bg-transparent',
|
||||
'dark:disabled:hover:bg-transparent'
|
||||
];
|
||||
|
||||
|
|
@ -92,12 +104,9 @@ export default function ModelMenu() {
|
|||
'dark:hover:bg-opacity-50',
|
||||
'dark:hover:bg-green-900',
|
||||
'dark:hover:text-gray-100',
|
||||
// 'dark:data-[state=open]:bg-gray-700',
|
||||
'dark:disabled:hover:bg-transparent'
|
||||
];
|
||||
|
||||
const requiredProp = required ? { required: true } : {};
|
||||
|
||||
const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps;
|
||||
const icon = model === 'bingai' ? <BingIcon button={true} /> : <GPTIcon button={true} />;
|
||||
|
||||
|
|
@ -121,109 +130,13 @@ export default function ModelMenu() {
|
|||
<DropdownMenuRadioGroup
|
||||
value={model}
|
||||
onValueChange={onChange}
|
||||
className="overflow-y-auto"
|
||||
>
|
||||
{models.map((modelItem, i) => (
|
||||
<ModelItem
|
||||
key={i}
|
||||
modelName={modelItem.name}
|
||||
value={modelItem.value}
|
||||
/>
|
||||
))}
|
||||
{/* <DropdownMenuRadioItem
|
||||
value="chatgpt"
|
||||
className="dark:font-semibold dark:hover:bg-gray-800"
|
||||
>
|
||||
ChatGPT <sup>$</sup>
|
||||
</DropdownMenuRadioItem>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuRadioItem
|
||||
value="chatgptCustom"
|
||||
className=" dark:hover:bg-gray-800"
|
||||
>
|
||||
CustomGPT <sup>$</sup>
|
||||
</DropdownMenuRadioItem>
|
||||
</DialogTrigger>
|
||||
<DropdownMenuRadioItem
|
||||
value="bingai"
|
||||
className=" dark:hover:bg-gray-800"
|
||||
>
|
||||
BingAI
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem
|
||||
value="chatgptBrowser"
|
||||
className=" dark:hover:bg-gray-800"
|
||||
>
|
||||
ChatGPT
|
||||
</DropdownMenuRadioItem> */}
|
||||
<MenuItems models={models} />
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DialogContent className="dark:bg-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Customize ChatGPT</DialogTitle>
|
||||
<DialogDescription>
|
||||
Note: important instructions are often better placed in your message rather than
|
||||
the prefix.{' '}
|
||||
<a
|
||||
href="https://platform.openai.com/docs/guides/chat/instructing-chat-models"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<u>More info here</u>
|
||||
</a>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="chatGptLabel"
|
||||
className="text-right"
|
||||
>
|
||||
Custom Name
|
||||
</Label>
|
||||
<Input
|
||||
id="chatGptLabel"
|
||||
value={chatGptLabel}
|
||||
ref={inputRef}
|
||||
onChange={(e) => setChatGptLabel(e.target.value)}
|
||||
placeholder="Set a custom name for ChatGPT"
|
||||
className="col-span-3 shadow-[0_0_10px_rgba(0,0,0,0.10)] invalid:border-red-400 invalid:text-red-600 invalid:placeholder-red-600
|
||||
invalid:placeholder-opacity-70 invalid:ring-opacity-20 focus:ring-opacity-20 focus:invalid:border-red-400 focus:invalid:ring-red-400 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:invalid:border-red-600 dark:invalid:text-red-300 dark:invalid:placeholder-opacity-80 dark:focus:invalid:ring-red-600 dark:focus:invalid:ring-opacity-50"
|
||||
{...requiredProp}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label
|
||||
htmlFor="promptPrefix"
|
||||
className="text-right"
|
||||
>
|
||||
Prompt Prefix
|
||||
</Label>
|
||||
<TextareaAutosize
|
||||
id="promptPrefix"
|
||||
value={promptPrefix}
|
||||
onChange={(e) => setPromptPrefix(e.target.value)}
|
||||
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
|
||||
className="col-span-3 flex h-20 w-full resize-none rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose>Cancel</DialogClose>
|
||||
<Button
|
||||
style={{ backgroundColor: 'rgb(16, 163, 127)' }}
|
||||
className="inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold text-white transition-colors"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<DialogClose
|
||||
onClick={submitHandler}
|
||||
className="inline-flex h-10 items-center justify-center rounded-md border-none bg-slate-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900"
|
||||
>
|
||||
Submit
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<ModelDialog mutate={trigger} />
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ export default function TextChat({ messages }) {
|
|||
parentMessageId: id,
|
||||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: null
|
||||
invocationId: null,
|
||||
chatGptLabel: model === 'chatgptCustom' ? chatGptLabel : null,
|
||||
promptPrefix: model === 'chatgptCustom' ? promptPrefix : null,
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createSlice, current } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
error: false,
|
||||
|
|
@ -8,6 +8,8 @@ const initialState = {
|
|||
conversationSignature: null,
|
||||
clientId: null,
|
||||
invocationId: null,
|
||||
chatGptLabel: null,
|
||||
promptPrefix: null,
|
||||
convosLoading: false
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,22 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||
const initialState = {
|
||||
models: [
|
||||
{
|
||||
_id: '0',
|
||||
name: 'ChatGPT',
|
||||
value: 'chatgpt'
|
||||
},
|
||||
{
|
||||
_id: '1',
|
||||
name: 'CustomGPT',
|
||||
value: 'chatgptCustom'
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: 'BingAI',
|
||||
value: 'bingai'
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'ChatGPT',
|
||||
value: 'chatgptBrowser'
|
||||
}
|
||||
|
|
@ -26,7 +30,8 @@ const currentSlice = createSlice({
|
|||
initialState,
|
||||
reducers: {
|
||||
setModels: (state, action) => {
|
||||
state.models = [...state.models, ...action.payload];
|
||||
console.log('setModels', action.payload);
|
||||
state.models = [...initialState.models, ...action.payload];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1419,10 +1419,6 @@ html {
|
|||
.border-white\/20 {
|
||||
border-color:hsla(0,0%,100%,.2)
|
||||
}
|
||||
.border-indigo-400 {
|
||||
--tw-border-opacity:1;
|
||||
border-color:rgba(129,140,248,var(--tw-border-opacity))
|
||||
}
|
||||
.border-gray-100 {
|
||||
--tw-border-opacity:1;
|
||||
border-color:rgba(236,236,241,var(--tw-border-opacity))
|
||||
|
|
@ -1431,24 +1427,9 @@ html {
|
|||
--tw-border-opacity:1;
|
||||
border-color:rgba(217,217,227,var(--tw-border-opacity))
|
||||
}
|
||||
.border-transparent {
|
||||
border-color:transparent
|
||||
}
|
||||
.border-black\/20 {
|
||||
border-color:rgba(0,0,0,.2)
|
||||
}
|
||||
.border-green-500 {
|
||||
--tw-border-opacity:1;
|
||||
border-color:rgba(25,195,125,var(--tw-border-opacity))
|
||||
}
|
||||
.border-orange-500 {
|
||||
--tw-border-opacity:1;
|
||||
border-color:rgba(224,108,43,var(--tw-border-opacity))
|
||||
}
|
||||
.border-red-500 {
|
||||
--tw-border-opacity:1;
|
||||
border-color:rgba(239,68,68,var(--tw-border-opacity))
|
||||
}
|
||||
.border-gray-500 {
|
||||
--tw-border-opacity:1;
|
||||
border-color:rgba(142,142,160,var(--tw-border-opacity))
|
||||
|
|
@ -1457,22 +1438,10 @@ html {
|
|||
--tw-bg-opacity:1;
|
||||
background-color:rgba(217,217,227,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-orange-500 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(224,108,43,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-red-500 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(239,68,68,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-\[\#5436DA\] {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(84,54,218,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-yellow-200 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(250,230,158,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-white {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(255,255,255,var(--tw-bg-opacity))
|
||||
|
|
@ -1489,28 +1458,9 @@ html {
|
|||
--tw-bg-opacity:1;
|
||||
background-color:rgba(247,247,248,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(236,236,241,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-transparent {
|
||||
background-color:transparent
|
||||
}
|
||||
.bg-gray-500\/90 {
|
||||
background-color:hsla(240,9%,59%,.9)
|
||||
}
|
||||
.bg-red-100 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(254,226,226,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-yellow-100 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(254,249,195,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-green-100 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(210,244,211,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-gray-900 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(32,33,35,var(--tw-bg-opacity))
|
||||
|
|
@ -1539,13 +1489,6 @@ html {
|
|||
--tw-bg-opacity:1!important;
|
||||
background-color:rgba(217,217,227,var(--tw-bg-opacity))!important
|
||||
}
|
||||
.bg-green-500 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(25,195,125,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-orange-500\/10 {
|
||||
background-color:rgba(224,108,43,.1)
|
||||
}
|
||||
.bg-red-500\/10 {
|
||||
background-color:rgba(239,68,68,.1)
|
||||
}
|
||||
|
|
@ -1557,10 +1500,6 @@ html {
|
|||
--tw-bg-opacity:1;
|
||||
background-color:rgba(172,172,190,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-green-600 {
|
||||
--tw-bg-opacity:1;
|
||||
background-color:rgba(16,163,127,var(--tw-bg-opacity))
|
||||
}
|
||||
.bg-opacity-75 {
|
||||
--tw-bg-opacity:0.75
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const swr = (path, successCallback) => {
|
|||
options.onSuccess = successCallback;
|
||||
}
|
||||
|
||||
return useSWR(path, fetcher);
|
||||
return useSWR(path, fetcher, options);
|
||||
}
|
||||
|
||||
export default function manualSWR(path, type, successCallback) {
|
||||
|
|
@ -27,4 +27,4 @@ export default function manualSWR(path, type, successCallback) {
|
|||
}
|
||||
const fetchFunction = type === 'get' ? fetcher : postRequest;
|
||||
return useSWRMutation(path, fetchFunction, options);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
// const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
/*We are basically telling webpack to take index.js from entry. Then check for all file extensions in resolve.
|
||||
After that apply all the rules in module.rules and produce the output and place it in main.js in the public folder.*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue