feat: add preset and edit preset.

This commit is contained in:
Wentao Lyu 2023-04-02 04:15:07 +08:00
parent 80ef5008dd
commit 45e17da241
29 changed files with 592 additions and 493 deletions

View file

@ -39,6 +39,7 @@ const App = () => {
const [user, setUser] = useRecoilState(store.user); const [user, setUser] = useRecoilState(store.user);
const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled); const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled);
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig); const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
const setPresets = useSetRecoilState(store.presets);
useEffect(() => { useEffect(() => {
// fetch if seatch enabled // fetch if seatch enabled
@ -70,6 +71,21 @@ const App = () => {
console.log('Not login!'); console.log('Not login!');
window.location.href = '/auth/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) if (user)

View file

@ -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 (
<OpenAISettings
model={preset?.model}
setModel={setOption('model')}
chatGptLabel={preset?.chatGptLabel}
setChatGptLabel={setOption('chatGptLabel')}
promptPrefix={preset?.promptPrefix}
setPromptPrefix={setOption('promptPrefix')}
temperature={preset?.temperature}
setTemperature={setOption('temperature')}
topP={preset?.top_p}
setTopP={setOption('top_p')}
freqP={preset?.presence_penalty}
setFreqP={setOption('presence_penalty')}
presP={preset?.frequency_penalty}
setPresP={setOption('frequency_penalty')}
/>
);
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 (
<Dialog
open={open}
onOpenChange={onOpenChange}
>
<DialogTemplate
title="Edit Preset"
className="max-w-full sm:max-w-4xl"
main=<div className="flex w-full flex-col items-center gap-2">
<div className="grid w-full gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Preset Name
</Label>
<Input
id="chatGptLabel"
value={preset?.title || ''}
onChange={e => 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'
)}
/>
</div>
<div className="col-span-1 flex flex-col items-start justify-start gap-2">
<Label
htmlFor="endpoint"
className="text-left text-sm font-medium"
>
Endpoint
</Label>
<Dropdown
id="endpoint"
value={preset?.endpoint || ''}
onChange={setOption('endpoint')}
options={availableEndpoints}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
)}
containerClassName="flex w-full resize-none"
/>
</div>
</div>
<div className="my-4 w-full border-t border-gray-300 dark:border-gray-500" />
<div className="w-full p-0">{renderSettings()}</div>
</div>
selection={{
selectHandler: submitPreset,
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
selectText: 'Save'
}}
/>
</Dialog>
);
};
export default EditPresetDialog;

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Button } from './Button.tsx'; import { Button } from '../ui/Button.tsx';
import SwitchIcon from '../svg/SwitchIcon'; import SwitchIcon from '../svg/SwitchIcon';
import SaveIcon from '../svg/SaveIcon'; import SaveIcon from '../svg/SaveIcon';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import ModelDropDown from './ModelDropDown'; import ModelDropDown from '../../ui/ModelDropDown';
import { Input } from '~/components/ui/Input.tsx'; import { Input } from '~/components/ui/Input.tsx';
import { Label } from '~/components/ui/Label.tsx'; import { Label } from '~/components/ui/Label.tsx';
import { Slider } from '~/components/ui/Slider.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 { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
import { cn } from '~/utils/'; import { cn } from '~/utils/';
const defaultTextProps = 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 = 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'; '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} model={model}
setModel={setModel} setModel={setModel}
endpoint="openAI" 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"
/> />
{/* <Label {/* <Label
htmlFor="model" htmlFor="model"

View file

@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react';
import { useSetRecoilState } 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 { cn } from '~/utils/';
import buildPresetByConversation from '~/utils/buildPresetByConversation';
import store from '~/store';
const SaveAsPresetDialog = ({ open, onOpenChange, conversation }) => {
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 (
<Dialog
open={open}
onOpenChange={onOpenChange}
>
<DialogTemplate
title="Save As Preset"
main=<div className="grid w-full items-center gap-2">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Preset Name
</Label>
<Input
id="chatGptLabel"
value={title || ''}
onChange={e => 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'
)}
/>
</div>
selection={{
selectHandler: submitPreset,
selectClasses: 'bg-green-600 hover:bg-green-700 dark:hover:bg-green-800 text-white',
selectText: 'Save'
}}
/>
</Dialog>
);
};
export default SaveAsPresetDialog;

View file

@ -1,17 +0,0 @@
import React from 'react';
import ModelItem from './ModelItem';
export default function MenuItems({ models, onSelect }) {
return (
<>
{models.map(modelItem => (
<ModelItem
key={modelItem._id}
value={modelItem.value}
onSelect={onSelect}
model={modelItem}
/>
))}
</>
);
}

View file

@ -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 (
<DialogContent className="shadow-2xl dark:bg-gray-800">
<DialogHeader>
<DialogTitle className="text-gray-800 dark:text-white">Customize ChatGPT</DialogTitle>
<DialogDescription className="text-gray-600 dark:text-gray-300">
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)] 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}
/>
</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 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"
/>
</div>
</div>
<DialogFooter>
<DialogClose className="dark:hover:gray-400 border-gray-700">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 dark:text-gray-200"
>
{saveText}
</Button>
<DialogClose
onClick={selectHandler}
className="inline-flex h-10 items-center justify-center rounded-md border-none bg-gray-900 py-2 px-4 text-sm font-semibold 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"
>
Select
</DialogClose>
</DialogFooter>
</DialogContent>
);
}

View file

@ -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 (
<DropdownMenuRadioItem
value={value}
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{name}
{model === 'chatgpt' && <sup>$</sup>}
</DropdownMenuRadioItem>
);
else if (model === 'chatgptCustom' && chatGptLabel === null && promptPrefix === null)
// base chatgptCustom model, click to add new chatgptCustom.
return (
<DialogTrigger className="w-full">
<DropdownMenuRadioItem
value={value}
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{name}
<sup>$</sup>
</DropdownMenuRadioItem>
</DialogTrigger>
);
// 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 (
<span
value={value}
className={itemClass.className}
onClick={e => {
if (isHovering) {
return;
}
onSelect('chatgptCustom', value);
}}
>
{currentConversation?.chatGptLabel === value && (
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<Circle className="h-2 w-2 fill-current" />
</span>
)}
{icon}
{renaming === true ? (
<input
ref={inputRef}
key={id}
type="text"
className="pointer-events-auto z-50 m-0 mr-2 w-3/4 border border-blue-500 bg-transparent p-0 text-sm leading-tight outline-none"
value={modelInput}
onClick={e => e.stopPropagation()}
onChange={e => setModelInput(e.target.value)}
// onBlur={onRename}
onKeyDown={handleKeyDown}
/>
) : (
<div className=" overflow-hidden">{modelInput}</div>
)}
{value === 'chatgpt' && <sup>$</sup>}
<RenameButton
twcss={`ml-auto mr-2 ${buttonClass.className}`}
onRename={onRename}
renaming={renaming}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
renameHandler={renameHandler}
/>
<button
{...buttonClass}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
onClick={onDelete}
>
<TrashIcon />
</button>
</span>
);
}

View file

@ -1,8 +1,8 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
// import ModelDialog from './ModelDialog'; import EditPresetDialog from '../../Endpoints/EditPresetDialog';
import EndpointItems from './EndpointItems'; import EndpointItems from './EndpointItems';
import { swr } from '~/utils/fetchers'; import PresetItems from './PresetItems';
import getIcon from '~/utils/getIcon'; import getIcon from '~/utils/getIcon';
import { Button } from '../../ui/Button.tsx'; import { Button } from '../../ui/Button.tsx';
@ -18,30 +18,22 @@ import { Dialog } from '../../ui/Dialog.tsx';
import store from '~/store'; import store from '~/store';
export default function EndpointMenu() { export default function NewConversationMenu() {
// const [modelSave, setModelSave] = useState(false); // const [modelSave, setModelSave] = useState(false);
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [presetModelVisible, setPresetModelVisible] = useState(false);
const [preset, setPreset] = useState(false);
// const models = useRecoilValue(store.models); // const models = useRecoilValue(store.models);
const availableEndpoints = useRecoilValue(store.availableEndpoints); const availableEndpoints = useRecoilValue(store.availableEndpoints);
// const setCustomGPTModels = useSetRecoilState(store.customGPTModels); // const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
const presets = useRecoilValue(store.presets);
const conversation = useRecoilValue(store.conversation) || {}; const conversation = useRecoilValue(store.conversation) || {};
const { endpoint, conversationId } = conversation; const { endpoint, conversationId } = conversation;
// const { model, promptPrefix, chatGptLabel, conversationId } = conversation; // const { model, promptPrefix, chatGptLabel, conversationId } = conversation;
const { newConversation } = store.useConversation(); 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 // update the default model when availableModels changes
// typically, availableModels changes => modelsFilter or customGPTModels changes // typically, availableModels changes => modelsFilter or customGPTModels changes
useEffect(() => { useEffect(() => {
@ -56,81 +48,31 @@ export default function EndpointMenu() {
}, [conversation]); }, [conversation]);
// set the current model // set the current model
const onChange = (newEndpoint, value = null) => { const onSelectEndpoint = newEndpoint => {
setMenuOpen(false); setMenuOpen(false);
if (!newEndpoint) return; if (!newEndpoint) return;
else if (newEndpoint === endpoint) return; else if (newEndpoint === endpoint) return;
else { 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 => { // set the current model
// mutate(); const onSelectPreset = newPreset => {
// if (!open) { setMenuOpen(false);
// setModelSave(false); if (!newPreset) return;
// } // else if (newEndpoint === endpoint) return;
// }; else {
newConversation({}, newPreset);
}
};
// const handleSaveState = value => { const onChangePreset = preset => {
// if (!modelSave) { setPresetModelVisible(true);
// return; 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({ const icon = getIcon({
size: 32, size: 32,
...conversation, ...conversation,
@ -157,32 +99,51 @@ export default function EndpointMenu() {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
className="w-56 dark:bg-gray-700" className="min-w-56 dark:bg-gray-700"
onCloseAutoFocus={event => event.preventDefault()} onCloseAutoFocus={event => event.preventDefault()}
> >
<DropdownMenuLabel className="dark:text-gray-300">Select an AI Endpoint</DropdownMenuLabel> <DropdownMenuLabel className="dark:text-gray-300">Select an Endpoint</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
value={endpoint} value={endpoint}
onValueChange={onChange} onValueChange={onSelectEndpoint}
className="overflow-y-auto" className="overflow-y-auto"
> >
{availableEndpoints.length ? ( {availableEndpoints.length ? (
<EndpointItems <EndpointItems
endpoints={availableEndpoints} endpoints={availableEndpoints}
onSelect={onChange} onSelect={onSelectEndpoint}
/> />
) : ( ) : (
<DropdownMenuLabel className="dark:text-gray-300">No endpoint available.</DropdownMenuLabel> <DropdownMenuLabel className="dark:text-gray-300">No endpoint available.</DropdownMenuLabel>
)} )}
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
<div className="mt-6 w-full" />
<DropdownMenuLabel className="dark:text-gray-300">Select a Preset</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
onValueChange={onSelectPreset}
className="overflow-y-auto"
>
{presets.length ? (
<PresetItems
presets={presets}
onSelect={onSelectPreset}
onChangePreset={onChangePreset}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">No preset yet.</DropdownMenuLabel>
)}
</DropdownMenuRadioGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
{/* <ModelDialog <EditPresetDialog
mutate={mutate} open={presetModelVisible}
setModelSave={setModelSave} onOpenChange={setPresetModelVisible}
handleSaveState={handleSaveState} preset={preset}
/> */} />
</Dialog> </Dialog>
); );
} }

View file

@ -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 (
<DropdownMenuRadioItem
value={value}
className="group dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{icon}
{preset?.title}
<small className="ml-2">({getPresetTitle()})</small>
{/* <RenameButton
twcss={`ml-auto mr-2 ${buttonClass.className}`}
onRename={onRename}
renaming={renaming}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
renameHandler={renameHandler}
/> */}
<div className="flex w-4 flex-1" />
<button
className="invisible m-0 rounded-md text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "
onClick={() => onChangePreset(preset)}
>
<EditIcon />
</button>
</DropdownMenuRadioItem>
);
}

View file

@ -0,0 +1,18 @@
import React from 'react';
import PresetItem from './PresetItem';
export default function PresetItems({ presets, onSelect, onChangePreset }) {
return (
<>
{presets.map(preset => (
<PresetItem
key={preset?.presetId}
value={preset}
onSelect={onSelect}
onChangePreset={onChangePreset}
preset={preset}
/>
))}
</>
);
}

View file

@ -2,7 +2,7 @@ import React from 'react';
export default function Footer() { export default function Footer() {
return ( return (
<div className="hidden md:block px-3 pt-2 pb-1 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-4"> <div className="hidden px-3 pt-2 pb-1 text-center text-xs text-black/50 dark:text-white/50 md:block md:px-4 md:pt-3 md:pb-4">
<a <a
href="https://github.com/danny-avila/chatgpt-clone" href="https://github.com/danny-avila/chatgpt-clone"
target="_blank" target="_blank"
@ -11,8 +11,8 @@ export default function Footer() {
> >
ChatGPT Clone ChatGPT Clone
</a> </a>
. Serves and searches all conversations reliably. All AI convos under one house. Pay per . Serves and searches all conversations reliably. All AI convos under one house. Pay per call and not
call and not per month (cents compared to dollars). per month (cents compared to dollars).
</div> </div>
); );
} }

View file

@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Settings2 } from 'lucide-react'; import { Settings2 } from 'lucide-react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import ModelSelect from './ModelSelect'; import ModelSelect from '../../ui/ModelSelect';
import EndpointOptionsPopover from '../../ui/EndpointOptionsPopover'; import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
import DialogTemplate from '../../ui/DialogTemplate'; import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
import { Button } from '../../ui/Button.tsx'; import { Button } from '../../ui/Button.tsx';
import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx'; import Settings from '../../Endpoints/OpenAI/Settings.jsx';
import Settings from './Settings.jsx';
import { cn } from '~/utils/'; import { cn } from '~/utils/';
import store from '~/store'; import store from '~/store';
@ -125,18 +124,11 @@ function OpenAIOptions() {
saveAsPreset={saveAsPreset} saveAsPreset={saveAsPreset}
switchToSimpleMode={switchToSimpleMode} switchToSimpleMode={switchToSimpleMode}
/> />
<Dialog <SaveAsPresetDialog
open={saveAsDialogShow} open={saveAsDialogShow}
onOpenChange={setSaveAsDialogShow} onOpenChange={setSaveAsDialogShow}
> conversation={conversation}
<DialogTemplate
title="title"
description="desc"
main="tttt"
buttons={null}
selection={{}}
/> />
</Dialog>
</> </>
); );
} }

View file

@ -5,7 +5,7 @@ import AdjustToneButton from './AdjustToneButton';
import OpenAIOptions from './OpenAIOptions'; import OpenAIOptions from './OpenAIOptions';
import BingAIOptions from './BingAIOptions'; import BingAIOptions from './BingAIOptions';
// import BingStyles from './BingStyles'; // import BingStyles from './BingStyles';
import EndpointMenu from './Endpoints/EndpointMenu'; import EndpointMenu from './Endpoints/NewConversationMenu';
import Footer from './Footer'; import Footer from './Footer';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import { useMessageHandler } from '../../utils/handleSubmit'; import { useMessageHandler } from '../../utils/handleSubmit';

View file

@ -53,7 +53,7 @@ const DialogContent = React.forwardRef<
> >
{children} {children}
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800"> <DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
<X className="h-4 w-4" /> <X className="h-4 w-4 text-black dark:text-white" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View file

@ -8,18 +8,18 @@ import {
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from './Dialog.tsx'; } 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 { 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 ( return (
<DialogContent className="shadow-2xl dark:bg-gray-800"> <DialogContent className={cn('shadow-2xl dark:bg-gray-800', className || '')}>
<DialogHeader> <DialogHeader>
<DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle> <DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle>
<DialogDescription className="text-gray-600 dark:text-gray-300"> <DialogDescription className="text-gray-600 dark:text-gray-300">{description}</DialogDescription>
{description}
</DialogDescription>
</DialogHeader> </DialogHeader>
{/* <div className="grid gap-4 py-4"> {/* <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"> //input template <div className="grid grid-cols-4 items-center gap-4"> //input template
@ -44,10 +44,12 @@ export default function DialogTemplate({ title, description, main, buttons, sele
{main ? main : null} {main ? main : null}
<DialogFooter> <DialogFooter>
<DialogClose className="dark:hover:gray-400 border-gray-700">Cancel</DialogClose> <DialogClose className="dark:hover:gray-400 border-gray-700">Cancel</DialogClose>
{ buttons ? buttons : null} {buttons ? buttons : null}
<DialogClose <DialogClose
onClick={selectHandler} onClick={selectHandler}
className={`${selectClasses || defaultSelect} inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold`} className={`${
selectClasses || defaultSelect
} inline-flex h-10 items-center justify-center rounded-md border-none py-2 px-4 text-sm font-semibold`}
> >
{selectText} {selectText}
</DialogClose> </DialogClose>

View file

@ -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 (
<div className={cn('flex items-center justify-center gap-2', containerClassName)}>
<div className="relative w-full">
<Listbox
value={value}
onChange={onChange}
>
<Listbox.Button
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:border-green-600 focus:outline-none focus:ring-1 focus:ring-green-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
className || ''
)}
>
<span className="inline-flex w-full truncate">
<span className="flex h-6 items-center gap-1 truncate text-sm text-black dark:text-white">
{value}
</span>
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
stroke="currentColor"
fill="none"
strokeWidth="2"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="h-4 w-4 text-gray-400"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</span>
</Listbox.Button>
<Listbox.Options className="absolute z-10 mt-2 max-h-60 w-full overflow-auto rounded bg-white text-base text-xs ring-1 ring-black/10 focus:outline-none dark:bg-gray-800 dark:ring-white/20 dark:last:border-0 md:w-[100%]">
{options.map((item, i) => (
<Listbox.Option
key={i}
value={item}
className="group relative flex h-[42px] cursor-pointer select-none items-center overflow-hidden border-b border-black/10 pl-3 pr-9 text-gray-900 last:border-0 hover:bg-[#ECECF1] dark:border-white/20 dark:text-white dark:hover:bg-gray-700"
>
<span className="flex items-center gap-1.5 truncate">
<span
className={cn(
'flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100',
value === item ? 'font-semibold' : ''
)}
>
{item}
</span>
{value === item && (
<span className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-800 dark:text-gray-100">
<CheckMark />
</span>
)}
</span>
</Listbox.Option>
))}
</Listbox.Options>
</Listbox>
</div>
</div>
);
}
export default Dropdown;

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import useDocumentTitle from '~/hooks/useDocumentTitle'; import useDocumentTitle from '~/hooks/useDocumentTitle';
import Templates from '../Prompts/Templates'; import Templates from '../ui/Templates';
import SunIcon from '../svg/SunIcon'; import SunIcon from '../svg/SunIcon';
import LightningIcon from '../svg/LightningIcon'; import LightningIcon from '../svg/LightningIcon';
import CautionIcon from '../svg/CautionIcon'; import CautionIcon from '../svg/CautionIcon';

View file

@ -1,22 +1,27 @@
import React from 'react'; import React from 'react';
import CheckMark from '../../svg/CheckMark'; import CheckMark from '../svg/CheckMark';
import { Listbox } from '@headlessui/react'; import { Listbox } from '@headlessui/react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { cn } from '~/utils/'; import { cn } from '~/utils/';
import store from '~/store'; import store from '~/store';
function ModelDropDown({ model, setModel, endpoint }) { function ModelDropDown({ model, setModel, endpoint, containerClassName, className }) {
const endpointsConfig = useRecoilValue(store.endpointsConfig); const endpointsConfig = useRecoilValue(store.endpointsConfig);
const models = endpointsConfig?.[endpoint]?.['availableModels'] || []; const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
return ( return (
<div className="flex items-center justify-center gap-2"> <div className={cn('flex items-center justify-center gap-2', containerClassName)}>
<div className="relative w-full"> <div className="relative w-full">
<Listbox <Listbox
value={model} value={model}
onChange={setModel} onChange={setModel}
> >
<Listbox.Button className="relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:border-green-600 focus:outline-none focus:ring-1 focus:ring-green-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm"> <Listbox.Button
className={cn(
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:border-green-600 focus:outline-none focus:ring-1 focus:ring-green-600 dark:border-white/20 dark:bg-gray-800 sm:text-sm',
className || ''
)}
>
<Listbox.Label <Listbox.Label
className="block text-xs text-gray-700 dark:text-gray-500" className="block text-xs text-gray-700 dark:text-gray-500"
id="headlessui-listbox-label-:r1:" id="headlessui-listbox-label-:r1:"
@ -25,7 +30,9 @@ function ModelDropDown({ model, setModel, endpoint }) {
Model Model
</Listbox.Label> </Listbox.Label>
<span className="inline-flex w-full truncate"> <span className="inline-flex w-full truncate">
<span className="flex h-6 items-center gap-1 truncate text-sm">{model}</span> <span className="flex h-6 items-center gap-1 truncate text-sm text-black dark:text-white">
{model}
</span>
</span> </span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"> <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg <svg

View file

@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from '../../ui/Button.tsx'; import { Button } from './Button.tsx';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -8,7 +8,7 @@ import {
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuRadioItem DropdownMenuRadioItem
} from '../../ui/DropdownMenu.tsx'; } from './DropdownMenu.tsx';
const ModelSelect = ({ model, onChange, availableModels, ...props }) => { const ModelSelect = ({ model, onChange, availableModels, ...props }) => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);

View file

@ -27,7 +27,6 @@ import getDefaultConversation from '~/utils/getDefaultConversation';
// clientId: null, // clientId: null,
// invocationId: 1, // invocationId: 1,
// toneStyle: null, // toneStyle: null,
// suggestions: []
// }; // };
const conversation = atom({ const conversation = atom({
@ -62,10 +61,10 @@ const useConversation = () => {
const switchToConversation = useRecoilCallback( const switchToConversation = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
async (_conversation, messages = null, targetEndpoint = null) => { async (_conversation, messages = null, preset = null) => {
const prevConversation = await snapshot.getPromise(conversation); const prevConversation = await snapshot.getPromise(conversation);
const endpointsFilter = await snapshot.getPromise(endpoints.endpointsFilter); const endpointsFilter = await snapshot.getPromise(endpoints.endpointsFilter);
_switchToConversation(_conversation, messages, targetEndpoint, { _switchToConversation(_conversation, messages, preset, {
endpointsFilter, endpointsFilter,
prevConversation prevConversation
}); });
@ -76,7 +75,7 @@ const useConversation = () => {
const _switchToConversation = ( const _switchToConversation = (
conversation, conversation,
messages = null, messages = null,
targetEndpoint = null, preset = null,
{ endpointsFilter = {}, prevConversation = {} } { endpointsFilter = {}, prevConversation = {} }
) => { ) => {
let { endpoint = null } = conversation; let { endpoint = null } = conversation;
@ -87,7 +86,7 @@ const useConversation = () => {
conversation, conversation,
endpointsFilter, endpointsFilter,
prevConversation, prevConversation,
targetEndpoint preset
}); });
setConversation(conversation); setConversation(conversation);
@ -95,7 +94,7 @@ const useConversation = () => {
resetLatestMessage(); resetLatestMessage();
}; };
const newConversation = (template = {}, targetEndpoint = null) => { const newConversation = (template = {}, preset) => {
switchToConversation( switchToConversation(
{ {
conversationId: 'new', conversationId: 'new',
@ -103,7 +102,7 @@ const useConversation = () => {
...template ...template
}, },
[], [],
targetEndpoint preset
); );
}; };

View file

@ -6,6 +6,7 @@ import user from './user';
import text from './text'; import text from './text';
import submission from './submission'; import submission from './submission';
import search from './search'; import search from './search';
import preset from './preset';
export default { export default {
...conversation, ...conversation,
@ -15,5 +16,6 @@ export default {
...user, ...user,
text, text,
...submission, ...submission,
...search ...search,
...preset
}; };

View file

@ -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
};

View file

@ -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;

View file

@ -3,14 +3,19 @@ import axios from 'axios';
import useSWR from 'swr'; import useSWR from 'swr';
import useSWRMutation from 'swr/mutation'; 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) => { const axiosFetcher = async (url, params) => {
console.log(params, 'params'); console.log(params, 'params');
return axios.get(url, params); return axios.get(url, params);
}; };
const postRequest = async (url, { arg }) => { export const postRequest = async (url, { arg }) => {
return await axios.post(url, { withCredentials: true, arg }); return await axios({
method: 'post',
url: url,
withCredentials: true,
data: { arg }
});
}; };
export const searchFetcher = async (pre, q, pageNumber, callback) => { export const searchFetcher = async (pre, q, pageNumber, callback) => {

View file

@ -44,7 +44,9 @@ const buildDefaultConversation = ({ conversation, endpoint, lastConversationSetu
return conversation; return conversation;
}; };
const getDefaultConversation = ({ conversation, prevConversation, endpointsFilter, targetEndpoint }) => { const getDefaultConversation = ({ conversation, prevConversation, endpointsFilter, preset }) => {
const { endpoint: targetEndpoint } = preset || {};
if (targetEndpoint) { if (targetEndpoint) {
// try to use current model // try to use current model
const endpoint = targetEndpoint; const endpoint = targetEndpoint;
@ -52,7 +54,7 @@ const getDefaultConversation = ({ conversation, prevConversation, endpointsFilte
conversation = buildDefaultConversation({ conversation = buildDefaultConversation({
conversation, conversation,
endpoint, endpoint,
lastConversationSetup: {} lastConversationSetup: preset
}); });
return conversation; return conversation;
} else { } else {