diff --git a/api/models/Preset.js b/api/models/Preset.js new file mode 100644 index 0000000000..de33370e60 --- /dev/null +++ b/api/models/Preset.js @@ -0,0 +1,46 @@ +const Preset = require('./schema/presetSchema'); + +const getPreset = async (user, presetId) => { + try { + return await Preset.findOne({ user, presetId }).exec(); + } catch (error) { + console.log(error); + return { message: 'Error getting single preset' }; + } +}; + +module.exports = { + Preset, + getPreset, + getPresets: async (user, filter) => { + try { + return await Preset.find({ ...filter, user }).exec(); + } catch (error) { + console.log(error); + return { message: 'Error retriving presets' }; + } + }, + savePreset: async (user, { presetId, newPresetId, ...preset }) => { + try { + const update = { presetId, ...preset }; + if (newPresetId) { + update.presetId = newPresetId; + } + + return await Preset.findOneAndUpdate( + { presetId, user }, + { $set: update }, + { new: true, upsert: true } + ).exec(); + } catch (error) { + console.log(error); + return { message: 'Error saving preset' }; + } + }, + deletePresets: async (user, filter) => { + let toRemove = await Preset.find({ ...filter, user }).select('presetId'); + const ids = toRemove.map(instance => instance.presetId); + let deleteCount = await Preset.deleteMany({ ...filter, user }).exec(); + return deleteCount; + } +}; diff --git a/api/models/index.js b/api/models/index.js index 6ff90f7c5b..e72b81846e 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,6 +1,13 @@ -const { getMessages, saveMessage, saveBingMessage, deleteMessagesSince, deleteMessages } = require('./Message'); +const { + getMessages, + saveMessage, + saveBingMessage, + deleteMessagesSince, + deleteMessages +} = require('./Message'); const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt'); const { getConvoTitle, getConvo, saveConvo, updateConvo } = require('./Conversation'); +const { getPreset, getPresets, savePreset, deletePresets } = require('./Preset'); module.exports = { getMessages, @@ -8,12 +15,19 @@ module.exports = { saveBingMessage, deleteMessagesSince, deleteMessages, + getConvoTitle, getConvo, saveConvo, updateConvo, + getCustomGpts, updateCustomGpt, updateByLabel, - deleteCustomGpts + deleteCustomGpts, + + getPreset, + getPresets, + savePreset, + deletePresets }; diff --git a/api/models/schema/conversationPreset.js b/api/models/schema/conversationPreset.js new file mode 100644 index 0000000000..aead0ab796 --- /dev/null +++ b/api/models/schema/conversationPreset.js @@ -0,0 +1,70 @@ +module.exports = { + // endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser] + endpoint: { + type: String, + default: null, + required: true + }, + // for azureOpenAI, openAI, chatGPTBrowser only + model: { + type: String, + default: null, + required: false + }, + // for azureOpenAI, openAI only + chatGptLabel: { + type: String, + default: null, + required: false + }, + promptPrefix: { + type: String, + default: null, + required: false + }, + temperature: { + type: Number, + default: 1, + required: false + }, + top_p: { + type: Number, + default: 1, + required: false + }, + presence_penalty: { + type: Number, + default: 0, + required: false + }, + frequency_penalty: { + type: Number, + default: 0, + required: false + }, + // for bingai only + jailbreak: { + type: Boolean, + default: false + }, + jailbreakConversationId: { + type: String, + default: null + }, + conversationSignature: { + type: String, + default: null + }, + clientId: { + type: String, + default: null + }, + invocationId: { + type: Number, + default: 1 + }, + toneStyle: { + type: String, + default: null + } +}; diff --git a/api/models/schema/convoSchema.js b/api/models/schema/convoSchema.js index 2506953299..fc29b55fb2 100644 --- a/api/models/schema/convoSchema.js +++ b/api/models/schema/convoSchema.js @@ -1,5 +1,6 @@ const mongoose = require('mongoose'); const mongoMeili = require('../plugins/mongoMeili'); +const conversationPreset = require('./conversationPreset'); const convoSchema = mongoose.Schema( { conversationId: { @@ -19,74 +20,7 @@ const convoSchema = mongoose.Schema( default: null }, messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }], - // endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser] - endpoint: { - type: String, - default: null, - required: true - }, - // for azureOpenAI, openAI, chatGPTBrowser only - model: { - type: String, - default: null, - required: false - }, - // for azureOpenAI, openAI only - chatGptLabel: { - type: String, - default: null, - required: false - }, - promptPrefix: { - type: String, - default: null, - required: false - }, - temperature: { - type: Number, - default: 1, - required: false - }, - top_p: { - type: Number, - default: 1, - required: false - }, - presence_penalty: { - type: Number, - default: 0, - required: false - }, - frequency_penalty: { - type: Number, - default: 0, - required: false - }, - // for bingai only - jailbreak: { - type: Boolean, - default: false - }, - jailbreakConversationId: { - type: String, - default: null - }, - conversationSignature: { - type: String, - default: null - }, - clientId: { - type: String, - default: null - }, - invocationId: { - type: Number, - default: 1 - }, - toneStyle: { - type: String, - default: null - } + ...conversationPreset }, { timestamps: true } ); diff --git a/api/models/schema/presetSchema.js b/api/models/schema/presetSchema.js new file mode 100644 index 0000000000..35a2e1c579 --- /dev/null +++ b/api/models/schema/presetSchema.js @@ -0,0 +1,27 @@ +const mongoose = require('mongoose'); +const conversationPreset = require('./conversationPreset'); +const presetSchema = mongoose.Schema( + { + presetId: { + type: String, + unique: true, + required: true, + index: true + }, + title: { + type: String, + default: 'New Chat', + meiliIndex: true + }, + user: { + type: String, + default: null + }, + ...conversationPreset + }, + { timestamps: true } +); + +const Preset = mongoose.models.Preset || mongoose.model('Preset', presetSchema); + +module.exports = Preset; diff --git a/api/server/index.js b/api/server/index.js index d70c13839c..a096901f88 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -57,6 +57,7 @@ const projectPath = path.join(__dirname, '..', '..', 'client'); app.use('/api/messages', routes.authenticatedOr401, routes.messages); app.use('/api/convos', routes.authenticatedOr401, routes.convos); app.use('/api/customGpts', routes.authenticatedOr401, routes.customGpts); + app.use('/api/presets', routes.authenticatedOr401, routes.presets); app.use('/api/prompts', routes.authenticatedOr401, routes.prompts); app.use('/auth', routes.auth); diff --git a/api/server/routes/index.js b/api/server/routes/index.js index 409c653927..e517b0bf48 100644 --- a/api/server/routes/index.js +++ b/api/server/routes/index.js @@ -1,9 +1,21 @@ const ask = require('./ask'); const messages = require('./messages'); const convos = require('./convos'); +const presets = require('./presets'); const customGpts = require('./customGpts'); -const prompts = require('./prompts'); +const prompts = require('./prompts'); const search = require('./search'); const { router: auth, authenticatedOr401, authenticatedOrRedirect } = require('./auth'); -module.exports = { search, ask, messages, convos, customGpts, prompts, auth, authenticatedOr401, authenticatedOrRedirect }; \ No newline at end of file +module.exports = { + search, + ask, + messages, + convos, + presets, + customGpts, + prompts, + auth, + authenticatedOr401, + authenticatedOrRedirect +}; diff --git a/api/server/routes/presets.js b/api/server/routes/presets.js new file mode 100644 index 0000000000..2e872fd84d --- /dev/null +++ b/api/server/routes/presets.js @@ -0,0 +1,48 @@ +const express = require('express'); +const router = express.Router(); +const { getPreset, getPresets, savePreset, deletePresets } = require('../../models'); +const crypto = require('crypto'); + +router.get('/', async (req, res) => { + const presets = (await getPresets(req?.session?.user?.username)).map(preset => { + return preset.toObject(); + }); + res.status(200).send(presets); +}); + +router.post('/', async (req, res) => { + const update = req.body || {}; + + update.presetId = update?.presetId || crypto.randomUUID(); + + try { + await savePreset(req?.session?.user?.username, update); + + const presets = (await getPresets(req?.session?.user?.username)).map(preset => { + return preset.toObject(); + }); + res.status(201).send(presets); + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}); + +router.post('/delete', async (req, res) => { + const { arg } = req.body; + + try { + await deletePresets(req?.session?.user?.username, arg); + + const presets = (await getPresets(req?.session?.user?.username)).map(preset => { + return preset.toObject(); + }); + res.status(201).send(presets); + // res.status(201).send(dbResponse); + } catch (error) { + console.error(error); + res.status(500).send(error); + } +}); + +module.exports = router; diff --git a/client/src/App.jsx b/client/src/App.jsx index d5df7345cc..b63e474677 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -39,6 +39,7 @@ const App = () => { const [user, setUser] = useRecoilState(store.user); const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled); const setEndpointsConfig = useSetRecoilState(store.endpointsConfig); + const setPresets = useSetRecoilState(store.presets); useEffect(() => { // fetch if seatch enabled @@ -70,6 +71,21 @@ const App = () => { console.log('Not login!'); window.location.href = '/auth/login'; }); + + // fetch presets + axios + .get('/api/presets', { + timeout: 1000, + withCredentials: true + }) + .then(({ data }) => { + setPresets(data); + }) + .catch(error => { + console.error(error); + console.log('Not login!'); + window.location.href = '/auth/login'; + }); }, []); if (user) diff --git a/client/src/components/Endpoints/EditPresetDialog.jsx b/client/src/components/Endpoints/EditPresetDialog.jsx new file mode 100644 index 0000000000..72c7f48adf --- /dev/null +++ b/client/src/components/Endpoints/EditPresetDialog.jsx @@ -0,0 +1,134 @@ +import React, { useEffect, useState } from 'react'; +import { useSetRecoilState, useRecoilValue } from 'recoil'; +import axios from 'axios'; +import DialogTemplate from '../ui/DialogTemplate'; +import { Dialog } from '../ui/Dialog.tsx'; +import { Input } from '../ui/Input.tsx'; +import { Label } from '../ui/Label.tsx'; +import Dropdown from '../ui/Dropdown'; +import { cn } from '~/utils/'; +import OpenAISettings from './OpenAI/Settings'; + +import store from '~/store'; + +const EditPresetDialog = ({ open, onOpenChange, preset: _preset }) => { + // const [title, setTitle] = useState('My Preset'); + const [preset, setPreset] = useState({}); + const setPresets = useSetRecoilState(store.presets); + + const availableEndpoints = useRecoilValue(store.availableEndpoints); + + const setOption = param => newValue => { + let update = {}; + update[param] = newValue; + setPreset(prevState => ({ + ...prevState, + ...update + })); + }; + + const renderSettings = () => { + const { endpoint } = preset || {}; + + if (endpoint === 'openAI') + return ( + + ); + else return null; + }; + + const defaultTextProps = + 'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0'; + + const submitPreset = () => { + axios({ + method: 'post', + url: '/api/presets', + data: preset, + withCredentials: true + }).then(res => { + setPresets(res?.data); + }); + }; + + useEffect(() => { + setPreset(_preset); + }, [open]); + + return ( + + +
+
+ + setOption('title')(e.target.value || '')} + placeholder="Set a custom name, in case you can find this preset" + className={cn( + defaultTextProps, + 'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0' + )} + /> +
+
+ + +
+
+
+
{renderSettings()}
+
+ selection={{ + selectHandler: submitPreset, + selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white', + selectText: 'Save' + }} + /> +
+ ); +}; + +export default EditPresetDialog; diff --git a/client/src/components/ui/EndpointOptionsPopover.jsx b/client/src/components/Endpoints/EndpointOptionsPopover.jsx similarity index 97% rename from client/src/components/ui/EndpointOptionsPopover.jsx rename to client/src/components/Endpoints/EndpointOptionsPopover.jsx index d27aaaa730..1d99571186 100644 --- a/client/src/components/ui/EndpointOptionsPopover.jsx +++ b/client/src/components/Endpoints/EndpointOptionsPopover.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button } from './Button.tsx'; +import { Button } from '../ui/Button.tsx'; import SwitchIcon from '../svg/SwitchIcon'; // import SaveIcon from '../svg/SaveIcon'; import { Save } from 'lucide-react'; diff --git a/client/src/components/Input/OpenAIOptions/OptionHover.jsx b/client/src/components/Endpoints/OpenAI/OptionHover.jsx similarity index 100% rename from client/src/components/Input/OpenAIOptions/OptionHover.jsx rename to client/src/components/Endpoints/OpenAI/OptionHover.jsx diff --git a/client/src/components/Input/OpenAIOptions/Settings.jsx b/client/src/components/Endpoints/OpenAI/Settings.jsx similarity index 94% rename from client/src/components/Input/OpenAIOptions/Settings.jsx rename to client/src/components/Endpoints/OpenAI/Settings.jsx index 8924557eea..b1d98bf6fd 100644 --- a/client/src/components/Input/OpenAIOptions/Settings.jsx +++ b/client/src/components/Endpoints/OpenAI/Settings.jsx @@ -1,6 +1,6 @@ import React from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -import ModelDropDown from './ModelDropDown'; +import ModelDropDown from '../../ui/ModelDropDown'; import { Input } from '~/components/ui/Input.tsx'; import { Label } from '~/components/ui/Label.tsx'; import { Slider } from '~/components/ui/Slider.tsx'; @@ -8,7 +8,7 @@ import OptionHover from './OptionHover'; import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx'; import { cn } from '~/utils/'; const defaultTextProps = - 'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0'; + 'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0'; const optionText = 'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors'; @@ -40,6 +40,11 @@ function Settings(props) { model={model} setModel={setModel} endpoint="openAI" + className={cn( + defaultTextProps, + 'flex w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0' + )} + containerClassName="flex w-full resize-none" /> {/*