Browse Source

feat: add model config to settings

Yifei Zhang 2 years ago
parent
commit
2f112ecc54
7 changed files with 516 additions and 257 deletions
  1. 13 8
      app/components/home.module.scss
  2. 7 1
      app/components/settings.module.scss
  3. 165 68
      app/components/settings.tsx
  4. 82 61
      app/locales/cn.ts
  5. 87 61
      app/locales/en.ts
  6. 10 2
      app/requests.ts
  7. 152 56
      app/store.ts

+ 13 - 8
app/components/home.module.scss

@@ -50,6 +50,8 @@
 .window-content {
   width: var(--window-content-width);
   height: 100%;
+  display: flex;
+  flex-direction: column;
 }
 
 .mobile {
@@ -111,7 +113,8 @@
   overflow: auto;
 }
 
-.chat-list {}
+.chat-list {
+}
 
 .chat-item {
   padding: 10px 14px;
@@ -165,12 +168,12 @@
   opacity: 0;
 }
 
-.chat-item:hover>.chat-item-delete {
+.chat-item:hover > .chat-item-delete {
   opacity: 0.5;
   right: 10px;
 }
 
-.chat-item:hover>.chat-item-delete:hover {
+.chat-item:hover > .chat-item-delete:hover {
   opacity: 1;
 }
 
@@ -182,9 +185,11 @@
   margin-top: 8px;
 }
 
-.chat-item-count {}
+.chat-item-count {
+}
 
-.chat-item-date {}
+.chat-item-date {
+}
 
 .sidebar-tail {
   display: flex;
@@ -232,7 +237,7 @@
   animation: slide-in ease 0.3s;
 }
 
-.chat-message-user>.chat-message-container {
+.chat-message-user > .chat-message-container {
   align-items: flex-end;
 }
 
@@ -271,7 +276,7 @@
   border: var(--border-in-light);
 }
 
-.chat-message-user>.chat-message-container>.chat-message-item {
+.chat-message-user > .chat-message-container > .chat-message-item {
   background-color: var(--second);
 }
 
@@ -346,4 +351,4 @@
   align-items: center;
   height: 100%;
   width: 100%;
-}
+}

+ 7 - 1
app/components/settings.module.scss

@@ -2,6 +2,7 @@
 
 .settings {
   padding: 20px;
+  overflow: auto;
 }
 
 .settings-title {
@@ -9,6 +10,11 @@
   font-weight: bolder;
 }
 
+.settings-sub-title {
+  font-size: 12px;
+  font-weight: normal;
+}
+
 .avatar {
   cursor: pointer;
-}
+}

+ 165 - 68
app/components/settings.tsx

@@ -1,4 +1,4 @@
-import { useState, useRef, useEffect } from "react";
+import { useState } from "react";
 
 import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
 
@@ -11,26 +11,50 @@ import ClearIcon from "../icons/clear.svg";
 import { List, ListItem, Popover } from "./ui-lib";
 
 import { IconButton } from "./button";
-import { SubmitKey, useChatStore, Theme } from "../store";
+import { SubmitKey, useChatStore, Theme, ALL_MODELS } from "../store";
 import { Avatar } from "./home";
 
-import Locale, { changeLang, getLang } from '../locales'
+import Locale, { changeLang, getLang } from "../locales";
+
+function SettingItem(props: {
+  title: string;
+  subTitle?: string;
+  children: JSX.Element;
+}) {
+  return (
+    <ListItem>
+      <div className={styles["settings-title"]}>
+        <div>{props.title}</div>
+        {props.subTitle && (
+          <div className={styles["settings-sub-title"]}>{props.subTitle}</div>
+        )}
+      </div>
+      <div>{props.children}</div>
+    </ListItem>
+  );
+}
 
 export function Settings(props: { closeSettings: () => void }) {
   const [showEmojiPicker, setShowEmojiPicker] = useState(false);
-  const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [
-    state.config,
-    state.updateConfig,
-    state.resetConfig,
-    state.clearAllData,
-  ]);
+  const [config, updateConfig, resetConfig, clearAllData] = useChatStore(
+    (state) => [
+      state.config,
+      state.updateConfig,
+      state.resetConfig,
+      state.clearAllData,
+    ]
+  );
 
   return (
     <>
       <div className={styles["window-header"]}>
         <div className={styles["window-header-title"]}>
-          <div className={styles["window-header-main-title"]}>{Locale.Settings.Title}</div>
-          <div className={styles["window-header-sub-title"]}>{Locale.Settings.SubTitle}</div>
+          <div className={styles["window-header-main-title"]}>
+            {Locale.Settings.Title}
+          </div>
+          <div className={styles["window-header-sub-title"]}>
+            {Locale.Settings.SubTitle}
+          </div>
         </div>
         <div className={styles["window-actions"]}>
           <div className={styles["window-action-button"]}>
@@ -61,8 +85,7 @@ export function Settings(props: { closeSettings: () => void }) {
       </div>
       <div className={styles["settings"]}>
         <List>
-          <ListItem>
-            <div className={styles["settings-title"]}>{Locale.Settings.Avatar}</div>
+          <SettingItem title={Locale.Settings.Avatar}>
             <Popover
               onClose={() => setShowEmojiPicker(false)}
               content={
@@ -84,51 +107,47 @@ export function Settings(props: { closeSettings: () => void }) {
                 <Avatar role="user" />
               </div>
             </Popover>
-          </ListItem>
+          </SettingItem>
 
-          <ListItem>
-            <div className={styles["settings-title"]}>{Locale.Settings.SendKey}</div>
-            <div className="">
-              <select
-                value={config.submitKey}
-                onChange={(e) => {
-                  updateConfig(
-                    (config) =>
-                      (config.submitKey = e.target.value as any as SubmitKey)
-                  );
-                }}
-              >
-                {Object.values(SubmitKey).map((v) => (
-                  <option value={v} key={v}>
-                    {v}
-                  </option>
-                ))}
-              </select>
-            </div>
-          </ListItem>
+          <SettingItem title={Locale.Settings.SendKey}>
+            <select
+              value={config.submitKey}
+              onChange={(e) => {
+                updateConfig(
+                  (config) =>
+                    (config.submitKey = e.target.value as any as SubmitKey)
+                );
+              }}
+            >
+              {Object.values(SubmitKey).map((v) => (
+                <option value={v} key={v}>
+                  {v}
+                </option>
+              ))}
+            </select>
+          </SettingItem>
 
           <ListItem>
-            <div className={styles["settings-title"]}>{Locale.Settings.Theme}</div>
-            <div className="">
-              <select
-                value={config.theme}
-                onChange={(e) => {
-                  updateConfig(
-                    (config) => (config.theme = e.target.value as any as Theme)
-                  );
-                }}
-              >
-                {Object.values(Theme).map((v) => (
-                  <option value={v} key={v}>
-                    {v}
-                  </option>
-                ))}
-              </select>
+            <div className={styles["settings-title"]}>
+              {Locale.Settings.Theme}
             </div>
+            <select
+              value={config.theme}
+              onChange={(e) => {
+                updateConfig(
+                  (config) => (config.theme = e.target.value as any as Theme)
+                );
+              }}
+            >
+              {Object.values(Theme).map((v) => (
+                <option value={v} key={v}>
+                  {v}
+                </option>
+              ))}
+            </select>
           </ListItem>
 
-          <ListItem>
-            <div className={styles["settings-title"]}>{Locale.Settings.TightBorder}</div>
+          <SettingItem title={Locale.Settings.TightBorder}>
             <input
               type="checkbox"
               checked={config.tightBorder}
@@ -138,31 +157,32 @@ export function Settings(props: { closeSettings: () => void }) {
                 )
               }
             ></input>
-          </ListItem>
+          </SettingItem>
 
-          <ListItem>
-            <div className={styles["settings-title"]}>{Locale.Settings.Lang.Name}</div>
+          <SettingItem title={Locale.Settings.Lang.Name}>
             <div className="">
               <select
                 value={getLang()}
                 onChange={(e) => {
-                  changeLang(e.target.value as any)
+                  changeLang(e.target.value as any);
                 }}
               >
-                <option value='en' key='en'>
+                <option value="en" key="en">
                   {Locale.Settings.Lang.Options.en}
                 </option>
 
-                <option value='cn' key='cn'>
+                <option value="cn" key="cn">
                   {Locale.Settings.Lang.Options.cn}
                 </option>
               </select>
             </div>
-          </ListItem>
+          </SettingItem>
         </List>
         <List>
-          <ListItem>
-            <div className={styles["settings-title"]}>{Locale.Settings.HistoryCount}</div>
+          <SettingItem
+            title={Locale.Settings.HistoryCount.Title}
+            subTitle={Locale.Settings.HistoryCount.SubTitle}
+          >
             <input
               type="range"
               title={config.historyMessageCount.toString()}
@@ -177,12 +197,12 @@ export function Settings(props: { closeSettings: () => void }) {
                 )
               }
             ></input>
-          </ListItem>
+          </SettingItem>
 
-          <ListItem>
-            <div className={styles["settings-title"]}>
-              {Locale.Settings.CompressThreshold}
-            </div>
+          <SettingItem
+            title={Locale.Settings.CompressThreshold.Title}
+            subTitle={Locale.Settings.CompressThreshold.SubTitle}
+          >
             <input
               type="number"
               min={500}
@@ -190,11 +210,88 @@ export function Settings(props: { closeSettings: () => void }) {
               value={config.compressMessageLengthThreshold}
               onChange={(e) =>
                 updateConfig(
-                  (config) => (config.compressMessageLengthThreshold = e.currentTarget.valueAsNumber)
+                  (config) =>
+                    (config.compressMessageLengthThreshold =
+                      e.currentTarget.valueAsNumber)
                 )
               }
             ></input>
-          </ListItem>
+          </SettingItem>
+        </List>
+
+        <List>
+          <SettingItem title={Locale.Settings.Model}>
+            <select
+              value={config.modelConfig.model}
+              onChange={(e) => {
+                updateConfig(
+                  (config) => (config.modelConfig.model = e.currentTarget.value)
+                );
+              }}
+            >
+              {ALL_MODELS.map((v) => (
+                <option value={v.name} key={v.name} disabled={!v.available}>
+                  {v.name}
+                </option>
+              ))}
+            </select>
+          </SettingItem>
+          <SettingItem
+            title={Locale.Settings.Temperature.Title}
+            subTitle={Locale.Settings.Temperature.SubTitle}
+          >
+            <input
+              type="range"
+              value={config.modelConfig.temperature.toFixed(1)}
+              min="0"
+              max="1"
+              step="0.1"
+              onChange={(e) => {
+                updateConfig(
+                  (config) =>
+                    (config.modelConfig.temperature =
+                      e.currentTarget.valueAsNumber)
+                );
+              }}
+            ></input>
+          </SettingItem>
+          <SettingItem
+            title={Locale.Settings.MaxTokens.Title}
+            subTitle={Locale.Settings.MaxTokens.SubTitle}
+          >
+            <input
+              type="number"
+              min={100}
+              max={4000}
+              value={config.modelConfig.max_tokens}
+              onChange={(e) =>
+                updateConfig(
+                  (config) =>
+                    (config.modelConfig.max_tokens =
+                      e.currentTarget.valueAsNumber)
+                )
+              }
+            ></input>
+          </SettingItem>
+          <SettingItem
+            title={Locale.Settings.PresencePenlty.Title}
+            subTitle={Locale.Settings.PresencePenlty.SubTitle}
+          >
+            <input
+              type="range"
+              value={config.modelConfig.presence_penalty.toFixed(1)}
+              min="-2"
+              max="2"
+              step="0.5"
+              onChange={(e) => {
+                updateConfig(
+                  (config) =>
+                    (config.modelConfig.presence_penalty =
+                      e.currentTarget.valueAsNumber)
+                );
+              }}
+            ></input>
+          </SettingItem>
         </List>
       </div>
     </>

+ 82 - 61
app/locales/cn.ts

@@ -1,72 +1,93 @@
-
 const cn = {
-    ChatItem: {
-        ChatItemCount: (count: number) => `${count} 条对话`,
+  ChatItem: {
+    ChatItemCount: (count: number) => `${count} 条对话`,
+  },
+  Chat: {
+    SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`,
+    Actions: {
+      ChatList: "查看消息列表",
+      CompressedHistory: "查看压缩后的历史 Prompt",
+      Export: "导出聊天记录",
+    },
+    Typing: "正在输入…",
+    Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
+    Send: "发送",
+  },
+  Export: {
+    Title: "导出聊天记录为 Markdown",
+    Copy: "全部复制",
+    Download: "下载文件",
+  },
+  Memory: {
+    Title: "上下文记忆 Prompt",
+    EmptyContent: "尚未记忆",
+    Copy: "全部复制",
+  },
+  Home: {
+    NewChat: "新的聊天",
+    DeleteChat: "确认删除选中的对话?",
+  },
+  Settings: {
+    Title: "设置",
+    SubTitle: "设置选项",
+    Actions: {
+      ClearAll: "清除所有数据",
+      ResetAll: "重置所有选项",
+      Close: "关闭",
+    },
+    Lang: {
+      Name: "Language",
+      Options: {
+        cn: "中文",
+        en: "English",
+      },
     },
-    Chat: {
-        SubTitle: (count: number) => `与 ChatGPT 的 ${count} 条对话`,
-        Actions: {
-            ChatList: '查看消息列表',
-            CompressedHistory: '查看压缩后的历史 Prompt',
-            Export: '导出聊天记录',
-        },
-        Typing: '正在输入…',
-        Input: (submitKey: string) => `输入消息,${submitKey} 发送`,
-        Send: '发送',
+    Avatar: "头像",
+    SendKey: "发送键",
+    Theme: "主题",
+    TightBorder: "紧凑边框",
+    HistoryCount: {
+      Title: "附带历史消息数",
+      SubTitle: "每次请求携带的历史消息数",
     },
-    Export: {
-        Title: '导出聊天记录为 Markdown',
-        Copy: '全部复制',
-        Download: '下载文件',
+    CompressThreshold: {
+      Title: "历史消息长度压缩阈值",
+      SubTitle: "当未压缩的历史消息超过该值时,将进行压缩",
     },
-    Memory: {
-        Title: '上下文记忆 Prompt',
-        EmptyContent: '尚未记忆',
-        Copy: '全部复制',
+    Model: "模型 (model)",
+    Temperature: {
+      Title: "随机性 (temperature)",
+      SubTitle: "值越大,回复越随机",
     },
-    Home: {
-        NewChat: '新的聊天',
-        DeleteChat: '确认删除选中的对话?',
+    MaxTokens: {
+      Title: "单次回复限制 (max_tokens)",
+      SubTitle: "单次交互所用的最大 Token 数",
     },
-    Settings: {
-        Title: '设置',
-        SubTitle: '设置选项',
-        Actions: {
-            ClearAll: '清除所有数据',
-            ResetAll: '重置所有选项',
-            Close: '关闭',
-        },
-        Lang: {
-            Name: 'Language',
-            Options: {
-                cn: '中文',
-                en: 'English'
-            }
-        },
-        Avatar: '头像',
-        SendKey: '发送键',
-        Theme: '主题',
-        TightBorder: '紧凑边框',
-        HistoryCount: '附带历史消息数',
-        CompressThreshold: '历史消息长度压缩阈值',
+    PresencePenlty: {
+      Title: "话题新鲜度 (presence_penalty)",
+      SubTitle: "值越大,越有可能扩展到新话题",
     },
-    Store: {
-        DefaultTopic: '新的聊天',
-        BotHello: '有什么可以帮你的吗',
-        Error: '出错了,稍后重试吧',
-        Prompt: {
-            History: (content: string) => '这是 ai 和用户的历史聊天总结作为前情提要:' + content,
-            Topic: "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
-            Summarize: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内',
-        },
-        ConfirmClearAll: '确认清除所有聊天、设置数据?',
+  },
+  Store: {
+    DefaultTopic: "新的聊天",
+    BotHello: "有什么可以帮你的吗",
+    Error: "出错了,稍后重试吧",
+    Prompt: {
+      History: (content: string) =>
+        "这是 ai 和用户的历史聊天总结作为前情提要:" + content,
+      Topic:
+        "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”",
+      Summarize:
+        "简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内",
     },
-    Copy: {
-        Success: '已写入剪切板',
-        Failed: '复制失败,请赋予剪切板权限',
-    }
-}
+    ConfirmClearAll: "确认清除所有聊天、设置数据?",
+  },
+  Copy: {
+    Success: "已写入剪切板",
+    Failed: "复制失败,请赋予剪切板权限",
+  },
+};
 
 export type LocaleType = typeof cn;
 
-export default cn;
+export default cn;

+ 87 - 61
app/locales/en.ts

@@ -1,71 +1,97 @@
-import type { LocaleType } from './index'
+import type { LocaleType } from "./index";
 
 const en: LocaleType = {
-    ChatItem: {
-        ChatItemCount: (count: number) => `${count} messages`,
+  ChatItem: {
+    ChatItemCount: (count: number) => `${count} messages`,
+  },
+  Chat: {
+    SubTitle: (count: number) => `${count} messages with ChatGPT`,
+    Actions: {
+      ChatList: "Go To Chat List",
+      CompressedHistory: "Compressed History Memory Prompt",
+      Export: "Export All Messages as Markdown",
     },
-    Chat: {
-        SubTitle: (count: number) => `${count} messages with ChatGPT`,
-        Actions: {
-            ChatList: 'Go To Chat List',
-            CompressedHistory: 'Compressed History Memory Prompt',
-            Export: 'Export All Messages as Markdown',
-        },
-        Typing: 'Typing…',
-        Input: (submitKey: string) => `Type something and press ${submitKey} to send`,
-        Send: 'Send',
+    Typing: "Typing…",
+    Input: (submitKey: string) =>
+      `Type something and press ${submitKey} to send`,
+    Send: "Send",
+  },
+  Export: {
+    Title: "All Messages",
+    Copy: "Copy All",
+    Download: "Download",
+  },
+  Memory: {
+    Title: "Memory Prompt",
+    EmptyContent: "Nothing yet.",
+    Copy: "Copy All",
+  },
+  Home: {
+    NewChat: "New Chat",
+    DeleteChat: "Confirm to delete the selected conversation?",
+  },
+  Settings: {
+    Title: "Settings",
+    SubTitle: "All Settings",
+    Actions: {
+      ClearAll: "Clear All Data",
+      ResetAll: "Reset All Settings",
+      Close: "Close",
     },
-    Export: {
-        Title: 'All Messages',
-        Copy: 'Copy All',
-        Download: 'Download',
+    Lang: {
+      Name: "语言",
+      Options: {
+        cn: "中文",
+        en: "English",
+      },
     },
-    Memory: {
-        Title: 'Memory Prompt',
-        EmptyContent: 'Nothing yet.',
-        Copy: 'Copy All',
+    Avatar: "Avatar",
+    SendKey: "Send Key",
+    Theme: "Theme",
+    TightBorder: "Tight Border",
+    HistoryCount: {
+      Title: "Attached Messages Count",
+      SubTitle: "Number of sent messages attached per request",
     },
-    Home: {
-        NewChat: 'New Chat',
-        DeleteChat: 'Confirm to delete the selected conversation?',
+    CompressThreshold: {
+      Title: "History Compression Threshold",
+      SubTitle:
+        "Will compress if uncompressed messages length exceeds the value",
     },
-    Settings: {
-        Title: 'Settings',
-        SubTitle: 'All Settings',
-        Actions: {
-            ClearAll: 'Clear All Data',
-            ResetAll: 'Reset All Settings',
-            Close: 'Close',
-        },
-        Lang: {
-            Name: '语言',
-            Options: {
-                cn: '中文',
-                en: 'English'
-            }
-        },
-        Avatar: 'Avatar',
-        SendKey: 'Send Key',
-        Theme: 'Theme',
-        TightBorder: 'Tight Border',
-        HistoryCount: 'History Message Count',
-        CompressThreshold: 'Message Compression Threshold',
+    Model: "Model",
+    Temperature: {
+      Title: "Temperature",
+      SubTitle: "A larger value makes the more random output",
     },
-    Store: {
-        DefaultTopic: 'New Conversation',
-        BotHello: 'Hello! How can I assist you today?',
-        Error: 'Something went wrong, please try again later.',
-        Prompt: {
-            History: (content: string) => 'This is a summary of the chat history between the AI and the user as a recap: ' + content,
-            Topic: "Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.",
-            Summarize: 'Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.',
-        },
-        ConfirmClearAll: 'Confirm to clear all chat and setting data?',
+    MaxTokens: {
+      Title: "Max Tokens",
+      SubTitle: "Maximum length of input tokens and generated tokens",
     },
-    Copy: {
-        Success: 'Copied to clipboard',
-        Failed: 'Copy failed, please grant permission to access clipboard',
-    }
-}
+    PresencePenlty: {
+      Title: "Presence Penalty",
+      SubTitle:
+        "A larger value increases the likelihood to talk about new topics",
+    },
+  },
+  Store: {
+    DefaultTopic: "New Conversation",
+    BotHello: "Hello! How can I assist you today?",
+    Error: "Something went wrong, please try again later.",
+    Prompt: {
+      History: (content: string) =>
+        "This is a summary of the chat history between the AI and the user as a recap: " +
+        content,
+      Topic:
+        "Provide a brief topic of the sentence without explanation. If there is no topic, return 'Chitchat'.",
+      Summarize:
+        "Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.",
+    },
+    ConfirmClearAll: "Confirm to clear all chat and setting data?",
+  },
+  Copy: {
+    Success: "Copied to clipboard",
+    Failed: "Copy failed, please grant permission to access clipboard",
+  },
+};
 
-export default en;
+export default en;

+ 10 - 2
app/requests.ts

@@ -1,7 +1,7 @@
 import type { ChatRequest, ChatReponse } from "./api/chat/typing";
-import { Message } from "./store";
+import { filterConfig, isValidModel, Message, ModelConfig } from "./store";
 
-const TIME_OUT_MS = 30000
+const TIME_OUT_MS = 30000;
 
 const makeRequestParam = (
   messages: Message[],
@@ -44,6 +44,7 @@ export async function requestChatStream(
   messages: Message[],
   options?: {
     filterBot?: boolean;
+    modelConfig?: ModelConfig;
     onMessage: (message: string, done: boolean) => void;
     onError: (error: Error) => void;
   }
@@ -53,6 +54,13 @@ export async function requestChatStream(
     filterBot: options?.filterBot,
   });
 
+  // valid and assign model config
+  if (options?.modelConfig) {
+    Object.assign(req, filterConfig(options.modelConfig));
+  }
+
+  console.log("[Request] ", req);
+
   const controller = new AbortController();
   const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS);
 

+ 152 - 56
app/store.ts

@@ -5,7 +5,7 @@ import { type ChatCompletionResponseMessage } from "openai";
 import { requestChatStream, requestWithPrompt } from "./requests";
 import { trimTopic } from "./utils";
 
-import Locale from './locales'
+import Locale from "./locales";
 
 export type Message = ChatCompletionResponseMessage & {
   date: string;
@@ -26,7 +26,7 @@ export enum Theme {
 }
 
 export interface ChatConfig {
-  maxToken?: number
+  maxToken?: number;
   historyMessageCount: number; // -1 means all
   compressMessageLengthThreshold: number;
   sendBotMessages: boolean; // send bot's message or not
@@ -34,6 +34,78 @@ export interface ChatConfig {
   avatar: string;
   theme: Theme;
   tightBorder: boolean;
+
+  modelConfig: {
+    model: string;
+    temperature: number;
+    max_tokens: number;
+    presence_penalty: number;
+  };
+}
+
+export type ModelConfig = ChatConfig["modelConfig"];
+
+export const ALL_MODELS = [
+  {
+    name: "gpt-4",
+    available: false,
+  },
+  {
+    name: "gpt-4-0314",
+    available: false,
+  },
+  {
+    name: "gpt-4-32k",
+    available: false,
+  },
+  {
+    name: "gpt-4-32k-0314",
+    available: false,
+  },
+  {
+    name: "gpt-3.5-turbo",
+    available: true,
+  },
+  {
+    name: "gpt-3.5-turbo-0301",
+    available: true,
+  },
+];
+
+export function isValidModel(name: string) {
+  return ALL_MODELS.some((m) => m.name === name && m.available);
+}
+
+export function isValidNumber(x: number, min: number, max: number) {
+  return typeof x === "number" && x <= max && x >= min;
+}
+
+export function filterConfig(config: ModelConfig): Partial<ModelConfig> {
+  const validator: {
+    [k in keyof ModelConfig]: (x: ModelConfig[keyof ModelConfig]) => boolean;
+  } = {
+    model(x) {
+      return isValidModel(x as string);
+    },
+    max_tokens(x) {
+      return isValidNumber(x as number, 100, 4000);
+    },
+    presence_penalty(x) {
+      return isValidNumber(x as number, -2, 2);
+    },
+    temperature(x) {
+      return isValidNumber(x as number, 0, 1);
+    },
+  };
+
+  Object.keys(validator).forEach((k) => {
+    const key = k as keyof ModelConfig;
+    if (!validator[key](config[key])) {
+      delete config[key];
+    }
+  });
+
+  return config;
 }
 
 const DEFAULT_CONFIG: ChatConfig = {
@@ -44,6 +116,13 @@ const DEFAULT_CONFIG: ChatConfig = {
   avatar: "1f603",
   theme: Theme.Auto as Theme,
   tightBorder: false,
+
+  modelConfig: {
+    model: "gpt-3.5-turbo",
+    temperature: 1,
+    max_tokens: 2000,
+    presence_penalty: 0,
+  },
 };
 
 export interface ChatStat {
@@ -107,7 +186,7 @@ interface ChatStore {
     updater: (message?: Message) => void
   ) => void;
   getMessagesWithMemory: () => Message[];
-  getMemoryPrompt: () => Message,
+  getMemoryPrompt: () => Message;
 
   getConfig: () => ChatConfig;
   resetConfig: () => void;
@@ -193,9 +272,9 @@ export const useChatStore = create<ChatStore>()(
       },
 
       onNewMessage(message) {
-        get().updateCurrentSession(session => {
-          session.lastUpdate = new Date().toLocaleString()
-        })
+        get().updateCurrentSession((session) => {
+          session.lastUpdate = new Date().toLocaleString();
+        });
         get().updateStat(message);
         get().summarizeSession();
       },
@@ -214,9 +293,9 @@ export const useChatStore = create<ChatStore>()(
           streaming: true,
         };
 
-        // get recent messages 
-        const recentMessages = get().getMessagesWithMemory()
-        const sendMessages = recentMessages.concat(userMessage)
+        // get recent messages
+        const recentMessages = get().getMessagesWithMemory();
+        const sendMessages = recentMessages.concat(userMessage);
 
         // save user's and bot's message
         get().updateCurrentSession((session) => {
@@ -224,12 +303,12 @@ export const useChatStore = create<ChatStore>()(
           session.messages.push(botMessage);
         });
 
-        console.log('[User Input] ', sendMessages)
+        console.log("[User Input] ", sendMessages);
         requestChatStream(sendMessages, {
           onMessage(content, done) {
             if (done) {
               botMessage.streaming = false;
-              get().onNewMessage(botMessage)
+              get().onNewMessage(botMessage);
             } else {
               botMessage.content = content;
               set(() => ({}));
@@ -241,32 +320,35 @@ export const useChatStore = create<ChatStore>()(
             set(() => ({}));
           },
           filterBot: !get().config.sendBotMessages,
+          modelConfig: get().config.modelConfig,
         });
       },
 
       getMemoryPrompt() {
-        const session = get().currentSession()
+        const session = get().currentSession();
 
         return {
-          role: 'system',
+          role: "system",
           content: Locale.Store.Prompt.History(session.memoryPrompt),
-          date: ''
-        } as Message
+          date: "",
+        } as Message;
       },
 
       getMessagesWithMemory() {
-        const session = get().currentSession()
-        const config = get().config
-        const n = session.messages.length
-        const recentMessages = session.messages.slice(n - config.historyMessageCount);
+        const session = get().currentSession();
+        const config = get().config;
+        const n = session.messages.length;
+        const recentMessages = session.messages.slice(
+          n - config.historyMessageCount
+        );
 
-        const memoryPrompt = get().getMemoryPrompt()
+        const memoryPrompt = get().getMemoryPrompt();
 
         if (session.memoryPrompt) {
-          recentMessages.unshift(memoryPrompt)
+          recentMessages.unshift(memoryPrompt);
         }
 
-        return recentMessages
+        return recentMessages;
       },
 
       updateMessage(
@@ -286,49 +368,63 @@ export const useChatStore = create<ChatStore>()(
 
         if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) {
           // should summarize topic
-          requestWithPrompt(
-            session.messages,
-            Locale.Store.Prompt.Topic
-          ).then((res) => {
-            get().updateCurrentSession(
-              (session) => (session.topic = trimTopic(res))
-            );
-          });
+          requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
+            (res) => {
+              get().updateCurrentSession(
+                (session) => (session.topic = trimTopic(res))
+              );
+            }
+          );
         }
 
-        const config = get().config
-        let toBeSummarizedMsgs = session.messages.slice(session.lastSummarizeIndex)
-        const historyMsgLength = toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0)
+        const config = get().config;
+        let toBeSummarizedMsgs = session.messages.slice(
+          session.lastSummarizeIndex
+        );
+        const historyMsgLength = toBeSummarizedMsgs.reduce(
+          (pre, cur) => pre + cur.content.length,
+          0
+        );
 
         if (historyMsgLength > 4000) {
-          toBeSummarizedMsgs = toBeSummarizedMsgs.slice(-config.historyMessageCount)
+          toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
+            -config.historyMessageCount
+          );
         }
 
         // add memory prompt
-        toBeSummarizedMsgs.unshift(get().getMemoryPrompt())
+        toBeSummarizedMsgs.unshift(get().getMemoryPrompt());
 
-        const lastSummarizeIndex = session.messages.length
+        const lastSummarizeIndex = session.messages.length;
 
-        console.log('[Chat History] ', toBeSummarizedMsgs, historyMsgLength, config.compressMessageLengthThreshold)
+        console.log(
+          "[Chat History] ",
+          toBeSummarizedMsgs,
+          historyMsgLength,
+          config.compressMessageLengthThreshold
+        );
 
         if (historyMsgLength > config.compressMessageLengthThreshold) {
-          requestChatStream(toBeSummarizedMsgs.concat({
-            role: 'system',
-            content: Locale.Store.Prompt.Summarize,
-            date: ''
-          }), {
-            filterBot: false,
-            onMessage(message, done) {
-              session.memoryPrompt = message
-              if (done) {
-                console.log('[Memory] ', session.memoryPrompt)
-                session.lastSummarizeIndex = lastSummarizeIndex
-              }
-            },
-            onError(error) {
-              console.error('[Summarize] ', error)
-            },
-          })
+          requestChatStream(
+            toBeSummarizedMsgs.concat({
+              role: "system",
+              content: Locale.Store.Prompt.Summarize,
+              date: "",
+            }),
+            {
+              filterBot: false,
+              onMessage(message, done) {
+                session.memoryPrompt = message;
+                if (done) {
+                  console.log("[Memory] ", session.memoryPrompt);
+                  session.lastSummarizeIndex = lastSummarizeIndex;
+                }
+              },
+              onError(error) {
+                console.error("[Summarize] ", error);
+              },
+            }
+          );
         }
       },
 
@@ -348,8 +444,8 @@ export const useChatStore = create<ChatStore>()(
 
       clearAllData() {
         if (confirm(Locale.Store.ConfirmClearAll)) {
-          localStorage.clear()
-          location.reload()
+          localStorage.clear();
+          location.reload();
         }
       },
     }),