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..c3ae5eceba --- /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: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 ( + + + + + + Preset Name + + 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' + )} + /> + + + + Endpoint + + + + + + {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 701eee1323..ad499d0b2e 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'; 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..22060dd05d 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: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" /> {/* { + const [title, setTitle] = useState('My Preset'); + const setPresets = useSetRecoilState(store.presets); + + 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'; + + const submitPreset = () => { + const preset = buildPresetByConversation({ + title, + conversation + }); + + axios({ + method: 'post', + url: '/api/presets', + data: preset, + withCredentials: true + }).then(res => { + setPresets(res?.data); + }); + }; + + useEffect(() => { + setTitle('My Preset'); + }, [open]); + + return ( + + + + Preset Name + + setTitle(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' + )} + /> + + selection={{ + selectHandler: submitPreset, + selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white', + selectText: 'Save' + }} + /> + + ); +}; + +export default SaveAsPresetDialog; diff --git a/client/src/components/Input/Endpoints/MenuItems.jsx b/client/src/components/Input/Endpoints/MenuItems.jsx deleted file mode 100644 index a8554ba598..0000000000 --- a/client/src/components/Input/Endpoints/MenuItems.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import ModelItem from './ModelItem'; - -export default function MenuItems({ models, onSelect }) { - return ( - <> - {models.map(modelItem => ( - - ))} - > - ); -} diff --git a/client/src/components/Input/Endpoints/ModelDialog.jsx b/client/src/components/Input/Endpoints/ModelDialog.jsx deleted file mode 100644 index 86d6b30abe..0000000000 --- a/client/src/components/Input/Endpoints/ModelDialog.jsx +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useState, useRef } from 'react'; -import TextareaAutosize from 'react-textarea-autosize'; -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'; - -import store from '~/store'; - -export default function ModelDialog({ mutate, setModelSave, handleSaveState }) { - const { newConversation } = store.useConversation(); - - 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(`/api/customGpts/`, 'post'); - - const selectHandler = e => { - if (chatGptLabel.length === 0) { - e.preventDefault(); - setRequired(true); - inputRef.current.focus(); - return; - } - - handleSaveState(chatGptLabel.toLowerCase()); - - // Set new conversation - newConversation({ - model: 'chatgptCustom', - chatGptLabel, - promptPrefix - }); - }; - - const saveHandler = e => { - e.preventDefault(); - setModelSave(true); - const value = chatGptLabel.toLowerCase(); - - if (chatGptLabel.length === 0) { - setRequired(true); - inputRef.current.focus(); - return; - } - - updateCustomGpt.trigger({ value, chatGptLabel, promptPrefix }); - - mutate(); - setSaveText(prev => prev + 'd!'); - setTimeout(() => { - setSaveText('Save'); - }, 2500); - - // dispatch(setCustomGpt({ chatGptLabel, promptPrefix })); - newConversation({ - model: 'chatgptCustom', - chatGptLabel, - promptPrefix - }); - }; - - // Commented by wtlyu - // 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 } : {}; - - return ( - - - Customize ChatGPT - - Note: important instructions are often better placed in your message rather than the prefix.{' '} - - More info here - - - - - - - Custom Name - - 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)] outline-none placeholder:text-gray-400 invalid:border-red-400 invalid:text-red-600 invalid:placeholder-red-600 invalid:placeholder-opacity-70 invalid:ring-opacity-10 focus:ring-0 focus:invalid:border-red-400 focus:invalid:ring-red-300 dark:border-none dark:bg-gray-700 - dark:text-gray-50 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:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0 dark:focus:invalid:ring-red-600 dark:focus:invalid:ring-opacity-50" - {...requiredProp} - /> - - - - Prompt Prefix - - 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 max-h-52 w-full resize-none rounded-md border border-gray-300 bg-transparent py-2 px-3 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-none dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-none dark:focus:border-transparent dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0" - /> - - - - Cancel - - {saveText} - - - Select - - - - ); -} diff --git a/client/src/components/Input/Endpoints/ModelItem.jsx b/client/src/components/Input/Endpoints/ModelItem.jsx deleted file mode 100644 index 5a0ad0b9e6..0000000000 --- a/client/src/components/Input/Endpoints/ModelItem.jsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; -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'; -import getIcon from '~/utils/getIcon'; - -import store from '~/store'; - -export default function ModelItem({ model: _model, value, onSelect }) { - const { name, model, _id: id, chatGptLabel = null, promptPrefix = null } = _model; - const setCustomGPTModels = useSetRecoilState(store.customGPTModels); - const currentConversation = useRecoilValue(store.conversation) || {}; - - const [isHovering, setIsHovering] = useState(false); - const [renaming, setRenaming] = useState(false); - const [currentName, setCurrentName] = useState(name); - const [modelInput, setModelInput] = useState(name); - const inputRef = useRef(null); - const rename = manualSWR(`/api/customGpts`, 'post', res => {}); - const deleteCustom = manualSWR(`/api/customGpts/delete`, 'post', res => { - const fetchedModels = res.data.map(modelItem => ({ - ...modelItem, - name: modelItem.chatGptLabel, - model: 'chatgptCustom' - })); - - setCustomGPTModels(fetchedModels); - }); - - const icon = getIcon({ - size: 20, - sender: chatGptLabel || model, - isCreatedByUser: false, - model, - chatGptLabel, - promptPrefix, - error: false, - className: 'mr-2' - }); - - if (model !== 'chatgptCustom') - // regular model - return ( - - {icon} - {name} - {model === 'chatgpt' && $} - - ); - else if (model === 'chatgptCustom' && chatGptLabel === null && promptPrefix === null) - // base chatgptCustom model, click to add new chatgptCustom. - return ( - - - {icon} - {name} - $ - - - ); - - // else: a chatgptCustom model - const handleMouseOver = () => { - setIsHovering(true); - }; - - const handleMouseOut = () => { - 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 === name) { - return; - } - rename.trigger({ - prevLabel: currentName, - chatGptLabel: modelInput, - value: modelInput.toLowerCase() - }); - setCurrentName(modelInput); - }; - - const onDelete = async e => { - e.preventDefault(); - await deleteCustom.trigger({ _id: id }); - onSelect('chatgpt'); - }; - - const handleKeyDown = e => { - if (e.key === 'Enter') { - onRename(e); - } - }; - - const buttonClass = { - className: - 'invisible group-hover:visible 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 group 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' - }; - - return ( - { - if (isHovering) { - return; - } - onSelect('chatgptCustom', value); - }} - > - {currentConversation?.chatGptLabel === value && ( - - - - )} - - {icon} - - {renaming === true ? ( - e.stopPropagation()} - onChange={e => setModelInput(e.target.value)} - // onBlur={onRename} - onKeyDown={handleKeyDown} - /> - ) : ( - {modelInput} - )} - - {value === 'chatgpt' && $} - - - - - - ); -} diff --git a/client/src/components/Input/Endpoints/EndpointMenu.jsx b/client/src/components/Input/Endpoints/NewConversationMenu.jsx similarity index 52% rename from client/src/components/Input/Endpoints/EndpointMenu.jsx rename to client/src/components/Input/Endpoints/NewConversationMenu.jsx index 3dd64aa48f..6ff3ca9d04 100644 --- a/client/src/components/Input/Endpoints/EndpointMenu.jsx +++ b/client/src/components/Input/Endpoints/NewConversationMenu.jsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { useRecoilValue } from 'recoil'; -// import ModelDialog from './ModelDialog'; +import EditPresetDialog from '../../Endpoints/EditPresetDialog'; import EndpointItems from './EndpointItems'; -import { swr } from '~/utils/fetchers'; +import PresetItems from './PresetItems'; import getIcon from '~/utils/getIcon'; import { Button } from '../../ui/Button.tsx'; @@ -18,30 +18,22 @@ import { Dialog } from '../../ui/Dialog.tsx'; import store from '~/store'; -export default function EndpointMenu() { +export default function NewConversationMenu() { // const [modelSave, setModelSave] = useState(false); const [menuOpen, setMenuOpen] = useState(false); + const [presetModelVisible, setPresetModelVisible] = useState(false); + const [preset, setPreset] = useState(false); // const models = useRecoilValue(store.models); const availableEndpoints = useRecoilValue(store.availableEndpoints); // const setCustomGPTModels = useSetRecoilState(store.customGPTModels); + const presets = useRecoilValue(store.presets); const conversation = useRecoilValue(store.conversation) || {}; const { endpoint, conversationId } = conversation; // const { model, promptPrefix, chatGptLabel, conversationId } = conversation; const { newConversation } = store.useConversation(); - // fetch the list of saved chatgptCustom - // const { data, isLoading, mutate } = swr(`/api/customGpts`, res => { - // const fetchedModels = res.map(modelItem => ({ - // ...modelItem, - // name: modelItem.chatGptLabel, - // model: 'chatgptCustom' - // })); - - // setCustomGPTModels(fetchedModels); - // }); - // update the default model when availableModels changes // typically, availableModels changes => modelsFilter or customGPTModels changes useEffect(() => { @@ -56,81 +48,31 @@ export default function EndpointMenu() { }, [conversation]); // set the current model - const onChange = (newEndpoint, value = null) => { + const onSelectEndpoint = newEndpoint => { setMenuOpen(false); if (!newEndpoint) return; else if (newEndpoint === endpoint) return; else { - newConversation({}, newEndpoint); + newConversation({}, { endpoint: newEndpoint }); } - // } else if (newModel === model && value === chatGptLabel) { - // // bypass if not changed - // return; - // } else if (newModel === 'chatgptCustom' && value === null) { - // // return; - // } else if (newModel !== 'chatgptCustom') { - // newConversation({ - // model: newModel, - // chatGptLabel: null, - // promptPrefix: null - // }); - // } else if (newModel === 'chatgptCustom') { - // const targetModel = models.find(element => element.value == value); - // if (targetModel) { - // const chatGptLabel = targetModel?.chatGptLabel; - // const promptPrefix = targetModel?.promptPrefix; - // newConversation({ - // model: newModel, - // chatGptLabel, - // promptPrefix - // }); - // } - // } }; - // const onOpenChange = open => { - // mutate(); - // if (!open) { - // setModelSave(false); - // } - // }; + // set the current model + const onSelectPreset = newPreset => { + setMenuOpen(false); + if (!newPreset) return; + // else if (newEndpoint === endpoint) return; + else { + newConversation({}, newPreset); + } + }; - // const handleSaveState = value => { - // if (!modelSave) { - // return; - // } + const onChangePreset = preset => { + setPresetModelVisible(true); + setPreset(preset); + }; - // setCustomGPTModels(value); - // setModelSave(false); - // }; - - // 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', - // 'dark:disabled:hover:bg-transparent' - // ]; - - // 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', - // 'dark:disabled:hover:bg-transparent' - // ]; - - // const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps; const icon = getIcon({ size: 32, ...conversation, @@ -157,32 +99,51 @@ export default function EndpointMenu() { event.preventDefault()} > - Select an AI Endpoint + Select an Endpoint {availableEndpoints.length ? ( ) : ( No endpoint available. )} + + + + Select a Preset + + + {presets.length ? ( + + ) : ( + No preset yet. + )} + - {/* */} + ); } diff --git a/client/src/components/Input/Endpoints/PresetItem.jsx b/client/src/components/Input/Endpoints/PresetItem.jsx new file mode 100644 index 0000000000..6c713653ff --- /dev/null +++ b/client/src/components/Input/Endpoints/PresetItem.jsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { DropdownMenuRadioItem } from '../../ui/DropdownMenu.tsx'; +import EditIcon from '../../svg/EditIcon'; +import getIcon from '~/utils/getIcon'; + +export default function PresetItem({ preset = {}, value, onSelect, onChangePreset }) { + const { endpoint } = preset; + + const icon = getIcon({ + size: 20, + endpoint: preset?.endpoint, + error: false, + className: 'mr-2' + }); + + const getPresetTitle = () => { + let _title = `${endpoint}`; + + if (endpoint === 'azureOpenAI' || endpoint === 'openAI') { + const { chatGptLabel, model } = preset; + if (model) _title += `: ${model}`; + if (chatGptLabel) _title += ` as ${chatGptLabel}`; + } else if (endpoint === 'bingAI') { + const { jailbreak, toneStyle } = preset; + if (toneStyle) _title += `: ${toneStyle}`; + if (jailbreak) _title += ` as Sydney`; + } else if (endpoint === 'chatGPTBrowser') { + const { model } = preset; + if (model) _title += `: ${model}`; + } else if (endpoint === null) { + null; + } else { + null; + } + return _title; + }; + + // regular model + return ( + + {icon} + {preset?.title} + ({getPresetTitle()}) + + {/* */} + + onChangePreset(preset)} + > + + + + ); +} diff --git a/client/src/components/Input/Endpoints/PresetItems.jsx b/client/src/components/Input/Endpoints/PresetItems.jsx new file mode 100644 index 0000000000..63d08620c2 --- /dev/null +++ b/client/src/components/Input/Endpoints/PresetItems.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import PresetItem from './PresetItem'; + +export default function PresetItems({ presets, onSelect, onChangePreset }) { + return ( + <> + {presets.map(preset => ( + + ))} + > + ); +} diff --git a/client/src/components/Input/Footer.jsx b/client/src/components/Input/Footer.jsx index 0015596749..33a29cc8ce 100644 --- a/client/src/components/Input/Footer.jsx +++ b/client/src/components/Input/Footer.jsx @@ -2,7 +2,7 @@ import React from 'react'; export default function Footer() { return ( - + ChatGPT Clone - . Serves and searches all conversations reliably. All AI convos under one house. Pay per - call and not per month (cents compared to dollars). + . Serves and searches all conversations reliably. All AI convos under one house. Pay per call and not + per month (cents compared to dollars). ); } diff --git a/client/src/components/Input/OpenAIOptions/index.jsx b/client/src/components/Input/OpenAIOptions/index.jsx index 801ae6c88b..a2e84c7315 100644 --- a/client/src/components/Input/OpenAIOptions/index.jsx +++ b/client/src/components/Input/OpenAIOptions/index.jsx @@ -1,12 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Settings2 } from 'lucide-react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import ModelSelect from './ModelSelect'; -import EndpointOptionsPopover from '../../ui/EndpointOptionsPopover'; -import DialogTemplate from '../../ui/DialogTemplate'; +import ModelSelect from '../../ui/ModelSelect'; +import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover'; +import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog'; import { Button } from '../../ui/Button.tsx'; -import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx'; -import Settings from './Settings.jsx'; +import Settings from '../../Endpoints/OpenAI/Settings.jsx'; import { cn } from '~/utils/'; import store from '~/store'; @@ -125,18 +124,11 @@ function OpenAIOptions() { saveAsPreset={saveAsPreset} switchToSimpleMode={switchToSimpleMode} /> - - - + conversation={conversation} + /> > ); } diff --git a/client/src/components/Input/index.jsx b/client/src/components/Input/index.jsx index c33e230da3..0f3764a2fc 100644 --- a/client/src/components/Input/index.jsx +++ b/client/src/components/Input/index.jsx @@ -5,7 +5,7 @@ import AdjustToneButton from './AdjustToneButton'; import OpenAIOptions from './OpenAIOptions'; import BingAIOptions from './BingAIOptions'; // import BingStyles from './BingStyles'; -import EndpointMenu from './Endpoints/EndpointMenu'; +import EndpointMenu from './Endpoints/NewConversationMenu'; import Footer from './Footer'; import TextareaAutosize from 'react-textarea-autosize'; import { useMessageHandler } from '../../utils/handleSubmit'; diff --git a/client/src/components/ui/Dialog.tsx b/client/src/components/ui/Dialog.tsx index 6f8875277b..b7019e8ae6 100644 --- a/client/src/components/ui/Dialog.tsx +++ b/client/src/components/ui/Dialog.tsx @@ -53,7 +53,7 @@ const DialogContent = React.forwardRef< > {children} - + Close diff --git a/client/src/components/ui/DialogTemplate.jsx b/client/src/components/ui/DialogTemplate.jsx index 63c19d4fb3..084faa123a 100644 --- a/client/src/components/ui/DialogTemplate.jsx +++ b/client/src/components/ui/DialogTemplate.jsx @@ -8,18 +8,18 @@ import { DialogHeader, DialogTitle } from './Dialog.tsx'; +import { cn } from '~/utils/'; -export default function DialogTemplate({ title, description, main, buttons, selection }) { +export default function DialogTemplate({ title, description, main, buttons, selection, className }) { const { selectHandler, selectClasses, selectText } = selection; - const defaultSelect = "bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900" + const defaultSelect = + 'bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900'; return ( - + {title} - - {description} - + {description} {/* //input template @@ -44,10 +44,12 @@ export default function DialogTemplate({ title, description, main, buttons, sele {main ? main : null} Cancel - { buttons ? buttons : null} + {buttons ? buttons : null} {selectText} diff --git a/client/src/components/ui/Dropdown.jsx b/client/src/components/ui/Dropdown.jsx new file mode 100644 index 0000000000..812ee94edd --- /dev/null +++ b/client/src/components/ui/Dropdown.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import CheckMark from '../svg/CheckMark'; +import { Listbox } from '@headlessui/react'; +import { cn } from '~/utils/'; + +function Dropdown({ value, onChange, options, className, containerClassName }) { + return ( + + + + + + + {value} + + + + + + + + + + {options.map((item, i) => ( + + + + {item} + + {value === item && ( + + + + )} + + + ))} + + + + + ); +} + +export default Dropdown; diff --git a/client/src/components/ui/Landing.jsx b/client/src/components/ui/Landing.jsx index f2400f72b5..3412018382 100644 --- a/client/src/components/ui/Landing.jsx +++ b/client/src/components/ui/Landing.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import useDocumentTitle from '~/hooks/useDocumentTitle'; -import Templates from '../Prompts/Templates'; +import Templates from '../ui/Templates'; import SunIcon from '../svg/SunIcon'; import LightningIcon from '../svg/LightningIcon'; import CautionIcon from '../svg/CautionIcon'; diff --git a/client/src/components/Input/OpenAIOptions/ModelDropDown.jsx b/client/src/components/ui/ModelDropDown.jsx similarity index 81% rename from client/src/components/Input/OpenAIOptions/ModelDropDown.jsx rename to client/src/components/ui/ModelDropDown.jsx index c4da1cbd21..5df5acc6f0 100644 --- a/client/src/components/Input/OpenAIOptions/ModelDropDown.jsx +++ b/client/src/components/ui/ModelDropDown.jsx @@ -1,22 +1,27 @@ import React from 'react'; -import CheckMark from '../../svg/CheckMark'; +import CheckMark from '../svg/CheckMark'; import { Listbox } from '@headlessui/react'; import { useRecoilValue } from 'recoil'; import { cn } from '~/utils/'; import store from '~/store'; -function ModelDropDown({ model, setModel, endpoint }) { +function ModelDropDown({ model, setModel, endpoint, containerClassName, className }) { const endpointsConfig = useRecoilValue(store.endpointsConfig); const models = endpointsConfig?.[endpoint]?.['availableModels'] || []; return ( - + - + - {model} + + {model} + { const [menuOpen, setMenuOpen] = useState(false); diff --git a/client/src/components/Prompts/Prompt.jsx b/client/src/components/ui/Prompt.jsx similarity index 100% rename from client/src/components/Prompts/Prompt.jsx rename to client/src/components/ui/Prompt.jsx diff --git a/client/src/components/Prompts/Templates.jsx b/client/src/components/ui/Templates.jsx similarity index 100% rename from client/src/components/Prompts/Templates.jsx rename to client/src/components/ui/Templates.jsx diff --git a/client/src/store/conversation.js b/client/src/store/conversation.js index 6088c9957d..0e312a633d 100644 --- a/client/src/store/conversation.js +++ b/client/src/store/conversation.js @@ -27,7 +27,6 @@ import getDefaultConversation from '~/utils/getDefaultConversation'; // clientId: null, // invocationId: 1, // toneStyle: null, -// suggestions: [] // }; const conversation = atom({ @@ -62,10 +61,10 @@ const useConversation = () => { const switchToConversation = useRecoilCallback( ({ snapshot }) => - async (_conversation, messages = null, targetEndpoint = null) => { + async (_conversation, messages = null, preset = null) => { const prevConversation = await snapshot.getPromise(conversation); const endpointsFilter = await snapshot.getPromise(endpoints.endpointsFilter); - _switchToConversation(_conversation, messages, targetEndpoint, { + _switchToConversation(_conversation, messages, preset, { endpointsFilter, prevConversation }); @@ -76,7 +75,7 @@ const useConversation = () => { const _switchToConversation = ( conversation, messages = null, - targetEndpoint = null, + preset = null, { endpointsFilter = {}, prevConversation = {} } ) => { let { endpoint = null } = conversation; @@ -87,7 +86,7 @@ const useConversation = () => { conversation, endpointsFilter, prevConversation, - targetEndpoint + preset }); setConversation(conversation); @@ -95,7 +94,7 @@ const useConversation = () => { resetLatestMessage(); }; - const newConversation = (template = {}, targetEndpoint = null) => { + const newConversation = (template = {}, preset) => { switchToConversation( { conversationId: 'new', @@ -103,7 +102,7 @@ const useConversation = () => { ...template }, [], - targetEndpoint + preset ); }; diff --git a/client/src/store/index.js b/client/src/store/index.js index e7c625b92c..f400853ac6 100644 --- a/client/src/store/index.js +++ b/client/src/store/index.js @@ -6,6 +6,7 @@ import user from './user'; import text from './text'; import submission from './submission'; import search from './search'; +import preset from './preset'; export default { ...conversation, @@ -15,5 +16,6 @@ export default { ...user, text, ...submission, - ...search + ...search, + ...preset }; diff --git a/client/src/store/preset.js b/client/src/store/preset.js new file mode 100644 index 0000000000..f2f342684e --- /dev/null +++ b/client/src/store/preset.js @@ -0,0 +1,40 @@ +import endpoints from './endpoints'; +import { atom, selector, useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil'; + +// preset structure is as same defination as conversation +// sample structure +// { +// presetId: 'new', +// title: 'New Chat', +// user: null, +// // endpoint: [azureOpenAI, openAI, bingAI, chatGPTBrowser] +// endpoint: 'azureOpenAI', +// // for azureOpenAI, openAI, chatGPTBrowser only +// model: 'gpt-3.5-turbo', +// // for azureOpenAI, openAI only +// chatGptLabel: null, +// promptPrefix: null, +// temperature: 1, +// top_p: 1, +// presence_penalty: 0, +// frequency_penalty: 0, +// // for bingAI only +// jailbreak: false, +// jailbreakConversationId: null, +// conversationSignature: null, +// clientId: null, +// invocationId: 1, +// toneStyle: null, +// }; + +// an array of saved presets. +// sample structure +// [preset1, preset2, preset3] +const presets = atom({ + key: 'presets', + default: [] +}); + +export default { + presets +}; diff --git a/client/src/utils/buildPresetByConversation.js b/client/src/utils/buildPresetByConversation.js new file mode 100644 index 0000000000..73799f08c1 --- /dev/null +++ b/client/src/utils/buildPresetByConversation.js @@ -0,0 +1,56 @@ +const buildPresetByConversation = ({ title, conversation, ...others }) => { + const { endpoint } = conversation; + + let preset = {}; + if (endpoint === 'azureOpenAI' || endpoint === 'openAI') { + preset = { + endpoint, + model: conversation?.model || 'gpt-3.5-turbo', + chatGptLabel: conversation?.chatGptLabel || null, + promptPrefix: conversation?.promptPrefix || null, + temperature: conversation?.temperature || 1, + top_p: conversation?.top_p || 1, + presence_penalty: conversation?.presence_penalty || 0, + frequency_penalty: conversation?.frequency_penalty || 0, + title, + ...others + }; + } else if (endpoint === 'bingAI') { + preset = { + endpoint, + jailbreak: conversation?.jailbreak || false, + jailbreakConversationId: conversation?.jailbreakConversationId || null, + conversationSignature: null, + clientId: null, + invocationId: 1, + toneStyle: conversation?.toneStyle || 'fast', + title, + ...others + }; + } else if (endpoint === 'chatGPTBrowser') { + preset = { + endpoint, + model: conversation?.model || 'text-davinci-002-render-sha', + title, + ...others + }; + } else if (endpoint === null) { + preset = { + ...conversation, + endpoint, + title, + ...others + }; + } else { + console.error(`Unknown endpoint ${endpoint}`); + preset = { + endpoint: null, + title, + ...others + }; + } + + return preset; +}; + +export default buildPresetByConversation; diff --git a/client/src/utils/fetchers.js b/client/src/utils/fetchers.js index 24c430dbd1..175967f37a 100644 --- a/client/src/utils/fetchers.js +++ b/client/src/utils/fetchers.js @@ -3,14 +3,19 @@ import axios from 'axios'; import useSWR from 'swr'; import useSWRMutation from 'swr/mutation'; -const fetcher = (url) => fetch(url, { credentials: 'include' }).then((res) => res.json()); +const fetcher = url => fetch(url, { credentials: 'include' }).then(res => res.json()); const axiosFetcher = async (url, params) => { console.log(params, 'params'); return axios.get(url, params); }; -const postRequest = async (url, { arg }) => { - return await axios.post(url, { withCredentials: true, arg }); +export const postRequest = async (url, { arg }) => { + return await axios({ + method: 'post', + url: url, + withCredentials: true, + data: { arg } + }); }; export const searchFetcher = async (pre, q, pageNumber, callback) => { diff --git a/client/src/utils/getDefaultConversation.js b/client/src/utils/getDefaultConversation.js index e736d5aefd..397f122ab4 100644 --- a/client/src/utils/getDefaultConversation.js +++ b/client/src/utils/getDefaultConversation.js @@ -44,7 +44,9 @@ const buildDefaultConversation = ({ conversation, endpoint, lastConversationSetu return conversation; }; -const getDefaultConversation = ({ conversation, prevConversation, endpointsFilter, targetEndpoint }) => { +const getDefaultConversation = ({ conversation, prevConversation, endpointsFilter, preset }) => { + const { endpoint: targetEndpoint } = preset || {}; + if (targetEndpoint) { // try to use current model const endpoint = targetEndpoint; @@ -52,7 +54,7 @@ const getDefaultConversation = ({ conversation, prevConversation, endpointsFilte conversation = buildDefaultConversation({ conversation, endpoint, - lastConversationSetup: {} + lastConversationSetup: preset }); return conversation; } else {