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

@ -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 { 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() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 dark:bg-gray-700"
className="min-w-56 dark:bg-gray-700"
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 />
<DropdownMenuRadioGroup
value={endpoint}
onValueChange={onChange}
onValueChange={onSelectEndpoint}
className="overflow-y-auto"
>
{availableEndpoints.length ? (
<EndpointItems
endpoints={availableEndpoints}
onSelect={onChange}
onSelect={onSelectEndpoint}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">No endpoint available.</DropdownMenuLabel>
)}
</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>
</DropdownMenu>
{/* <ModelDialog
mutate={mutate}
setModelSave={setModelSave}
handleSaveState={handleSaveState}
/> */}
<EditPresetDialog
open={presetModelVisible}
onOpenChange={setPresetModelVisible}
preset={preset}
/>
</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() {
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
href="https://github.com/danny-avila/chatgpt-clone"
target="_blank"
@ -11,8 +11,8 @@ export default function Footer() {
>
ChatGPT Clone
</a>
. 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).
</div>
);
}

View file

@ -1,78 +0,0 @@
import React from 'react';
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 }) {
const endpointsConfig = useRecoilValue(store.endpointsConfig);
const models = endpointsConfig?.[endpoint]?.['availableModels'] || [];
return (
<div className="flex items-center justify-center gap-2">
<div className="relative w-full">
<Listbox
value={model}
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.Label
className="block text-xs text-gray-700 dark:text-gray-500"
id="headlessui-listbox-label-:r1:"
data-headlessui-state=""
>
Model
</Listbox.Label>
<span className="inline-flex w-full truncate">
<span className="flex h-6 items-center gap-1 truncate text-sm">{model}</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%]">
{models.map((modelOption, i) => (
<Listbox.Option
key={i}
value={modelOption}
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',
modelOption === model ? 'font-semibold' : ''
)}
>
{modelOption}
</span>
{modelOption === model && (
<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 ModelDropDown;

View file

@ -1,52 +0,0 @@
import React, { useState } from 'react';
import { Button } from '../../ui/Button.tsx';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuRadioItem
} from '../../ui/DropdownMenu.tsx';
const ModelSelect = ({ model, onChange, availableModels, ...props }) => {
const [menuOpen, setMenuOpen] = useState(false);
return (
<DropdownMenu
open={menuOpen}
onOpenChange={setMenuOpen}
>
<DropdownMenuTrigger asChild>
<Button {...props}>
<span className="w-full text-center text-xs font-medium font-normal">Model: {model}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 dark:bg-gray-700"
onCloseAutoFocus={event => event.preventDefault()}
>
<DropdownMenuLabel className="dark:text-gray-300">Select a model</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={model}
onValueChange={onChange}
className="overflow-y-auto"
>
{availableModels.map(model => (
<DropdownMenuRadioItem
key={model}
value={model}
className="dark:font-semibold dark:text-gray-100 dark:hover:bg-gray-800"
>
{model}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
export default ModelSelect;

View file

@ -1,33 +0,0 @@
import React from 'react';
import { HoverCardPortal, HoverCardContent } from '~/components/ui/HoverCard.tsx';
const types = {
temp: 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
max: "The max tokens to generate. The total length of input tokens and generated tokens is limited by the model's context length.",
topp: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
freq: "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.",
pres: "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics."
};
function OptionHover({ type, side }) {
// const options = {};
// if (type === 'pres') {
// options.sideOffset = 45;
// }
return (
<HoverCardPortal>
<HoverCardContent
side={side}
className="w-80 "
// {...options}
>
<div className="space-y-2">
<p className="text-sm text-gray-600 dark:text-gray-300">{types[type]}</p>
</div>
</HoverCardContent>
</HoverCardPortal>
);
}
export default OptionHover;

View file

@ -1,437 +0,0 @@
import React from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import ModelDropDown from './ModelDropDown';
import { Input } from '~/components/ui/Input.tsx';
import { Label } from '~/components/ui/Label.tsx';
import { Slider } from '~/components/ui/Slider.tsx';
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';
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';
function Settings(props) {
const {
model,
setModel,
chatGptLabel,
setChatGptLabel,
promptPrefix,
setPromptPrefix,
temperature,
setTemperature,
topP,
setTopP,
freqP,
setFreqP,
presP,
setPresP
} = props;
return (
<>
<div className="grid gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
<ModelDropDown
model={model}
setModel={setModel}
endpoint="openAI"
/>
{/* <Label
htmlFor="model"
className="text-left text-sm font-medium"
>
Model
</Label>
<Input
id="model"
value={model}
// ref={inputRef}
onChange={e => setModel(e.target.value)}
placeholder="Set a custom name for ChatGPT"
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="grid w-full items-center gap-2">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Custom Name <small className="opacity-40">(default: blank)</small>
</Label>
<Input
id="chatGptLabel"
value={chatGptLabel || ''}
// ref={inputRef}
onChange={e => setChatGptLabel(e.target.value || null)}
placeholder="Set a custom name for ChatGPT"
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="grid w-full items-center gap-2">
<Label
htmlFor="promptPrefix"
className="text-left text-sm font-medium"
>
Prompt Prefix <small className="opacity-40">(default: blank)</small>
</Label>
<TextareaAutosize
id="promptPrefix"
value={promptPrefix || ''}
onChange={e => setPromptPrefix(e.target.value || null)}
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
className={cn(
defaultTextProps,
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
)}
// onFocus={() => {
// textareaRef.current.classList.remove('max-h-10');
// textareaRef.current.classList.add('max-h-52');
// }}
// onBlur={() => {
// textareaRef.current.classList.remove('max-h-52');
// textareaRef.current.classList.add('max-h-10');
// }}
// ref={textareaRef}
/>
</div>
</div>
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<HoverCard>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Temperature <small className="opacity-40">(default: 1)</small>
</Label>
<Input
id="temp-int"
value={temperature}
onChange={e => setTemperature(e.target.value)}
className={cn(
defaultTextProps,
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
)}
/>
</div>
<Slider
value={[temperature]}
onValueChange={value => setTemperature(value[0])}
max={2}
min={0}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="temp"
side="left"
/>
</HoverCard>
{/* <HoverCard>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Max tokens
</Label>
<Input
id="max-tokens-int"
value={maxTokens}
onChange={e => setMaxTokens(e.target.value)}
className={cn(
defaultTextProps,
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
)}
/>
</div>
<Slider
value={[maxTokens]}
onValueChange={value => setMaxTokens(value[0])}
max={2048} // should be dynamic to the currently selected model
min={1}
step={1}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="max"
side="left"
/>
</HoverCard> */}
<HoverCard>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Top P <small className="opacity-40">(default: 1)</small>
</Label>
<Input
id="top-p-int"
value={topP}
onChange={e => setTopP(e.target.value)}
className={cn(
defaultTextProps,
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
)}
/>
</div>
<Slider
value={[topP]}
onValueChange={value => setTopP(value[0])}
max={1}
min={0}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="topp"
side="left"
/>
</HoverCard>
<HoverCard>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Frequency Penalty <small className="opacity-40">(default: 0)</small>
</Label>
<Input
id="freq-penalty-int"
value={freqP}
onChange={e => setFreqP(e.target.value)}
className={cn(
defaultTextProps,
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
)}
/>
</div>
<Slider
value={[freqP]}
onValueChange={value => setFreqP(value[0])}
max={2}
min={-2}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="freq"
side="left"
/>
</HoverCard>
<HoverCard>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Presence Penalty <small className="opacity-40">(default: 0)</small>
</Label>
<Input
id="pres-penalty-int"
value={presP}
onChange={e => setPresP(e.target.value)}
className={cn(
defaultTextProps,
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
)}
/>
</div>
<Slider
value={[presP]}
onValueChange={value => setPresP(value[0])}
max={2}
min={-2}
step={0.01}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="pres"
side="left"
/>
</HoverCard>
{/* <div className="flex justify-around">
<HoverCard>
<HoverCardTrigger className="group/temp mr-4 flex w-full items-center justify-end gap-4">
<Label
htmlFor="temperature"
className="mr-2 text-right"
>
Temperature
</Label>
<Input
id="temp-int"
value={temperature}
onChange={e => setTemperature(e.target.value)}
className={cn(defaultTextProps, cn(optionText, 'w-10 group-hover/temp:border-gray-200'))}
/>
</HoverCardTrigger>
<OptionHover
type="temp"
side="right"
/>
</HoverCard>
<HoverCard>
<HoverCardTrigger className="group/max mr-4 flex w-full items-center justify-end gap-4">
<Label
htmlFor="max-tokens"
className="mr-2 w-full text-right"
>
Max tokens
</Label>
<Input
id="max-tokens-int"
value={maxTokens}
onChange={e => setMaxTokens(e.target.value)}
className={cn(defaultTextProps, cn(optionText, 'w-11 group-hover/max:border-gray-200'))}
/>
</HoverCardTrigger>
<OptionHover
type="max"
side="left"
/>
</HoverCard>
</div>
<div className="grid grid-cols-2 items-center gap-5">
<Slider
value={[maxTokens]}
onValueChange={value => setMaxTokens(value)}
max={2048} // should be dynamic to the currently selected model
min={1}
step={1}
className="w-full"
/>
</div>
<div className="flex justify-around">
<HoverCard>
<HoverCardTrigger className="group/top mr-4 flex w-full items-center justify-end gap-4">
<Label
htmlFor="top-p"
className="mr-2 text-right"
>
Top P
</Label>
<Input
id="top-p-int"
value={topP}
onChange={e => setTopP(e.target.value)}
className={cn(defaultTextProps, cn(optionText, 'w-10 group-hover/top:border-gray-200'))}
/>
<OptionHover
type="top-p"
side="right"
/>
</HoverCardTrigger>
</HoverCard>
<HoverCard>
<HoverCardTrigger className="group/freq mr-4 flex w-full items-center justify-end gap-4">
<Label
htmlFor="freq-penalty"
className="mr-2 w-full text-right"
>
Frequency Penalty
</Label>
<Input
id="freq-penalty-int"
value={freqP}
onChange={e => setFreqP(e.target.value)}
className={cn(defaultTextProps, cn(optionText, 'w-10 group-hover/freq:border-gray-200'))}
/>
</HoverCardTrigger>
<OptionHover
type="freq"
side="left"
/>
</HoverCard>
</div>
<div className="grid grid-cols-2 items-center gap-5">
<Slider
value={[topP]}
onValueChange={value => setTopP(value)}
max={1}
min={0}
step={0.01}
className="w-full"
/>
<Slider
value={[freqP]}
onValueChange={value => setFreqP(value)}
max={2}
min={-2}
step={0.01}
className="w-full"
/>
</div>
<div className="flex justify-end">
<HoverCard>
<HoverCardTrigger className="group/pres mr-4 flex items-center justify-end gap-4">
<Label
htmlFor="pres-penalty"
className="mr-2 text-right"
>
Presence Penalty
</Label>
<Input
id="pres-penalty-int"
value={presP}
onChange={e => setPresP(e.target.value)}
className={cn(defaultTextProps, cn(optionText, 'w-10 group-hover/pres:border-gray-200'))}
/>
</HoverCardTrigger>
<OptionHover
type="pres"
side="left"
/>
</HoverCard>
</div>
<div className="grid grid-cols-2 items-center gap-5">
<Slider
value={[presP]}
onValueChange={value => setPresP(value)}
max={2}
min={-2}
step={0.01}
className="w-full opacity-0"
/>
<Slider
value={[presP]}
onValueChange={value => setPresP(value)}
max={2}
min={-2}
step={0.01}
className="w-full"
/>
</div> */}
</div>
</div>
</>
);
}
export default Settings;

View file

@ -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}
/>
<Dialog
<SaveAsPresetDialog
open={saveAsDialogShow}
onOpenChange={setSaveAsDialogShow}
>
<DialogTemplate
title="title"
description="desc"
main="tttt"
buttons={null}
selection={{}}
/>
</Dialog>
conversation={conversation}
/>
</>
);
}

View file

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