diff --git a/api/models/CustomGpt.js b/api/models/CustomGpt.js index 7df5f45f5f..33bb75b124 100644 --- a/api/models/CustomGpt.js +++ b/api/models/CustomGpt.js @@ -60,6 +60,17 @@ module.exports = { return { message: 'Error updating customGpt' }; } }, + updateByLabel: async ({ prevLabel, ...update }) => { + try { + return await CustomGpt.findOneAndUpdate({ chatGptLabel: prevLabel }, 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(); diff --git a/api/models/index.js b/api/models/index.js index 7b77a7c79e..569cc5cd29 100644 --- a/api/models/index.js +++ b/api/models/index.js @@ -1,5 +1,5 @@ const { saveMessage, deleteMessages } = require('./Message'); -const { getCustomGpts, updateCustomGpt, deleteCustomGpts } = require('./CustomGpt'); +const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('./CustomGpt'); const { saveConvo } = require('./Conversation'); module.exports = { @@ -8,5 +8,6 @@ module.exports = { saveConvo, getCustomGpts, updateCustomGpt, + updateByLabel, deleteCustomGpts }; diff --git a/api/server/routes/customGpts.js b/api/server/routes/customGpts.js index d99ecb9062..a27b640842 100644 --- a/api/server/routes/customGpts.js +++ b/api/server/routes/customGpts.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { getCustomGpts, updateCustomGpt, deleteCustomGpts } = require('../../models'); +const { getCustomGpts, updateCustomGpt, updateByLabel, deleteCustomGpts } = require('../../models'); router.get('/', async (req, res) => { const models = (await getCustomGpts()).map(model => { @@ -8,20 +8,14 @@ router.get('/', async (req, res) => { 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 }; - } +router.post('/delete', async (req, res) => { + const { arg } = req.body; try { - const dbResponse = await deleteCustomGpts(filter); + const dbResponse = await deleteCustomGpts(arg); res.status(201).send(dbResponse); } catch (error) { console.error(error); @@ -44,8 +38,14 @@ router.post('/delete/:_id', async (req, res) => { router.post('/', async (req, res) => { const update = req.body.arg; + let setter = updateCustomGpt; + + if (update.prevLabel) { + setter = updateByLabel; + } + try { - const dbResponse = await updateCustomGpt(update); + const dbResponse = await setter(update); res.status(201).send(dbResponse); } catch (error) { console.error(error); diff --git a/client/src/components/Models/MenuItems.jsx b/client/src/components/Models/MenuItems.jsx index 399dd9e87f..aa899e8cf3 100644 --- a/client/src/components/Models/MenuItems.jsx +++ b/client/src/components/Models/MenuItems.jsx @@ -1,7 +1,7 @@ import React from 'react'; import ModelItem from './ModelItem'; -export default function MenuItems({ models }) { +export default function MenuItems({ models, onSelect }) { return ( <> {models.map((modelItem, i) => ( @@ -9,6 +9,7 @@ export default function MenuItems({ models }) { key={i} modelName={modelItem.name} value={modelItem.value} + onSelect={onSelect} /> ))} diff --git a/client/src/components/Models/ModelDialog.jsx b/client/src/components/Models/ModelDialog.jsx index d7dd1a9321..93005976e3 100644 --- a/client/src/components/Models/ModelDialog.jsx +++ b/client/src/components/Models/ModelDialog.jsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; -import { useDispatch } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { setModel, setCustomGpt } from '~/store/submitSlice'; import manualSWR from '~/utils/fetchers'; import { Button } from '../ui/Button.tsx'; @@ -16,8 +16,9 @@ import { DialogTitle } from '../ui/Dialog.tsx'; -export default function ModelDialog({ mutate, modelMap, setModelSave, handleSaveState }) { +export default function ModelDialog({ mutate, setModelSave, handleSaveState }) { const dispatch = useDispatch(); + const { modelMap, initial } = useSelector((state) => state.models); const [chatGptLabel, setChatGptLabel] = useState(''); const [promptPrefix, setPromptPrefix] = useState(''); const [saveText, setSaveText] = useState('Save'); @@ -65,9 +66,12 @@ export default function ModelDialog({ mutate, modelMap, setModelSave, handleSave if ( chatGptLabel !== 'chatgptCustom' && modelMap[chatGptLabel.toLowerCase()] && + !initial[chatGptLabel.toLowerCase()] && saveText === 'Save' ) { setSaveText('Update'); + } else if (!modelMap[chatGptLabel.toLowerCase()] && saveText === 'Update') { + setSaveText('Save'); } const requiredProp = required ? { required: true } : {}; diff --git a/client/src/components/Models/ModelItem.jsx b/client/src/components/Models/ModelItem.jsx index b9d8218455..8b7d8d082d 100644 --- a/client/src/components/Models/ModelItem.jsx +++ b/client/src/components/Models/ModelItem.jsx @@ -1,13 +1,22 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { useSelector } from 'react-redux'; import { DropdownMenuRadioItem } from '../ui/DropdownMenu.tsx'; +import { Circle } from 'lucide-react'; import { DialogTrigger } from '../ui/Dialog.tsx'; import RenameButton from '../Conversations/RenameButton'; import TrashIcon from '../svg/TrashIcon'; +import manualSWR from '~/utils/fetchers'; -export default function ModelItem({ modelName, value }) { +export default function ModelItem({ modelName, value, onSelect }) { + const { customModel } = useSelector((state) => state.submit); const { initial } = useSelector((state) => state.models); const [isHovering, setIsHovering] = useState(false); + const [renaming, setRenaming] = useState(false); + const [currentName, setCurrentName] = useState(modelName); + const [modelInput, setModelInput] = useState(modelName); + const inputRef = useRef(null); + const rename = manualSWR(`http://localhost:3080/api/customGpts`, 'post'); + const deleteCustom = manualSWR(`http://localhost:3080/api/customGpts/delete`, 'post'); if (value === 'chatgptCustom') { return ( @@ -23,6 +32,18 @@ export default function ModelItem({ modelName, value }) { ); } + if (initial[value]) { + return ( + + {modelName} + {value === 'chatgpt' && $} + + ); + } + const handleMouseOver = () => { setIsHovering(true); }; @@ -31,31 +52,101 @@ export default function ModelItem({ modelName, value }) { setIsHovering(false); }; + const renameHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + setRenaming(true); + setTimeout(() => { + inputRef.current.focus(); + }, 25); + }; + + const onRename = (e) => { + e.preventDefault(); + setRenaming(false); + if (modelInput === modelName) { + return; + } + rename.trigger({ + prevLabel: currentName, + chatGptLabel: modelInput, + value: modelInput.toLowerCase() + }); + setCurrentName(modelInput); + }; + + const onDelete = async (e) => { + e.preventDefault(); + await deleteCustom.trigger({ value: currentName.toLowerCase() }); + // await mutate(); + onSelect('chatgpt', true); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Enter') { + onRename(e); + } + }; + const buttonClass = { className: - 'rounded-md m-0 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200' + 'z-50 rounded-md m-0 text-gray-400 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200' + }; + + const itemClass = { + className: + 'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none hover:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:hover:bg-slate-700 dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800' }; const showButtons = isHovering && !initial[value]; return ( - { + onSelect(value, true); + }} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} > - {modelName} - {value === 'chatgpt' && $} + {customModel === value && ( + + + + )} + {renaming === true ? ( + e.stopPropagation()} + onChange={(e) => setModelInput(e.target.value)} + onBlur={onRename} + onKeyDown={handleKeyDown} + /> + ) : ( + modelInput + )} + {value === 'chatgpt' && $} {showButtons && ( <> - - )} - + ); } diff --git a/client/src/components/Models/ModelMenu.jsx b/client/src/components/Models/ModelMenu.jsx index 832d95ace0..80b5dd9dc3 100644 --- a/client/src/components/Models/ModelMenu.jsx +++ b/client/src/components/Models/ModelMenu.jsx @@ -24,13 +24,10 @@ import { Dialog } from '../ui/Dialog.tsx'; export default function ModelMenu() { const dispatch = useDispatch(); const [modelSave, setModelSave] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); const { model, customModel } = useSelector((state) => state.submit); const { models, modelMap, initial } = useSelector((state) => state.models); const { trigger } = manualSWR(`http://localhost:3080/api/customGpts`, 'get', (res) => { - if (models.length + res.length === models.length) { - return; - } - const fetchedModels = res.map((modelItem) => ({ ...modelItem, name: modelItem.chatGptLabel @@ -53,7 +50,7 @@ export default function ModelMenu() { localStorage.setItem('model', JSON.stringify(model)); }, [model]); - const onChange = (value) => { + const onChange = (value, custom = false) => { if (!value) { return; } else if (value === 'chatgptCustom') { @@ -62,12 +59,18 @@ export default function ModelMenu() { dispatch(setModel(value)); dispatch(setDisabled(false)); dispatch(setCustomModel(null)); + if (custom) { + trigger(); + } } else if (!initial[value]) { const chatGptLabel = modelMap[value]?.chatGptLabel; const promptPrefix = modelMap[value]?.promptPrefix; dispatch(setCustomGpt({ chatGptLabel, promptPrefix })); dispatch(setModel('chatgptCustom')); dispatch(setCustomModel(value)); + if (custom) { + setMenuOpen((prevOpen) => !prevOpen); + } } else if (!modelMap[value]) { dispatch(setCustomModel(null)); } @@ -101,7 +104,9 @@ export default function ModelMenu() { const defaultColorProps = [ 'text-gray-500', 'hover:bg-gray-100', + 'hover:bg-opacity-20', 'disabled:hover:bg-transparent', + 'dark:data-[state=open]:bg-gray-800', 'dark:hover:bg-opacity-20', 'dark:hover:bg-gray-900', 'dark:hover:text-gray-400', @@ -110,9 +115,11 @@ export default function ModelMenu() { const chatgptColorProps = [ 'text-green-700', + 'data-[state=open]:bg-green-100', 'dark:text-emerald-300', 'hover:bg-green-100', 'disabled:hover:bg-transparent', + 'dark:data-[state=open]:bg-green-900', 'dark:hover:bg-opacity-50', 'dark:hover:bg-green-900', 'dark:hover:text-gray-100', @@ -124,14 +131,17 @@ export default function ModelMenu() { return ( - + @@ -140,11 +150,14 @@ export default function ModelMenu() { Select a Model 0 ? customModel : model} + value={customModel ? customModel : model} onValueChange={onChange} className="overflow-y-auto" > - + diff --git a/client/src/store/modelSlice.js b/client/src/store/modelSlice.js index c13c9a899a..473d0dc407 100644 --- a/client/src/store/modelSlice.js +++ b/client/src/store/modelSlice.js @@ -32,6 +32,7 @@ const currentSlice = createSlice({ initialState, reducers: { setModels: (state, action) => { + console.log('setModels', action.payload); const models = [...initialState.models, ...action.payload]; state.models = models; const modelMap = {}; diff --git a/client/src/store/submitSlice.js b/client/src/store/submitSlice.js index 464a96c5a5..f55b4df0d8 100644 --- a/client/src/store/submitSlice.js +++ b/client/src/store/submitSlice.js @@ -6,7 +6,7 @@ const initialState = { model: 'chatgpt', promptPrefix: '', chatGptLabel: '', - customModel: '' + customModel: null }; const currentSlice = createSlice({