feat: customise default model parameters and system message

Fixes #97, Fixes #89, Fixes #35
This commit is contained in:
Jing Hua 2023-03-20 16:06:46 +08:00
parent 92f09c275b
commit b0bfe56fd3
14 changed files with 374 additions and 104 deletions

View file

@ -21,5 +21,8 @@
"frequencyPenalty": {
"label": "Frequency Penalty",
"description": "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. (Default: 0)"
}
},
"defaultChatConfig": "Default Chat Config",
"defaultSystemMessage": "Default System Message",
"resetToDefault": "Reset To Default"
}

View file

@ -21,5 +21,8 @@
"frequencyPenalty": {
"label": "频率惩罚",
"description": "数值在 -2.0 到 2.0 之间。正值会根据新 token 在文本中的现有频率来惩罚它们,降低模型直接重复相同语句的可能性。(默认: 0)"
}
},
"defaultChatConfig": "默认聊天配置",
"defaultSystemMessage": "默认系统消息",
"resetToDefault": "重置为默认"
}

View file

@ -21,5 +21,8 @@
"frequencyPenalty": {
"label": "頻率懲罰",
"description": "係一個 -2.0 到 2.0 之間嘅數值。正嘅數值表示,如果 token 喺之前嘅文字中出現頻率越高,輸出嗰陣就會越大力噉懲罰佢,令到佢被揀中嘅機率降低,即係可以降低模型重複同一句説話嘅機會。(預設: 0)"
}
},
"defaultChatConfig": "預設聊天配置",
"defaultSystemMessage": "預設系統消息",
"resetToDefault": "重置為預設"
}

View file

@ -4,7 +4,7 @@ import { shallow } from 'zustand/shallow';
import useStore from '@store/store';
import ConfigMenu from '@components/ConfigMenu';
import { ChatInterface, ConfigInterface } from '@type/chat';
import { defaultChatConfig } from '@constants/chat';
import { _defaultChatConfig } from '@constants/chat';
const ChatTitle = React.memo(() => {
const { t } = useTranslation('model');
@ -35,7 +35,7 @@ const ChatTitle = React.memo(() => {
const chats = useStore.getState().chats;
if (chats && chats.length > 0 && currentChatIndex !== -1 && !config) {
const updatedChats: ChatInterface[] = JSON.parse(JSON.stringify(chats));
updatedChats[currentChatIndex].config = { ...defaultChatConfig };
updatedChats[currentChatIndex].config = { ..._defaultChatConfig };
setChats(updatedChats);
}
}, [currentChatIndex]);

View file

@ -0,0 +1,168 @@
import React, { useState } from 'react';
import useStore from '@store/store';
import { useTranslation } from 'react-i18next';
import PopupModal from '@components/PopupModal';
import {
FrequencyPenaltySlider,
MaxTokenSlider,
ModelSelector,
PresencePenaltySlider,
TemperatureSlider,
TopPSlider,
} from '@components/ConfigMenu/ConfigMenu';
import { ModelOptions } from '@type/chat';
import { _defaultChatConfig, _defaultSystemMessage } from '@constants/chat';
const ChatConfigMenu = () => {
const { t } = useTranslation('model');
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
return (
<div>
<button className='btn btn-neutral' onClick={() => setIsModalOpen(true)}>
{t('defaultChatConfig')}
</button>
{isModalOpen && <ChatConfigPopup setIsModalOpen={setIsModalOpen} />}
</div>
);
};
const ChatConfigPopup = ({
setIsModalOpen,
}: {
setIsModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const config = useStore.getState().defaultChatConfig;
const setDefaultChatConfig = useStore((state) => state.setDefaultChatConfig);
const setDefaultSystemMessage = useStore(
(state) => state.setDefaultSystemMessage
);
const [_systemMessage, _setSystemMessage] = useState<string>(
useStore.getState().defaultSystemMessage
);
const [_model, _setModel] = useState<ModelOptions>(config.model);
const [_maxToken, _setMaxToken] = useState<number>(config.max_tokens);
const [_temperature, _setTemperature] = useState<number>(config.temperature);
const [_topP, _setTopP] = useState<number>(config.top_p);
const [_presencePenalty, _setPresencePenalty] = useState<number>(
config.presence_penalty
);
const [_frequencyPenalty, _setFrequencyPenalty] = useState<number>(
config.frequency_penalty
);
const { t } = useTranslation('model');
const handleSave = () => {
setDefaultChatConfig({
model: _model,
max_tokens: _maxToken,
temperature: _temperature,
top_p: _topP,
presence_penalty: _presencePenalty,
frequency_penalty: _frequencyPenalty,
});
setDefaultSystemMessage(_systemMessage);
setIsModalOpen(false);
};
const handleReset = () => {
_setModel(_defaultChatConfig.model);
_setMaxToken(_defaultChatConfig.max_tokens);
_setTemperature(_defaultChatConfig.temperature);
_setTopP(_defaultChatConfig.top_p);
_setPresencePenalty(_defaultChatConfig.presence_penalty);
_setFrequencyPenalty(_defaultChatConfig.frequency_penalty);
_setSystemMessage(_defaultSystemMessage);
};
return (
<PopupModal
title={t('defaultChatConfig') as string}
setIsModalOpen={setIsModalOpen}
handleConfirm={handleSave}
>
<div className='p-6 border-b border-gray-200 dark:border-gray-600 w-[90vw] max-w-full text-sm text-gray-900 dark:text-gray-300'>
<DefaultSystemChat
_systemMessage={_systemMessage}
_setSystemMessage={_setSystemMessage}
/>
<ModelSelector _model={_model} _setModel={_setModel} />
<MaxTokenSlider
_maxToken={_maxToken}
_setMaxToken={_setMaxToken}
_model={_model}
/>
<TemperatureSlider
_temperature={_temperature}
_setTemperature={_setTemperature}
/>
<TopPSlider _topP={_topP} _setTopP={_setTopP} />
<PresencePenaltySlider
_presencePenalty={_presencePenalty}
_setPresencePenalty={_setPresencePenalty}
/>
<FrequencyPenaltySlider
_frequencyPenalty={_frequencyPenalty}
_setFrequencyPenalty={_setFrequencyPenalty}
/>
<div
className='btn btn-neutral cursor-pointer mt-5'
onClick={handleReset}
>
{t('resetToDefault')}
</div>
</div>
</PopupModal>
);
};
const DefaultSystemChat = ({
_systemMessage,
_setSystemMessage,
}: {
_systemMessage: string;
_setSystemMessage: React.Dispatch<React.SetStateAction<string>>;
}) => {
const { t } = useTranslation('model');
const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
e.target.style.height = 'auto';
e.target.style.height = `${e.target.scrollHeight}px`;
e.target.style.maxHeight = `${e.target.scrollHeight}px`;
};
const handleOnFocus = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {
e.target.style.height = 'auto';
e.target.style.height = `${e.target.scrollHeight}px`;
e.target.style.maxHeight = `${e.target.scrollHeight}px`;
};
const handleOnBlur = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {
e.target.style.height = 'auto';
e.target.style.maxHeight = '2.5rem';
};
return (
<div>
<div className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('defaultSystemMessage')}
</div>
<textarea
className='my-2 mx-0 px-2 resize-none rounded-lg bg-transparent overflow-y-hidden leading-7 p-1 border border-gray-400/50 focus:ring-1 focus:ring-blue w-full max-h-10 transition-all'
onFocus={handleOnFocus}
onBlur={handleOnBlur}
onChange={(e) => {
_setSystemMessage(e.target.value);
}}
onInput={handleInput}
value={_systemMessage}
rows={1}
></textarea>
</div>
);
};
export default ChatConfigMenu;

View file

@ -0,0 +1 @@
export { default } from './ChatConfigMenu';

View file

@ -52,92 +52,25 @@ const ConfigMenu = ({
_setMaxToken={_setMaxToken}
_model={_model}
/>
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('temperature.label')}: {_temperature}
</label>
<input
id='default-range'
type='range'
value={_temperature}
onChange={(e) => {
_setTemperature(Number(e.target.value));
}}
min={0}
max={2}
step={0.1}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('temperature.description')}
</div>
</div>
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('topP.label')}: {_topP}
</label>
<input
id='default-range'
type='range'
value={_topP}
onChange={(e) => {
_setTopP(Number(e.target.value));
}}
min={0}
max={1}
step={0.05}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('topP.description')}
</div>
</div>
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('presencePenalty.label')}: {_presencePenalty}
</label>
<input
id='default-range'
type='range'
value={_presencePenalty}
onChange={(e) => {
_setPresencePenalty(Number(e.target.value));
}}
min={-2}
max={2}
step={0.1}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('presencePenalty.description')}
</div>
</div>
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('frequencyPenalty.label')}: {_frequencyPenalty}
</label>
<input
id='default-range'
type='range'
value={_frequencyPenalty}
onChange={(e) => {
_setFrequencyPenalty(Number(e.target.value));
}}
min={-2}
max={2}
step={0.1}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('frequencyPenalty.description')}
</div>
</div>
<TemperatureSlider
_temperature={_temperature}
_setTemperature={_setTemperature}
/>
<TopPSlider _topP={_topP} _setTopP={_setTopP} />
<PresencePenaltySlider
_presencePenalty={_presencePenalty}
_setPresencePenalty={_setPresencePenalty}
/>
<FrequencyPenaltySlider
_frequencyPenalty={_frequencyPenalty}
_setFrequencyPenalty={_setFrequencyPenalty}
/>
</div>
</PopupModal>
);
};
const ModelSelector = ({
export const ModelSelector = ({
_model,
_setModel,
}: {
@ -184,7 +117,7 @@ const ModelSelector = ({
);
};
const MaxTokenSlider = ({
export const MaxTokenSlider = ({
_maxToken,
_setMaxToken,
_model,
@ -226,4 +159,136 @@ const MaxTokenSlider = ({
);
};
export const TemperatureSlider = ({
_temperature,
_setTemperature,
}: {
_temperature: number;
_setTemperature: React.Dispatch<React.SetStateAction<number>>;
}) => {
const { t } = useTranslation('model');
return (
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('temperature.label')}: {_temperature}
</label>
<input
id='default-range'
type='range'
value={_temperature}
onChange={(e) => {
_setTemperature(Number(e.target.value));
}}
min={0}
max={2}
step={0.1}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('temperature.description')}
</div>
</div>
);
};
export const TopPSlider = ({
_topP,
_setTopP,
}: {
_topP: number;
_setTopP: React.Dispatch<React.SetStateAction<number>>;
}) => {
const { t } = useTranslation('model');
return (
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('topP.label')}: {_topP}
</label>
<input
id='default-range'
type='range'
value={_topP}
onChange={(e) => {
_setTopP(Number(e.target.value));
}}
min={0}
max={1}
step={0.05}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('topP.description')}
</div>
</div>
);
};
export const PresencePenaltySlider = ({
_presencePenalty,
_setPresencePenalty,
}: {
_presencePenalty: number;
_setPresencePenalty: React.Dispatch<React.SetStateAction<number>>;
}) => {
const { t } = useTranslation('model');
return (
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('presencePenalty.label')}: {_presencePenalty}
</label>
<input
id='default-range'
type='range'
value={_presencePenalty}
onChange={(e) => {
_setPresencePenalty(Number(e.target.value));
}}
min={-2}
max={2}
step={0.1}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('presencePenalty.description')}
</div>
</div>
);
};
export const FrequencyPenaltySlider = ({
_frequencyPenalty,
_setFrequencyPenalty,
}: {
_frequencyPenalty: number;
_setFrequencyPenalty: React.Dispatch<React.SetStateAction<number>>;
}) => {
const { t } = useTranslation('model');
return (
<div className='mt-5 pt-5 border-t border-gray-500'>
<label className='block text-sm font-medium text-gray-900 dark:text-white'>
{t('frequencyPenalty.label')}: {_frequencyPenalty}
</label>
<input
id='default-range'
type='range'
value={_frequencyPenalty}
onChange={(e) => {
_setFrequencyPenalty(Number(e.target.value));
}}
min={-2}
max={2}
step={0.1}
className='w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer'
/>
<div className='min-w-fit text-gray-500 dark:text-gray-300 text-sm mt-2'>
{t('frequencyPenalty.description')}
</div>
</div>
);
};
export default ConfigMenu;

View file

@ -8,6 +8,7 @@ import ThemeSwitcher from '@components/Menu/MenuOptions/ThemeSwitcher';
import LanguageSelector from '@components/LanguageSelector';
import AutoTitleToggle from './AutoTitleToggle';
import PromptLibraryMenu from '@components/PromptLibraryMenu';
import ChatConfigMenu from '@components/ChatConfigMenu';
const SettingsMenu = () => {
const { t } = useTranslation();
@ -39,6 +40,7 @@ const SettingsMenu = () => {
<ThemeSwitcher />
<AutoTitleToggle />
<PromptLibraryMenu />
<ChatConfigMenu />
</div>
</PopupModal>
)}

View file

@ -1,4 +1,5 @@
import { ChatInterface, ConfigInterface, ModelOptions } from '@type/chat';
import useStore from '@store/store';
const date = new Date();
const dateString =
@ -9,7 +10,7 @@ const dateString =
('0' + date.getDate()).slice(-2);
// default system message obtained using the following method: https://twitter.com/DeminDimin/status/1619935545144279040
export const defaultSystemMessage = `You are ChatGPT, a large language model trained by OpenAI.
export const _defaultSystemMessage = `You are ChatGPT, a large language model trained by OpenAI.
Knowledge cutoff: 2021-09
Current date: ${dateString}`;
@ -35,7 +36,7 @@ export const modelMaxToken = {
export const defaultUserMaxToken = 4000;
export const defaultChatConfig: ConfigInterface = {
export const _defaultChatConfig: ConfigInterface = {
model: defaultModel,
max_tokens: defaultUserMaxToken,
temperature: 1,
@ -46,8 +47,10 @@ export const defaultChatConfig: ConfigInterface = {
export const generateDefaultChat = (title?: string): ChatInterface => ({
title: title ? title : 'New Chat',
messages: [{ role: 'system', content: defaultSystemMessage }],
config: { ...defaultChatConfig },
messages: [
{ role: 'system', content: useStore.getState().defaultSystemMessage },
],
config: { ...useStore.getState().defaultChatConfig },
titleSet: false,
});

View file

@ -4,7 +4,7 @@ import { ChatInterface, MessageInterface } from '@type/chat';
import { getChatCompletion, getChatCompletionStream } from '@api/api';
import { parseEventSource } from '@api/helper';
import { limitMessageTokens } from '@utils/messageUtils';
import { defaultChatConfig } from '@constants/chat';
import { _defaultChatConfig } from '@constants/chat';
const useSubmit = () => {
const error = useStore((state) => state.error);
@ -24,13 +24,13 @@ const useSubmit = () => {
data = await getChatCompletion(
useStore.getState().apiEndpoint,
message,
defaultChatConfig
_defaultChatConfig
);
} else if (apiKey) {
data = await getChatCompletion(
useStore.getState().apiEndpoint,
message,
defaultChatConfig,
_defaultChatConfig,
apiKey
);
}

View file

@ -1,19 +1,27 @@
import { StoreSlice } from './store';
import { Theme } from '@type/theme';
import { ConfigInterface } from '@type/chat';
import { _defaultChatConfig, _defaultSystemMessage } from '@constants/chat';
export interface ConfigSlice {
openConfig: boolean;
theme: Theme;
autoTitle: boolean;
defaultChatConfig: ConfigInterface;
defaultSystemMessage: string;
setOpenConfig: (openConfig: boolean) => void;
setTheme: (theme: Theme) => void;
setAutoTitle: (autoTitle: boolean) => void;
setDefaultChatConfig: (defaultChatConfig: ConfigInterface) => void;
setDefaultSystemMessage: (defaultSystemMessage: string) => void;
}
export const createConfigSlice: StoreSlice<ConfigSlice> = (set, get) => ({
openConfig: false,
theme: 'dark',
autoTitle: false,
defaultChatConfig: _defaultChatConfig,
defaultSystemMessage: _defaultSystemMessage,
setOpenConfig: (openConfig: boolean) => {
set((prev: ConfigSlice) => ({
...prev,
@ -32,4 +40,16 @@ export const createConfigSlice: StoreSlice<ConfigSlice> = (set, get) => ({
autoTitle: autoTitle,
}));
},
setDefaultChatConfig: (defaultChatConfig: ConfigInterface) => {
set((prev: ConfigSlice) => ({
...prev,
defaultChatConfig: defaultChatConfig,
}));
},
setDefaultSystemMessage: (defaultSystemMessage: string) => {
set((prev: ConfigSlice) => ({
...prev,
defaultSystemMessage: defaultSystemMessage,
}));
},
});

View file

@ -7,7 +7,7 @@ import {
LocalStorageInterfaceV5ToV6,
} from '@type/chat';
import {
defaultChatConfig,
_defaultChatConfig,
defaultModel,
defaultUserMaxToken,
} from '@constants/chat';
@ -17,7 +17,7 @@ import defaultPrompts from '@constants/prompt';
export const migrateV0 = (persistedState: LocalStorageInterfaceV0ToV1) => {
persistedState.chats.forEach((chat) => {
chat.titleSet = false;
if (!chat.config) chat.config = { ...defaultChatConfig };
if (!chat.config) chat.config = { ..._defaultChatConfig };
});
};
@ -33,8 +33,8 @@ export const migrateV2 = (persistedState: LocalStorageInterfaceV2ToV3) => {
persistedState.chats.forEach((chat) => {
chat.config = {
...chat.config,
top_p: defaultChatConfig.top_p,
frequency_penalty: defaultChatConfig.frequency_penalty,
top_p: _defaultChatConfig.top_p,
frequency_penalty: _defaultChatConfig.frequency_penalty,
};
});
persistedState.autoTitle = false;

View file

@ -53,6 +53,8 @@ const useStore = create<StoreState>()(
theme: state.theme,
autoTitle: state.autoTitle,
prompts: state.prompts,
defaultChatConfig: state.defaultChatConfig,
defaultSystemMessage: state.defaultSystemMessage,
}),
version: 6,
migrate: (persistedState, version) => {

View file

@ -3,7 +3,7 @@ import jsPDF from 'jspdf';
import { ChatInterface, ConfigInterface, MessageInterface } from '@type/chat';
import { roles } from '@type/chat';
import { Theme } from '@type/theme';
import { defaultChatConfig } from '@constants/chat';
import { _defaultChatConfig } from '@constants/chat';
export const validateAndFixChats = (chats: any): chats is ChatInterface[] => {
if (!Array.isArray(chats)) return false;
@ -32,21 +32,21 @@ const validateMessage = (messages: MessageInterface[]) => {
};
const validateAndFixChatConfig = (config: ConfigInterface) => {
if (config === undefined) config = defaultChatConfig;
if (config === undefined) config = _defaultChatConfig;
if (!(typeof config === 'object')) return false;
if (!config.temperature) config.temperature = defaultChatConfig.temperature;
if (!config.temperature) config.temperature = _defaultChatConfig.temperature;
if (!(typeof config.temperature === 'number')) return false;
if (!config.presence_penalty)
config.presence_penalty = defaultChatConfig.presence_penalty;
config.presence_penalty = _defaultChatConfig.presence_penalty;
if (!(typeof config.presence_penalty === 'number')) return false;
if (!config.top_p) config.top_p = defaultChatConfig.top_p;
if (!config.top_p) config.top_p = _defaultChatConfig.top_p;
if (!(typeof config.top_p === 'number')) return false;
if (!config.frequency_penalty)
config.frequency_penalty = defaultChatConfig.frequency_penalty;
config.frequency_penalty = _defaultChatConfig.frequency_penalty;
if (!(typeof config.frequency_penalty === 'number')) return false;
return true;