feat: support edit preset,

feat: support view current conversation options.
feat: save current conversation as a preset.
feat: preset delete all.
This commit is contained in:
Wentao Lyu 2023-04-04 01:12:14 +08:00
parent dae0c2d5e3
commit d2579b44d1
15 changed files with 340 additions and 269 deletions

View file

@ -16,7 +16,7 @@ const optionText =
function Settings(props) {
// const [showSystemMessage, setShowSystemMessage] = React.useState(false);
const { context, setContext, systemMessage, setSystemMessage, jailbreak, setJailbreak } = props;
const { readonly, context, setContext, systemMessage, setSystemMessage, jailbreak, setJailbreak } = props;
const showSystemMessage = jailbreak;
return (
@ -32,6 +32,7 @@ function Settings(props) {
</Label>
<TextareaAutosize
id="context"
disabled={readonly}
value={context || ''}
onChange={e => setContext(e.target.value || null)}
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
@ -47,6 +48,7 @@ function Settings(props) {
<div className="flex items-center space-x-3">
<Checkbox
id="jailbreak"
disabled={readonly}
checked={jailbreak}
className="dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 focus:ring-opacity-20 dark:focus:ring-opacity-50 dark:focus:ring-offset-0 dark:focus:ring-gray-600"
// onCheckedChange={setJailbreak}
@ -79,6 +81,7 @@ function Settings(props) {
</Label> */}
<TextareaAutosize
id="systemMessage"
disabled={readonly}
value={systemMessage || ''}
onChange={e => setSystemMessage(e.target.value || null)}
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"

View file

@ -8,7 +8,10 @@ import { Input } from '../ui/Input.tsx';
import { Label } from '../ui/Label.tsx';
import Dropdown from '../ui/Dropdown';
import { cn } from '~/utils/';
import cleanupPreset from '~/utils/cleanupPreset';
import OpenAISettings from './OpenAI/Settings';
import BingAISettings from './BingAI/Settings.jsx';
import store from '~/store';
@ -50,39 +53,29 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset }) => {
setPresP={setOption('frequency_penalty')}
/>
);
else if (endpoint === 'bingAI')
return (
<BingAISettings
readonly={true}
context={preset?.context}
setContext={setOption('context')}
systemMessage={preset?.systemMessage}
setSystemMessage={setOption('systemMessage')}
jailbreak={preset?.jailbreak}
setJailbreak={setOption('jailbreak')}
/>
);
else return null;
};
const defaultTextProps =
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
// in order not to use wrong data from other endpoint
const clearPreset = () => {
if (preset?.endpoint === 'openAI')
return {
title: preset?.title,
endpoint: preset?.endpoint,
model: preset?.model,
chatGptLabel: preset?.chatGptLabel,
promptPrefix: preset?.promptPrefix,
temperature: preset?.temperature,
top_p: preset?.top_p,
presence_penalty: preset?.presence_penalty,
frequency_penalty: preset?.frequency_penalty
};
else
return {
title: preset?.title,
endpoint: preset?.endpoint
};
// TODO: else
};
const submitPreset = () => {
axios({
method: 'post',
url: '/api/presets',
data: clearPreset(),
data: cleanupPreset(preset),
withCredentials: true
}).then(res => {
setPresets(res?.data);
@ -91,7 +84,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset }) => {
const exportPreset = () => {
exportFromJSON({
data: clearPreset(),
data: cleanupPreset(preset),
fileName: `${preset?.title}.json`,
exportType: exportFromJSON.types.json
});
@ -168,7 +161,7 @@ const EditPresetDialog = ({ open, onOpenChange, preset: _preset }) => {
<>
<DialogButton
onClick={exportPreset}
className="dark:hover:gray-400 bg-red border-gray-700"
className="dark:hover:gray-400 border-gray-700"
>
Export
</DialogButton>

View file

@ -0,0 +1,134 @@
import React, { useEffect, useState } from 'react';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import axios from 'axios';
import exportFromJSON from 'export-from-json';
import DialogTemplate from '../ui/DialogTemplate.jsx';
import { Dialog, DialogClose, DialogButton } from '../ui/Dialog.tsx';
import { Input } from '../ui/Input.tsx';
import { Label } from '../ui/Label.tsx';
import Dropdown from '../ui/Dropdown.jsx';
import SaveAsPresetDialog from './SaveAsPresetDialog';
import { cn } from '~/utils/';
import cleanupPreset from '~/utils/cleanupPreset';
import OpenAISettings from './OpenAI/Settings.jsx';
import BingAISettings from './BingAI/Settings.jsx';
import store from '~/store';
// A preset dialog to show readonly preset values.
const EndpointOptionsDialog = ({ open, onOpenChange, preset: _preset, title }) => {
// const [title, setTitle] = useState('My Preset');
const [preset, setPreset] = useState({});
const [saveAsDialogShow, setSaveAsDialogShow] = useState(false);
const setOption = param => newValue => {
let update = {};
update[param] = newValue;
setPreset(prevState => ({
...prevState,
...update
}));
};
const renderSettings = () => {
const { endpoint } = preset || {};
if (endpoint === 'openAI')
return (
<OpenAISettings
readonly={true}
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 if (endpoint === 'bingAI')
return (
<BingAISettings
readonly={true}
context={preset?.context}
setContext={setOption('context')}
systemMessage={preset?.systemMessage}
setSystemMessage={setOption('systemMessage')}
jailbreak={preset?.jailbreak}
setJailbreak={setOption('jailbreak')}
/>
);
else return null;
};
const saveAsPreset = () => {
setSaveAsDialogShow(true);
};
const exportPreset = () => {
exportFromJSON({
data: cleanupPreset(preset),
fileName: `${preset?.title}.json`,
exportType: exportFromJSON.types.json
});
};
useEffect(() => {
setPreset(_preset);
}, [open]);
return (
<>
<Dialog
open={open}
onOpenChange={onOpenChange}
>
<DialogTemplate
title={title || 'View Options'}
className="max-w-full sm:max-w-4xl"
main={
<div className="flex w-full flex-col items-center gap-2">
<div className="w-full p-0">{renderSettings()}</div>
</div>
}
buttons={
<>
<DialogButton
onClick={saveAsPreset}
className="dark:hover:gray-400 border-gray-700 bg-green-600 text-white hover:bg-green-700 dark:hover:bg-green-800"
>
Save As Preset
</DialogButton>
</>
}
leftButtons={
<>
<DialogButton
onClick={exportPreset}
className="dark:hover:gray-400 border-gray-700"
>
Export
</DialogButton>
</>
}
/>
</Dialog>
<SaveAsPresetDialog
open={saveAsDialogShow}
onOpenChange={setSaveAsDialogShow}
preset={preset}
/>
</>
);
};
export default EndpointOptionsDialog;

View file

@ -4,6 +4,7 @@ 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';
import { InputNumber } from '../../ui/InputNumber';
import OptionHover from './OptionHover';
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
import { cn } from '~/utils/';
@ -15,6 +16,7 @@ const optionText =
function Settings(props) {
const {
readonly,
model,
setModel,
chatGptLabel,
@ -38,6 +40,7 @@ function Settings(props) {
<div className="grid w-full items-center gap-2">
<ModelDropDown
model={model}
disabled={readonly}
setModel={setModel}
endpoint="openAI"
className={cn(
@ -73,6 +76,7 @@ function Settings(props) {
</Label>
<Input
id="chatGptLabel"
disabled={readonly}
value={chatGptLabel || ''}
// ref={inputRef}
onChange={e => setChatGptLabel(e.target.value || null)}
@ -92,6 +96,7 @@ function Settings(props) {
</Label>
<TextareaAutosize
id="promptPrefix"
disabled={readonly}
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.'"
@ -123,6 +128,7 @@ function Settings(props) {
</Label>
<Input
id="temp-int"
disabled
value={temperature}
onChange={e => setTemperature(e.target.value)}
className={cn(
@ -132,6 +138,7 @@ function Settings(props) {
/>
</div>
<Slider
disabled={readonly}
value={[temperature]}
onValueChange={value => setTemperature(value[0])}
max={2}
@ -157,6 +164,7 @@ function Settings(props) {
</Label>
<Input
id="max-tokens-int"
disabled
value={maxTokens}
onChange={e => setMaxTokens(e.target.value)}
className={cn(
@ -166,6 +174,7 @@ function Settings(props) {
/>
</div>
<Slider
disabled={readonly}
value={[maxTokens]}
onValueChange={value => setMaxTokens(value[0])}
max={2048} // should be dynamic to the currently selected model
@ -191,6 +200,7 @@ function Settings(props) {
</Label>
<Input
id="top-p-int"
disabled
value={topP}
onChange={e => setTopP(e.target.value)}
className={cn(
@ -200,6 +210,7 @@ function Settings(props) {
/>
</div>
<Slider
disabled={readonly}
value={[topP]}
onValueChange={value => setTopP(value[0])}
max={1}
@ -225,6 +236,7 @@ function Settings(props) {
</Label>
<Input
id="freq-penalty-int"
disabled
value={freqP}
onChange={e => setFreqP(e.target.value)}
className={cn(
@ -234,6 +246,7 @@ function Settings(props) {
/>
</div>
<Slider
disabled={readonly}
value={[freqP]}
onValueChange={value => setFreqP(value[0])}
max={2}
@ -259,6 +272,7 @@ function Settings(props) {
</Label>
<Input
id="pres-penalty-int"
disabled
value={presP}
onChange={e => setPresP(e.target.value)}
className={cn(
@ -268,6 +282,7 @@ function Settings(props) {
/>
</div>
<Slider
disabled={readonly}
value={[presP]}
onValueChange={value => setPresP(value[0])}
max={2}
@ -281,158 +296,6 @@ function Settings(props) {
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>
</>

View file

@ -6,11 +6,11 @@ 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 cleanupPreset from '~/utils/cleanupPreset';
import store from '~/store';
const SaveAsPresetDialog = ({ open, onOpenChange, conversation }) => {
const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
const [title, setTitle] = useState('My Preset');
const setPresets = useSetRecoilState(store.presets);
@ -18,15 +18,15 @@ const SaveAsPresetDialog = ({ open, onOpenChange, conversation }) => {
'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
const _preset = cleanupPreset({
...preset,
title
});
axios({
method: 'post',
url: '/api/presets',
data: preset,
data: _preset,
withCredentials: true
}).then(res => {
setPresets(res?.data);